跳到主要内容

13 · 沙箱到底要隔离什么

按文件、网络、系统调用和提权风险选择隔离层,并知道何时交给容器

本章任务

要回答的问题

文件、网络、系统调用和身份中,哪些边界必须由沙箱强制,而不是靠 Agent 自律?

读完你能

  • 按资产和攻击面选择文件、网络、进程与系统调用隔离
  • 区分策略检查、运行时沙箱和容器边界
  • 设计逃逸、挂载、网络出口和资源耗尽测试
适合现在读
正在选择容器、Seatbelt、Landlock、Seccomp 或远端执行环境的工程师
先修知识
理解操作系统权限与 Shell 执行风险
实践产物
一个按任务分级的 Sandbox Profile 与逃逸测试计划
证据边界
沙箱不是完整安全系统;内核、容器配置、密钥和外部服务仍需单独建模

场景:任务声称只读源码,但 Agent 执行 npm install,依赖的 postinstall 脚本读取 ~/.ssh 并访问公网;或者通过工作区软链接写到允许目录之外。仅限制“工具名”或项目目录都无法覆盖真实副作用。

通过标准:文件读写按真实解析路径限制,默认无网络和无无关凭据;子进程、系统调用、CPU、内存和磁盘受限;安装与执行使用不同 Profile;所有拒绝能映射到具体策略。

四系统的沙箱方案:自管三层、跨平台配置、ExecHost 3 backend、TERMINAL_ENV 6 后端
同一个「防止 agent 破坏系统」,四家在自管、外包两端各占一头。

四家在沙箱 5 件事上的覆盖:

维度 CodexClaude CodeOpenClawHermes
沙箱基础设施 Linux:bubblewrap 加 seccomp 加 landlock。macOS:seatbelt。Windows:独立 cratemacOS: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
沙箱这件事的工程化程度

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 前缀的配置,allowManagedDomainsOnlyallowManagedReadPathsOnly。它们的语义是:“开了这个之后忽略所有用户层配置,只信 policySettings”。这是企业 IT 管理员的”我说了算”心态:管理员开了之后,无论用户在 userSettingsprojectSettingslocalSettingscliArg 里怎么放行,都不生效;

只有管理员在 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 目录到容器里」这种事故。

本文引用的四个源码快照在实现深度上差异很大,但以下三类边界反复出现。它们是对这些快照的工程归纳,不是所有沙箱都已满足的行业结论。

