
05-权限不是开关:工具调用如何在放行、拦截与追问之间做决定
本章大纲
- 从一次"拒绝还是确认"的请求开始,拆解权限管线的多阶段决策。
- 区分
deny、ask、allow与passthrough的语义和覆盖关系。 - 追踪规则来源,理解会话级配置为何可能改变最终结果。
- 通过并发检查说明
ToolPermissionContext为什么必须提供稳定快照。 - 分析 Hook、分类器和用户之间如何用
ResolveOnce竞争一次决策。 - 对比不同权限模式保留的边界,判断自动化究竟放宽了什么。
- 将 Bash 权限细化到命令匹配粒度,并观察超时后的回退路径。
- 区分内存更新与持久化更新,确认权限变化能否跨越重启保存。
1. 一次请求为什么会从拒绝转成追问
工具调用并不只有"允许"和"拒绝"两个出口。它可能先经过输入结构验证,再进入规则匹配;没有被前面阶段终结时,才会继续接受工具上下文评估或交互式处理。
输入验证使用 Zod Schema。输入不符合结构定义时,调用通常会被拒绝。
但解析失败并不只有硬拒绝这一条路径。默认的 passthrough 可能降级为 ask,把决定交给用户确认。
checkPermissions 的结果包括 allow、deny、ask 和 passthrough。其中 allow 还可能携带经过更新的输入,passthrough 则表示当前检查没有给出最终意见。
概念上,可以把一次请求理解为四个职责阶段:输入验证、规则匹配、工具上下文评估、交互式提示。只要前面的阶段已经作出终局决定,后面的阶段就不会继续执行。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-1.svg" alt="05-权限管线-Agent-的护栏 图 1" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
这张图表达的是职责上的阶段模型。实现层面还需要保留一个差异:源文将 checkPermissions 概述为第三阶段,但实现说明指出,它会在规则匹配流程中较早调用,结果还可能被后续的 allow 规则覆盖。因此,不能把这四个阶段机械理解成所有代码路径都严格串行。
2. 四种结果并不等价
deny、ask、allow 和 passthrough 代表不同的决策意图,而不是四种可以互换的状态。
deny 表示拒绝。规则匹配时,行为优先级是 deny > ask > allow,显式拒绝不能由允许规则覆盖。
ask 表示当前结果必须经过确认。它不是"暂时没有结论",而是明确要求用户或其他后续决策者参与。
passthrough 的含义不同:当前工具不表达自身意见,把处理权交给后续阶段。后续的 allow 规则可能覆盖它,但不能覆盖明确的 ask。
因此,"进入人工处理"不能概括所有非放行结果。一个请求可能是工具暂不表态,也可能是工具已经明确要求确认。
json
{
"permissions": {
"allow": [
"Bash(npm test)",
"Bash(npm run lint)",
"Bash(git:*)",
"Read",
"Glob",
"Grep"
],
"deny": [
"Bash(npm publish)",
"Bash(rm -rf *)"
]
}
}
这份配置同时展示了不同粒度的规则:既有对 Read、Glob、Grep 这类工具的整体放行,也有针对具体 Bash 命令的限制。
allow 还可能附带更新后的输入,所以它不一定只是一个布尔结论。相反,passthrough 只说明当前检查没有做出最终判断。
3. 规则结果还取决于它从哪里来
同一个工具、同一组参数,在不同项目或不同会话中得到不同结果,并不一定意味着工具行为改变了。规则来源本身就是决策的一部分。
规则来源包括:
userSettingsprojectSettingslocalSettingsflagSettingspolicySettingscliArgcommandsession
这些来源大体遵循从全局到局部、从远处到当前会话的就近关系。会话级优先级最高,全局用户级优先级最低。
因此,不能只把配置理解为用户、项目、本地三层。命令行参数、命令级设置、策略设置和功能标志也可能参与最终匹配。
规则按来源建立索引后,可以追踪一条决定来自全局、项目、本地,还是其他配置层。这个信息对于解释"为什么这次结果不同"十分关键。
工具级规则既可以匹配整个工具,也可以使用 MCP 服务器级通配匹配。
Bash 存在一个有条件的例外:在沙箱模式下,如果启用了沙箱自动放行,沙箱命令可能跳过工具级的 ask 规则。这里的前提是沙箱隔离已经降低了命令影响范围,不能把它推广为普通 Bash 请求的通用行为。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-2.svg" alt="05-权限管线-Agent-的护栏 图 2" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
第二份配置展示了通配规则如何扩大覆盖面:
json
{
"permissions": {
"allow": [
"Bash(npm run *)",
"Bash(npx eslint *)"
],
"deny": [
"Bash(npm publish *)",
"Bash(* > /etc/*)"
]
}
}
Bash(npm run *) 比 Bash(npm run lint) 覆盖更广。规则越宽,能够自动放行的命令范围越大,误放行的潜在范围也随之扩大。
4. 不可变上下文如何保护并发检查
设想两个工具调用同时运行,其中一个正在更新权限。如果另一个调用直接读取同一个可变对象,它可能在一次检查过程中看到前后不一致的规则状态。
ToolPermissionContext 携带权限模式、目录、allow、deny、ask 规则、bypass 可用性和提示控制等信息。
这些字段是只读的。每次更新都会生成新的上下文对象,而不是就地修改当前对象。
这样,每个并发检查都可以使用自己的稳定快照。一个工具的权限更新不会改变另一个工具已经开始执行的检查。
不可变性解决的是权限上下文更新导致的状态污染问题。它并不等于自动解决所有并发问题,源文只论述了权限快照一致性的边界。
权限决策的来源包括 Hook、用户和分类器。它们的信任顺序是 Hook 高于用户,用户高于分类器。这个顺序与上下文快照解决的是不同问题:前者决定谁更可信,后者保证检查过程中读到的状态稳定。
5. 一次确认只能由一个决策者提交
请求进入 ask 后,权限管线可能同时触发 Hook、分类器和用户交互。
Hook 可以先给出结果。如果 Hook 没有处理,请求可能继续触发投机性分类器检查,并同时渲染交互式提示。
此时用户和分类器存在竞赛关系。谁先完成有效决策,谁的结果就对这次请求生效。
ResolveOnce 通过 claimed 标志和 claim() 实现一次性、先到先得的提交机制。它的目标是防止用户和分类器同时解决同一个请求。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-3.svg" alt="05-权限管线-Agent-的护栏 图 3" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
用户决策还带有 permanent 属性,用于区分这次选择是否需要写入配置。
分类器的参与并不代表它拥有最高信任级别。某些安全检查会限制分类器决策,后文的模式分析也会说明,自动批准并不能覆盖所有防线。
6. 权限模式不是一条简单的宽松刻度
权限模式包括 default、plan、auto、bypassPermissions,以及内部使用的 bubble。
default 通常要求没有被明确允许的工具调用逐次确认。它的安全性较高,但大量操作会增加交互中断。
plan 主要限制写入工具,读取工具通常可用。不过,在 bypass 可用标志为真的情况下,部分 bypass 放行逻辑仍可能生效,因此不能把它简单理解为"所有写入都绝对禁止"。
auto 使用 AI 分类器处理部分 ask 请求,并结合安全工具白名单、快速路径和连续拒绝回退机制减少开销或避免循环。
自动分类器并不拥有全部安全路径的批准权。涉及 .git/、.claude/ 的某些安全检查不受分类器批准影响,这些检查可以视为"分类器免疫"的部分路径,而不是所有敏感操作的统一规则。
bypassPermissions 通常会跳过其他权限确认,但仍保留 deny、交互要求、内容级 ask 和 safetyCheck 等防线。因此,它不等于完全没有安全约束。
bubble 是内部模式。子智能体的权限向主智能体上下文冒泡,不应超出主智能体边界;它也不是对外公开的普通配置模式。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-4.svg" alt="05-权限管线-Agent-的护栏 图 4" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
这张图表达的是从严格到宽松的使用倾向,不应被当作所有模式在任何场景下都能互相替代。每种模式仍保留自己的例外和边界。
7. Bash 必须细化到命令匹配粒度
Bash 不能只按照工具名称做一次统一放行,因为同一个工具入口可以承载风险差异很大的命令。
它支持精确匹配、前缀匹配和通配符匹配。
精确匹配只覆盖特定命令,例如 Bash(npm test)。前缀匹配可以覆盖一组以相同前缀开头的命令,例如 Bash(npm:*)。通配符匹配则能覆盖更宽的参数范围,例如 Bash(git commit *)。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-5.svg" alt="05-权限管线-Agent-的护栏 图 5" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
表达能力越强,匹配面越宽,潜在误放行范围也越大。Bash 通配匹配还会处理转义字符,特定尾部通配写法可以让参数部分变成可选。
Bash 权限请求还可能触发投机性分类器检查。分类器与 2 秒定时器竞争;如果分类器在限定时间内没有完成,系统就转交交互式提示。
这个 2 秒是交互体验上的权衡,不应被描述成经过外部验证的普遍最优标准。
8. 当前生效不代表重启后仍然存在
权限更新包括添加规则、替换规则、移除规则、设置模式、添加目录和移除目录六类操作。
这些操作可以作用于五类配置源,因此形成 30 种可能的更新组合。但"存在一种组合"不代表每个来源都支持持久化。
applyPermissionUpdates 负责立即更新内存中的权限上下文,persistPermissionUpdates 负责把更新写入文件系统。
内存更新先于持久化生效。即使文件写入失败,当前会话仍然使用新的权限状态;但重启后,未成功保存的规则可能丢失。
只有 local、user、project 三类设置支持持久化。session 和 cliArg 级别的更新只在当前运行时有效。
<figure class="columnforge-visual"><img src="../assets/05-权限管线-Agent-的护栏/05-权限管线-Agent-的护栏-6.svg" alt="05-权限管线-Agent-的护栏 图 6" loading="lazy" decoding="async" style="display:block;width:100%;max-width:960px;max-height:720px;height:auto;object-fit:contain;margin-inline:auto"></figure>
配置位置可以按使用范围选择。团队共享规则适合放在项目级配置,个人偏好适合放在本地配置,临时需求适合使用会话级规则。
这里需要区分"当前调用已经看到变化"和"文件系统已经保存变化"。前者由内存更新保证,后者只有在支持持久化的配置源写入成功后才成立。源文没有说明持久化失败后的重试、并发写入、崩溃恢复或文件一致性策略,因此不能把 persistPermissionUpdates 描述成可靠落盘机制。
结语:把每次决定定位到具体边界
遇到一次权限请求,可以依次追问几个问题:
- 输入是否符合 Zod Schema 的结构定义?
- 哪一层规则先作出了决定?
- 结果是
passthrough,还是明确的ask? - 规则来自哪个配置源?
- 当前检查使用的是不是稳定的
ToolPermissionContext快照? - Hook、用户和分类器中,谁获得了这次决策权?
- 当前模式保留了哪些 deny、交互要求或
safetyCheck防线? - Bash 规则覆盖的是单条命令、命令前缀,还是更宽的通配范围?
- 更新只改变了内存,还是已经写入 local、user、project 配置?
可靠的权限系统并不是让所有请求自动通过,而是让放行、拒绝和追问都能落到清楚的规则、明确的上下文与可追踪的状态变化上。