14 条命令 × 4 策略的真实录制:回放、对照、核对拒绝判定
本课要做的:用真实录制检验本章的三个结论——「沙箱挡写、挡不住读」「只放行工作区拦不住 .git」「拒绝判定只是启发式,会漏会误」;顺带确认页面上的判定函数和原实现一致。
需要的时间:约 30 分钟。全部在浏览器里回放,不联网,也不执行任何命令。
数据从哪来
- 录制 T9:本机(macOS 26.6.2)用
{M.t9.sandbox_exec}加载 Codex 真策略(codex@8cabf5a:codex-rs/sandboxing/src/seatbelt_base_policy.sbpl),把 14 条命令 × 4 种策略(none / 朴素只放行工作区 / Codex workspace-write / Codex read-only)逐条真跑,录下 stdout / stderr / 退出码与真实副作用。策略如何拼出见_meta.how_built;全程伪 HOME,真实~/.ssh从未被读写。 - 拒绝判定:
is_likely_sandbox_denied从{M.pins.codex}的sandboxing/src/denial.rs:13-69逐行移植成 JS,在下面对每条录制现场判定。 - 三方对照:JS 移植、本机
rustc编译的逐行 Rust 转写、项目miniagent/denial.py,三者在全部 64 条用例(56 条真录制 + 8 条构造边界)上判定一致。
实验台
全部为 T9 录制(2026-09-26 · macOS 26.6.2 · 伪 HOME,不碰真实 ~/.ssh);策略源 codex@8cabf5a:codex-rs/sandboxing/src/seatbelt_base_policy.sbpl。区域可写性由各策略 SBPL 文本推导并经录制结果核对;判定为 denial.rs 的 JS 移植现场算出。
任务
任务 1 · 找分歧:选 r03(写 .git/config),在 naive-workspace-only 与 codex-workspace-write 之间切。哪个拦下了、哪个没拦?对照地图上 .git 的颜色(可写 / 受保护),说清「只放行工作区」为什么不够。再看 r05(新建 .codex)是不是同样的分歧。
任务 2 · 挡写不挡读:选 r12(读伪 HOME 的假私钥)。把四种策略都切一遍——它在哪几种下被拦?想清楚:沙箱把什么收窄了、把什么留着了。
任务 3 · 漏报与误报:选 r11(curl 外网):它真被拦了吗?下方「is_likely_sandbox_denied」判成了什么?这属于 TP/TN/FP/FN 里的哪一类?再选 r14(ls 一个不存在的文件,输出里恰好有 sandbox 一词):它被拦了吗?判定又是什么?
任务 4 · 读自检:实验台底部的 confusion 汇总——6 个漏报都是什么命令?3 个误报是哪一条?为什么关键词启发式会在这两处出错?
把结论写进项目:在 04-实践项目/DECISIONS.md 记下 ① 你为 miniagent 选的沙箱层级与后端及理由(单工具 / 整进程;sandbox-exec / Docker / 不做);② 任务 2 的观察——沙箱不挡读,你打算怎么处理敏感文件(不放进可读范围 / 上 deny-read / 接受这个口子);③ 任务 3/4 里拒绝判定的漏报与误报,以及你决定「只作提示、原始 stderr 回灌模型」。这三条对应本章里程碑 M4 的验收材料(见章首页)。
读数时注意
- 朴素策略这里也断网:我们录制的「只放行工作区」策略里加了
(deny network*),所以curl(r10/r11)在朴素和 Codex 下都被拦——本实验的朴素-vs-Codex 分歧集中在工作区内的元数据(.git/.codex),不在网络。设计文档里「朴素策略允许 curl」是另一种更宽松的朴素写法,不是我们这一组录制。 - 拒绝判定是启发式,不是真值:地图里的「被拦 / 没拦」是真值(按副作用是否发生核对),「is_likely_sandbox_denied」是 Codex 的启发式判定;两者不一致的格子就是 FP/FN。真值靠录制时核对文件是否写出 / stdout 是否含预期内容得到,与退出码、stderr 无关。
- 只回放,不模拟 SBPL:页面不实现 SBPL 求值器(规则优先级语义未一手核实),只回放本机跑出的结果。想验证策略本身,去 pin 源码看
seatbelt.rs与seatbelt_base_policy.sbpl。 - 录制模型无关:这一组是命令级录制,不涉及模型;真实运行里模型发出的 bash 只有跑测试一种,本章的越界命令是构造的(标「构造 · 真算法」),退出码和 stderr 是真跑出来的。
- 上一章 只有进程内规则A3 权限规则、Pi 的确认弹窗解决了在执行前按「工具名 + 命令文字」裁决,拦住模型明着提出的危险调用。卡在规则看不见子进程实际做什么:不套沙箱时,
python -m unittest …; python -c "写 ~/.ssh"里权限层按前缀放行(rule_index 8),复合命令里的 python -c 把内容真的写进了伪 HOME(T9 r07)。 - 第一代 朴素 OS 沙箱:只放行工作区allow default 再挖掉写与网络解决了约束进程实际发出的系统调用(含全部子进程):写工作区外、联网被内核拒。14 条命令里拦下 6 条。卡在工作区里面不设防:同一条写
.git/config的命令,朴素策略下照样成功(T9 r03)——能借此改 remote、植入 git hook。 - Codex Seatbelt(seatbelt_base_policy.sbpl,122 行)解决了从
(deny default)起步,可写根用参数注入,.git/.codex/.agents用锚定正则单独保护,默认断网。workspace-write 下拦到 9 / 14 条。卡在沙箱挡写和联网,不挡读:读伪 HOME 里的假私钥,在四种策略下全部成功(T9 r12)——读到的东西还能经合法命令传出去。 - 第三代 分层:沙箱 + 审批 + 容器Claude Code(OS 沙箱 + 审批)、claude.ai(gVisor 容器)、Cowork(完整 VM)解决了把边界移出进程,按「爆炸半径」分档;规则管「允许调用什么」,隔离管「调用之后能碰到什么」。卡在私有数据 + 不可信内容 + 外发能力三者同在时,数据可经已放行的域名外传——规则与沙箱都放行(lethal trifecta,→ 岔路 B1)。
- 并行选择 干脆不做进程内沙箱Pi 核心、bypassPermissions解决了不做「半个沙箱」以免被误认为边界;坦白这是 YOLO,把真正的隔离交给容器 / 微虚拟机。卡在什么都不套时,T9 的 14 条越界命令全部发生(none 策略拦下 0 条);所以 Pi 说:要边界就上容器。
数字口径:全部由本机 sandbox-exec 加载 Codex 真策略录制(T9,2026-09-26 · macOS 26.6.2 · 伪 HOME,不碰真实 ~/.ssh);a04-sandbox-fixtures.json,2026-09-26 生成。
这一课的结论
同一组命令,换一个策略,边界就变:只放行工作区拦不住工作区里的 .git,Codex 把它单独保护;而无论哪种策略,读都不被拦。拒绝判定能认出大多数写被拒(关键词 Operation not permitted),却漏掉网络被拒(curl 报 exit 6/7、没有关键词),又会把输出里恰好带 sandbox 一词的普通失败误判成拒绝——所以它只能当提示,原始 stderr 必须原样回灌给模型。 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条项目实测 T9:14 命令 × 4 策略,伪 HOME,2026-09-26;启发式在 42 次沙箱运行里 TP20 / TN13 / FN6 / FP3。JS 移植、rustc 逐行转写、python 三者判定一致。 出处待补
下一章:谁拦、拦在哪、拦不住什么,到这里讲完了。边界这条线告一段落,换个维度——一个真实任务要跑很多轮,对话越堆越长,超过模型窗口怎么办?A5 讲上下文管理。