# 20. 沙箱机制
# 20.1 威胁模型
Agent 执行的命令可能来自模型幻觉、Prompt Injection、恶意仓库内容、被污染的 MCP 结果或供应链脚本。沙箱要假设命令本身不可信,至少限制:
- 文件读:密钥、浏览器 Cookie、其他项目和用户数据。
- 文件写:工作区外文件、配置、Git Hook、Agent/Skill 自动加载目录。
- 网络:数据外传、下载执行、访问云元数据服务和内网。
- 进程:提权、ptrace、危险 syscall、后台残留和资源耗尽。
- 环境:敏感环境变量、凭证代理、Unix Socket 和设备。
容器不是天然安全边界;只改 cwd 也不是沙箱。必须由内核机制、受限 Token、文件系统视图或独立执行环境强制实施。
# 20.2 Policy 与 Enforcement 分离
Policy 描述“允许什么”,Enforcement Backend 负责“如何阻止其他行为”。一个跨平台实现通常将统一 Profile 编译为不同后端:
| 平台 | 常见机制 | 重点 |
|---|---|---|
| macOS | Seatbelt sandbox-exec profile | 文件、网络、进程规则 |
| Linux | bubblewrap mount namespace + seccomp/no_new_privs | 只读根、可写挂载、网络 namespace、syscall |
| Windows | Restricted Token、ACL、Job/Object、WFP | 身份降权、目录 ACL、进程与网络隔离 |
| 远程 | VM/container/microVM + egress proxy | 强租户隔离、镜像与资源配额 |
沙箱 Profile 应默认拒绝,按任务开放最小读写根和网络目标;对 .git/.codex/.agents/skills 等能改变后续执行逻辑的元数据路径应保留只读或禁止创建。
# 20.3 执行生命周期
model call // 模型只提出待执行的工具或命令意图,不能直接触达操作系统
→ normalize command/path/env // 规范化命令、路径和环境变量,使策略与执行看到一致参数
→ authorization + approval // 根据主体、资源和风险做授权,必要时请求具体范围的用户审批
→ compile effective permission profile // 将基础策略、批准增量和强制 Deny 编译成单次有效权限 Profile
→ select platform backend // 按操作系统和 Profile 选择 Seatbelt、bubblewrap 或受限 Token 等后端
→ construct isolated filesystem/network/process view // 构造最小文件系统、网络和进程视图,从内核层阻断未授权访问
→ spawn with timeout/resource limits // 在隔离环境启动子进程,并施加时间、CPU、内存和进程数限制
→ capture bounded stdout/stderr/exit status // 有界采集输出和退出码,避免无限输出耗尽内存或上下文窗口
→ classify violation // 综合退出码、系统事件和错误特征判断是否发生沙箱违规
→ return structured result or request narrow escalation // 正常返回结构化结果;确属权限不足时仅申请最小增量权限
→ cleanup process tree/temp mounts/proxy // 无论成功失败都清理子进程树、临时挂载和网络代理,避免残留
“沙箱失败后自动去沙箱重试”非常危险。正确条件是:确实检测到沙箱拒绝、策略允许询问、用户看到具体扩大范围并批准、命令参数未变化、且提升后仍保留不能丢失的 Deny 约束。
# 20.4 Claude Code 源码映射
Claude Code 本地代码通过 adapter 包装 @anthropic-ai/sandbox-runtime,因此源码快照能看到配置转换与产品集成,底层 OS 实现主要在外部包:sandbox-adapter.ts:1、sandbox-adapter.ts:7。
adapter 把 WebFetch 与权限规则转换为网络域名 Allow/Deny,把 Edit/Read 与 sandbox settings 转换为文件系统路径规则;默认允许当前目录和 Claude 临时目录写入,同时禁止修改 Settings、managed settings 目录和 .claude/skills:sandbox-adapter.ts:172、sandbox-adapter.ts:222、sandbox-adapter.ts:230、sandbox-adapter.ts:247。
它还专门防御“伪造裸 Git 仓库 + core.fsmonitor”的逃逸风险,并把 --add-dir/Session 额外目录同步到 Bash 沙箱的 AllowWrite,而不是只在文件工具层放行:sandbox-adapter.ts:257、sandbox-adapter.ts:290。这体现了应用权限和 OS enforcement 必须使用一致资源视图。
# 20.5 Codex 源码映射
Codex 的旧统一 SandboxPolicy 包含 read-only/workspace-write/danger-full-access/external-sandbox;WorkspaceWrite 会生成可写根,同时在根内保留只读子路径与受保护元数据名:protocol.rs:1000、protocol.rs:1027、protocol.rs:1054。新 PermissionProfile 进一步把文件系统、网络和 enforcement ownership 分开,见第 19 章。
SandboxManager 根据权限 Profile、工具偏好、网络要求和平台选择 None/MacosSeatbelt/LinuxSeccomp/WindowsRestrictedToken,再把统一请求编译成平台命令:manager.rs:34、manager.rs:60、manager.rs:272、manager.rs:310。
Linux 当前组合是:bubblewrap 构造文件系统视图,进程内安装 no_new_privs + seccomp;Landlock 文件系统路径保留为 legacy/backup。bubblewrap 默认根只读,再叠加可写根,并把 .git/.agents/.codex 等敏感子路径保持只读:linux-sandbox/lib.rs:1、bwrap.rs:1、bwrap.rs:227。网络模式支持完整、隔离与仅代理三种视图:bwrap.rs:85。
执行后 Codex 会用退出码与典型错误文本保守判断是否为沙箱拒绝,并记录文件系统或网络违规;代码也明确承认仅靠输出无法做到完全确定:denial.rs:5、violation.rs:133、violation.rs:185。
# 20.6 沙箱执行伪代码
function sandboxedExec(request, baseProfile): // 定义一次命令从授权到隔离执行和最小范围升级的完整入口
normalized = normalize(request) // 固化命令、工作目录、路径和环境的规范形式,消除匹配歧义
decision = authorize(normalized) // 对规范化请求执行策略判断,获得允许范围或拒绝原因
if decision.denied: return deniedResult(decision) // 被拒绝时返回结构化结果,绝不启动任何子进程
granted = maybeApprove(decision.requestedAdditionalPermissions) // 对策略要求的附加权限发起审批,并返回实际获准的最小范围
effective = compileProfile(baseProfile, granted) // 合并基础 Profile 与批准增量,同时保留不可覆盖的 Deny 约束
backend = selectBackend(os, effective, request.sandboxPreference) // 依据平台、权限需求和工具偏好选择可实施该策略的沙箱后端
attempt = backend.spawn( // 通过选定后端创建受限进程,返回可监控的执行句柄
command=normalized.command, // 执行审批时看到的同一规范化命令,禁止审批后替换
cwd=normalized.cwd, // 将进程限制在已解析的工作目录,并由 Profile 控制可见路径
env=filterEnv(normalized.env), // 移除密钥和危险注入变量,只传递工具运行所需环境
profile=effective, // 把本次有效文件、网络和进程规则交给 OS 后端强制实施
limits={cpu, memory, pids, wallTime, outputBytes}) // 同时限制资源和输出规模,防止死循环、进程炸弹与内存耗尽
result = captureAndCleanup(attempt) // 等待执行、采集有界结果,并确保退出后清理整个隔离资源
violation = classifySandboxViolation(result) // 根据结构化系统信号和保守错误特征识别沙箱拒绝类型
audit(result, violation, effective.hash) // 记录脱敏结果、违规分类和策略哈希,支持追责与规则调优
if violation && canRequestNarrowEscalation(violation, effective): // 仅在确认受沙箱阻断且策略允许询问时考虑提升
return approvalRequired(minimalDelta(violation, effective)) // 计算解决该违规所需最小权限差异,并交由用户明确批准
return result // 正常成功或不可提升的失败都原样返回,禁止自动无沙箱重试
# 20.7 高频面试题
问:为什么 workspace-write 仍需保护 .git 和 Agent 配置目录?
这些文件可以改变后续可信执行:Git Hook、配置中的外部命令、自动发现的 Agent/Skill 都可能把一次普通写权限变成持续代码执行。它们属于控制面元数据,风险高于普通源码文件。
问:如何验证沙箱真的有效?
建立跨平台 conformance suite:允许的读写必须成功,越界读写、网络、symlink、缺失路径创建、进程逃逸必须失败;同时测试审批后是否只开放获准范围,以及取消清理、子进程继承和升级回归。只做单元测试 Policy 编译不够,还要在真实内核后端跑攻击用例。
# 20.8 原理深化:沙箱是 Profile 编译器与 OS Enforcement 的组合
统一 Permission Profile 是中间表示,平台后端是编译目标。编译器要把 read/write roots、只读子路径、网络模式、环境清理和进程限制转换成 Seatbelt、bubblewrap/seccomp、Restricted Token/WFP 或远程隔离配置;编译后还应验证后端是否能完整表达策略。若某平台不能保留 denied-read 或域名级网络限制,应拒绝执行或选择更强后端,不能静默降级为“基本隔离”。
Codex SandboxManager 按 Profile 与平台选择后端,Linux 先构造只读根,再叠加写目录并保护 .git/.agents/.codex,网络视图也独立选择完整、隔离或仅代理。这说明“workspace-write”并非给工作区整棵树递归写权限,而是一组有例外的挂载规则;安全元数据属于控制面,必须比普通源码更严格。manager.rs:272、bwrap.rs:85、bwrap.rs:227。
运行时还要把取消和清理纳入安全边界:超时后终止整个进程树,撤销临时挂载与代理,限制 stdout/stderr、CPU、内存和进程数。沙箱拒绝的检测可能来自内核事件、退出码和错误文本,源码也承认文本分类不完全可靠,因此自动升级必须保守:只有策略允许、违规范围可精确描述、用户批准且原命令 hash 未变时,才以最小增量重新编译 Profile;不能把任意非零退出码当成“沙箱挡住了”并去沙箱重试。