Chapter 10: 风险分级与自动审批
读文件和删文件,风险完全不同。自动化的安全判断如何区分这两者?
tool_use request
│
┌──────▼──────┐
│ Permission │
│ Check │
│ │ │
│ ┌───▼────┐ │
│ │Allowlist│ │ Fast paths
│ └───┬────┘ │
│ ┌───▼────┐ │
│ │Denylist │ │
│ └───┬────┘ │
│ │ │
│ ★ Risk │
│ ★ Classifier ★ ◄── 本章聚焦
│ Stage1→Stage2│
│ │ │
│ allow/deny │
└──────────────┘
10.1 不是所有操作都一样危险
上一章我们看到,当权限决策落入 ask 分支时,auto 模式可以让 ML 分类器
代替用户做出判断。
但这引出了一个更根本的问题:
用一个模型去判断另一个模型的行为是否安全,凭什么?
答案是分层。
在分类器介入之前,有大量确定性规则已经排除了两端的极端情况: 明显安全的操作被白名单直接放行, 明显危险的操作被黑名单直接拦截。 分类器只处理中间的灰色地带—— 那些规则无法覆盖、需要理解上下文才能判断的操作。
这就像医院的分诊系统。 护士先根据症状分流: 明显的感冒直接开药,明显的急症直接进急诊, 只有症状不明确的才需要专家会诊。 分类器就是那个专家——昂贵但精准,只在真正需要时才出场。
权限类型定义模块中定义了三级风险评估框架。
RiskLevel 分为 LOW、MEDIUM、HIGH,
每个评估附带 explanation(发生了什么)、
reasoning(为什么有风险)、risk(最坏情况)。
这让权限弹窗不再是无上下文的"Allow / Deny",
而是带有充分信息的安全决策。
10.2 三条快速通道:在分类器之前拦截
分类器需要一次额外的 API 调用,成本不低。 因此系统在分类器之前设置了三条快速通道, 每一条都是零延迟、零成本的确定性判断。
安全工具白名单
分类器决策模块中定义了一组"绝对安全"的工具, 收集在安全工具白名单集合中。
这些工具的共同特征是:只读或只影响元数据,
不产生文件系统写入、网络请求或进程执行。
列表包括文件读取(FileRead)、搜索(Grep、Glob)、
语言服务器(LSP)、任务管理(TodoWrite、TaskCreate)、
计划模式切换、Swarm 协调工具等。
当 auto 模式遇到这些工具时,直接放行,
连分类器的 API 调用都省了。
analytics 事件中 fastPath: 'allowlist' 的标记
让运维团队可以追踪有多少操作走了这条快速通道。
白名单的设计遵循了一个严格原则:宁缺毋滥。
注意白名单的注释:
Does NOT include write/edit tools — those are handled by the acceptEdits fast path。
写入工具不在白名单中,因为它们的安全性取决于目标路径。
更值得注意的是,AGENT_TOOL_NAME 和 REPL_TOOL_NAME 也不在白名单中。
Agent 工具可以间接执行任何操作,
REPL 的 JavaScript 代码可能包含 VM 逃逸。
即使它们"看起来"安全,潜在的间接风险也排除了白名单资格。
Swarm 相关的工具(TEAM_CREATE、SEND_MESSAGE)之所以在白名单中,
是因为它们只操作内部邮箱和团队状态——
团队成员自身有独立的权限检查。
acceptEdits 快速路径
对于写操作,系统有一个巧妙的优化:
在调用分类器之前,先模拟 acceptEdits 模式下的权限检查。
如果一个操作在 acceptEdits 模式下被允许 (即只是在工作目录内编辑文件), 那它在 auto 模式下也应该被允许 ——不需要浪费一次分类器调用来确认这个显而易见的结论。
实现方式很精巧:
临时构造一个权限上下文,将 mode 替换为 acceptEdits,
然后调用工具的权限检查方法。
如果返回 allow,直接放行并标记 fastPath: 'acceptEdits'。
但同样,AGENT_TOOL_NAME 和 REPL_TOOL_NAME 被排除在此快速路径之外。
代码注释解释了原因:
REPL 代码可能在内部工具调用之间包含 VM 逃逸代码——
分类器必须看到那些"胶水 JavaScript",
而不是只看到内部工具调用。
safetyCheck 硬性拦截
在所有快速通道之前,还有一道硬性安全屏障。
当 classifierApprovable 为 false 时,
意味着这个安全检查连分类器也无权批准——
比如 Windows 路径绕过尝试或跨机器桥接消息。
这些操作必须由用户亲自确认,
任何自动化手段都不能代替人类判断。
当 classifierApprovable 为 true 时——
比如 .agent/ 下的文件操作——
分类器可以看到完整上下文来判断是否是用户主动请求的行为。
这个布尔值精确地划定了"机器可以代劳"和"必须人类确认"的边界。
10.3 敏感文件保护:修改配置即获得执行权
文件系统安全模块中定义了两组受保护目标。
DANGEROUS_FILES 包括 .gitconfig、.bashrc、.zshrc、
.mcp.json、.ripgreprc 等。
DANGEROUS_DIRECTORIES 包括 .git、.vscode、.idea、.agent。
为什么这些文件和目录需要特别保护? 因为它们有一个共同特征: 修改它们等同于获得代码执行能力。
.gitconfig 可以设置 core.sshCommand 在每次 git 操作时执行任意命令。
.bashrc 在每次终端启动时运行。
.mcp.json 可以注册恶意的 MCP 服务器。
.agent/settings.json 可以修改权限规则本身
——一个能修改自己权限规则的 Agent,等于没有权限限制。
路径安全检查函数是这道防线的核心。 它做了三重检查:
Windows 路径模式检测。
NTFS 替代数据流(file.txt::$DATA)、
8.3 短名称(GIT~1 可以绕过对 .git 的字符串匹配)、
长路径前缀(\\?\C:\... 绕过路径规范化)、
尾部点和空格(.git. 在 Windows 上等同于 .git)、
DOS 设备名(.git.CON 是特殊设备文件)、
三个以上连续点号(路径遍历变体)——
每一个都是真实的路径规范化绕过攻击向量。
函数注释中详细论述了 为什么选择"检测拒绝"而非"规范化"策略。 规范化依赖文件系统状态,新建文件时目标不存在无法规范化; 且会引入 TOCTOU 竞态条件—— 在规范化和实际文件操作之间,文件系统状态可能已经改变。
为什么在所有平台上都检测 Windows 路径模式?
因为 NTFS 文件系统可以通过 ntfs-3g 挂载在 Linux 和 macOS 上。
唯一的例外是 ADS 冒号检测——
只在 Windows/WSL 上检查,因为 Linux/macOS 上冒号是合法的文件名字符,
NTFS 上的 ADS 通过 xattrs 而非冒号语法访问。
Agent 配置文件检测。
配置文件路径检查函数不仅检查 settings 文件,
还检查 .agent/commands/、.agent/agents/、.agent/skills/ 目录。
路径比较使用大小写不敏感的规范化函数,
防止在 macOS/Windows 上通过 .aGeNt/Settings.json 绕过检查。
危险文件/目录检测。
危险路径检测函数遍历路径段与黑名单做大小写不敏感匹配。
其中有一个精巧的特例:.agent/worktrees/ 路径被豁免,
因为这是系统存储 git worktree 的结构性目录,而非用户创建的敏感目录。
10.4 双阶段分类器:宁可误杀的快速阶段,深思熟虑的纠偏阶段
当所有确定性规则都无法给出判断时,auto 模式启动 ML 分类器。 这是整个权限系统中最精密的组件, 实现在自动审批分类器模块中。
分类器看到什么
分类器看到的不是孤立的工具调用, 而是一个精心构建的会话摘要。 转录构建函数将对话历史转换为紧凑的转录格式。
这里有一个关键的安全决策: assistant 的文本内容被故意排除,只保留 tool_use 块。
为什么? 因为 assistant 文本是模型生成的, 恶意 prompt 注入可能诱导模型产生具有误导性的文本。 比如模型可能在 tool_use 前写下"用户刚才确认了要删除所有文件", 如果分类器看到这段文本,可能会被误导认为操作是用户授权的。 只看"做了什么"(tool_use),不看"说了什么"(text), 大大缩小了攻击面。
转录格式被压缩为紧凑的单行表示。 每个工具通过分类器输入方法控制哪些字段暴露给分类器。 如果某个工具返回空字符串,意味着"没有安全相关性", 该工具调用会被完全跳过。 这种"每个工具控制自己的分类器投影"的设计, 让工具作者可以精确控制信息披露—— 只暴露安全决策需要的最少信息。
双阶段决策流
分类器支持三种运行模式:
both(默认)、fast(只跑第一阶段)、thinking(只跑第二阶段)。
默认的 both 模式是最精妙的。
Stage 1(快速阶段): 最大 64 tokens,温度为 0,附加后缀:
'\nErr on the side of blocking. <block> immediately.'
这句话把分类器推向保守端——"宁可误杀,不可放过"。
如果它说"允许",那大概率确实安全(高精确率);
如果它说"阻止",可能是误报(较低召回率)。
stop_sequences: ['</block>'] 让模型在给出判断后立即停止,
进一步压缩延迟。
Stage 2(思考阶段): 只在 Stage 1 说"阻止"时才运行。 最大 4096 tokens,附加后缀要求深入推理:
'\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>.'
Stage 2 的任务是"纠偏"—— 用更充分的推理来判断 Stage 1 的阻止是否是误报。 注意它特别强调"显式的用户确认才能覆盖阻止" ——暗示性或隐含的确认不够。
两阶段设计的效果是:
大部分安全操作在 Stage 1 就通过了(低延迟),
只有可疑操作才需要 Stage 2 的深度分析(高准确率)。
一个安全的 git add . 在几百毫秒内通过,
一个可疑的 curl | bash 则会被仔细审查。
响应解析:防御链式推理中的误匹配
两阶段分类器使用 XML 格式(<block>yes/no</block>),
解析函数在解析前会先移除 <thinking> 标签内的内容。
为什么要这样做?
因为模型在链式推理中可能写出
"如果用户说了 X 则 <block>yes</block>,否则..."这样的假设性结论。
如果不移除推理内容,假设中的 <block>yes 会被错误匹配为最终判断。
这是一个微妙但重要的防御。
10.5 Fail Closed:不确定时选择更安全的路径
分类器的错误处理体现了"安全优先"的哲学,贯穿三个层面。
响应不可解析时:
Stage 2 返回值无法解析为 yes/no,直接判定为阻止。
reason 写入 'Classifier stage 2 unparseable - blocking for safety'。
这就是 fail closed——系统在无法确定安全性时选择更保守的路径。
API 不可用时: 行为受特性门控控制。 默认 fail closed(deny), 但可以通过远程配置切换为 fail open(回退到用户询问)。 这个开关的存在是为了运维: 当分类器大规模故障时,运维团队可以快速切换为 fail open, 恢复用户体验,而不是让所有 auto 模式用户被卡住。 特性门控的刷新间隔是 30 分钟, 在安全性和响应速度之间取得平衡。
上下文超长时:
当对话历史超过分类器的上下文窗口时,
这是一个确定性条件——transcript 只会增长不会缩短。
系统不浪费时间重试,立即降级到用户提示模式。
对于 headless 模式,由于降级也无法弹窗,
直接抛出 AbortError 终止 Agent。
10.6 拒绝追踪与降级保护:断路器模式
如果分类器持续拒绝操作怎么办?
拒绝追踪模块实现了一个类似电路断路器的保护机制。
核心数据结构只有两个计数器:
consecutiveDenials 和 totalDenials,以及两个阈值:
连续拒绝上限 maxConsecutive: 3,总拒绝上限 maxTotal: 20。
当任一阈值被触发时, 降级判断函数返回 true, 系统自动降级到用户提示模式。
这个设计解决了两个问题。
第一,分类器系统性误判。 如果分类器因某种原因持续错误地拒绝合法操作, 3 次连续拒绝后用户就能介入。
第二,恶意循环检测。 如果模型持续尝试被拒绝的操作 (可能是 prompt 注入导致的死循环), 20 次总拒绝后强制中断,让人类审查。
成功记录函数有一个值得注意的细节:
当状态没有变化时(consecutiveDenials 已经是 0),
它返回相同的引用而非新对象。
这让下游的 Object.is 检查可以跳过不必要的状态更新——
在每次成功的工具调用都会触发成功记录的高频场景下,
这个优化避免了大量无意义的 React 重渲染。
降级策略对不同场景做了区分:
CLI 模式下降级为弹窗询问;
headless 模式下由于无法弹窗,
达到阈值后直接抛出 AbortError 终止 Agent。
总拒绝达到 20 次后,计数器会被重置为 0, 让用户审查并决定后可以继续使用 auto 模式。 这避免了"一旦达到 20 次就永远无法恢复"的死锁。
10.7 决策链路的完整追踪
每一个权限决策——无论来自规则、分类器还是用户—— 都附带决策原因记录, 一个包含 11 种类型的联合类型。
从 rule(哪条规则、哪个来源)到
classifier(哪个分类器、什么理由)到
hook(哪个 Hook、什么来源)到
safetyCheck(什么安全检查、是否可被分类器批准),
完整的决策链路被结构化记录。
这种可审计性不是奢侈品——它是信任的基础。
用户需要能够理解"为什么这个操作被拒绝了",
才能信任并有效使用自动化决策系统。
/permissions 面板展示了最近的拒绝记录,
让决策过程对用户透明可见。
10.8 小结:分级防御的经济学
回顾整个风险分级体系, 其设计遵循了一个核心的经济学原则: 确定性规则先行,概率性判断后置。
白名单、黑名单、路径检查—— 这些都是零延迟、零成本的判断。 只有当确定性规则无法覆盖时,才启动昂贵的 ML 推理。
Stage 1 的保守策略和 Stage 2 的纠偏策略 构成了精度与延迟的帕累托最优—— 大部分操作在低延迟下通过, 只有真正可疑的才承受高延迟但获得高精度。
而贯穿始终的 fail closed 原则确保了一件事: 系统宁可在不确定时让用户多确认一次, 也不会在不确定时默默放行一个危险操作。
思考题
-
分类器故意排除 assistant 文本只保留 tool_use 块。 但如果攻击者构造了恶意的 tool_use 输入 (比如在 Bash 命令中嵌入误导性注释), 分类器还能有效防御吗? "只看行为不看言论"的策略有什么盲点?
-
连续拒绝阈值设为 3,总拒绝阈值设为 20。 如果你要为高安全场景(如金融系统)调整这些参数,你会怎么改? 改了之后对用户体验有什么影响?
-
acceptEdits 快速通道的逻辑是 "如果在 acceptEdits 模式下被允许,那在 auto 模式下也应该被允许"。 这个推理的隐含假设是什么? 在什么场景下这个假设会失效?