学 AI 产品 · 专业 AI 产品经理播客第 3 章 · T3 解剖 OpenAI Codex:把安全观写进类型系统 · EP 12
第 3 章 · EP 12

execpolicy:让策略文件自带测试用例 · Guardian:让一个模型去审批另一个模型

时长 13:49音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 白名单在赌两件事1:29
1:29规则和例子写在同一处1:58
3:27前缀匹配 · 多规则取最严1:35
5:03先拆包装 · 拆不出来整段回退1:12
6:15审批 · 两颗旋钮绑在一起2:05
8:21问过之后 · 记住了什么1:42
10:03Guardian · 让一个模型审批另一个模型2:22
12:26可带走的原则1:22
解读全文

ep23 · execpolicy 自带测试 · 审批两颗旋钮 · Guardian 模型审模型

本集对应课程章节: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
最严聚合说明多规则与组合命令如何取 maxpolicy.rs L402–411
包装拆解说明 bash -lc 如何被拆回内层 argvexec_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

二、execpolicy · 规则和例子写在同一处

问题:每条白名单都在赌两件事 —— pattern 刚好覆盖想拦的,且不会误伤想放的。这两件事通常要等线上才知道。

做法

字段含义
match这条规则必须命中的命令(正例)
not_match这条规则必须放过的命令(反例)

时序:prefix_rule 不当场跑例子,先把规则与例子推进待校验队列;整份 Starlark 求值完,parse 再统一校验。

校验顺序固定

  1. 先跑反例 → 命中则报 ExampleDidMatch
  2. 再跑正例 → 一个都没命中则报 ExampleDidNotMatch
  3. 两种错都让 parse 返回失败,Policy 不会被 build 出去

出处:codex-rs/execpolicy/src/parser.rs L57–78、L133–151

crate README 的一句话:它们是加载期校验的示例调用,可以当成单元测试。字符串与 token 数组两种写法进解析器后都变成 token 列表(字符串走 shlex 拆词)。校验时启发式回调传空 —— 正例必须靠真实前缀规则命中,不能靠启发式凑数。

为什么长期成立:白名单的典型失败是「作者以为 pattern 是这个意思」。规则和例子写在同一处,策略文件被拷贝或下发时例子跟着走。漏写例子等于漏写测试,加载器不会替你编例子,写了就会强制执行。

提示1 · 写规则时先写反例

先写 not_match(你怕误伤哪条命令),再写 match。顺序反了很容易把反例写成正例的镜像,钉不住真实边界。


三、前缀匹配 · 多规则取最严

匹配语义:规则本体是一段前缀,匹配为精确字符串相等 —— 没有 glob,没有正则。

情形结果
git reset --hard 后跟 origin/main命中(多出的 token 不参与比较)
git --config X reset --hard不命中(第二个 token 对不上)

出处:execpolicy/src/rule.rs L46–59

查找与聚合

  • 规则按第一个 token 放进桶;先按 argv[0] 精确取,取不到再考虑把绝对路径收成 basename
  • 命中多条 → 对 decision 取 max
  • Decision 派生 Ord,变体书写顺序即严重程度:Allow < Prompt < Forbidden
  • 组合命令:先拆段再摊平,再取一次 max。git status(Prompt)+ git commit(Forbidden)→ 整条管道 Forbidden

出处:policy.rs L265–287、L402–411

性质:叠加只能加严,不能放宽。序写在枚举变体上,运行时没有另一张优先级表可以写错 —— 用户层的 allow 盖不住系统层的 forbidden。

代价:插在中间的参数会让规则失效,作者必须写更短的前缀或另写一条。正反例正是用来接住这个代价的 —— 写短了,反例会在加载期先崩。


四、先拆包装 · 拆不出来整段回退

判定前先走 parse_shell_lc_plain_commands:

  • 要求:脚本只由纯词命令 + && / || / ; / 管道组成
  • 拒绝:重定向、替换、括号、控制流
  • 过关后:按命令节点切成多段 argv。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 —— 十分钟前还会弹窗的命令,现在不弹了。你没动审批旋钮。

原因:两颗旋钮在同一个判定函数里。

旋钮取值
AskForApprovalNever / UnlessTrusted / OnRequest / Granular
文件系统沙箱种类默认 Restricted(只读或只写工作区);danger-full-access → Unrestricted

默认逻辑(default_exec_approval_requirement)

策略RestrictedUnrestricted
NeverSkipSkip
UnlessTrustedNeedsApprovalNeedsApproval
OnRequest / Granular要问Skip

出处:core/src/tools/sandboxing.rs L198–230、protocol.rs L924–947、permissions.rs L227–232

三个易错点

  1. Never 遇到策略要求提问 → 升级成拒绝,它不表示什么都准跑
  2. Granular 多一道闸:需要审批且 sandbox_approval 关掉 → Forbidden;但文件系统已 Unrestricted 时该分支进不去
  3. 加载期硬拒绝:requirements 不允许 danger-full-access 而配置写了 approval_policy = "never" → 加载器先把权限档案回落到只读,该组合判非法

出处:exec_policy.rs L214–236、config/mod.rs L3607–3625 / L3969–3979

提示2 · 改沙箱会静默带走审批行为

