从0打造编码Agent 阅读 ≈ 30 分钟
A4 沙箱与隔离:规则拦不住一个自己写文件的 Python 脚本,谁拦? 第 3 课

沙箱、容器、干脆不做——各家怎么选,代价是什么?

各家怎么选
「有容器就安全」经不起逐项核对:Hermes 的 docker 后端只有 cap-drop 加三项 cap-add、no-new-privileges、两个 tmpfs,没有 seccomp、默认容器内 root、默认联网,用户参数还追加在最后能覆盖硬化;而且一旦选了容器后端,pin 上审批门整层跳过。五家在 OS 沙箱、容器、不做之间的选择差别极大,关键不是「有没有」,而是隔离责任交给了谁、边界画在哪、默认开不开。
❓

本课一问:Codex 把沙箱做到 122 行白名单,可别家未必这么做。沙箱、容器、干脆不做——五家怎么选,各自的代价是什么?

这一课先不碰:两道闸都开着数据还能从哪漏(岔路 B1);Pi 不做沙箱的完整论证(岔路 B2);具体怎么给自己的项目配沙箱(项目推进在章首页)。

从已有出发

前两课都在讲 Codex 的 OS 沙箱。但「用什么隔离」是一道设计选择题,不是只有一个答案。往回退一步问:一个 harness 可以把隔离责任放在哪?

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

先自己设计:「有容器就安全」

一个很自然的判断:把 agent 塞进 Docker,不就隔离了吗?有容器 = 安全。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案有容器就等于安全

「容器」这个词覆盖的范围很宽:从「只是换个文件系统视图」到「dropped 权限 + seccomp + 只读根 + 断网」。把两者都叫「容器」,就会把前者的安全感套到后者的场景上。

改进:五家并排,点开看出处

五家怎么选:隔离机制、默认是否开、覆盖面、责任在谁、自认的边界
containment 三档:同一个 Claude,爆炸半径按产品分档

三档出自 Anthropic《How we contain Claude across products》(2026-05-25,官方一手,需联网);圈内「是否与宿主共享」为文章表述。

🎯

试三件事:① 点 OpenCode:它的「机制」一栏——静态源码里没有 OS 沙箱(grep seatbelt/bwrap/landlock 零命中),用 cross-spawn 裸 spawn;② 点 Hermes 的「覆盖面」和「责任」两行,看硬化清单和「选容器后审批门跳过」;③ 点最下面 containment 三档同心圆的每一圈,看它与宿主共享什么。

Hermes 的硬化,逐行核对

先看它给每个容器加的安全参数——这是它的全部硬化:

docker.py hermes-agent@56dc01d9:tools/environments/docker.py:343-351需联网 MIT
343_BASE_SECURITY_ARGS = [
344 "--cap-drop", "ALL",
345 "--cap-add", "DAC_OVERRIDE",
346 "--cap-add", "CHOWN",
347 "--cap-add", "FOWNER",
348 "--security-opt", "no-new-privileges",
349 "--tmpfs", "/tmp:rw,nosuid,size=512m",
350 "--tmpfs", "/var/tmp:rw,noexec,nosuid,size=256m",
351]
👆 带下划线的行可以点:点哪行,讲哪行。

再看关键的一条:一旦选了隔离容器后端,危险命令的审批检查整层跳过:

approval.py hermes-agent@56dc01d9:tools/approval.py:3402-3413需联网 MIT
3402def _should_skip_container_guards(env_type: str, has_host_access: bool = False) -> bool:
3403 """Return True when the backend is isolated enough to skip dangerous-command prompts.
3404
3405 Isolated container backends sandbox the agent away from the host, so their
3406 commands can't damage real files/services and we skip the approval layer.
3407 Docker is the exception once host paths are bind-mounted into the container:
3408 at that point a command like ``rm -rf /workspace`` reaches host files, so it
3409 must go through the normal approval flow.
3410 """
3411 if env_type == "docker":
3412 return not has_host_access
3413 return env_type in ("singularity", "modal", "daytona", "vercel_sandbox")
👆 带下划线的行可以点:点哪行,讲哪行。
⚖️

这不是说 Hermes 不安全,也不是说它「没有容器隔离」。它有 7 种 terminal backend、docker 后端确有部分硬化;它的立场写在 SECURITY.md 里,而且很诚实:

唯一的安全边界,是操作系统。

—— 进程内的任何筛查(审批门、输出脱敏、模式扫描、工具白名单)都只是在一个被攻击者影响的字符串上做启发式,它自己就这么说。

演进地图

隔离设计的演进:规则拦不住的,交给一层比一层硬的边界
  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. 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 生成。

⚠
常见误解一:「Hermes 没有容器隔离。」

反过来说也不对。它有 7 种 backend、docker 后端做了 cap-drop ALL 等部分硬化——只是默认 local(不隔离)、硬化不含 seccomp、可被用户参数覆盖、且选容器后审批门跳过。准确的说法是「默认不开、开了也有边界但要逐项看」,不是「有」或「没有」两个字能概括的。

常见误解二:「OpenCode 官方建议用 Docker 作沙箱。」

未核实。官方文档只把 Docker 列为一种运行方式,TUI 里有一条「在容器里跑」的 tip,都不是安全建议。可以确证的只有:静态源码里没有 OS 沙箱实现,bash 工具描述自认以宿主用户的权限运行。

证据三问

问本课的回答
① 它解决什么问题?「用什么隔离」没有唯一答案;把选择讲清,才能判断一个 harness 的边界够不够
② 为什么不能只看「有没有容器」?「容器」覆盖的强度差别极大;真正决定安全的是硬化参数、默认值、可否覆盖、以及隔离与审批是否叠加
③ 各家怎么选?Claude Code / Codex 内置 OS 沙箱;OpenCode 静态源码无 OS 沙箱、交给用户;Pi 故意不做、推荐外部隔离;Hermes 默认 local、可选容器后端

一句话

📌
沙箱、容器、干脆不做,都是真实的设计选项;关键不是「有没有」,而是写清楚隔离责任在谁、边界画在哪、默认开不开。
🚦

先停在这里:记住「逐项核对硬化」和「隔离与审批可能互斥」两件事。
再深一层:两条岔路——两道闸都开着,数据还能从哪漏出去? 与 Pi 为什么宁可不做沙箱?

下一个缺口

边界设好了,任务一长,上下文装不下怎么办? → A5 上下文管理

我们已经把「谁拦、拦在哪、拦不住什么」讲完了:规则管提出的调用,沙箱管实际的系统调用,容器/VM 把边界移出进程。边界这条线到此告一段落。下一章换一个维度:一个真实任务要跑很多轮,对话越堆越长,超过模型窗口怎么办?

你可能会问