execpolicy:让策略文件自带测试用例
正反例写进规则本身,加载期就跑一遍;前缀精确匹配,多规则取最严
本页解决的问题
先给结论「execpolicy:让策略文件自带测试用例」要解决的关键问题是什么?
正反例写进规则本身,加载期就跑一遍;前缀精确匹配,多规则取最严
跟着交接处走,不要只看 Demo。 系统是否可靠,往往取决于模型、工具、状态、权限和人的交接处。把每次交接都当成可以观察、测试和恢复的地方。
为一个自动化步骤写清输入、负责人、审批和恢复动作。
一次运行成功了,却说不清发生了什么,也无法安全重放。
这一步看见的 argv
策略自带的测试用例
- 读入策略文本,开始 parseparser.rs L57
- Starlark 求值 prefix_rule,缺 decision 时默认 allowparser.rs L348
- 规则和正反例先挂到待校验队列parser.rs L405
- 先跑反例,命中就报 ExampleDidMatchparser.rs L145
- 再跑正例,没命中就报 ExampleDidNotMatchparser.rs L147
- 包装命令先拆成内层 argvexec_policy.rs L831
- 按第一个 token 精确取规则桶policy.rs L334
- 多规则或组合命令取最严policy.rs L403
- 判定映射成 Skip、NeedsApproval 或 Forbiddenexec_policy.rs L375
你给团队写一条禁令,pattern 写成 git 加 reset。本意是拦 --hard。过了一周有人报,--keep 也被拦了。前缀一短,后面跟什么都命中同一条。
再过一周,模型用 bash -lc 包了一层。禁令按字面 argv 去匹 bash,第一条对不上,命令进了启发式通道。
白名单每条都在赌两件事。一是 pattern 刚好覆盖你想拦的。二是它不会误伤你想放的。这两件事通常要等线上才知道。下一次改 pattern 的人,也看不到作者当初怕误伤哪条命令。
Codex 把正反例写进规则本身。match 里是这条规则必须命中的命令,not_match 里是它必须放过的命令。prefix_rule 并不当场跑例子,它先把规则和例子推进待校验队列。整份 Starlark 求值完,parse 再统一校验。
顺序固定。先跑反例,再跑正例。反例误命中,报 ExampleDidMatch。正例一个都没命中,报 ExampleDidNotMatch。两种错都让 parse 返回失败,Policy 不会被 build 出去。出处:codex-rs/execpolicy/src/parser.rs 第 57 至 78 行,以及第 133 至 151 行
crate README 把这件事写成一句话:它们是加载期校验的示例调用,可以当成单元测试。字符串和 token 数组两种写法进解析器后都会变成 token 列表,字符串走 shlex 拆词。校验时启发式回调传空,正例必须靠真实前缀规则命中,不能靠启发式凑数。
白名单的典型失败,是作者以为 pattern 是这个意思。把以为写成例子,机器可以反对。这条不依赖 Starlark,换成 JSON 或 YAML 也能抄。
规则和例子写在同一处,策略文件被拷进用户目录或被 overlay 下发时,例子跟着走。加载器没有只读规则、不跑例子的分支。漏写例子等于漏写测试,加载器不会替你编例子,写了就会强制执行。
正则能写 git reset 后面跟任意参数,也能写出作者自己都读不懂的例外。叠加规则时如果取最宽,用户层一条宽松的 allow 就能盖住系统层的 forbidden。
规则本体是一段前缀。匹配是精确字符串相等,没有 glob,没有正则。git reset --hard 能吃后面再跟 origin/main 的命令,因为多出来的 token 不参与比较。它吃不了中间插了 --config 的写法,因为第二个 token 对不上。出处:codex-rs/execpolicy/src/rule.rs 第 46 至 59 行
规则按第一个 token 放进桶。查找时先按 argv 第零项精确取。取不到,再考虑把绝对路径收成 basename。命中多条时,对 decision 做 max。Decision 派生了 Ord,变体书写顺序就是严重程度:Allow 小于 Prompt 小于 Forbidden。
组合命令先拆段再摊平,再取一次 max。一段 git status 是 Prompt,一段 git commit 是 Forbidden,整条管道仍是 Forbidden。出处:codex-rs/execpolicy/src/policy.rs 第 265 至 287 行,以及第 402 至 411 行
叠加只能加严,不能放宽。这个序写在枚举变体上,运行时没有另一张优先级表可以写错。换语言重写,三个字符串做成有序枚举,聚合用一次 max 就够。
前缀强迫你把意图落成 token 序列。代价是插在中间的参数会让规则失效,作者必须用更短的前缀或另写一条。正反例就是用来接住这条代价的:写短了,反例会在加载期先崩。
禁令写的是 git reset --hard。模型写成 bash -lc 包一层。按字面 argv 去匹,第一条是 bash,禁令不亮。
判定前先走 parse_shell_lc_plain_commands。它要求脚本只由纯词命令加 &&、||、分号、管道组成,拒绝重定向、替换、括号、控制流。过关后按命令节点切成多段 argv。bash -lc 包着 git reset --hard,会被拆成内层那三段,再拿去匹前缀。出处:codex-rs/core/src/exec_policy.rs 第 831 至 858 行
拆不出来时,整段 argv 当一条命令,交给启发式和后续沙箱。空引号插在参数里还能还原。空引号插在命令名里,word-only 解析失败,前缀规则看不见 git,这条命令掉进启发式。
解析器承认自己吃不下的东西,就不假装已经看懂脚本。这是 fail closed 的通用形状。它挡得住解析成功但漏掉危险段。拆失败之后,启发式和沙箱还在。
DeepSeek Harness:两个旋钮加一个下拉框
DSH 不写命令级 pattern。权限预设把沙箱模式和审批策略捆成一包。默认表只有两行:workspace-write 配 ask,danger-full-access 配 never。审批策略本身只有 ask 和 never。
旋钮好懂。你无法在预设里写出只禁 git reset --hard、其他 git 照常。那种例外要么进沙箱拒绝后的升级审批,要么进自定义旋钮组合,界面上显示为保留名 custom。下拉框覆盖日常切换,点名禁止的长尾它盖不住。
Claude Code:工具名加可选内容的 allowlist
规则字符串长成 Bash,或 Bash(npm install),或 Bash(git *)。解析器按括号切开工具名和内容。shell 规则再分成精确、前缀、通配三类。
检索 match、not_match、example 作为规则字段,没有加载期正反例校验。git * 一旦写进 allow,git reset --hard 也会被这条通配吃掉,除非另写一条更具体的 deny。Codex 用更短的精确前缀加反例,把例外钉在加载期。Claude Code 把例外留给规则叠放顺序和运行时确认。
前缀写短,加载还是判定
把禁止 git reset --hard 的 pattern 改短成 git 加 reset,not_match 仍写 --keep。加载时会走 ExampleDidMatch 还是 ExampleDidNotMatch,命令还有没有机会进入判定。
进阶一问:同一条坏规则下,ls -l 会不会先出 allow。演示里把策略切到「前缀写短」再播一遍,对一下你的推演。
「先玩一遍 · 一条命令过策略」的能力藏在每次交接里
「你给团队写一条禁令,pattern 写成 git 加 reset。本意是拦 --hard 。过了一周有人报, --keep 也被拦了。前缀一短,后面跟什么都命中同一条」说明,Agent 的表现不只由模型决定。模型、上下文、工具、状态、权限和人之间的每次交接,都会改变任务能否继续以及出了问题能否恢复。
先写清状态,再增加能力
从「再过一周,模型用 bash -lc 包了一层。禁令按字面 argv 去匹 bash ,第一条对不上,命令进了启发式通道」出发,可以把流程拆成起始状态、下一步动作、工具返回、状态更新和停止条件。这样调试时找的是第一处丢失信息或权限的位置,而不是笼统地说“模型变笨了”。
- 读入策略文本,开始 parse parser.rs L57
- Starlark 求值 prefix_rule,缺 decision 时默认 allow parser.rs L348
- 规则和正反例先挂到待校验队列 parser.rs L405
成功路径不能代表系统可靠
用「进阶一问:同一条坏规则下, ls -l 会不会先出 allow。演示里把策略切到「前缀写短」再播一遍,对一下你的推演」重放一次成功和一次失败,记录每一轮真正传入的上下文、工具结果和负责人;只要第二个人能复述这条链,系统才有可维护性。
从「先玩一遍 · 一条命令过策略」走到「思路一 · 规则和例子写在同一处」
「先玩一遍 · 一条命令过策略」先把问题落在「同一份策略,四条命令,三种写法 播放 单步 重置 命令 ls -l git reset --hard git reset --keep bash -lc 包装 安全的、带危险参数的、看起来像危险的、伪装成包装的。点一条再播。 策略 正确规则 前缀写短 反例写错 判定 1 拆解 等待 2 加载校验 等待 3 前缀匹配 等待 4 最严判定 等待 这一步看见的 argv 还没有切词。 策略自带的测试用例 正例必须命中 未跑…」上;到了「思路一 · 规则和例子写在同一处」,讨论继续推进到「你给团队写一条禁令,pattern 写成 git 加 reset。本意是拦 --hard 。过了一周有人报, --keep 也被拦了。前缀一短,后面跟什么都命中同一条」。两段连起来,重点就不只是记住一个结论,而是看清它成立所依赖的条件。
把这条判断带到下一个场景
分析 Agent 时,沿着状态、动作、工具结果和下一步的顺序走一遍;每次交接都要能说明信息从哪里来、由谁确认、失败时停在哪里。
- 「先玩一遍 · 一条命令过策略」:同一份策略,四条命令,三种写法 播放 单步 重置 命令 ls -l git reset --hard git reset --keep bash -lc 包装 安全的、带危险参数的、看起来像危险的、伪装成包装的。点一条再播。 策略 正确规则 前缀写短 反例写错 判定 1 拆解 等待 2 加载校验 等待 3 前缀匹配 等待 4 最严判定 等待 这一步看见的 argv 还没有切词。 策略自带的测试用例 正例必须命中 未跑…
- 「思路一 · 规则和例子写在同一处」:你给团队写一条禁令,pattern 写成 git 加 reset。本意是拦 --hard 。过了一周有人报, --keep 也被拦了。前缀一短,后面跟什么都命中同一条
- 「最后的要点」:再跑正例,没命中就报 ExampleDidNotMatch parser.rs L147
最后的「最后的要点」把讨论落到「再跑正例,没命中就报 ExampleDidNotMatch parser.rs L147」。回看这条线索时,最值得保留的是:当输入、规模或风险改变,哪些判断需要重新做一遍。
我把这篇文章里的一个判断改写成了今天可以验证的小实验。比记住结论更有用的是,知道下一步要观察什么。
读完以后我先回头找它成立的条件,而不是直接把方法搬进项目。这个顺序让后面的取舍清楚很多。
如果把这个判断放到真实工作里,最先需要补的约束是什么?我想知道从阅读到第一次实践之间,哪一步最值得先做。
还没有这篇文章的讨论。