跳转到内容

沙盒与安全

智能体会代你读取文件、运行 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__* 工具照常经过 门控。存在后端时,那些看不见的调用仍发生在沙盒包裹之内,因此操作系统就是它们 周围的边界。不存在后端时,就什么都没有。每个适配器都在自己的执行矩阵中说明这 一点——见适配器的诚实度 矩阵。

不存在按智能体的权限标志。智能体能否创建提交、推送、打开 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,并发出同一事件类型 ——那里没有系统日志挂钩。这些事件会进入本次运行自己的事件流,因此被拒绝的系统 调用会出现在转录中,而不只是出现在日志里。遥测只在原生沙盒确实包裹着进程时才 存在——在没有后端的主机上没有什么可观测的。它也与护栏无关:违规来自被拒绝的 系统调用,而从不来自规则查询。

沙盒没有审批路径。它启动、执行、发出事件然后拆除;其中没有任何地方可供人回答 问题。当你在空间里看到一条确认——批准这次推送、批准这次删除——它是由护栏引擎 (或运行器的命令策略)在动作真正到达运行时_之前_就抛出的。

这个顺序是刻意的。发生在系统调用之后的批准只会是一条审计记录,而不是一道闸门。 这也是“失败即关闭”规则存在于护栏中的原因:在没有连接任何审批者时,裁决在任何 东西运行之前就已作出,而且是拒绝。

运行智能体诊断 是唯一说明哪些内容可以 手动检查的地方。

  • 机台与隔离舱:沙盒之上的一步——当工作需要的是一台带内核边界的一次性整机,而不是一个被包裹的进程时
  • 护栏:操作系统底线之上的策略层,以及批准实际上来自哪里
  • 智能体派发生命周期:隔离在派发流程中的位置
  • 模式:模式如何收窄工具面与命令策略
  • 沙盒后端参考:各后端及其配置