两颗旋钮绑定换来的是少弹窗,代价是改沙箱会静默改变审批行为。排查「为什么不再问我了」时,先看沙箱种类,而不是审批策略。


六、问过之后 · 记住了什么

两层记忆

层寿命key范围
会话缓存与会话同寿命规范化后的完整命令 + cwd + 环境 + execpolicy 指纹认完整命令,不认通配符
持久层跨会话前缀(写进 execpolicy)可扩张范围

关键细节

  • 所有 key 都必须是 ApprovedForSession 才命中;只批准一次(Approved)不进 map
  • npm run test 批准进会话,npm run lint 是另一把 key
  • /bin/bash -lc 与 bash -lc 在能拆出单一明文命令时,缓存成同一组 token
  • 改审批策略不清空这张表(key 里没有 AskForApproval);只有改 execpolicy 改了指纹,缓存才失效

出处:sandboxing.rs L64–116、approvals.rs L314–347

弹窗菜单:常规 exec 弹窗默认菜单里甚至没有 ApprovedForSession,只有带网络上下文时才带上。用户最常碰到的「记住」是前缀那一条。

说明书必须同步:权限说明是一段 developer 消息,先选沙箱模板再选审批模板。Never 那份只有一句 —— 不要再给 sandbox_permissions,命令会被拒。运行时与说明书是同一份合同的两面。


七、Guardian · 让一个模型审批另一个模型

触发条件:审批疲劳 —— 弹窗叠弹窗,人只看命令头两个词。防线还在,判断已经不在。

两个不能踩的坑:交给主模型 = 申请人兼审批人;失败时放行 = 超时与坏 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停住否,清零连续拒绝计数
坏 JSONFailedClosed停住否,清零连续拒绝计数
明确拒绝Deny停住 + 写上禁止改道的指示是

熔断器:Standard 下连续三次明确拒绝即打断当前 turn;窗口内累计达阈值同样触发。触发后不再开新审查,因为这一轮已被 abort。

把基础设施抖动与模型的真实意图分开计数,是这套设计里最容易被忽略、也最必要的一处。

八、横向对比

项目命令级策略审批旋钮记忆
Codexexecpolicy 前缀 + 加载期正反例校验;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;判定先查整工具级 denyalwaysAllowRules 可按来源记允许项,session 是其中一档;下次按规则匹配,不必完整 argv 相等,代价是匹配函数必须自己防包装

关键差异:git * 一旦写进 allow,git reset --hard 也会被通配吃掉(除非另写更具体的 deny)。Codex 用更短的精确前缀 + 反例把例外钉在加载期;Claude Code 把例外留给规则叠放顺序与运行时确认。


九、审查清单

  • 每条规则是否同时写了 match 与 not_match,反例是否覆盖真实怕误伤的命令
  • pattern 是否过短(用反例在加载期验证,而非等线上)
  • 是否考虑了模型用 bash -lc 包装的情形;拆不开时是否整段回退
  • 多规则 / 组合命令的聚合是否取 max(而非被宽松规则覆盖)
  • 审批行为变化是否先查沙箱种类(两颗旋钮耦合)
  • 会话缓存是否误以为能匹配通配符(不能)
  • 改策略文件时是否预期缓存失效(指纹变了才会)
  • 修改审批判定后,给模型的说明书是否同步修改
  • Guardian 超时 / 坏 JSON 是否停住动作,且未污染熔断器计数
  • 拒绝是否写上禁止改道的指示

提示3 · 课堂练习:前缀写短之后

把禁止 git reset --hard 的 pattern 改短成 git + reset,not_match 仍写 --keep:

  • 加载时走 ExampleDidMatch 还是 ExampleDidNotMatch?
  • 命令还有没有机会进入判定?
  • 进阶:同一条坏规则下,ls -l 会不会先出 allow?

提示4 · 课堂练习:拧沙箱,弹窗还在吗

审批留在 on-request,沙箱从 workspace-write 拧到 danger-full-access,再跑一条会写工作区外路径的命令:弹窗还在不在?为什么?

接着把 npm run test 按会话批准,再提交 npm run lint:缓存该不该命中?如果点的是按前缀记住,答案会不会变?


十、约束说明

  1. 源码口径:依据本地仓库 openai/codex,commit 4f39251a01,核对日期 2026-08-22。
  2. 教学化内容:演示用命令、故障注入与策略切换为课程化设定,用于展示判定结构;逻辑轨迹行号对应真实源码。
  3. 横向对比口径:DSH 依据审批与权限源码(2026-08-22);Claude Code 依据 restored-src 权限类型与判定源码(2026-08-22)。
  4. Guardian 边界:只接管 on-request 类审批,不替代沙箱与网络代理;默认审查员仍是用户。

规则和例子写在同一处,加载时就地跑。正例必须命中,反例必须放过,打架就拒绝加载。前缀精确比较,多规则取最严。包装先拆开,拆不出来整段回退。审批看策略与沙箱两颗旋钮;问过之后的记忆分两层。Guardian 用一次锁死的会话接管审批,失败一律关闸,只有明确拒绝才记入熔断器。


来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本页为二次演绎的解读与音频稿件版本,音频与本页配套发布。