13 · 沙箱到底要隔离什么
按文件、网络、系统调用和提权风险选择隔离层,并知道何时交给容器
本章任务
要回答的问题
文件、网络、系统调用和身份中,哪些边界必须由沙箱强制,而不是靠 Agent 自律?
读完你能
- 按资产和攻击面选择文件、网络、进程与系统调用隔离
- 区分策略检查、运行时沙箱和容器边界
- 设计逃逸、挂载、网络出口和资源耗尽测试
- 适合现在读
- 正在选择容器、Seatbelt、Landlock、Seccomp 或远端执行环境的工程师
- 先修知识
- 理解操作系统权限与 Shell 执行风险
- 实践产物
- 一个按任务分级的 Sandbox Profile 与逃逸测试计划
- 证据边界
- 沙箱不是完整安全系统;内核、容器配置、密钥和外部服务仍需单独建模
沙箱限制的是副作用
Section titled “沙箱限制的是副作用”场景:任务声称只读源码,但 Agent 执行 npm install,依赖的 postinstall 脚本读取 ~/.ssh 并访问公网;或者通过工作区软链接写到允许目录之外。仅限制“工具名”或项目目录都无法覆盖真实副作用。
通过标准:文件读写按真实解析路径限制,默认无网络和无无关凭据;子进程、系统调用、CPU、内存和磁盘受限;安装与执行使用不同 Profile;所有拒绝能映射到具体策略。
先列出要隔离的副作用
Section titled “先列出要隔离的副作用”四家在沙箱 5 件事上的覆盖:
| 维度 | Codex | Claude Code | OpenClaw | Hermes |
|---|---|---|---|---|
| 沙箱基础设施 | Linux:bubblewrap 加 seccomp 加 landlock。macOS:seatbelt。Windows:独立 crate | macOS:seatbelt。Linux:新增(NVIDIA enterprise rollout 引入)。可选 `enabledPlatforms` 限制只在某平台启用 | `ExecHost` 3 backend:sandbox、gateway、node。sandbox 走外部容器 | 6 后端:local、docker、singularity、modal、daytona、ssh。TERMINAL_ENV 切换 |
| 文件系统隔离 | `PermissionProfile.file_system`:writable_roots、read_only、full。bubblewrap 负责 enforce | `SandboxFilesystemConfig`:allowWrite、denyWrite、denyRead、allowRead、allowManagedReadPathsOnly | 由 backend 进程决定(sandbox host 的实际隔离取决于部署) | 容器通常提供边界,但挂载会改变结果;可选 `TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE` 映射 cwd 到 /workspace |
| 网络隔离 | seccomp filter 拦截 connect()、sendto()。可走 managed proxy 例外 | `SandboxNetworkConfig`:allowedDomains、allowUnixSockets、httpProxyPort、socksProxyPort。macOS only allowUnixSockets | 由 backend、network policy 决定 | 容器 network mode 控制。ssh 走真实远程网络 |
| 权限模型 | `PR_SET_NO_NEW_PRIVS` 加 seccomp BPF。apply 到当前线程,子进程继承 | allowedDomains 跟 permission rules 合并。managed-only 模式忽略用户层规则 | 逐 binary `SafeBinProfile`(Shell 执行章详解)加 ExecHost 隔离 | 靠后端隔离(container、VM、SSH),无应用层 seccomp、landlock |
| 失败处理 | 沙箱失败走 SandboxErr 错误码。上层可决定 fail_open、fail_closed | `failIfUnavailable: true` 走启动 fail。false 走退化为无沙箱加警告 | backend spawn 失败走 approval 流程兜底 | backend 不可用走提示用户切换 TERMINAL_ENV |
源码证据:内核、配置与宿主
Section titled “源码证据:内核、配置与宿主”Codex · 在 Linux 上把三种独立的内核隔离能力按各自最擅长的事拼起来
Section titled “Codex · 在 Linux 上把三种独立的内核隔离能力按各自最擅长的事拼起来”本机隔离要同时处理文件、网络和 syscall,单一开关很难覆盖它们。Codex 在 Linux 组合 bubblewrap、seccomp、landlock,并分别维护 macOS 与 Windows 路径;这是控制力与维护面的交换。
在 Linux 上,Codex 把三种独立的内核能力按各自最擅长的事拼了起来:
Codex codex/codex-rs/linux-sandbox/src/landlock.rs:1-70 Linux 沙箱:bubblewrap 做 FS,seccomp 拦网络,landlock 作备用
//! In-process Linux sandbox primitives: `no_new_privs` and seccomp.//!//! Filesystem restrictions are enforced by bubblewrap in `linux_run_main`.//! Landlock helpers remain available here as legacy/backup utilities.
/// Apply sandbox policies inside this thread so only the child inherits/// them, not the entire CLI process.////// This function is responsible for:/// - enabling `PR_SET_NO_NEW_PRIVS` when restrictions apply, and/// - installing the network seccomp filter when network access is disabled.////// Filesystem restrictions are intentionally handled by bubblewrap.pub(crate) fn apply_permission_profile_to_current_thread( permission_profile: &PermissionProfile, cwd: &Path, apply_landlock_fs: bool, allow_network_for_proxy: bool, proxy_routed_network: bool,) -> Result<()> { let (file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); let network_seccomp_mode = network_seccomp_mode( network_sandbox_policy, allow_network_for_proxy, proxy_routed_network, );
// `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents // setuid privilege elevation. Many `bwrap` deployments rely on setuid, so // we avoid this unless we need seccomp or we are explicitly using the // legacy Landlock filesystem pipeline. if network_seccomp_mode.is_some() || (apply_landlock_fs && !file_system_sandbox_policy.has_full_disk_write_access()) { set_no_new_privs()?; }
if let Some(mode) = network_seccomp_mode { install_network_seccomp_filter_on_current_thread(mode)?; } // ...三件套各自扮演的角色:
bubblewrap(缩写 bwrap) 是用户空间的轻量级沙箱工具,主要负责文件系统挂载隔离。它能把宿主机的某个目录以只读方式挂进沙箱、把另一个目录以可写方式挂进沙箱、把临时目录挂成 tmpfs(只在内存里、退出就消失)。这是 Codex Linux 路径的核心组件,但实际覆盖面仍取决于命名空间、权限和上层 syscall/network 策略;它并非完全脱离内核能力的普通二进制。
seccomp 是 Linux 内核的一项基础设施,能让用户层注册一段 BPF 字节码作为「系统调用过滤器」:每次进程要发起系统调用时,内核先跑这段 BPF 判断「这个调用允不允许」。Codex 用它做一件聚焦的事:在线程级别拦截网络相关的系统调用(connect()、sendto()),让进程根本无法发起任何网络连接。注意是「线程级别」:这件事很关键,意味着 Codex 的主进程本身仍能上网(用来发 OpenAI 请求),但 spawn 出来跑用户工具的子进程继承隔离规则之后连不出去。
landlock 是 Linux 5.13+ 引入的内核 LSM(Linux Security Module),同样做文件系统访问控制,跟 bubblewrap 比是更新的内核机制。Codex 把它当 legacy/backup 用,主路径还是 bubblewrap,原因主要是 bubblewrap 在老内核也能跑,兼容性更广。
代码注释里有一段关于 trade-off 的决定特别值得看:PR_SET_NO_NEW_PRIVS 这个 prctl 调用是启用 seccomp 的硬性前提(内核要求「如果你要装系统调用过滤器,需要先承诺这个进程及其子进程永远不能通过 setuid 获得新特权」),但这个承诺同时也会让 setuid 提权失效:而很多 bubblewrap 的部署方式正好依赖 setuid 来让普通用户能创建命名空间。
Codex 的处理是:只在需要 seccomp 或者需要 landlock 文件系统隔离时才设这个 flag,其他情况下不设,让 bwrap 那边的 setuid 路径能用。这是一段经典的「安全性对比兼容性」的工程平衡,做得很克制。
跨平台分工方面:Linux 走 linux-sandbox/ crate 加 bubblewrap 二进制;macOS 用 sandbox-exec(业界叫”seatbelt”)配 .sb 策略文件,这是苹果系统自带的沙箱机制;Windows 走一个独立的 windows-sandbox-rs/ crate,搭配 setuid 用户管理。
Claude Code · 把沙箱做成可以被 IT 管理员精确配置的 JSON schema
Section titled “Claude Code · 把沙箱做成可以被 IT 管理员精确配置的 JSON schema”企业部署更关心配置是否能被管理员审阅。Claude Code 用 network/filesystem schema 暴露边界,也把较弱网络隔离作为显式选项;配置本身不等于隔离效果。
Claude Code claude-code/src/entrypoints/sandboxTypes.ts:90-145 SandboxSettings 顶层:enabled + failIfUnavailable + 平台限制 + nested/network 退化档
export const SandboxSettingsSchema = lazySchema(() => z .object({ enabled: z.boolean().optional(), failIfUnavailable: z .boolean() .optional() .describe( 'Exit with an error at startup if sandbox.enabled is true but the sandbox cannot start ' + '(missing dependencies, unsupported platform, or platform not in enabledPlatforms). ' + 'When false (default), a warning is shown and commands run unsandboxed. ' + 'Intended for managed-settings deployments that require sandboxing as a hard gate.', ), // Note: enabledPlatforms is an undocumented setting read via .passthrough() // Added to unblock NVIDIA enterprise rollout: they want to enable // autoAllowBashIfSandboxed but only on macOS initially, since Linux/WSL // sandbox support is newer and less battle-tested. autoAllowBashIfSandboxed: z.boolean().optional(), allowUnsandboxedCommands: z .boolean() .optional() .describe( 'Allow commands to run outside the sandbox via the dangerouslyDisableSandbox parameter. ' + 'When false, the dangerouslyDisableSandbox parameter is completely ignored and all commands must run sandboxed. ' + 'Default: true.', ), network: SandboxNetworkConfigSchema(), filesystem: SandboxFilesystemConfigSchema(), ignoreViolations: z.record(z.string(), z.array(z.string())).optional(), enableWeakerNestedSandbox: z.boolean().optional(), enableWeakerNetworkIsolation: z .boolean() .optional() .describe( 'macOS only: Allow access to com.apple.trustd.agent in the sandbox. ' + 'Needed for Go-based CLI tools (gh, gcloud, terraform, etc.) to verify TLS certificates ' + 'when using httpProxyPort with a MITM proxy and custom CA. ' + '**Reduces security** — opens a potential data exfiltration vector through the trustd service. Default: false', ), // ... }) .passthrough(),)这段 schema 定义里有几段注释是整个章节的精华,值得拆开讲:
第一处值得拆开讲的是 failIfUnavailable 的 trade-off 处理:它问的是一个很实际的问题:当用户配置说「启用沙箱」但沙箱启动失败(缺少依赖、平台不支持、平台不在 enabledPlatforms 白名单里),系统应该怎么办?两种选择:直接报错退出,强制管理员去解决;或者降级到无沙箱模式加警告,让用户能继续工作。这两种选择没有绝对正确:把沙箱当辅助保护的本地开发流程可以选择降级,把沙箱写进安全策略、作为硬门槛的托管部署则应拒绝无沙箱运行。Claude Code 把这个决定权交给配置,注释里写得很清楚「Intended for managed-settings deployments that require sandboxing as a hard gate」。
第二处是 enabledPlatforms:这个配置项的存在背景是真实企业部署的故事。Claude Code 的注释里直接写了「Added to unblock NVIDIA enterprise rollout: they want to enable autoAllowBashIfSandboxed but only on macOS initially, since Linux/WSL sandbox support is newer and less battle-tested」。这是少见的工程透明度:一个 undocumented 的配置项,连「为什么存在」都直接在代码注释里写了出来。背后讲的是一个很真实的企业部署问题:NVIDIA 想在 macOS 上启用一个「沙箱内自动放行 Bash」的功能(因为沙箱够强,可以信任),但 Linux 上的沙箱代码比较新还在 battle-test,他们暂时不敢在 Linux 上启用。enabledPlatforms 让管理员能精确说「这个功能只在 macOS 上启用,Linux、WSL 不要」。这种「undocumented setting」是用 zod 的 .passthrough() 机制兼容到 schema 里的,schema 不验证它但也不剔除它。
第三处是 enableWeakerNetworkIsolation,一个明确标注「Reduces security」的开关:它存在的背景是另一个具体的工具链问题:Go 写的命令行工具(gh、gcloud、terraform 等)通过 httpProxyPort 走 MITM 代理加自签 CA 时,需要访问 macOS 的 com.apple.trustd.agent 服务来验证 TLS 证书;但默认的沙箱配置不允许这个访问。打开 enableWeakerNetworkIsolation 会放开这个访问,但代价是 trustd 服务理论上能被用作数据泄漏渠道。Claude Code 的处理负责任:把这个开关做出来给用户用,但在注释里直接用粗体写明「Reduces security, opens a potential data exfiltration vector」,让管理员知道这是带代价的便利开关。
network 和 filesystem 各自有独立的 schema 子集,因为网络隔离和文件系统隔离本来就是两件独立的事。network 维度的配置项有:允许访问的域名列表、是否只信管理面下发的域名、是否允许 Unix socket、是否允许本地 binding、HTTP/SOCKS 代理端口;filesystem 维度的配置项有:允许写的路径、显式拒绝写的路径、显式拒绝读的路径、允许读的路径、是否只信管理面下发的读路径。
特别值得讲的是两个带 Managed 前缀的配置,allowManagedDomainsOnly 和 allowManagedReadPathsOnly。它们的语义是:“开了这个之后忽略所有用户层配置,只信 policySettings”。这是企业 IT 管理员的”我说了算”心态:管理员开了之后,无论用户在 userSettings、projectSettings、localSettings、cliArg 里怎么放行,都不生效;
只有管理员在 policySettings 里下发的规则才算数。这种”管理面凌驾用户面”的设计是企业部署的基本盘。
OpenClaw · 把沙箱看成”选哪个后端的问题”而不是”自己实现哪些隔离能力的问题”
Section titled “OpenClaw · 把沙箱看成”选哪个后端的问题”而不是”自己实现哪些隔离能力的问题””平台框架不应假定自己知道所有宿主。OpenClaw 用 ExecHost 表达 sandbox、gateway、node,具体 enforce 交给部署方;宿主若不隔离,枚举也不会产生保护。
export type ExecHost = "sandbox" | "gateway" | "node";这三档的含义是:选 sandbox 表示这个命令要在隔离的执行环境里跑(具体怎么隔离由部署方决定,可以是 Docker、Firecracker microVM、AWS Lambda、Kubernetes Job 等任何东西),选 gateway 表示在 gateway 进程内跑(不开新进程,主要用于轻量级文件操作),选 node 表示直接在宿主 Node.js 进程里跑(用于已经被严格 allowlist 的可信命令,比如 git status 这种)。
这种设计的工程哲学是把”沙箱实现”跟”agent 逻辑”彻底解耦。OpenClaw 自己的代码只关心”这个命令应该走哪个 backend”,至于 sandbox backend 内部到底用了什么技术做隔离,是部署方的事。好处是 agent 代码不需要维护 Linux/macOS/Windows 三套独立的沙箱代码,部署方爱用 Docker 用 Docker、爱用 Firecracker 用 Firecracker;
坏处是 OpenClaw 自己不提供”开箱即用”的沙箱实现,部署方需要自己接一套容器或者隔离技术,不接就等于没沙箱。
Hermes · 把整件事委托给 6 种容器化方案,让用户用一个环境变量选
Section titled “Hermes · 把整件事委托给 6 种容器化方案,让用户用一个环境变量选”Hermes 把执行环境交给 TERMINAL_ENV。local、docker、远程容器和 SSH 各自提供边界;Agent 只负责选择后端,安全验证要落到后端配置。
Hermes hermes-agent/tools/terminal_tool.py:765-820 TERMINAL_ENV 6 后端 + per-backend 配置:image / cpu / memory / disk / persistent 都有
def _get_env_config() -> Dict[str, Any]: """Get terminal environment configuration from environment variables.""" # Default image with Python and Node.js for maximum compatibility default_image = "nikolaik/python-nodejs:python3.11-nodejs20" env_type = os.getenv("TERMINAL_ENV", "local")
mount_docker_cwd = os.getenv("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", "false").lower() in ("true", "1", "yes")
# Default cwd: local uses the host's current directory, everything # else starts in the user's home (~ resolves to whatever account # is running inside the container/remote). if env_type == "local": default_cwd = os.getcwd() elif env_type == "ssh": default_cwd = "~" else: default_cwd = "/root"
# ... return { "env_type": env_type, "modal_mode": coerce_modal_mode(os.getenv("TERMINAL_MODAL_MODE", "auto")), "docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image), "singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"), "modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image), "daytona_image": os.getenv("TERMINAL_DAYTONA_IMAGE", default_image), # ... "container_cpu": _parse_env_var("TERMINAL_CONTAINER_CPU", "1", float, "number"), "container_memory": _parse_env_var("TERMINAL_CONTAINER_MEMORY", "5120"), # MB (default 5GB) "container_disk": _parse_env_var("TERMINAL_CONTAINER_DISK", "51200"), # MB (default 50GB) "container_persistent": os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("true", "1", "yes"), # ... }这 6 个后端各自服务很具体的场景:
- local:在用户的开发机上直接跑,没有任何隔离。这是默认值,启动最快、调试最方便,适合开发场景。
- docker:在本地 Docker daemon 里跑一个容器,隔离和可重复性取决于 daemon、镜像和挂载策略。
- singularity:HPC(高性能计算)领域常见的容器格式,科研集群可能因权限策略选择它而不是 Docker;具体要看集群配置。
- modal:把整个命令委托给 Modal.com 的 serverless GPU/CPU 容器服务,按需启停,适合需要 GPU 但不想自己运维的场景。
- daytona:把命令委托给 Daytona 的 dev-environment-as-a-service,给每个 agent session 一个独立的开发环境。
- ssh:把命令通过 SSH 发到远端机器上跑,agent 本身只是个客户端。适合「agent 跑在用户笔记本上、命令在云上 VM 跑」的场景。
每个后端都支持 5 个独立的配置维度:image(容器镜像)、CPU、内存、磁盘、是否持久化容器。TERMINAL_CONTAINER_PERSISTENT 默认开启意味着同一个 session 内的多次命令复用同一个容器,避免重复创建环境;关闭则每次命令都新建容器,减少跨命令状态继承,但整体隔离仍取决于挂载、网络、身份和清理策略。交互调试通常需要复用环境;CI 是否按命令重建,应与它的 step 边界和威胁模型一致。
还有一个重要的安全细节叫 mount_docker_cwd:它的默认值是关闭,意思是默认不会把宿主机的当前工作目录挂载到容器里。只有用户显式设置 TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE=true 之后,宿主机的 /Users/xxx/repo 才会被映射到容器内的 /workspace。这种「默认安全加可选便利」是合理的工程取舍:避免了「用户没意识到自己挂载了整个 home 目录到容器里」这种事故。
隔离边界共享的底线
Section titled “隔离边界共享的底线”本文引用的四个源码快照在实现深度上差异很大,但以下三类边界反复出现。它们是对这些快照的工程归纳,不是所有沙箱都已满足的行业结论。
第一件事是需要承认沙箱会失败。沙箱启动需要依赖(bubblewrap 二进制、Docker daemon、SSH 连接),任一依赖缺失都会让沙箱启不动。运行时也可能因为内核版本、权限不足、配额耗尽等原因失败。引用的代码分别暴露了失败路径:Codex 把沙箱失败抽象成专门的错误类型,让上层调用者决定继续还是中止;Claude Code 用 failIfUnavailable 让管理员配「沙箱不可用时报错还是降级到警告」;OpenClaw 在 backend spawn 失败时回退到审批流程让用户兜底;Hermes 在后端不可用时提示用户切换 TERMINAL_ENV。
第二件事是网络和文件系统要单独管。网络隔离和文件系统隔离是两个独立维度:严格限制文件写入并不等于不能联外网,一个不能联网的进程也可能改坏被挂载的宿主机文件。引用的实现用不同深度表达这两个维度:Codex 在 PermissionProfile 里分开建模 file_system 和 network,Claude Code 把 SandboxNetworkConfig 和 SandboxFilesystemConfig 拆成两个 schema,OpenClaw 组合 ExecHost 与 NetworkPolicy,Hermes 由容器挂载和 network mode 决定边界。
第三件事是需要 explicit 处理平台差异。沙箱技术在 Linux、macOS、Windows 上不同:Linux 可用 bubblewrap、seccomp、landlock,macOS 有 sandbox-exec,Windows 有 Windows Sandbox。快照中的处理方式包括 Codex 的平台实现、Claude Code 的 enabledPlatforms、Hermes 的后端选择,以及 OpenClaw 交给 host 的执行契约。统一 API 可以共享,但 enforce 能力和测试矩阵仍要逐平台声明。
按副作用选择隔离层
Section titled “按副作用选择隔离层”四种典型场景:
- 要做桌面级安全 agent:参考 Codex bubblewrap + seccomp + landlock 三件套。门槛高,但用户拿到的是真隔离。
- 要在企业 IDE 集成中部署:参考 Claude Code schema 化配置 + enabledPlatforms + managed-only。让 IT 管理员能精确控制。
- 要做 SaaS / 跨平台部署:参考 Hermes 6 后端,让用户选 docker / modal / daytona / ssh。
- 想把沙箱外包给基础设施:参考 OpenClaw ExecHost 3 backend,把”具体怎么隔离”交给 host 实现。
选择:默认隔离,放宽要有证据
Section titled “选择:默认隔离,放宽要有证据”这里不做星级评分。沙箱选择取决于要限制的副作用和你能维护的隔离层。
| 约束 | 可先读 | 代价与边界 |
|---|---|---|
| 需要在本机限制文件、网络和 syscall | Codex 的 bubblewrap、seccomp、landlock 与平台实现 | 平台代码和内核兼容性要自己维护 |
| IT 需要通过配置控制网络和文件权限 | Claude Code 的 schema 与平台开关 | 配置放宽时要显式记录安全代价 |
| 运行时已有 Docker、Firecracker 等宿主 | OpenClaw 的 ExecHost contract | 宿主不 enforce 就没有隔离 |
| 想把执行后端交给本地或云容器 | Hermes 的 TERMINAL_ENV 后端 | 安全边界随后端和远程信任变化 |
从最小执行沙箱开始
Section titled “从最小执行沙箱开始”下面是从引用实现整理出的起步清单。先定义要限制的副作用,再按目标平台补 enforce、失败处理和逃逸测试。
复刻方案
最小可行
- Linux 上用 bubblewrap binary 做 FS 隔离(参考 Codex):read-only / writable / tmpfs 三种挂载方式,外部进程 enforce 边界;它能减少自写 namespace + chroot 的工作,但仍需按内核和部署环境验证
- 网络隔离用 seccomp BPF 拦 connect/sendto(参考 Codex):thread-level apply 让子进程继承;seccomp 比 iptables 轻量(不需 root),但需要熟悉 BPF 字节码
- macOS 走 sandbox-exec + .sb 配置文件:macOS 不支持 bubblewrap / seccomp 但有 seatbelt(沙箱)+ sandbox-exec 命令;写 .sb 配置(基于 SchemeML)描述权限边界
- fail_open vs fail_closed 给配置(参考 Claude Code 的 failIfUnavailable):把沙箱当辅助保护的交互环境可选择告警后降级;安全策略要求沙箱作为硬门槛的托管环境应拒绝无沙箱执行
进阶
- 三平台分开 crate(参考 Codex):linux-sandbox(bubblewrap + seccomp)/ 默认走 mac seatbelt / windows-sandbox-rs;共享调用契约,平台 enforce 与测试分开维护
- PR_SET_NO_NEW_PRIVS 跟 setuid 的 trade-off 写注释里(参考 Codex):这个 flag 防止 setuid 提权但也破坏了 sudo / mount 等需要 setuid 的工具;不是一律开,是按需启用并 document trade-off
- enabledPlatforms 选项(参考 Claude Code):让管理员在 macOS 启用 sandbox + 在 Linux 暂停(如果 Linux 上 bubblewrap 还有问题),逐平台 rollout 比一刀切安全
- allowManagedDomainsOnly / allowManagedReadPathsOnly(参考 Claude Code):managed-only 模式忽略用户层配置(用户的「allow github.com」被忽略),只用企业管理员推下来的 domain 白名单;这是大企业 SSO 集成的关键
- enableWeakerXxx 配置直接标注 "Reduces security"(参考 Claude Code):让用户在 schema 里看到这是带安全成本的开关(不是普通 flag),决策时自然会三思
- ExecHost 抽象(参考 OpenClaw):让沙箱跟 agent 解耦,agent 调用 ExecHost.run({argv}),部署方决定 ExecHost 是 docker / firecracker / native sandbox / cloud sandbox;这是 SaaS / 多租户的关键
- 6 后端切换(参考 Hermes 的 TERMINAL_ENV=local/docker/singularity/modal/daytona/ssh):满足不同部署需求(本地用 docker / 云端用 modal / 学术用 singularity / 生产用 daytona / 调远程机器用 ssh)
- persistent container 可配(参考 Hermes 的 TERMINAL_CONTAINER_PERSISTENT):同一 session 复用环境能减少重复启动,但会继承文件和进程状态;用短命令时长与跨任务污染测试决定默认值
- 默认不挂 host cwd(参考 Hermes):显式开 TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE 才映射;这能减少宿主文件暴露,但不等于容器逃逸后宿主绝对安全,仍要检查 daemon、mount 和 runtime 权限
一开始别做
- 别假设沙箱永远可用:bubblewrap 不在 PATH(极简 Linux)/ docker daemon 没起 / seatbelt 不支持的 macOS 版本(很老)都要处理;fail_open vs fail_closed 决策需要显式
- 别在沙箱里挂用户 home:默认应该是 cwd + 临时 tmpfs,不是 ~(挂了 home 等于把 ~/.ssh / ~/.aws 等敏感文件全部暴露给沙箱内进程)
- 别让网络默认通:seccomp 拦 connect 是默认行为;要让 agent 联网应该走代理(通过 proxy_routed_network 显式开),让流量都被代理审计
- 别忽视 setuid trade-off:PR_SET_NO_NEW_PRIVS 会破坏依赖 setuid 的工具(sudo / mount / ping 等);如果用户的 build 流程依赖这些工具就要慎用
- 别把「安全」写死:enterprise 部署经常需要 enableWeaker* 开关(如某团队需要 sandbox 内访问 git ssh 但默认不允许),加进 schema 但标注代价("Reduces security")让用户知道
把隔离差异放回部署场景
Section titled “把隔离差异放回部署场景”把 4 种放一起,“沙箱归谁实现”的差异一眼可见:Codex 自己写,Claude Code 让 IT 配置,OpenClaw 让 host 决定,Hermes 让容器 / 远程后端做。
隔离边界与降级路径怎么核验
Section titled “隔离边界与降级路径怎么核验”本章带走什么与下一步实验
Section titled “本章带走什么与下一步实验”沙箱隔离的是资产与副作用,不是“看起来危险的命令”。文件、网络、进程、系统调用、凭据和资源预算需要分别建模;容器也必须说明挂载和出口,而不是当成安全同义词。
下一步实验:建立逃逸套件,覆盖软链接、../、postinstall、读取 home、访问 metadata service、fork bomb、磁盘填满、后台进程和信号处理。记录每个向量在哪层被阻止、宿主是否受影响、日志能否解释拒绝。目标平台没有跑过这套测试,就不要声称隔离成立。
附录:练习与复盘
Section titled “附录:练习与复盘”按需展开练习和十道复盘题
- 🟢 用 bubblewrap 跑 echo:写一个脚本,用
bwrap --ro-bind /usr /usr --tmpfs /tmp -- echo hi验证基本隔离工作。 - 🟠 seccomp 拦 connect:写一个 Python 程序,用
prctl+ seccomp filter 拦截connect(2)。验证:curl example.com失败,ls /tmp成功。 - 🟠 多后端切换:实现
run_terminal(cmd, env_type),env_type ∈{local, docker, ssh}。docker 走docker run,ssh 走ssh user@host。验证:env_type=docker 时pwd输出容器内路径。 - 🔴 enabledPlatforms 限制:实现
should_enable_sandbox(settings),sandbox 只在 settings.enabledPlatforms 包含当前 platform 时启用。验证:enabledPlatforms=[“macos”] 在 Linux 上返回 false。
Q1 · 概念:bubblewrap、seccomp、landlock 这三个工具各自的边界是什么?
Codex 在 Linux 上同时用这三个,但目的完全不同:
bubblewrap (bwrap):用户空间的容器化工具。把 root 文件系统重新拼装:哪些目录 --ro-bind 只读,哪些 --bind 可写,--tmpfs 给临时目录,--proc 挂 /proc。这是 FS 隔离的主力。看上去像「mini Docker」但走的是 Linux user namespace + mount namespace,不需要 daemon。
seccomp:内核级 BPF 过滤系统调用。Codex 用它拦 connect() / sendto() 阻止网络访问。它能拦的是系统调用号,不能根据 IP / 端口做策略(那是 netfilter / iptables 的事)。seccomp 一旦启用就不可撤销,所以是 thread-level apply 让子进程精准继承。
landlock:Linux 5.13+ 的 LSM (Linux Security Module),做 FS 访问控制。原理跟 bubblewrap 不同:bubblewrap 重新挂载,landlock 在内核里拦 syscall。Codex 把它当 legacy / backup,主路径还是 bubblewrap。原因:landlock 在老内核没有;bubblewrap 在用户空间不需要内核版本支持。
怎么协同?
bwrap (FS 挂载隔离) └─ seccomp BPF (拦 connect/sendto) └─ landlock (备用 FS 控制,老内核 fallback)三层叠加,每一层防一类逃逸。例如 bwrap 可能被某 bypass 绕过,seccomp 仍能拦网络;seccomp 不能管 FS,bwrap / landlock 兜底。
追问:「为什么不用 Docker?」Docker 需要 daemon + root + 镜像管理。bubblewrap 是 setuid binary,不需要 root,单 binary 启停,更轻。Codex 是 CLI 工具,启动开销敏感。
Q2 · 概念:Claude Code 的 enableWeakerNetworkIsolation 注释为什么直接写「Reduces security」?
这是「带成本的开关」典型设计。具体场景:
Go 工具(gh / gcloud / terraform / aws-cli)在沙箱里走 MITM 代理 + 自签 CA 时,需要访问 com.apple.trustd.agent(macOS 系统服务)来验证 TLS 证书。但 trustd 服务本身能被滥用做数据泄漏(往 Apple 系统服务转发恶意 payload)。
Claude Code 的选择:
- 不允许 trustd 访问(默认)→ Go 工具在 MITM 代理后不能用 → 企业部署中很多用户需要 gh/gcloud
- 允许 trustd 访问 → Go 工具能用 → 多一个数据泄漏向量
两个都不完美。所以做成 enableWeakerNetworkIsolation 配置项 + 注释里明确写 “Reduces security”,让用户知道开了这个之后安全等级是降的,不是「开了之后就完美」。
这种设计的精髓:
不假装「一个配置开关让所有事都变好」。承认 trade-off 客观存在,让用户做明确的取舍。注释直接出现在 Zod schema 里,文档自动生成时同步进 settings.json 的 hover 提示。
类似的”weaker” 开关还有 enableWeakerNestedSandbox(允许嵌套沙箱,绕开某些限制)。两个开关都带 “weaker” 命名前缀,IDE 智能提示让用户一眼看出「这是安全敏感的」。
追问:「这种 trade-off 注释应当在 schema 里还是文档里?」schema 里。因为 IDE auto-completion / settings.json hover 提示直接拿 schema 注释。如果只在外部文档,用户开了忘了去读。把告警放在最接近触发点的地方。
源码:claude-code/src/entrypoints/sandboxTypes.ts:90-160。
Q3 · 架构:Hermes 6 后端的实际取舍是什么?
6 个后端不是炫技。每个对应一种真实部署形态:
| 后端 | 额外启动路径 | 隔离边界 | 典型场景 |
|---|---|---|---|
| local | 无容器启动 | 跟随宿主进程 | 可信开发机调试 |
| docker | 本地容器创建或复用 | 取决于容器配置 | 本地长跑 |
| singularity | 本地容器创建或复用 | 取决于集群策略 | HPC 集群 / 科研 |
| modal | 远程容器调度 | 取决于云端配置 | serverless 按需 |
| daytona | 远程开发环境调度 | 取决于远程环境 | dev-env-as-a-service |
| ssh | 网络连接与远程 shell | 取决于远端主机 | 自有 dev VM |
为什么需要这么多?
不同部署形态的 trade-off 不同:
- 开发机:要快,不需要隔离 → local
- demo / 教学:要重现,需要环境一致 → docker
- 科研:HPC 集群只允许 singularity → singularity
- CI / 短任务:按需启动 + 用完销毁 → modal
- 多人协作:每个开发者一个独立 dev env → daytona
- 企业内部:自有 dev VM 已经在用 → ssh
如果只支持 local / docker,HPC、远程开发环境和自有 VM 还要另写接入层。六种后端的价值在于减少这层适配,不代表六种实现拥有同样的隔离强度。
每个后端 5 维配置(image / cpu / mem / disk / persistent):
- image:每个后端可单独配镜像(singularity 用
docker://image转换,modal 直接读 docker image) - persistent=true:一个 session 复用同一个容器,启动只付一次。session 结束后清理。
- persistent=false:每个命令新建容器,减少命令之间的状态继承,但会重复支付后端启动或调度成本。具体延迟要在目标环境测量。
追问:「6 个 backend 维护负担怎么办?」modal / daytona 有官方 Python SDK,docker / singularity / ssh 可调用现成 CLI。Hermes 把入口集中到 dispatch 层,但版本兼容、认证、失败恢复和资源清理仍由项目承担。
Q4 · 概念:OpenClaw 为什么不自己提供沙箱实现?
OpenClaw 的核心定位是「企业 SaaS agent 平台」,不是「桌面工具」。这个定位决定了沙箱设计:
为什么不自己写?
- 企业已经有基础设施。SaaS 公司用 Kubernetes / Firecracker / Lambda / EC2 都是常见选择。OpenClaw 提供 ExecHost 抽象,让客户选自己已有的隔离方案。
- 跨云多样化。AWS / GCP / Azure / 私有云的沙箱方案不同。如果 OpenClaw 自己写一套,等于跟每家云都耦合。抽象出来让 host 实现,跨云零代码改动。
- 专业事专业人做。Firecracker(AWS Lambda 底层)专做轻量 VM 隔离,比 agent 团队自己写好得多。委派给基础设施层比自己做更可靠。
坏处:
- 小客户没基础设施时没法用。一个开发者想自己装 OpenClaw 但没 K8s / Firecracker → 沙箱档位等于没沙箱。OpenClaw 这块文档要写清楚。
- “沙箱在哪里实现”对用户不透明。openclaw 的代码里看不到 sandbox 实际怎么 enforce。容易出现「我以为是沙箱实际没沙箱」错觉。
为什么这是合理的?
这份源码快照提供了面向托管或企业部署的 host-enforced 路径,但不证明每个用户都已有隔离基础设施。是否采用 ExecHost 抽象,应由现有容器或远程执行能力、目标平台和运维边界决定。
抽象的好处:sandbox 这件事跟「我用什么 LLM」「我用什么前端 UI」一样,都可以独立替换。OpenClaw 把 27 矩阵的 ExecHost / ExecSecurity / ExecAsk 全部抽出来 = 「我提供决策框架,你提供具体实现」。
源码:openclaw/src/infra/exec-host.ts + infra/exec-approvals.ts。
追问:「自己创业要做 agent 要不要学 OpenClaw 这种方式?」分阶段:MVP 时参考 Codex 内嵌 bubblewrap(用户即开即用);商业化进 enterprise 客户时再抽 ExecHost 抽象。先一体化再解耦。
Q5 · 工程:seccomp 在 thread-level apply 而不是 process-level,为什么?
Codex 用 apply_permission_profile_to_current_thread() 函数应用 seccomp,而不是整个进程。原因:
1. 子进程 fork 时的精确继承。seccomp 的语义是「当前线程 + fork 后的子进程继承」。Codex 的工作流是:
agent 主进程 └─ fork 一个 thread 来准备执行 └─ apply seccomp 在这个 thread └─ exec 用户命令(子进程继承 thread 的 seccomp)主进程的其他 thread(事件循环 / IPC / 日志)不受 seccomp 影响。只有「即将执行用户命令」的 thread 戴上枷锁。
2. PR_SET_NO_NEW_PRIVS 的代价
seccomp 需要先 set PR_SET_NO_NEW_PRIVS,这会阻止 setuid 提权。但 bubblewrap 自己是 setuid binary(依赖提权 mount user namespace)。所以 Codex 需要让 bwrap 先跑,再在 bwrap 的子进程里 apply seccomp。如果 seccomp 在主进程 apply:
- 主进程拿不到 setuid → bwrap 起不来 → 链条断
- 或者:主进程后启 bwrap 但 bwrap 已经处理过 prctl 了
thread-level apply 让两者井水不犯河水:bwrap 在 fresh thread 里跑(无 NO_NEW_PRIVS),seccomp 在 user command thread 里 apply。
3. 测试 / 调试更容易
整个进程 seccomp 之后,调试器 / strace / 日志 syscall 全被拦。thread-level 让其他 thread 继续工作。
Linux 文档原话:
A process can apply seccomp filters in one thread; the filter will apply to that thread and any child threads/processes created via fork()/clone().
Codex 利用了这个语义。
追问:「Python 怎么做?」Python 用 prctl + seccomp 库,但 Python 主线程跑 GIL,做 thread-level 没意义。所以 Python agent 沙箱通常是 fork-exec 时在 child process 里 apply。CPython 的 multiprocessing 是另一条路。
Q6 · 实战:你给自己的 agent 加沙箱,从 0 到生产怎么走?
按 基线 → 文件隔离 → 网络边界 → schema → 多后端 五个 scope 推进;每层都以 threat vector 和资源清理 fixture 验收。
阶段 1 · 无沙箱基线
def run_command_unsafe(cmd: list[str]) -> str: return subprocess.check_output(cmd)在一组代表性真实场景上记录 cmd 统计,判断哪些命令需要拦:
- 网络访问(curl / wget / pip install 联网)
- FS 写(rm / mv / 任何 -o 输出文件)
- 解释器执行(python / node / bash -c)
验收门槛:基线 trace 固定下来,网络、文件写入和解释器执行向量都有明确的 allow/block 预期。
阶段 2 · bubblewrap FS 隔离
前置:基线已列出需要隔离的路径和命令。
def run_command_sandboxed(cmd: list[str], cwd: Path, writable: list[Path]) -> str: bwrap_args = [ "bwrap", "--ro-bind", "/usr", "/usr", "--ro-bind", "/etc", "/etc", "--tmpfs", "/tmp", "--proc", "/proc", "--dev", "/dev", ] for w in writable: bwrap_args += ["--bind", str(w), str(w)] bwrap_args += ["--chdir", str(cwd), "--"] bwrap_args += cmd return subprocess.check_output(bwrap_args)参考 Codex bubblewrap 风格。FS 默认 read-only,writable paths 显式列出。
验收门槛:只读挂载、显式 writable path、临时目录和 cwd 在固定 fixture 中符合预期,进程退出后挂载被清理。
阶段 3 · seccomp 拦网络
前置:FS 隔离已经通过,threat model 明确哪些网络 syscall 或 namespace 必须阻断。
import ctypes
def install_network_seccomp(): libc = ctypes.CDLL("libc.so.6") PR_SET_NO_NEW_PRIVS = 38 libc.prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) # Install BPF filter blocking SYS_connect / SYS_sendto # Use python-prctl or libseccomp Python binding或更简单:bwrap --unshare-net 直接禁网络命名空间,不需要 seccomp。
验收门槛:网络 allowlist/blocklist 和子进程继承行为可测试,sandbox 失败不会静默绕过强制策略。
阶段 4 · schema 配置
前置:后端能力和 fail-open/closed 策略已经写成 schema。
class SandboxConfig(BaseModel): enabled: bool = True fail_if_unavailable: bool = False network_allowed_domains: list[str] = [] fs_writable: list[Path] = [] fs_read_only: list[Path] = []
def run_with_config(cmd, config: SandboxConfig): if not config.enabled: return run_command_unsafe(cmd) try: return run_command_sandboxed(cmd, config) except SandboxUnavailable: if config.fail_if_unavailable: raise warn("Sandbox unavailable, running unsandboxed") return run_command_unsafe(cmd)参考 Claude Code failIfUnavailable 思路。
验收门槛:配置校验、不可用后端、警告和错误传播都有契约测试;默认值不会扩大权限。
阶段 5 · 多后端切换(按需)
前置:真实部署需要 local、container 或 remote 之外的后端。
class Backend(Enum): LOCAL = "local" BWRAP = "bwrap" DOCKER = "docker" SSH = "ssh"
BACKENDS = { Backend.LOCAL: run_command_unsafe, Backend.BWRAP: run_with_bwrap, Backend.DOCKER: run_with_docker, Backend.SSH: run_with_ssh,}
def run(cmd, backend: Backend, **opts): return BACKENDS[backend](cmd, **opts)参考 Hermes TERMINAL_ENV 思路。
验收门槛:每个后端的切换、资源回收、不可用降级和错误格式都能在目标环境重放。
关键经验:
- 先做 bwrap:验证文件挂载、网络与子进程边界,再根据 threat model 决定是否加 seccomp 或 landlock
- schema 化早做:让用户配置远比硬编码值好维护
- fail_open vs fail_closed:明确配置,不要默认
- 多后端是 enterprise 才需要:自用 / MVP 阶段 bwrap-only 够
追问:「Mac / Windows 怎么办?」Mac 用 sandbox-exec(系统自带)+ 写 .sb 文件。Windows 用 Windows Sandbox API(需要 Pro 版本)或者直接 Docker Desktop。Codex 三平台代码是参考。
Q7 · 概念:failIfUnavailable: false 退化为「无沙箱 + 警告」,这种设计是不是反模式?
不一定。关键不是 dev 或 prod 标签,而是沙箱在当前策略里是辅助保护,还是不可缺少的强制控制。
什么时候可以选择 fail_open?
本地交互环境可能缺少 bubblewrap,或没有可用的 Windows Sandbox。若威胁模型允许无沙箱运行,fail_open + warning 可以保留工作流,但必须让用户清楚看到「当前没有沙箱保护」,并限制高风险工具。
什么时候需要 fail_closed?
当组织策略把沙箱列为强制控制时,依赖缺失就应阻止启动或执行,不能静默退化。failIfUnavailable: true 表达的是这类 hard-gate 策略;并非只要挂上「生产」标签就自动得到同一个答案。
Claude Code 的注释直接讲了这个意图:
When false (default), a warning is shown and commands run unsandboxed. Intended for managed-settings deployments that require sandboxing as a hard gate.
同一个 bool 可以承载这两种策略,但默认值必须由威胁模型和部署政策决定。
实践建议:
辅助保护策略:fail_open = true + 显著 warning + 缩小工具范围强制隔离策略:fail_open = false + 由 policySettings 强制CI / 自动化:根据 runner 信任边界决定;要求隔离的任务设 fail_open=false反模式版本:
- ❌ 全局 fail_closed → dev 痛苦
- ❌ 全局 fail_open → prod 风险
- ❌ 没有 warning → 用户不知道无沙箱
- ❌ warning 在调试日志里 → 用户看不见
无论产品把 warning 放在启动横幅、状态栏还是命令结果里,它都应在执行前可见,不能只留在调试日志中。
源码:claude-code/src/entrypoints/sandboxTypes.ts:120-135。
追问:「fail_open=true 时为什么不直接禁用所有副作用工具?」可以,但会挡住 npm install、git pull 等常见开发动作。一个可审计的折中是缩小 tool allowlist,并把无沙箱状态明确显示给用户。
Q8 · 概念:把沙箱跟权限审批解耦的好处和坏处?
12 章讲权限审批,13 章讲沙箱。这两件事很容易混淆。
它们的本质区别:
- 权限审批:「我要不要让 agent 做这件事」(人类决策)
- 沙箱:「即使 agent 做了这件事,能造成多大破坏」(技术约束)
解耦的好处:
- 独立演化:审批策略调整不影响沙箱实现。新增「audit 模式」(记录所有命令但不弹窗)只在审批层加,沙箱代码不动。
- 不同维度的失败:审批通过 ≠ 沙箱安全。可能用户审批了
rm -rf /tmp/some_specific_file,沙箱仍要确认/tmp/some_specific_file在 writable_roots 里。 - 可以单独测试:审批模拟可以纯单测,沙箱测试需要真实 syscall。
Codex 的做法:
agent → 审批层 → permission_profile → 沙箱层 → 执行 (writable_roots / network policy 等)permission_profile 是审批层「答应了什么」的具体表达,沙箱层 enforce。两层职责清晰。
解耦的坏处:
- 配置面变大:用户既要配审批规则,又要配沙箱 policy。容易冗余 / 漂移。
- 认知负担:「为什么我审批通过了还是被沙箱拦了?」这种问题需要文档解释。
- 二者之间的 plumbing:审批层产出的 permission_profile 要序列化给沙箱层。Codex 用
to_runtime_permissions()这种转换函数。
OpenClaw 选择「半解耦」:ExecHost 既是审批维度(决定 host 怎么决策),也是沙箱维度(决定具体哪里执行)。一个枚举管两件事。简化了配置,但 trade-off 是「沙箱实现」对用户不透明。
适用场景:
- 复杂 enterprise → 解耦(Codex / Claude Code)
- 简单 SaaS → 半解耦(OpenClaw)
- 个人 agent → 一体化(Hermes 把沙箱当后端选择)
追问:「实际工程里这两层怎么协调?」契约层用 schema:审批层产出 PermissionProfile,沙箱层消费 PermissionProfile。schema 是契约,两层独立演化但前后兼容。
Q9 · 工程:persistent container 默认开启,吞吐和隔离该怎么取舍?
没有脱离部署边界的统一答案。persistent=true 复用环境,persistent=false 减少跨命令状态继承;多租户还要先确认 session 是否真的对应单一租户。
两个选择的对比:
| 策略 | 启动开销 | 隔离强度 | 状态隔离 |
|---|---|---|---|
| persistent=true(默认) | 每 session 1 次 | 中 | session 内共享 |
| persistent=false | 每命令 1 次 | 取决于后端策略 | 不继承上一命令的容器状态 |
为什么默认选 persistent=true?
- agent 工作流是连续的:一个任务里 agent 经常
cd repo && npm install && npm run build && npm test。如果每个命令新建容器,启动和环境准备会反复发生;增加多少延迟取决于后端与镜像缓存。 - 状态共享可能是工作流要求:
npm install装了依赖,下一条npm test需要这些依赖。逐命令重建环境时需要另行保存依赖或工作区状态。 - session 可以作为隔离单元,但需要验证:只有在 session 与租户或任务绑定、结束时确实清理容器且挂载不泄漏时,它才形成有效边界。
为什么提供 persistent=false 选项?
- 审计 / forensics 场景:法医分析时要确保每个命令在 clean env 跑,避免上一条污染下一条。
- CI 场景:每个 step 独立 container,跟 GitHub Actions 一致。
- 多租户 agent:不同 user / org 之间需要严格隔离。
可验证的部署起点:
- 个人 dev / 自用:persistent=true 默认
- 团队协作:persistent=true,但每 task 重启 session
- SaaS 多租户:按租户或任务重建环境并验证清理;persistent=false 只是其中一项控制
- CI / 自动化:persistent=false 跟 step 边界对齐
追问:「persistent=true 时 attacker 怎么利用?」假设 agent 执行用户 A 的任务后没有退出容器,残留文件又被用户 B 的任务读取,就会出现跨租户污染。多租户部署因此要按租户或任务重建环境,并验证清理流程;不能仅凭 persistent=false 推定隔离已经成立。
源码:hermes-agent/tools/terminal_tool.py:230-270(container 生命周期)。
Q10 · 开放:按威胁模型设计一个可组合沙箱框架,并写出验证矩阵。
6 层 API,按需启用:
Layer 1 · 后端枚举(需要)
enum SandboxBackend { None = 'none', // 无沙箱 Bwrap = 'bwrap', // Linux bubblewrap Seatbelt = 'seatbelt', // macOS sandbox-exec WindowsSandbox = 'windows-sandbox', Docker = 'docker', // 跨平台容器 Modal = 'modal', // serverless SSH = 'ssh', // 远程}参考 Codex 三平台 + Hermes 6 后端。
Layer 2 · 平台过滤(需要)
interface SandboxConfig { enabled: boolean; failIfUnavailable: boolean; // 参考 Claude Code enabledPlatforms: Platform[]; // 参考 Claude Code backend: SandboxBackend;}
function selectBackend(config: SandboxConfig): SandboxBackend | null { if (!config.enabled) return null; const platform = currentPlatform(); if (config.enabledPlatforms.length && !config.enabledPlatforms.includes(platform)) { return null; } return config.backend;}Layer 3 · 文件系统配置(需要)
interface SandboxFilesystemConfig { allowWrite: string[]; // 显式可写路径 denyWrite: string[]; // 显式禁写路径 allowRead: string[]; // 显式可读 denyRead: string[]; // 显式禁读 allowManagedReadPathsOnly: boolean; // 参考 Claude Code:忽略 user-level}Layer 4 · 网络配置(需要)
interface SandboxNetworkConfig { enabled: boolean; allowedDomains: string[]; // 显式允许域 allowedPorts: number[]; // 显式允许端口 httpProxyPort?: number; // MITM 代理 enableWeakerNetworkIsolation: boolean; // 参考 Claude Code:明确标注代价}Layer 5 · 后端配置(可选)
interface DockerBackendConfig { image: string; cpu: number; memory: string; disk: string; persistent: boolean; // 参考 Hermes mountCwdToWorkspace: boolean; // 参考 Hermes:默认 false}
interface SSHBackendConfig { host: string; user: string; port: number; cwd: string;}Layer 6 · 失败处理(需要)
function runSandboxed(cmd: string[], config: SandboxConfig): Result { const backend = selectBackend(config); if (!backend) { if (config.failIfUnavailable) { throw new SandboxUnavailable("backend not selected"); } warn("Sandbox unavailable, running unsandboxed"); return runUnsandboxed(cmd); } try { return BACKENDS[backend].run(cmd, config); } catch (e: SandboxUnavailable) { if (config.failIfUnavailable) throw e; warn(`Sandbox ${backend} failed: ${e}, running unsandboxed`); return runUnsandboxed(cmd); }}参考 Claude Code failIfUnavailable + Codex SandboxErr。
vs 四家:
- Codex 贡献:三平台分离 + thread-level apply + seccomp/landlock 分工
- Claude Code 贡献:schema 配置 + enabledPlatforms + enableWeaker* trade-off 标注
- OpenClaw 贡献:ExecHost 抽象 + 跟权限审批解耦
- Hermes 贡献:6 后端 + per-backend 5 维配置 + persistent 默认值
按 scope 评估:
- Layer 1-3 · 单一后端与边界:先定义挂载、网络、子进程和不可用时的策略。验收:固定命令集验证读写路径、网络向量和子进程边界,并记录 sandbox verdict。
- Layer 4-5 · schema 与后端选择:只有在配置需要跨平台或部署形态分流时增加。前置:每个 backend 有 capability/失败契约;验收:同一 fixture 在目标平台上验证 fail-open/closed、清理和错误传播。
- Layer 6 · 多后端运营:仅在真实部署需要容器、远程或 HPC 后端时增加。前置:为每个后端准备 threat vector 和回收测试;验收:后端切换、不可用降级和资源清理均可重放。
关键决策:
- 首批后端选 3 个:bwrap + docker + ssh 分别验证本机、容器和远程三种部署形态;覆盖率要用真实用户任务统计
- schema 化早做:直接 Zod / pydantic
- fail_open default + policy override:仅适用于允许无沙箱降级的环境;硬门槛策略设为 closed
- persistent 策略按状态需求选择:多租户要按租户或任务重建并验证清理,不能只靠一个布尔值
追问:「跨语言怎么共享?」schema 用 JSON Schema 定义,codegen 类型;具体 backend 实现各语言独立(Rust 写 Codex bubblewrap wrapper,Python 写 Hermes docker wrapper)。协议跨语言共享,实现各管各。
源码组合(提案):Codex linux-sandbox/ → Claude Code entrypoints/sandboxTypes.ts → OpenClaw infra/exec-host.ts → Hermes tools/terminal_tool.py。组合前仍需重新定义契约、威胁模型和测试矩阵。