从0打造编码Agent 实验 实验 ≈ 30 分钟 页内实验 ×1
A4 沙箱与隔离:规则拦不住一个自己写文件的 Python 脚本,谁拦? 实验课

14 条命令 × 4 策略的真实录制:回放、对照、核对拒绝判定

沙箱录制回放
把本机 sandbox-exec 加载 Codex 真策略录下的 14 条命令 × 4 策略全量摊开,自己找出:朴素策略和 Codex 策略在 .git/config 上的分歧、读在所有策略下都不被拦、以及拒绝判定漏报和误报的那几格。全部在浏览器里回放,不联网、不执行任何命令。
🧪

本课要做的:用真实录制检验本章的三个结论——「沙箱挡写、挡不住读」「只放行工作区拦不住 .git」「拒绝判定只是启发式,会漏会误」;顺带确认页面上的判定函数和原实现一致。

需要的时间:约 30 分钟。全部在浏览器里回放,不联网,也不执行任何命令。

数据从哪来

从已有出发 · 累计数据流 点已学环节看它回答什么
每一轮循环都经过

实验台

进程能碰到什么 —— 切换策略看边界怎么变

全部为 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 的验收材料(见章首页)。

读数时注意

隔离设计的演进:规则拦不住的,交给一层比一层硬的边界
  1. 上一章 只有进程内规则
    A3 权限规则、Pi 的确认弹窗
    解决了在执行前按「工具名 + 命令文字」裁决,拦住模型明着提出的危险调用。
    卡在规则看不见子进程实际做什么:不套沙箱时,python -m unittest …; python -c "写 ~/.ssh" 里权限层按前缀放行(rule_index 8),复合命令里的 python -c 把内容真的写进了伪 HOME(T9 r07)。
  2. allow default 再挖掉写与网络
    解决了约束进程实际发出的系统调用(含全部子进程):写工作区外、联网被内核拒。14 条命令里拦下 6 条。
    卡在工作区里面不设防:同一条写 .git/config 的命令,朴素策略下照样成功(T9 r03)——能借此改 remote、植入 git hook。
  3. 第二代 deny default + 白名单 你在这里
    Codex Seatbelt(seatbelt_base_policy.sbpl,122 行)
    解决了从 (deny default) 起步,可写根用参数注入,.git / .codex / .agents 用锚定正则单独保护,默认断网。workspace-write 下拦到 9 / 14 条。
    卡在沙箱挡写和联网,不挡读:读伪 HOME 里的假私钥,在四种策略下全部成功(T9 r12)——读到的东西还能经合法命令传出去。
  4. Claude Code(OS 沙箱 + 审批)、claude.ai(gVisor 容器)、Cowork(完整 VM)
    解决了把边界移出进程,按「爆炸半径」分档;规则管「允许调用什么」,隔离管「调用之后能碰到什么」。
    卡在私有数据 + 不可信内容 + 外发能力三者同在时,数据可经已放行的域名外传——规则与沙箱都放行(lethal trifecta,→ 岔路 B1)。
  5. 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 必须原样回灌给模型。

下一章:谁拦、拦在哪、拦不住什么,到这里讲完了。边界这条线告一段落,换个维度——一个真实任务要跑很多轮,对话越堆越长,超过模型窗口怎么办?A5 讲上下文管理。