沙箱与隔离:规则拦不住一个自己写文件的 Python 脚本,谁拦?
主线定位
- 上游 · A3 权限与审批:harness 在每次调用执行前,按「工具名 + 命令文字」决定放行、拒绝还是问人(人或另一个模型)。第 4 课的压轴就留了一个洞:
python3 -c "…"骑着 allow 前缀过去,规则和审批看到的都只是一行命令文字。 - 本章 · A4 沙箱与隔离:命令真正执行时,由操作系统按进程实际发出的系统调用裁决——包括它派生的全部子进程。不管命令怎么写,写
~/.ssh、连外网这些系统调用越界了就被内核拒绝。 - 下游 · A5 上下文管理:边界设好了,任务一长,对话装不下怎么办?
两层为什么互补:规则在执行前、按模型提出的调用裁决;沙箱在运行中、按进程实际的行为裁决。规则能读懂「你想干什么」但拦不住改写过的写法;沙箱不管你怎么写,只认真实的系统调用,但它不知道你「本来想干什么」。缺一层都不够。 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方文档称权限规则与沙箱是互补的两层,深度调研报告 §9 三方核查 3-0 通过。 出处Claude Code 文档 · Sandboxing ↗需联网
本章问题链
每一课只回答一个问题;上一课的失败,逼出下一课的问题。
| 课 | 这一课的问题 | 朴素方案在哪个真数字上失败 | 一句话 |
|---|---|---|---|
| 第 1 课 | 规则说不许写 ~/.ssh,一个 python -c 脚本写了,谁拦? | 不套沙箱时,权限层按前缀放行(规则 #8),python -c 把内容真写进了伪 HOME(T9 r07 副作用发生);同一条命令在 Codex 沙箱下被拦 | 规则管模型提出的调用,沙箱管进程实际做的事 |
| 第 2 课 | 沙箱到底拦在哪、拦了什么? | 只放行工作区的朴素策略下,写工作区里的 .git/config 照样成功(T9 r03),能借此改 remote、植入 hook;Codex 策略把它单独保护 | 好沙箱是「默认全拒 + 一份被 bug 磨出来的白名单」,连工作区里的 .git 也要单独保护 |
| 第 3 课 | 沙箱、容器、干脆不做——各家怎么选,代价是什么? | 「有容器就安全」:Hermes docker 后端没有 seccomp、默认容器内 root、默认联网,用户参数还能覆盖硬化;选容器后审批门整层跳过 | 容器、沙箱、不做都是真实选项,关键是写清隔离责任在谁、边界在哪 |
| 岔路 | 两道闸都开着,数据还能从哪漏出去? | — | 断了写权限不等于断了泄露:私有数据 + 不可信内容 + 外发能力三者同在就危险 |
| 岔路 | Pi 为什么宁可不做沙箱? | — | 半个进程内沙箱会被误认为安全边界;要边界就上容器 |
| 实验课 | 14 条命令 × 4 策略的真实录制回放 | — | 沙箱挡写和联网、挡不住读;拒绝判定只是关键词 + 退出码启发式,会误报会漏报 |
演进地图
- 上一章 只有进程内规则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 生成。
贯穿任务:给 test_parse_date 的 trace 加一列「沙箱列」
全课的贯穿任务是让失败的 test_parse_date 通过。这一章给它的 trace 加上沙箱列:每条 bash 命令在系统调用层是否被拦下、退出码与 stderr、以及 Codex 的拒绝判定。
- 真实数据 T9:在本机(macOS 26.6.2)用
{M.t9.sandbox_exec}加载 Codex 的真策略(codex@8cabf5a:codex-rs/sandboxing/src/seatbelt_base_policy.sbpl),把 14 条命令 × 4 种策略逐条真跑,录下 stdout / stderr / 退出码与真实副作用。全程用临时伪 HOME,真实~/.ssh从未被读写(子进程 HOME=workspaces/t9/<policy>/fakehome;id_fake 是写死的假内容;真实 ~/.ssh 从未被读写)。第 1、2 课和实验课都直接用它。 - 本章换了例子:真实运行里模型发出的 bash 命令只有跑测试这一种(26 次运行、37 次 bash,全是
python -m unittest …),不够检验沙箱。所以 T9 里除了这条真实命令,还有一组构造的越界命令(写伪 HOME、写.git、curl、python -c起子进程),页面标「构造 · 真算法」——命令是构造的,退出码和 stderr 是真跑出来的。 - 沙箱只回放录制,浏览器里不执行任何命令:SBPL 的规则优先级语义我们没有一手核实,所以本章不在页面上做 SBPL 求值器,只回放本机跑出的结果。
三层交互
| 层 | 本章里的组件 | 在哪一课 |
|---|---|---|
| 概念动画 | 两道闸流水线(权限规则 → OS 沙箱 → 执行),A3 的绕过样例逐个过闸,先预测哪道闸拦下再揭示 | 第 1 课 |
| 算法模拟器(真算法、小数据) | Codex is_likely_sandbox_denied 的 JS 移植,对每条录制现场判定「是否被沙箱拒绝」,与真值比出真/假阳阴 | 第 2 课、实验课 |
| 真实数据 | 本机 sandbox-exec 加载 Codex 真策略录下的 14×4 结果:切换策略看边界怎么变、每条命令的真实 stderr 与退出码 | 第 2 课、实验课 |
沙箱拒绝判定在浏览器里现算,不联网:is_likely_sandbox_denied 从 pin 副本(8cabf5a 的 sandboxing/src/denial.rs:13-69,Apache-2.0)逐行移植成 JS;期望值取自项目 miniagent/denial.py,并用本机 rustc 编译的逐行 Rust 转写做第三方对照——三者在全部 64 条用例上判定一致(见实验课与交付报告)。
项目推进:给 miniagent 的 bash 工具外包一层 sandbox-exec
对应 04-实践项目/MILESTONES.md 的「A4 · 沙箱与隔离」。实现已经在 miniagent/sandbox.py 与 denial.py 里:bash 工具的命令先清洗环境变量(只留最小 PATH/HOME/LANG),再用 sandbox-exec 套一层策略;被拒的结果附上 is_likely_sandbox_denied 的判定回灌模型。代码 AI 来写,决策你来做。
- 沙箱放哪一层?单个工具(只包 bash)还是整个进程?项目选了只包 bash,代价是
read_file这类工具不受 OS 限制——先想清楚这个口子能不能接受。 - 用什么后端?
sandbox-exec(仅 macOS,且 Apple 已标 deprecated)、Docker、或像 Pi 那样不做但写出诚实的论证。项目用sandbox-exec,并在 DECISIONS.md 记下长期可用性风险。 - 默认联网吗?项目:不联网。想联网就得像 Codex 那样上代理 + 把连接归因到具体工具调用(第 3 课、岔路 B1)。
- 限制读吗?两种 Codex 策略都不限制读——T9 的 r12 在四种策略下都读出了伪 HOME 里的假私钥。沙箱挡住了写和联网,挡不住读后回传给模型。
验收标准(全部可测,对应设计卡 M4 ①-⑥):
- ① M3 记为已知局限的
python -c绕过(allow 前缀 + 复合命令写~/.ssh/config)被 OS 拦下,stderr 含Operation not permitted:测试CodexSeatbeltOnDarwin.test_python_c_bypass_writing_fake_home_is_blocked,对应 T9 r07。 - ② 写工作区外(伪 HOME)被拒(T9 r06/r08/r09);③ 工作区内
.git/config被拒(T9 r03/r04);④ 默认curl外连失败(T9 r10/r11)。 - ⑤ 被拒结果以 tool result 回灌模型,附
is_likely_sandbox_denied判定与命中关键词。 - ⑥ 真机测试只在 darwin 且 pin 存在时跑,其他平台显式 skip;启发式的分支对照测试(
DenialHeuristicParity)任何平台都跑。
坑与带走
| 症状 | 后果 | 怎么办 |
|---|---|---|
| 以为 deny 规则能拦住子进程 | python -c 照写不误 | 上 OS 级沙箱,约束进程实际发出的系统调用 |
| 只把工作区设为可写 | .git/config 仍可改,能植入 hook、改 remote | 受保护元数据(.git/.codex/.agents)单独 deny |
| 沙箱里默认联网 | 数据经合法命令外传 | 默认断网;要联网就上代理 + 连接归因 |
| 以为断了写权限就不会泄露 | 沙箱不挡读,读到的东西还能外传 | 明白沙箱挡写和网络、挡不住读(岔路 B1) |
| 把「有容器」当「安全」 | 可能默认 root、默认联网、无 seccomp、参数可覆盖 | 逐项核对硬化参数,别看营销页 |
| 把拒绝判定当精确结论 | 误报(输出里有 sandbox 一词)与漏报(网络被拒无关键词) | 只作提示,保留原始 stderr 给模型 |
依赖 sandbox-exec | Apple 已标 deprecated,长期可用性存疑 | 在 DECISIONS.md 记录风险与替代方案 |
带走这些句子(各课「一句话」):
- 规则管的是模型提出的调用,沙箱管的是进程实际做的事;
python -c的删除发生在子进程里,只有沙箱看得见。 - 好沙箱是「默认全拒 + 一份被真实 bug 一条条磨出来的白名单」,连工作区里的
.git都要单独保护;它挡写和网络,挡不住读。 - 沙箱、容器、干脆不做,都是真实的设计选项;关键不是「有没有」,而是写清楚隔离责任在谁、边界画在哪、默认开不开。
深入这一章
按你愿意花的时间选。每条都实际打开核对过(2026-09-26);外部链接都需要联网,离线包里只保留链接。
🎮 想更直观 10–30 分钟
🔬 想吃透原理 1–3 小时
📄 想读原文 按需
我替你排除了什么:「OpenCode 官方建议用 Docker 作沙箱」这类二手说法(未核实——官方文档只把 Docker 列为运行方式,TUI 里有一条容器运行 tip,都不是安全建议)· Hermes 营销页「五后端 + 容器硬化 + namespace 隔离」(deep-research 0-3 否决;源码直证是 7 种 backend、默认 local 不隔离、docker 部分硬化且可被参数覆盖)· 基于逆向的沙箱细节(cc-notebook 一类,未经官方核实)· 专讲沙箱实现机制的视频:本档暂缺——检索到的多为 VM/容器跑 YOLO 的泛讲(MIT L7 只到这一层),没有讲清 Seatbelt / bwrap 具体机制且经过验证的,故不收。