第一件事是需要承认沙箱会失败。沙箱启动需要依赖(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 能力和测试矩阵仍要逐平台声明。

四家沙箱方案在自管深度 x 部署灵活度上的相对位置
Hermes 6 backend 全靠容器外包;OpenClaw 3 host 让部署方决定;Claude Code schema 化配置;Codex 三平台自管沙箱代码。

四种典型场景:

  • 要做桌面级安全 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 “选择:默认隔离,放宽要有证据”

这里不做星级评分。沙箱选择取决于要限制的副作用和你能维护的隔离层。

约束可先读代价与边界
需要在本机限制文件、网络和 syscallCodex 的 bubblewrap、seccomp、landlock 与平台实现平台代码和内核兼容性要自己维护
IT 需要通过配置控制网络和文件权限Claude Code 的 schema 与平台开关配置放宽时要显式记录安全代价
运行时已有 Docker、Firecracker 等宿主OpenClaw 的 ExecHost contract宿主不 enforce 就没有隔离
想把执行后端交给本地或云容器Hermes 的 TERMINAL_ENV 后端安全边界随后端和远程信任变化

下面是从引用实现整理出的起步清单。先定义要限制的副作用,再按目标平台补 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")让用户知道
四种沙箱方案并列对照
Codex 自管三平台沙箱;Claude Code schema 配置 + enabledPlatforms;OpenClaw ExecHost 3 backend 外包;Hermes TERMINAL_ENV 6 backend 容器化。

把 4 种放一起,“沙箱归谁实现”的差异一眼可见:Codex 自己写,Claude Code 让 IT 配置,OpenClaw 让 host 决定,Hermes 让容器 / 远程后端做。

沙箱隔离的是资产与副作用,不是“看起来危险的命令”。文件、网络、进程、系统调用、凭据和资源预算需要分别建模;容器也必须说明挂载和出口,而不是当成安全同义词。

下一步实验:建立逃逸套件,覆盖软链接、../、postinstall、读取 home、访问 metadata service、fork bomb、磁盘填满、后台进程和信号处理。记录每个向量在哪层被阻止、宿主是否受影响、日志能否解释拒绝。目标平台没有跑过这套测试,就不要声称隔离成立。

按需展开练习和十道复盘题
  1. 🟢 用 bubblewrap 跑 echo:写一个脚本,用 bwrap --ro-bind /usr /usr --tmpfs /tmp -- echo hi 验证基本隔离工作。
  2. 🟠 seccomp 拦 connect:写一个 Python 程序,用 prctl + seccomp filter 拦截 connect(2)。验证:curl example.com 失败,ls /tmp 成功。
  3. 🟠 多后端切换:实现 run_terminal(cmd, env_type),env_type ∈ {local, docker, ssh}。docker 走 docker run,ssh 走 ssh user@host。验证:env_type=docker 时 pwd 输出容器内路径。
  4. 🔴 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 工具,启动开销敏感。

源码codex/codex-rs/linux-sandbox/src/landlock.rs:1-100

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 的选择

  1. 不允许 trustd 访问(默认)→ Go 工具在 MITM 代理后不能用 → 企业部署中很多用户需要 gh/gcloud
  2. 允许 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 层,但版本兼容、认证、失败恢复和资源清理仍由项目承担。

源码hermes-agent/tools/terminal_tool.py:765-870

Q4 · 概念:OpenClaw 为什么不自己提供沙箱实现?

OpenClaw 的核心定位是「企业 SaaS agent 平台」,不是「桌面工具」。这个定位决定了沙箱设计:

为什么不自己写?

  1. 企业已经有基础设施。SaaS 公司用 Kubernetes / Firecracker / Lambda / EC2 都是常见选择。OpenClaw 提供 ExecHost 抽象,让客户选自己已有的隔离方案。
  2. 跨云多样化。AWS / GCP / Azure / 私有云的沙箱方案不同。如果 OpenClaw 自己写一套,等于跟每家云都耦合。抽象出来让 host 实现,跨云零代码改动。
  3. 专业事专业人做。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 是另一条路。

源码codex/codex-rs/linux-sandbox/src/landlock.rs:60-130

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 思路。

验收门槛:每个后端的切换、资源回收、不可用降级和错误格式都能在目标环境重放。

关键经验

  1. 先做 bwrap:验证文件挂载、网络与子进程边界,再根据 threat model 决定是否加 seccomp 或 landlock
  2. schema 化早做:让用户配置远比硬编码值好维护
  3. fail_open vs fail_closed:明确配置,不要默认
  4. 多后端是 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 做了这件事,能造成多大破坏」(技术约束)

解耦的好处

  1. 独立演化:审批策略调整不影响沙箱实现。新增「audit 模式」(记录所有命令但不弹窗)只在审批层加,沙箱代码不动。
  2. 不同维度的失败:审批通过 ≠ 沙箱安全。可能用户审批了 rm -rf /tmp/some_specific_file,沙箱仍要确认 /tmp/some_specific_file 在 writable_roots 里。
  3. 可以单独测试:审批模拟可以纯单测,沙箱测试需要真实 syscall。

Codex 的做法

agent → 审批层 → permission_profile → 沙箱层 → 执行
(writable_roots / network policy 等)

permission_profile 是审批层「答应了什么」的具体表达,沙箱层 enforce。两层职责清晰。

解耦的坏处

  1. 配置面变大:用户既要配审批规则,又要配沙箱 policy。容易冗余 / 漂移。
  2. 认知负担:「为什么我审批通过了还是被沙箱拦了?」这种问题需要文档解释。
  3. 二者之间的 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?

  1. agent 工作流是连续的:一个任务里 agent 经常 cd repo && npm install && npm run build && npm test。如果每个命令新建容器,启动和环境准备会反复发生;增加多少延迟取决于后端与镜像缓存。
  2. 状态共享可能是工作流要求npm install 装了依赖,下一条 npm test 需要这些依赖。逐命令重建环境时需要另行保存依赖或工作区状态。
  3. session 可以作为隔离单元,但需要验证:只有在 session 与租户或任务绑定、结束时确实清理容器且挂载不泄漏时,它才形成有效边界。

为什么提供 persistent=false 选项?

  1. 审计 / forensics 场景:法医分析时要确保每个命令在 clean env 跑,避免上一条污染下一条。
  2. CI 场景:每个 step 独立 container,跟 GitHub Actions 一致。
  3. 多租户 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 和回收测试;验收:后端切换、不可用降级和资源清理均可重放。

关键决策:

  1. 首批后端选 3 个:bwrap + docker + ssh 分别验证本机、容器和远程三种部署形态;覆盖率要用真实用户任务统计
  2. schema 化早做:直接 Zod / pydantic
  3. fail_open default + policy override:仅适用于允许无沙箱降级的环境;硬门槛策略设为 closed
  4. 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。组合前仍需重新定义契约、威胁模型和测试矩阵。