跳转到内容

沙盒后端

后端 取值 平台 可用性
封闭虚拟机 microvm macOS、Linux、Windows(microVM 虚拟机管理程序) 受 microVM 运行时的探测门控;适用于交互式终端——shell 运行在一台机台内,而不是主机上
原生 native macOS(Seatbelt)、Linux 和 WSL2(bubblewrap) 在启动时探测;Linux 会拒绝一次需要基于文件名通配符的写入拒绝的运行,而 bubblewrap 无法强制执行这一点
无隔离 none 全部 解析时的默认值、终端的默认值,以及没有可用后端或设置了 --sandbox off 时的回退

SandboxBackend.fromName 对 null 或无法识别的已存值返回 none。已存的 "docker" 会映射为 native。

microvm 是唯一带内核边界的后端——工作负载确实不在主机上运行。它在任何一个方向上都不会成为静默回退:在无法启动 VM 的主机上显式请求它会大声失败(“我要的是一台 VM,拿到的却是自己的笔记本”是这套封闭方案唯一承受不起的降级),而只有未指定的默认值才会降级到 native,并打出一行日志说明这一点。会话实际使用的后端会回报给客户端并打上徽标,因此一台终端绝不可能在主机上运行的同时声称自己已被封闭。VM 自身的生命周期、镜像和出站访问由机台参考介绍。

Windows 没有原生沙盒。 NativeSandboxAdapter.probe 在所有非 macOS 也非 Linux/WSL2 的主机上都返回 available: false。因此 Windows 上的智能体派发使用 none。microvm 后端是另一回事:它受 microVM 运行时的探测门控,适用于交互式终端和机台,而不是在主机上包裹 bash。

探测才是决定是否强制执行的那道门。它在服务器启动时运行一次。

主机 available 说明 / 提示
macOS true sandbox-exec(Seatbelt)——命名空间隔离,没有内核边界
装有 bwrap 和 socat 的 Linux 或 WSL2 true bubblewrap——命名空间隔离,没有内核边界
Linux 或 WSL2 缺少其中任一工具 false requiresInstall: true,并带有指明缺失软件包的安装提示
任何其他平台 false 此平台不提供原生沙盒

服务器会在启动时用一行日志记录结果:agent sandbox ON 加说明、no OS-native agent sandbox on this host 加提示,或 agent sandbox DISABLED by --sandbox off。GET /healthz 不携带任何沙盒字段。

--sandbox / CC_SERVER_SANDBOX 接受 on(默认)或 off。它是退出开关,而不是启用开关:on 的意思是“主机提供后端时就包裹”,而绝不是“要求必须有后端”。

使用 Apple 的 Seatbelt 沙盒机制(与 macOS App Sandbox 使用的是同一套系统)。

生成的配置默认宽松((allow default)),再在其上划出一系列拒绝,因为一份完全默认拒绝的配置会让交互式 shell 无法使用——需要枚举的 dyld、XPC 和 Mach 调用太多了。实际后果是智能体仍然可以读取主机的大部分内容;你已注册的检出靠的是显式的写入和重命名拒绝来保护,而不是靠不可见。

  • 文件系统读取:对机密路径(~/.ssh、~/.aws、~/.gnupg、~/.config/gh、~/Library/Keychains 以及拒绝读取集合中的其余路径)拒绝
  • 文件系统写入:先重置为拒绝,再显式放行子路径,其中强制拒绝的路径(shell rc 文件、.git/hooks、Claude 配置、受保护的检出)即使在可写根目录之内也会被阻止。file-write-unlink 在每一条被拒绝路径及其祖先路径上都被拒绝,因此重命名无法绕过一次写入拒绝
  • 进程:对始终危险的二进制文件执行 process-exec 拒绝,并阻止从任何可写目录执行任何东西
  • 网络:限制为进程内代理端口加 DNS——回环并非一律放行,Unix 套接字也不允许。代理强制执行按会话的域名允许列表(见网络出站)
  • 配置:配置在每次启动时根据 SandboxSpec 生成

要求:

  • sandbox-exec(/usr/bin/sandbox-exec),每次 macOS 安装都自带——无需额外的工具

