沙盒与安全
为什么这里需要隔离
标题为“为什么这里需要隔离”的部分智能体会代你读取文件、运行 shell 命令并调用外部 API。你真正担心的故障很少 出自恶意——更常见的是一个智能体看错了路径,从而改写你正在使用的检出,或者 推送了一个你还没审查完的分支。所以产品必须回答的问题不是“这个智能体是否 值得信任”,而是“它出错时的爆炸半径有多大”。
Control Center 用分层来回答这个问题,而且这些层刻意彼此独立。即便缺了一层, 其余各层仍然成立。这一点很重要,因为最外层——操作系统沙盒——是唯一有条件的 一层:它取决于主机是否提供后端,而在 Windows 上没有任何主机会提供。
究竟是什么在约束智能体
标题为“究竟是什么在约束智能体”的部分无论有没有沙盒,每一次智能体运行都受五样东西约束。它们都不是内核边界, 但每一样都真实有效。当主机提供后端时,操作系统沙盒位于它们之上, 它对整体有什么改变,见下文的“沙盒增加了什么,又在哪里停下”。
它自己的写时复制工作树。 每次对话都会为每个已链接的仓库准备自己的检出,
通过内置的 rift 原生实现以写时复制的方式克隆,而不是逐字节复制,因此隔离
足够廉价,可以成为默认做法。你添加到工作区的已注册仓库并不是智能体工作的
目录——两个接触同一仓库的对话看不到彼此的修改,而且两者都不是在编辑你的检出。
这个粒度值得说清楚:工作树是按对话划分的,而不是按智能体划分的。 同一
对话中的两个智能体会得到各自不同的覆盖层工作目录,它们_共享_该对话的
repos/——这正是重点所在,因为它们是在协作完成同一处改动——而它们各自的
私有智能体目录保持分离。凡是随附 rift 的地方,写时复制都是唯一后端:无法
reflink 的文件系统,或者与仓库不在同一卷上的数据目录,会让这次准备直接失败,
而不是回退。Windows 是例外——那里没有写时复制后端,因此 git worktree 就是
后端,而不是静默降级。
内置运行器的文件工具中的路径限制。 运行器的 read、write、edit、
apply_patch、search、find 和 search_files 工具会基于本次运行的工作目录
加上共享根目录——即对话的 repos/ 目录(工作树所在之处),以及空间存在时的
附件目录——来解析每一个路径,并拒绝任何落在其外的路径。绝对路径只有在原本就
位于其中时才会被接受。工作树内部指向外部的符号链接也会被解析并拒绝。这就是
为什么即使完全没有沙盒,智能体也无法 write 到 ~/.ssh。
模式的工具面。 对话的模式决定这次运行究竟会
具体化哪些工具。在只读模式(plan、review、orchestrate)下,变更类工具
根本不在注册表中,而且这些模式的 shell 命令策略会直接拒绝所有变更命令——
git commit、git push、npm install、docker 以及其他——而不是弹出询问。
不存在的工具无法被调用,这比一个存在但受门控的工具是更强的保证。
统一的动作护栏。 每个变更类工具都会声明它可能产生的影响类别,策略引擎会在
调用真正运行之前把每一个类别解析为允许、询问或拒绝。在没有连接任何审批者时,
prompt 会解析为_拒绝_——一个没人能回答的问题永远不算同意。解析模型见
护栏。
代理签发的凭据与净化过的环境。 凭据在每次启动时签发,并在运行结束时撤销;
一次运行持有的 GitHub 令牌可以读取,但不能推送——见下文“推送经由 Control Center”。
ACP、运行器的 bash 以及处于原生包裹下的 Claude Code,会在进程启动前获得一个
已剥除动态链接器和解释器注入途径(LD_PRELOAD、DYLD_INSERT_LIBRARIES、
NODE_OPTIONS、GIT_SSH_COMMAND 及其同类)的环境。经由 NoSandboxAdapter 的
Claude Code 则继承主机环境。
这些边界在哪里变薄
标题为“这些边界在哪里变薄”的部分运行器的文件工具运行_在服务器进程内_,而不是子进程中,因此即使挂载了沙盒配置,
也没有任何配置能约束它们——只有 bash 会经过沙盒化的命令运行器。对它们的限制
就是上文描述的路径检查,因此这项检查是承重的,而不是第二道防线。
bash 工具是另一处需要说清楚的地方。它的命令会先对照模式的命令策略进行评估,
然后用净化过的环境和作为工作目录的工作树启动。存在后端时,这次启动会被包裹,
因此沙盒才是阻止 shell 命令写到工作树之外的东西;不存在后端时——Windows,
或没有 bwrap 和 socat 的 Linux——命令策略就是命令与磁盘其余部分之间唯一的
东西。在 chat 模式下,该策略会硬拒绝一份固定清单(提权、破坏系统与磁盘、
逃逸容器与命名空间),并对改变远程状态的变更(git push、gh pr create、
软件包发布与安装)弹出询问。两份清单都没有命中的命令会照常运行。管道、命令链
和子 shell 都会被解析,每个子命令单独评估,并以限制最严的一项裁决为准。
Claude Code 作为适配器使用时,正是沙盒分量最重的场景。Control Center 启动它时
带有 --dangerously-skip-permissions,因为非交互式的 claude -p 否则会永远
阻塞在它自己的审批提示上。它的读取、写入和编辑调用都发生在它自己的进程内部,
Control Center 从来看不到。它的 Bash 调用会在运行前被发送到服务器上的一个钩子,
由钩子对命令行分类并询问相应的规则;Control Center 提供的 mcp__* 工具照常经过
门控。存在后端时,那些看不见的调用仍发生在沙盒包裹之内,因此操作系统就是它们
周围的边界。不存在后端时,就什么都没有。每个适配器都在自己的执行矩阵中说明这
一点——见适配器的诚实度
矩阵。
推送经由 Control Center
标题为“推送经由 Control Center”的部分不存在按智能体的权限标志。智能体能否创建提交、推送、打开 pull request、写入 跟踪器或访问网络,由护栏决定——允许、先询问或拒绝—— 无论由哪个适配器运行智能体,都适用同样的规则。
推送是即使面对试图绕过规则的智能体也必须守住的操作,所以它靠智能体手里持有什么
来强制执行,而不是靠它运行了什么。配置了 GitHub App 时,在 GitHub 工作树中的运行
会拿到一个限定到仓库的安装令牌,它能读取内容、处理 pull request,但不能推送。
该运行的 git 配置会把每个 GitHub 推送 URL(https://github.com/、
git@github.com:、ssh://git@github.com/)改写到智能体运行网关——
cc_server 上的一个回环端点。网关读取这次推送更新了哪些 ref,询问推送到远程
仓库规则——因此像“拒绝推送到 main”这样限定分支的规则同样生效——然后才用它
专门为此签发、且从不交给智能体的写入令牌把推送转发到 GitHub。被拒绝的推送会出现
在 git 自己的输出中:
! [remote rejected] main -> main (Control Center policy: …)网关只会把推送转发到工作树自己的仓库,或与工作区关联的 GitHub 仓库。fetch 则用 只读令牌直接访问 GitHub。
其他 shell 命令遇到的是一道更软的门。运行器的 bash 工具,以及经由钩子的
Claude Code Bash 工具,会对每条命令行分类并询问它所属的规则:git commit、
merge、rebase、cherry-pick、revert 和 am 属于创建提交;
gh pr create 属于打开 pull request;gh pr merge、review、comment 和
close 属于发布评审或合并;gh issue create、edit、comment 和 close
属于写入外部跟踪器;安装软件包属于安装软件包;curl 和 wget 属于
访问网络。这能识别一项操作的常规写法,而一个铁了心要隐藏它的智能体(eval、
脚本文件、直接调用 API)可以做到。推送不依赖于此。
沙盒中不会放入任何工单 API 密钥。智能体通过 Control Center 的 MCP 工具写工单, 这些工具会询问写入外部跟踪器规则。
访问网络规则在运行开始时读取一次。沙盒的网络只有开或关两种状态,所以拒绝会
关闭它,而允许和先询问会让它保持开启;运行器的 web_fetch 和 web_search 工具
仍会在每次调用时询问。凭据会在运行拆除时,连同网关签发过的所有写入令牌一起被
撤销,因此句柄不会比运行活得更久。
沙盒增加了什么,又在哪里停下
标题为“沙盒增加了什么,又在哪里停下”的部分SandboxBackend 枚举有三个取值——native(macOS 上是 Seatbelt,Linux 和 WSL2
上是 bubblewrap)、none 和 microvm。智能体命令包裹只使用前两个:microvm
是用于交互式终端和机台的封闭虚拟机后端,而不是对 bash 包裹的静默升级。
理解原生形态,有助于说明挂载后端时你得到了什么,以及它本来就不会给你的东西。
cc_server 在启动时探测主机一次,只要后端有响应,它就会同时接入派发路径上的
两个接缝,因为各传输层并不共用同一个。第一个(SandboxPort)包裹 Claude Code
CLI。第二个(SandboxManager)包裹 ACP 适配器和内置运行器的 bash 工具。
只接入其中一个,会让其余调用在未包裹的情况下启动——而这正是过去的情形。在
没有后端的主机上,或者在 --sandbox off 下,它们全都在未包裹的情况下启动,
服务器会在启动行中说明你属于哪种情况。ACP 和运行器的 bash 仍会净化环境;
经由 NoSandboxAdapter 的 Claude Code 则继承主机环境。
有一个界面刻意不参与:服务器托管的终端按会话接受一个后端参数,而 cc_server
构造它时默认使用主机 shell,因此你从应用打开的终端会以无沙盒方式运行,除非
客户端显式指名 native。那是服务器主机上一个真正的 shell,不是一次智能体运行。
在 Linux 上,bubblewrap 会构建一个挂载命名空间,因此智能体确实只能看到被
绑定进去的内容。在 macOS 上,sandbox-exec 会应用一份 默认宽松 的 Seatbelt
配置——一份完全默认拒绝的配置会让交互式 shell 无法使用,因为需要枚举的 dyld、
XPC 和 Mach 调用数量多得离谱。因此 macOS 配置从 (allow default) 出发,
再划出一系列拒绝:对机密路径的读取;把写策略重置为拒绝,再在其上叠加显式允许;
对每条被拒绝路径及其祖先路径阻止 unlink,这样重命名就无法绕过写入拒绝;对
始终危险的二进制文件以及任何被复制进可写目录的东西执行 exec 拒绝;并把网络
限制在回环代理端口加 DNS。
实际后果是:在 Linux 上,智能体看到的文件系统被收窄了;在 macOS 上,智能体仍然 可以_读取_主机的大部分内容,你已注册的检出靠的是显式的写入和重命名拒绝来保护, 而不是靠不可见。两者都是命名空间级的隔离,而不是内核安全边界——各后端在自己的 能力说明中也是这样讲的。
跨智能体边界正画在绑定挂载集合上。覆盖层派发(Claude Code 和 ACP)最多挂载
四个路径,按运行解析:智能体的覆盖层工作目录以读写方式挂载,智能体的配置目录
以只读方式挂载,对话共享的 repos/ 目录在存在时以读写方式挂载,空间的附件
目录在存在时以只读方式挂载。同级智能体的目录永远不会被挂载。运行器的 bash
包裹更窄:它只绑定挂载本次运行的工作目录。这与运行器的进程内路径检查在软件中
强制的形态一致,所以两者相符——但只有路径检查覆盖运行器自己的文件工具,这些
工具根本不会到达任何挂载命名空间。
在此之上,你已注册的检出会加入拒绝写入集合,涉及所有模式以及全部三条被包裹的
路径(Claude Code、ACP、运行器 bash),而且每条规则都会以字面形式和符号链接
解析后的形式各输出一遍。第二种形式不是锦上添花:
内核是按解析后的路径来匹配拒绝规则的,
而 macOS 让解析成为常态而非特例——/tmp 实际上是 /private/tmp,
/var/folders/… 实际上是 /private/var/folders/…——所以经由其中任一路径到达
的检出,本会在配置看似禁止写入的情况下仍然可写。
在沙盒下,网络是一份按会话生效的 域名 允许列表,由进程内的回环代理强制
执行。出站默认拒绝:网络关闭时,代理仍会启动,但其允许列表为空,因此所有出站
连接都会被拒绝。开启时,解析器会从一份经过筛选的基线——LLM API 提供方、git
托管、软件包注册表、模型目录——再加上 GitHub 的域名,构建出允许列表,并把每个
连接的目标主机与之比对。通配符有效(*.example.com 匹配子域名,不匹配裸域)。
一份单独的始终拒绝列表优先于允许列表,涵盖云元数据端点和遥测接收端。_不可_配置
的是列表本身:规范带有一个用于额外域名的 egressAllowlist 字段,但派发路径中
没有任何东西会填充它,所以实际上每次运行得到的都是基线加上 GitHub,别无其他。
在 macOS 上,拒绝是可观测的:一个监视器会接入系统日志抓取 Sandbox 拒绝记录,
过滤已知噪声,对重试风暴去重,并把每一条作为 SandboxViolation 呈现出来,
说明尝试了什么,以及什么被拦下了。在 Linux 上,包裹路径会解析 stderr 中的
EPERM、Operation not permitted 和 Permission denied,并发出同一事件类型
——那里没有系统日志挂钩。这些事件会进入本次运行自己的事件流,因此被拒绝的系统
调用会出现在转录中,而不只是出现在日志里。遥测只在原生沙盒确实包裹着进程时才
存在——在没有后端的主机上没有什么可观测的。它也与护栏无关:违规来自被拒绝的
系统调用,而从不来自规则查询。
确认来自护栏,而不是沙盒
标题为“确认来自护栏,而不是沙盒”的部分沙盒没有审批路径。它启动、执行、发出事件然后拆除;其中没有任何地方可供人回答 问题。当你在空间里看到一条确认——批准这次推送、批准这次删除——它是由护栏引擎 (或运行器的命令策略)在动作真正到达运行时_之前_就抛出的。
这个顺序是刻意的。发生在系统调用之后的批准只会是一条审计记录,而不是一道闸门。 这也是“失败即关闭”规则存在于护栏中的原因:在没有连接任何审批者时,裁决在任何 东西运行之前就已作出,而且是拒绝。
运行智能体诊断 是唯一说明哪些内容可以 手动检查的地方。
相关概念
标题为“相关概念”的部分- 机台与隔离舱:沙盒之上的一步——当工作需要的是一台带内核边界的一次性整机,而不是一个被包裹的进程时
- 护栏:操作系统底线之上的策略层,以及批准实际上来自哪里
- 智能体派发生命周期:隔离在派发流程中的位置
- 模式:模式如何收窄工具面与命令策略
- 沙盒后端参考:各后端及其配置