Atuin AI 工具与权限系统完全指南:permissions.ai.toml 配置、作用域规则与安全实践
【免费下载链接】atuin✨ Making your shell magical项目地址: https://gitcode.com/gh_mirrors/at/atuin
Atuin AI 是 Atuin 项目内置的 AI Agent,它通过AtuinHistory、AtuinOutput、Read、Write、Shell等客户端工具与你的系统交互——在帮你回忆历史命令、分析文件内容、修改配置或执行多步操作时,每个工具都可以被"允许(allow)/ 拒绝(deny)/ 询问(ask)"。本文基于官方文档 docs/docs/ai/tools-permissions.md,并结合仓库源码(crates/atuin-ai/src/permissions/与crates/atuin-ai/src/tools/),系统讲解权限文件的位置与查找顺序、规则优先级、五种工具的作用域写法、Shell 命令通配符语义以及能力开关配置。读完本文,你将能写出精确、安全、可复用的permissions.ai.toml,在不失控的前提下把 AI 的"动手能力"交给它。
权限系统总览
Atuin AI 采用**默认询问(ask-first)**的设计:默认情况下,AI 在调用任何客户端工具前都必须先征得你的许可。你可以通过一个称为permission file(权限文件)的 TOML 配置来改变这一默认行为——把高频、低风险的操作用allow自动放行,把危险操作用deny直接拦截,剩下的保持ask由你逐次确认。
从源码看,整个判定链路清晰且分层:
- 收集:
PermissionWalker从 AI 的当前工作目录出发,沿目录逐级向上查找权限文件,并追加全局权限文件(walker.rs); - 解析:每个文件被解析为
RuleFileContent { permissions: { allow, deny, ask } },每条规则解析为Rule { tool, scope }(file.rs、rule.rs); - 裁决:
PermissionChecker按"文件从深到浅、文件内 ask → deny → allow"的顺序逐一匹配,命中即返回结果,全部未命中则默认Ask(check.rs); - 入口:
PermissionResolver负责组装 walker 与 checker,并把每次工具调用(ClientToolCall)转成PermissionRequest进行裁决(resolver.rs)。
裁决结果
PermissionChecker::check的返回类型PermissionResponse只有三种取值(check.rs):
| 结果 | 含义 | 触发条件 |
|---|---|---|
Allowed | 直接放行 | 命中 allow 规则,且未被更优先的 ask/deny 拦截 |
Denied | 直接拒绝 | 命中 deny 规则(且未被更优先的 ask 覆盖) |
Ask | 弹窗询问 | 命中 ask 规则,或所有文件都无匹配规则 |
权限文件:位置、查找顺序与优先级
文件位置与查找顺序
权限文件有两种放置位置:
- 项目级:任意项目目录下的
.atuin/permissions.ai.toml。当 AI 想要运行某个工具时,Atuin AI 会检查它的工作目录,并向上逐级检查所有父目录直到文件系统根目录; - 全局级:Atuin 配置目录下的
permissions.ai.toml(默认是~/.config/atuin/permissions.ai.toml)。
源码与文档完全对应:PermissionWalker::walk通过self.start.ancestors()枚举当前目录到根目录的每一级,并行检查每个目录下是否存在.atuin/permissions.ai.toml,最后单独检查全局文件(walker.rs)。路径拼接逻辑见 writer.rs:
pub fn project_permissions_path(project_root: &Path) -> std::path::PathBuf { project_root.join(".atuin").join("permissions.ai.toml") } pub fn global_permissions_path() -> std::path::PathBuf { atuin_common::utils::config_dir().join("permissions.ai.toml") }也就是说,一个项目可以同时拥有"项目根级"与"各子目录级"多层权限文件,再加上一份全局文件,共同参与裁决。
文件格式
权限文件是一个 TOML 文件,固定包含[permissions]表,表内三个数组:
[permissions] allow = [ # rules for automatically allowed tools ] deny = [ # rules for automatically denied tools ] ask = [ # rules for tools that require asking for permission ]每个数组元素是一条规则字符串。从源码看(rule.rs),规则由正则^(\w+)(?:\((.*)\))?$解析:Tool或Tool(scope),例如Read、Read(**/*.md)、Shell(git commit *)。ask数组在文档示例中不常出现,但它是完整格式的一部分,需要时同样可用。
文件级优先级:越深越优先
位于文件系统更深处的权限文件,优先于更上层的权限文件。例如,当前工作目录下的权限文件允许某工具,那么即使父目录的权限文件拒绝它,也会以工作目录的为准放行。这一语义在源码中有明确的注释支撑:"Files are in order from deepest to shallowest, so we can stop at the first match"(check.rs)——walker 收集时按深度排序(walker.rs),checker 按此顺序遍历,命中第一个匹配文件即终止,即使后续文件存在相反规则也不再考虑。
文件内优先级:ask > deny > allow
在同一个权限文件内部,ask规则优先于deny规则,deny规则优先于allow规则。例如某文件既有一条允许某工具的规则,又有一条对该工具"询问"的规则,则 AI 会先弹出询问而非直接放行。这与 check.rs 的实现完全一致:对每个文件依次遍历ask→deny→ 检查allow(all_covered_by),命中即返回。
默认行为:无匹配即询问
如果所有权限文件中都没有匹配的规则,Atuin AI 默认询问用户后再执行工具(PermissionResponse::Ask,check.rs)。这是安全兜底:即使你从未配置过任何权限文件,AI 也绝不会在未经确认的情况下擅自执行工具。
权限作用域(Permission Scopes)
大多数规则都可以限定作用域到特定路径或其他上下文。对于文件操作类规则,作用域是一个匹配文件路径的 glob 模式。例如,你可以允许 AI 读取某个目录下的文件,同时禁止读取其他目录。
作用域写在工具名后的括号里,例如:
Read(**/*.md)—— 匹配当前目录及子目录下的所有 Markdown 文件;Read(.secret/**)—— 匹配.secret目录下的所有文件;- 缺省 glob(如
Read)—— 匹配所有文件。
从源码看,path_matches_scope的匹配逻辑很宽容(tools/mod.rs):相对路径会先解析为绝对路径再做匹配;非绝对路径的 scope 会依次尝试"文件名匹配""完整绝对路径匹配""相对当前工作目录匹配"三种方式;路径中的\会归一化为/以兼容 Windows。这意味着*.md、crates/**/*.rs、src/*.rs这类写法都能按直觉工作。
完整示例配置
官方文档给出如下示例:允许 AI 读写当前项目内所有 Markdown 文件(因为 Write 隐含 Read,见下文),但拒绝访问任何.env文件;对其他文件,AI 会在读写前向你询问。
[permissions] allow = [ "Write(**/*.md)" ] deny = [ "Read(.env)" ]这是一个非常典型的"最小信任 + 明确例外"配置:日常只写文档,敏感文件直接封死,其余情况保持人工确认。
工具详解
Atuin AI 的客户端工具通过统一的ClientToolCall枚举管理(tools/mod.rs),每个工具类别对应唯一的权限规则名。特别要注意的是:Edit(编辑文件)与Write(写入文件)共享"Write"规则名——一个 Write 权限同时覆盖"字符串替换式编辑"和"整文件创建/覆盖"(tools/mod.rs)。
AtuinHistory:搜索历史命令
AtuinHistory工具允许 AI 搜索你的 Atuin 历史,找出相关命令。它只读,不会修改任何数据。当你问"我之前跑过什么命令""我的某个命令为什么失败了"时,AI 可能会请求使用它。
- 权限规则与作用域:
AtuinHistory - 配置开关:
ai.capabilities.enable_history_search(见 settings 文档) - 示例权限文件:
[permissions] allow = ["AtuinHistory"]AtuinOutput:读取命令输出
AtuinOutput工具允许 AI 读取历史命令的已捕获输出。它同样只读,适用于"那条命令跑出了什么结果"或排查失败命令。前提条件:命令输出捕获依赖 daemon 与 pty-proxy 的正确搭建,详见 Reading Command Output(相关组件文档见 pty-proxy 与 daemon)。
- 权限规则与作用域:
AtuinOutput - 配置开关:
ai.capabilities.enable_history_output(见 settings 文档) - 示例权限文件:
[permissions] allow = ["AtuinOutput"]Read:读取文件
Read工具允许 AI 读取系统上的文件。当你请它分析文件内容、帮你修改文件、或提出"最好看看文件内容才能回答"的问题时,它都可能被请求使用。除文本文件外,Read 也支持读取目录列表(源码中目录会返回Directory contents:形式的清单,见 tools/mod.rs)。
- 权限规则与作用域:
Read(<glob_pattern>)。例如Read(**/*.md)允许读取当前目录及子目录下所有 Markdown 文件;缺省 glob(Read)匹配所有文件。 - 配置开关:
ai.capabilities.enable_file_tools(见 settings 文档)——该开关同时启用Read与Write两个工具。 - 示例权限文件:
[permissions] allow = ["Read(**/*.md)"] deny = ["Read(.secret/**)"]⚠️ 警告:Write 隐含 Read
为防止意外数据丢失,Atuin AI 在写入文件前必须先读取该文件的内容。这意味着,任何允许
Write工具作用于某文件(或某组文件)的规则,都会自动允许Read作用于同样的文件。例如你配置了Write(**/*.md),即使没有显式的Read(**/*.md)规则,AI 也能读取当前目录及子目录下所有 Markdown 文件。这一点在源码的ReadToolCall::matches_rule中直接体现:规则工具名为"Read"或"Write"都会命中(tools/mod.rs)。
Write:创建与编辑文件
Write工具允许 AI 创建和编辑系统上的文件。当你请它更新某个工具的配置、或协助排查问题时,它可能被请求使用。Edit(edit_file)与Write(write_file)共用Write规则名,作用域匹配逻辑也一致(tools/mod.rs)。
- 权限规则与作用域:
Write(<glob_pattern>)。例如Write(**/*.md);缺省 glob(Write)匹配所有文件。 - 配置开关:
ai.capabilities.enable_file_tools(见 settings 文档)。 - 示例权限文件:
[permissions] allow = ["Write(**/*.md)"] deny = ["Write(.secret/**)"]📝 备注:文件备份
在同一个会话(session)中,Atuin AI 首次写入某个文件时,会先创建该文件的备份。备份存放于 Atuin 数据目录下,按会话隔离;目录内有一份 manifest 文件,将原始文件路径映射到备份文件路径,并记录快照时间与字节数(snapshots.rs)。
从源码看,备份目录的实际结构为
<data_dir>/ai/snapshots/<session_id>/,备份文件名是对原路径做百分号编码(/→%2F、\→%5C)后生成的扁平文件名,便于直接用ls浏览(如/Users/me/.config/foo.toml对应Users%2Fme%2F.config%2Ffoo.toml),manifest.json中的每个条目包含original_path、snapshot_at与size_bytes三个字段(snapshots.rs)。同一会话内重复写入同一文件不会重复快照(幂等)。官方文档同时说明:未来会提供更方便的数据恢复手段。
Shell:执行命令
Shell工具允许 AI 在你的系统上执行 shell 命令。当你请它"直接跑一条命令来达成目的"、协助调试失败命令、或执行多步工作流时,它都可能被请求使用。
- 权限规则与作用域:
Shell(<command pattern>)。例如Shell(git *)允许任何以git开头的命令;缺省命令模式(Shell)匹配所有命令。 - 配置开关:
ai.capabilities.enable_command_execution(见 settings 文档)。 - 示例权限文件:
[permissions] allow = [ "Shell(git add *)", "Shell(git commit *)" ]Shell 作用域的通配符语义
Shell规则中的命令模式是针对命令的各个词(word)进行匹配的,*通配符出现的位置不同,行为也不同:
| 模式 | 匹配 | 不匹配 |
|---|---|---|
* | 任意命令 | — |
git commit * | git commit、git commit -m "msg" | git、git push |
ls* | ls、ls -a、lsof | cat |
git * --amend | git commit --amend、git rebase --amend | git commit |
git commit | git commit | git、git push、git commit -m "msg" |
注意ls *(带空格)与ls*(不带空格)的区别:
- 空格分隔的形式使用词边界匹配——
ls *匹配ls和ls -a,但不匹配lsof; - 紧贴的形式使用前缀匹配——
ls*能匹配上述全部,包括lsof。
源码any_subcommand_matches(shell.rs)按顺序处理几种情况:空串/*全匹配;"xxx *"结尾的词边界前缀匹配;"xxx*"结尾的前缀/glob 匹配;含*的中间通配(每个*匹配零到多个词);以及无通配符的精确/前缀匹配。这些语义都有大量 rstest 参数化测试用例背书(如ls_word_boundary、ls_glob_prefix、middle_wildcard_amend等,见 shell.rs)。
allow/ask与deny的无通配符差异:
- 对
allow和ask规则,无通配符的模式(如git commit)是精确匹配——只有当命令的词完全一致时才命中。想让git commit带任意参数都放行,请写git commit *; - 对
deny规则,无通配符的模式(如rm)是前缀匹配——任何以该前缀开头的命令都会命中。也就是说deny = ["Shell(rm)"]会同时拒绝rm、rm -rf /和rm ./README.md。写 deny 规则时务必小心,不带显式通配符的 deny 覆盖面比你想的更大。
这一差异在源码中体现为any_subcommand_matches的prefix_bare参数:allow 走严格精确路径(prefix_bare: false),deny/ask 走宽泛前缀路径(prefix_bare: true),并明确注释了"denyingrmalso blocksrm -rf /"的意图(shell.rs)。
复合命令的处理
当 AI 运行复合命令(例如git add . && npm test)时,Atuin 会先把它解析成一个个子命令。只有所有子命令都被允许,整条命令才会自动放行;否则就会落入询问流程。例如git add . && npm test必须同时被Shell(git add *)和Shell(npm test)两条规则覆盖,才能自动通过。
解析实现位于 shell.rs:启用tree-sitter特性时,bash/sh/zsh/dash/ksh 用 tree-sitter-bash 解析、fish 用 tree-sitter-fish 解析,能够识别&&/||/;/管道、命令替换$(...)、子 shell( ... )、if/for/while/case等结构;在无法交叉编译的平台或未知 shell(如 nushell)上,回退到按&&、||、;、|切分并取每段首词的简化策略(parse_fallback)。all_covered_by明确要求"每个子命令都必须被至少一条规则单独覆盖",且解析为空时不做事后放行(tools/mod.rs)。
⚠️ 警告:复合命令需要谨慎:官方文档明确提示,Atuin 的命令解析并非完美,存在无法正确识别子命令的边界情况,某些 shell 上的解析能力也有限。因此,不建议用宽泛模式(如Shell(*))放行复合命令——一个解析失误就可能把一条本应被拦截的rm -rf放进 allow 集合。
能力开关:在配置层面控制工具曝光
除了权限文件,Atuin AI 还提供一组[ai.capabilities]配置,用于控制哪些能力会写入发送给 LLM 的上下文——LLM 只会请求它"知道存在"的工具。四个开关默认均为true(详见 settings 文档):
| 配置项 | 默认值 | 控制的工具 |
|---|---|---|
enable_history_search | true | AtuinHistory |
enable_history_output | true | AtuinOutput(依赖 pty-proxy 与 daemon) |
enable_file_tools | true | Read与Write |
enable_command_execution | true | Shell |
示例:关闭历史搜索能力:
[ai.capabilities] enable_history_search = false能力开关与权限文件是两层互补的防线:前者决定 LLM 是否"知道"某工具存在、是否会请求调用;后者决定即使它请求了,该次调用是否放行。此外,settings 文档 中还提到一个全局yolo模式(默认false):开启后自动放行所有权限检查(但它不会启用任何被关闭的能力,只是绕过权限裁决)。请谨慎使用yolo。
实战建议与常见陷阱
结合文档与源码,这里总结几条最实用的配置建议:
- 从"最小允许"开始:默认的 ask-first 行为是最安全的状态。先按需添加少数几条
allow,再逐步观察日志(权限命中时会输出Permission 'ALLOW' by rule ...之类的 debug 日志,见 check.rs)补全规则,而不是一开始就写宽泛通配。 - 把
.env、密钥、~/.ssh等敏感路径写进deny:文档示例中的deny = ["Read(.env)"]思路可推广——用deny做安全兜底,永远比依赖"AI 不主动去读"可靠。 - 用文件级优先级做"项目覆盖全局":如果你在全局文件里 deny 了某工具,但某个可信项目确实需要它,可以在该项目根目录的
.atuin/permissions.ai.toml里用更深的文件覆盖(注意:文件内ask > deny > allow的优先级不会因为深度而改变,深文件整体先于浅文件生效)。 allow写精确、deny写前缀:记住不对称语义——allow中git commit只放行精确命令,需要参数请写git commit *;deny中rm会连带拦截rm -rf /。这正是"允许从严、拒绝从宽"的安全姿态。- 对复合命令保持警惕:宽泛的
Shell(*)加上不完美的命令解析,是权限体系最大的潜在漏洞。如果确实需要放行多步工作流,请用 tree-sitter 能可靠解析的&&/;结构,并确保每个子命令都有独立规则覆盖。
通过权限文件(.atuin/permissions.ai.toml与全局~/.config/atuin/permissions.ai.toml)、能力开关([ai.capabilities])与工具作用域三者的组合,你可以把 Atuin AI 从"每步都要确认"的助手,调教成"该放手时放手、该拦截时绝不手软"的可靠自动化伙伴。
【免费下载链接】atuin✨ Making your shell magical项目地址: https://gitcode.com/gh_mirrors/at/atuin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考