使用 bubblewrap(bwrap)创建挂载和 PID 命名空间。

  • 文件系统:创建一个挂载命名空间,其中只有被绑定的路径可见
  • 网络:运行在未共享的网络命名空间中;流量经由 socat 的 Unix 套接字桥到达主机代理
  • 进程:PID 命名空间隔离

要求:

  • 已安装 bubblewrap 和 socat(Debian/Ubuntu 上为 sudo apt-get install bubblewrap socat)
  • 内核命名空间支持(现代 Linux 上为标准配置)

Bubblewrap 可以遮蔽一个已存在的路径,但无法禁止可写绑定内新增的 .env、*.key 或 *.pem 文件。标准派发策略要求这些基于文件名的通配符拒绝,因此原生智能体派发目前在 Linux/WSL2 上即便探测发现 bubblewrap 也会失败关闭。它不会以静默削弱的策略运行。要恢复 Linux 原生派发,需要一个具备创建时文件名规则的后端。关闭沙盒是一种显式的无限制退出选择,而不是等效的变通办法。

没有沙盒。进程直接运行在主机上,拥有主机的文件系统、网络和进程访问权限。ACP 和运行器的 bash 工具仍会获得环境净化和命令策略;经由 NoSandboxAdapter 的 Claude Code 则继承主机环境。这就是在没有可用后端或设置了 --sandbox off 的主机上智能体派发所回退到的状态,也是客户端未指名任何后端时终端的默认值。

终端按自己的规则被沙盒包裹,而不是由派发探测决定。TerminalSessionService 的 defaultBackend 是 none,而 cc_server 不会覆盖它,因此终端会话运行的是主机 shell,除非客户端显式请求 native——桌面端的终端面板不会这么做。

请求 native 时,shell 会在一个固定的终端配置下经由 SandboxManager 包裹:$HOME 和会话的工作目录可写,网络开放,而 ~/.ssh、~/.aws、~/.gnupg、~/.config/gh 和 ~/Library/Keychains 被拒绝读取。只读代码托管令牌和推送网关不适用——那是派发关心的事。如果无法进行包裹,会话会失败,而不是在主机 shell 的 PTY 上显示一个原生徽标。

沙盒启动时,出站网络访问由进程内代理强制执行,而不是靠内核防火墙规则。在没有可用后端的主机上,这一切都不适用,被启动的进程会与服务器用户本人完全一样地访问网络。

  • 沙盒运行时在 cc_server 进程内部于 127.0.0.1 上运行一个 HTTP/HTTPS 代理和一个 SOCKS5 代理,每个受限会话一对(http_proxy.dart、socks_proxy.dart)。并发运行不共享同一对。
  • 被沙盒包裹的进程会收到代理环境变量(HTTP_PROXY/HTTPS_PROXY → HTTP 代理,ALL_PROXY → SOCKS 代理;GIT_SSH_COMMAND 让 git-over-SSH 走 SOCKS 代理)。在 Linux 上网络命名空间是未共享的,因此代理是唯一的出路;在 macOS 上沙盒与主机共享回环,可以直接与它们通信。
  • 每个连接的目标主机都会与会话的域名允许列表比对;其他一切都被拒绝。有两种通配符形式,各自恰好含一个 *:前缀式 *.example.com 匹配任意子域名(包括 a.b.example.com),但不匹配裸域本身;中间标签式 bedrock.*.amazonaws.com 恰好匹配一个标签(bedrock.us-east-1.amazonaws.com,而不是 bedrock.evil.attacker.amazonaws.com)。
  • 允许列表按会话从经过筛选的基线(kBaselineAllowedDomains:LLM 提供方、Git 托管、软件包注册表)解析而来,再加上 GitHub 域名(kGithubDomains),以及 SandboxSpec.egressAllowlist。派发路径从不传入额外的 egressAllowlist 条目,因此实际上每次运行得到的都是基线加上 GitHub。
  • 一份固定的拒绝列表(kBaselineDeniedDomains:云元数据端点、遥测接收端)优先于所有允许项。
  • networkEnabled: false 仍会启动按会话的代理,但允许列表为空,因此所有出站连接都会被拒绝。派发在运行开始时根据访问网络规则设置该值:拒绝会关闭网络,允许和先询问会让它保持开启。

