本集对应课程章节:OpenAI Codex · 代码模式(execpolicy / 审批策略 / Guardian)
内容来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》),本页为二次演绎的解读与音频稿件版本。
三条白名单 / 审批防线,各自都会以一种不显眼的方式失效:
| 防线 | 典型失效 |
|---|---|
| 命令策略(execpolicy) | pattern 写短 → --keep 被误拦;模型用 bash -lc 包一层 → 按字面 argv 匹配 bash,禁令不亮 |
| 审批弹窗 | 弹窗叠弹窗 → 审批疲劳,人只看命令头两个词,拇指形成肌肉记忆 |
| 自动放行 | 交给主模型 = 申请人兼审批人;失败时放行 = 超时与坏 JSON 变免费通行证 |
一句话骨架:把「以为」写成例子,机器就能在加载期反对你。
| 维度 | 读完本集你能做到 | 对应证据 |
|---|---|---|
| 自带测试 | 用 match / not_match 把误伤钉死在加载期 | parser.rs L57–78、L133–151 |
| 前缀匹配 | 解释精确字符串相等带来的两个后果 | rule.rs L46–59 |
| 最严聚合 | 说明多规则与组合命令如何取 max | policy.rs L402–411 |
| 包装拆解 | 说明 bash -lc 如何被拆回内层 argv | exec_policy.rs L831–858 |
| 两颗旋钮 | 说清审批策略与沙箱种类的耦合 | protocol.rs L924–947、sandboxing.rs L198–230 |
| 两套记住 | 区分会话层精确 key 与持久层前缀 | sandboxing.rs L64–116、approvals.rs L314–347 |
| Guardian | 复述四步法、失败关闸与熔断器 | review.rs L186–210 / L711 / L717、mod.rs L53 / L176 |
问题:每条白名单都在赌两件事 —— pattern 刚好覆盖想拦的,且不会误伤想放的。这两件事通常要等线上才知道。
做法
| 字段 | 含义 |
|---|---|
match | 这条规则必须命中的命令(正例) |
not_match | 这条规则必须放过的命令(反例) |
时序:prefix_rule 不当场跑例子,先把规则与例子推进待校验队列;整份 Starlark 求值完,parse 再统一校验。
校验顺序固定
ExampleDidMatchExampleDidNotMatchparse 返回失败,Policy 不会被 build 出去出处:codex-rs/execpolicy/src/parser.rs L57–78、L133–151
crate README 的一句话:它们是加载期校验的示例调用,可以当成单元测试。字符串与 token 数组两种写法进解析器后都变成 token 列表(字符串走 shlex 拆词)。校验时启发式回调传空 —— 正例必须靠真实前缀规则命中,不能靠启发式凑数。
为什么长期成立:白名单的典型失败是「作者以为 pattern 是这个意思」。规则和例子写在同一处,策略文件被拷贝或下发时例子跟着走。漏写例子等于漏写测试,加载器不会替你编例子,写了就会强制执行。
先写 not_match(你怕误伤哪条命令),再写 match。顺序反了很容易把反例写成正例的镜像,钉不住真实边界。
匹配语义:规则本体是一段前缀,匹配为精确字符串相等 —— 没有 glob,没有正则。
| 情形 | 结果 |
|---|---|
git reset --hard 后跟 origin/main | 命中(多出的 token 不参与比较) |
git --config X reset --hard | 不命中(第二个 token 对不上) |
出处:execpolicy/src/rule.rs L46–59
查找与聚合
argv[0] 精确取,取不到再考虑把绝对路径收成 basenamedecision 取 maxDecision 派生 Ord,变体书写顺序即严重程度:Allow < Prompt < Forbiddenmax。git status(Prompt)+ git commit(Forbidden)→ 整条管道 Forbidden出处:policy.rs L265–287、L402–411
性质:叠加只能加严,不能放宽。序写在枚举变体上,运行时没有另一张优先级表可以写错 —— 用户层的 allow 盖不住系统层的 forbidden。
代价:插在中间的参数会让规则失效,作者必须写更短的前缀或另写一条。正反例正是用来接住这个代价的 —— 写短了,反例会在加载期先崩。
判定前先走 parse_shell_lc_plain_commands:
&& / || / ; / 管道组成bash -lc 包着的 git reset --hard 会被拆成内层三段再匹配前缀出处:core/src/exec_policy.rs L831–858
拆不出来:整段 argv 当一条命令,交给启发式与后续沙箱。
细节:空引号插在参数里还能还原;插在命令名里则 word-only 解析失败,前缀规则看不见 git,命令掉进启发式。
吃不下的脚本,就不要假装已经看懂。 这是 fail closed 的通用形状。
现象:审批留在 on-request,沙箱从 workspace-write 拧到 danger-full-access —— 十分钟前还会弹窗的命令,现在不弹了。你没动审批旋钮。
原因:两颗旋钮在同一个判定函数里。
| 旋钮 | 取值 |
|---|---|
AskForApproval | Never / UnlessTrusted / OnRequest / Granular |
| 文件系统沙箱种类 | 默认 Restricted(只读或只写工作区);danger-full-access → Unrestricted |
默认逻辑(default_exec_approval_requirement)
| 策略 | Restricted | Unrestricted |
|---|---|---|
Never | Skip | Skip |
UnlessTrusted | NeedsApproval | NeedsApproval |
OnRequest / Granular | 要问 | Skip |
出处:core/src/tools/sandboxing.rs L198–230、protocol.rs L924–947、permissions.rs L227–232
三个易错点
sandbox_approval 关掉 → Forbidden;但文件系统已 Unrestricted 时该分支进不去requirements 不允许 danger-full-access 而配置写了 approval_policy = "never" → 加载器先把权限档案回落到只读,该组合判非法出处:exec_policy.rs L214–236、config/mod.rs L3607–3625 / L3969–3979
两颗旋钮绑定换来的是少弹窗,代价是改沙箱会静默改变审批行为。排查「为什么不再问我了」时,先看沙箱种类,而不是审批策略。
两层记忆
| 层 | 寿命 | key | 范围 |
|---|---|---|---|
| 会话缓存 | 与会话同寿命 | 规范化后的完整命令 + cwd + 环境 + execpolicy 指纹 | 认完整命令,不认通配符 |
| 持久层 | 跨会话 | 前缀(写进 execpolicy) | 可扩张范围 |
关键细节
ApprovedForSession 才命中;只批准一次(Approved)不进 mapnpm run test 批准进会话,npm run lint 是另一把 key/bin/bash -lc 与 bash -lc 在能拆出单一明文命令时,缓存成同一组 tokenAskForApproval);只有改 execpolicy 改了指纹,缓存才失效出处:sandboxing.rs L64–116、approvals.rs L314–347
弹窗菜单:常规 exec 弹窗默认菜单里甚至没有 ApprovedForSession,只有带网络上下文时才带上。用户最常碰到的「记住」是前缀那一条。
说明书必须同步:权限说明是一段 developer 消息,先选沙箱模板再选审批模板。Never 那份只有一句 —— 不要再给 sandbox_permissions,命令会被拒。运行时与说明书是同一份合同的两面。
触发条件:审批疲劳 —— 弹窗叠弹窗,人只看命令头两个词。防线还在,判断已经不在。
两个不能踩的坑:交给主模型 = 申请人兼审批人;失败时放行 = 超时与坏 JSON 变免费通行证。
路由很窄:只接管「本来会弹给用户的 on-request 审批」;策略必须是 OnRequest / Granular,审查员必须是 AutoReview。默认审查员仍是用户。
四步法
| 步 | 动作 |
|---|---|
| 1 | 重建 transcript:只留用户原话与近期工具痕迹;合成脚手架、普通 developer 消息丢掉;超长内容打 truncated 标记 |
| 2 | 送进锁死会话:只读、策略设为 Never、MCP 已清空(克隆父配置) |
| 3 | 要求交回 JSON:先打风险等级与授权情况,再给结论 |
| 4 | 应用结果:allow 继续,其余一律停住 |
出处:guardian/review.rs L186–210、review_session.rs L1407 / L617、prompt.rs L506 / L714
失败收场(最见功力)
| 故障 | 分支 | 动作 | 是否记入熔断器 |
|---|---|---|---|
| 超时(上限 90 秒) | TimedOut | 停住 | 否,清零连续拒绝计数 |
| 坏 JSON | FailedClosed | 停住 | 否,清零连续拒绝计数 |
| 明确拒绝 | Deny | 停住 + 写上禁止改道的指示 | 是 |
熔断器:Standard 下连续三次明确拒绝即打断当前 turn;窗口内累计达阈值同样触发。触发后不再开新审查,因为这一轮已被 abort。
把基础设施抖动与模型的真实意图分开计数,是这套设计里最容易被忽略、也最必要的一处。
| 项目 | 命令级策略 | 审批旋钮 | 记忆 |
|---|---|---|---|
| Codex | execpolicy 前缀 + 加载期正反例校验;bash -lc 拆内层 | 审批策略与沙箱种类绑在同一判定函数 | 会话层精确 key + 持久层前缀 |
| DeepSeek Harness | 不写命令级 pattern。权限预设把沙箱模式与审批策略捆成一包,默认表两行:workspace-write 配 ask,danger-full-access 配 never。审批策略只有 ask / never | 两颗旋钮独立,可单独把审批留 ask、沙箱拧到全盘可写 | 授权结果只有 allowed-once;无会话缓存、无前缀修正。问过一次,下次还问 |
| Claude Code | 规则字符串形如 Bash、Bash(npm install)、Bash(git *);按括号切开工具名与内容,shell 规则分精确 / 前缀 / 通配三类。没有加载期正反例校验 | 五档权限模式 + 内部 auto / bubble;规则来源含 userSettings / projectSettings / session / cliArg;判定先查整工具级 deny | alwaysAllowRules 可按来源记允许项,session 是其中一档;下次按规则匹配,不必完整 argv 相等,代价是匹配函数必须自己防包装 |
关键差异:git * 一旦写进 allow,git reset --hard 也会被通配吃掉(除非另写更具体的 deny)。Codex 用更短的精确前缀 + 反例把例外钉在加载期;Claude Code 把例外留给规则叠放顺序与运行时确认。
match 与 not_match,反例是否覆盖真实怕误伤的命令bash -lc 包装的情形;拆不开时是否整段回退max(而非被宽松规则覆盖)把禁止 git reset --hard 的 pattern 改短成 git + reset,not_match 仍写 --keep:
ExampleDidMatch 还是 ExampleDidNotMatch?ls -l 会不会先出 allow?审批留在 on-request,沙箱从 workspace-write 拧到 danger-full-access,再跑一条会写工作区外路径的命令:弹窗还在不在?为什么?
接着把 npm run test 按会话批准,再提交 npm run lint:缓存该不该命中?如果点的是按前缀记住,答案会不会变?
openai/codex,commit 4f39251a01,核对日期 2026-08-22。规则和例子写在同一处,加载时就地跑。正例必须命中,反例必须放过,打架就拒绝加载。前缀精确比较,多规则取最严。包装先拆开,拆不出来整段回退。审批看策略与沙箱两颗旋钮;问过之后的记忆分两层。Guardian 用一次锁死的会话接管审批,失败一律关闸,只有明确拒绝才记入熔断器。
来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为二次演绎的解读与音频稿件版本,音频与本页配套发布。