传给 SandboxPort.launch 的规范:

字段 类型 说明
sessionId String 稳定的会话标识符(通常是空间 id)
workspaceId String 所属工作区 id
agentId String? 当前绑定到该沙盒的智能体(如果有)
bindMounts List<SandboxBindMount> 主机→客户机挂载;沙盒只会看到这里列出的主机路径
networkEnabled bool(默认 true) 派发时根据网络出站规则设置(只有解析为拒绝时才关闭)。关闭时,按会话的代理仍会启动,但允许列表为空,因此所有出站连接都会被拒绝
egressAllowlist List<String>(默认 []) 额外允许的出站域名;支持通配符(*.example.com,外加一种单标签的中间形式)。派发从不填充它,因此被包裹的运行得到的是经过筛选的基线加上 GitHub 域名
guestWorkdir String? 沙盒内的默认工作目录;单次 exec 调用可以覆盖它
mode Mode(默认 chat) 划定文件系统写入规则:chat 保持绑定挂载可写;review、plan 和 orchestrate 为只读
protectedPaths List<String>(默认 []) 在任何模式下都绝不允许在沙盒内可写的主机路径
runnerStateDirs List<String>(默认 []) 运行器存放自身状态的主机目录,在所有模式下都可写(当前是 Control Center 管理的 CLAUDE_CONFIG_DIR)
execGrantRoots List<String>(默认 []) 运维人员批准、用来在 macOS 上重新打开可写目录 exec 阻止的目录树。Linux 忽略它们——bwrap 没有可供重新打开的 $HOME exec 阻止

protectedPaths 保存着已注册的原始仓库检出,在每次派发时从工作区的仓库解析而来。智能体只在按对话划分的写时复制工作树中工作,因此原始检出从沙盒内部绝不能可写——在 macOS 上,Seatbelt 对 $HOME 的一揽子写入放行否则会让它们经由 bash 仍可写。它们在所有模式下都会加入机密拒绝写入集合,chat 也不例外。

每一条拒绝读取和拒绝写入路径都会同时以它的字面写法和经符号链接解析后的写法输出。内核是按解析后的路径匹配拒绝规则的,因此针对符号链接路径写下的规则永远不会触发——而这在 macOS 上是常态而非特例,因为 /tmp 会解析为 /private/tmp,/var/folders/… 会解析为 /private/var/folders/…。包含 * 或 ? 的条目会被当作通配符模式,保持原样;无法解析的路径则按原样保留。

字段 类型 说明
hostPath String 主机上的绝对路径
guestPath String 沙盒内的路径(使用原生沙盒时它等于 hostPath;保留它是为了适配器的对称性)
readOnly bool(默认 false) 为 true 时以只读方式挂载

由 SandboxPort.launch 返回:

字段 类型 说明
sessionId String 会话标识符
backend SandboxBackend 正在运行的是哪个后端
state SandboxState 生命周期状态
error Object? 当 state 为 error 时设置
details Map<String, Object?> 适配器私有的簿记信息(工作目录、PID、配置路径等)
状态 说明
created 规范已创建,但 VM 尚未启动
warm 正在运行,可以执行命令
active 当前正在执行命令
suspended 已暂停 / 已检查点保存到磁盘以节省 RAM
destroyed 已拆除;句柄不再有效
error 启动失败——见 SandboxHandle.error
事件类型 说明
stdout 来自沙盒的标准输出
stderr 来自沙盒的标准错误
exit 沙盒进程已退出(携带 exitCode)
starting 沙盒正在启动
ready 沙盒已准备好接受 exec 调用
killed 进程被主机终止(OOM、超时、手动停止)
violation 有一条沙盒策略被违反(携带 SandboxViolation)
字段 类型 说明
action String 被拒绝的动词(例如 file-read*、network-outbound)
target String 被拒绝操作的目标路径或主机
raw String? 原始日志行,便于调试