跳转到内容

护栏——为每一种会改变状态的操作统一策略

Control Center 中的智能体可以删除文件、推送分支、发布 pull request, 也可以调用外部 API。这些操作各自属于不同 种类 的风险,而在很长一段时间 里,其中只有一种——shell 命令——有策略约束。护栏的存在,就是为了对智能体 能执行的每一种会改变状态的操作,统一回答同一个问题,无论它是哪个运行时或 哪个工具提出的:这是被允许的、需要批准的,还是被拒绝的?

策略引擎有且只有一个。凡是操作可能离开系统的位置,都会向它查询:内置运行 器的工具注册表、服务器端的 MCP 调度器门禁,以及仓库操作的变更。因此,一条 写下的规则会同时作用于内置运行时和外部智能体 CLI——不存在第二条更隐蔽的 绕行路径。更早的、仅针对 bash 的命令策略仍然有效:规则可以匹配命令前缀, 而不是操作类别,这两种形式共存于同一个存储中。

不过,这套引擎不能替代操作系统沙盒,沙盒也不能替代它。它们回答的是不同的 问题:护栏决定一次 Control Center 看得见的调用究竟能否发生,而沙盒限定由此 产生的进程可以触达哪里。当宿主机提供后端时,cc_server 会应用沙盒——但 Windows 不提供任何后端,而 Linux 需要 bwrap 和 socat。 在这类宿主机上,护栏是最外层的强制执行层,而不是坐落在一层底线之上的策略; 在你决定要把尺度放多宽时,这一点值得了解。

每一种会改变状态的工具都会声明自己属于哪些 ActionClasses,取值来自一个 封闭的、由十三种效果类别组成的集合。其中五种默认先询问;其余默认允许, 需要靠规则来改变。

类别 内置默认值 涵盖内容
fileDelete 先询问 删除文件(形如删除的写入,rm)
gitPush 先询问 推送到 git 远程仓库
prCreate 先询问 创建 pull request
prPublish 先询问 发布评审、合并 PR
vendorSyncWrite 先询问 写入外部工单服务商(Linear、Jira、GitHub 同步)
fileWriteOutsideWorktree 允许 写入隔离工作树之外的文件
gitCommit 允许 创建 git 提交
networkEgress 允许 网络出站(网页抓取、任意 HTTP)
secretAccess 允许 读取密钥或凭据
packageInstall 允许 安装软件包(npm、pip、pub、brew)
processSpawn 允许 派生进程(shell 以及任何会调用 shell 的操作)
workspaceMutation 允许 改变工作区结构(仓库、空间、智能体)
enclosureControl 允许 操控隔离舱(rig):启动虚拟机、向它发送输入

那八个默认允许的类别,是在假设其下存在沙盒底线的前提下选定的。在宿主机提供 后端的场合,这条底线确实存在;而在 Windows 上,或者在缺少 bwrap 和 socat 的 Linux 机器上,它并不存在——所以如果你希望在这些环境里给 fileWriteOutsideWorktree 或 packageInstall 加上门禁,就必须自己写一条 规则。

这个集合是刻意封闭的。新的会改变状态的工具若不声明任何类别,就无法通过棘轮 测试,因此分类法只能通过明确且有论证的决定来扩张——“分类法的蔓延会杀死这个 功能”。十三个类别,足以表达人们真正会写的策略(未经询问绝不推送、绝不在 工作树之外删除、读取一律允许),又少到足以让设置界面保持可理解。

一条规则把 (scope, ActionClass | command prefix) → allow | prompt | deny 映射成一个决定。作用域从最具体到最宽泛依次嵌套:

空间 > 智能体 > 工作区 > 模式预设 > 内置默认值

解析遵循具体优先:第一个命中规则的作用域作出决定,解析就此停止。最严格 只是平局时的裁决依据——包括同一作用域内两条同样具体的规则之间,以及把单个 操作声明的多个类别合并时。在同一个作用域内,命令规则按最长的命令前缀匹配。 在任何地方,允许都压不过拒绝。

有两项决定带有硬性保证:

  • 先询问的失效即拒绝。 没有批准人连接时,prompt 决定会解析为 拒绝。 没有得到回答的问题永远不算“是”。
  • 最严格组合。 一个操作若跨越多个类别,这些类别会按最严格的方式合并; 当多个类别都要求先询问时,一次确认会把它们全部列出。

这里刻意不设按轮次生效的闩锁。每一次工具调用都会从头重新解析策略,因此同一个 操作被反复询问,只受限于操作者自己是否选择“记住这个决定”,而不受任何以轮次 为范围的东西限制。

规则约束的不只是一个操作的动作词,还可以是它的参数。“可以推送”和“可以推送 到 feature/*”是两种不同的主张,只有后者才是一项管控——只授权动作词的能力 门禁,正是大多数已公开的智能体事故背后的形态。

约束可以指定:

  • 路径——glob 模式(repos/**、!**/.env)
  • 引用——git ref,可用 ! 取反(['**', '!main'] 读作“除 main 之外的 任何分支”)
  • 主机——网络目标,支持子域通配(*.internal)
  • 命令——命令前缀,按词边界匹配
  • 上限——maxCount / maxCents,因此“最多删除 50 个文件”也能表达 出来

保持这套文法封闭且无环,会带出三个特性,而这三者正是重点:每条规则都能表述 成一句可以读回来的话;每次拒绝都能指出命中的是哪个约束;整份策略都可以被 枚举——你能在智能体运行 之前 看到它可以做什么。

有三条规则很重要:

  • 在同一作用域下,带约束的规则比不带约束的规则更具体,因此“拒绝对 main 的推送”无需任何人考虑顺序,就能压过“允许推送”。
  • 限制型规则(拒绝/先询问)在请求触及它所列出的 任何 一个值时生效; 放行型规则(允许)只在请求 完全 落在其范围内时生效。若把两者按同一 种方式解读,智能体就能把一条被禁止的路径和一条无辜的路径打包在一起,把 前者洗白。
  • 如果规则指定了某个维度,而请求对它只字未提,这条规则既不会被应用,也不会 被跳过:它会升级为一次批准。静默跳过会让每一条保护分支的规则,对那些 不上报自身 ref 的工具彻底失效;静默拒绝则会拒绝提取器无法描述的所有推送。 在没有批准人连接时,既有的失效即拒绝规则会把这次升级变成拒绝。

commandPrefix 规则按词边界匹配:git push 能覆盖 git push origin main,但覆盖不了 git pushx。它不对 shell 做规范化。 git push(两个空格)、cd sub && git push 和 GIT_DIR=… git push 都是 不同的字符串,没有一条能命中关于 git push 的规则。

这是一个真实的局限,我们把它讲明,而不是粉饰过去:命令规则是操作者用来塑造 智能体日常行为的便利手段,而不是对抗蓄意攻击者的边界。真正的边界是效果 类别(由工具自己声明,且无法改写措辞)、策略存储之下的模式命令网,以及位于 两者之下的操作系统沙盒。一条解析为 allow 的命令规则绝不会放松其中任何 一道——模式命令网会在其后运行,它的 deny 是最终决定。

用“记住这个”来回答一次询问,会写入一条真正的策略规则:它带有作用域(空间/ 智能体/工作区)、受参数约束(批准一次推送到 feature/login,授予的是 feature/**,而不是 main),并会在明确的 TTL 到期时自动撤销。答应 接下来几个小时的请求,并不等于悄悄改写你的永久策略;它写入的规则会落在同一 个存储中,带有 remembered 来源和到期时间,因此可以提前撤销。

每条规则都带有一种级别:

  • 建议——允许,但记录该规则命中过。这是一条落地路径:先把一条严格规则 以建议级别铺开,读一周的审计追踪,看看它 本会 拦住什么,然后再把它提升。 没有这一层级,严格策略永远不会被真正打开。
  • 软性——拒绝,但持有覆盖权限的人可以覆盖,并需记录理由。覆盖本身也是 一次被审计的事件。
  • 硬性(默认)——拒绝。任何东西都无法覆盖它。

安装操作者可以钉住一些策略,任何工作区管理员都无法放松它们。托管规则存放在 服务器端,并与每个工作区自身的规则链按最严格方式合并——它们并不是位于 规则链之首的一个作用域,因为一条位于链首的托管 allow 会让安装级设置覆盖 工作区的拒绝,而钳制的目的恰恰相反。因此,托管规则只可能收紧,不可能放松。

把 CC_SERVER_MANAGED_POLICY 指向一个 JSON 文件,其优先级会完全高于已存储 的规则;正因如此,操作者才能钉住一种任何管理界面都无法翻转的姿态——这就是 “我能阻止开发者关掉这些安全控制吗?”的答案。

自主程度旋钮是一套配置档,而不是一套并行系统

标题为“自主程度旋钮是一套配置档,而不是一套并行系统”的部分

按空间设置的自主程度旋钮,是同一份策略存储之上的一套具名配置档,而不是第二 套机制。它的三档设置在存储和传输时分别是 proposeOnly、actWithApproval 和 actFreely(带连字符的形式只用于界面文案,autonomy.setForSpace 会拒绝 其他任何取值)。不设置它时,行为等同于 actWithApproval。

每一档对已解析出的决定会做什么:

  • proposeOnly 直接拒绝所有受门禁管控的工具。智能体仍然可以推理和 回复;它只是不能行动,并且它的消息会说明原因。
  • actWithApproval(以及未设置)就是上文描述的失效即拒绝批准门禁。
  • actFreely 会对一切未解析为硬性 deny 的操作预先批准。

最后一档值得直白说明,而不是轻描淡写:在 actFreely 下,prompt 决定 不会升级给批准人——它直接被允许,不会询问任何人。只有显式的 deny 规则 能在这档下存活。因此 actFreely 是一次刻意的自主权授予,而不是一个图方便的 开关;而失效即拒绝规则恰好保护了真正要紧的那种情况(actWithApproval, 也就是默认值)。

智能体之间的委派,在 delegate_task 这一卡点由全部四道防线把守:深度上限 (默认 3)、循环检测、自主程度上限(受托方行动时的自主程度永远不能高于 其委派方——权限无法通过把工作交给一个更自由的智能体来洗白),以及预算 封套(委派会从委派方的剩余预算中扣减,且无法凭空造出更多预算)。任何一项 被触发都会被高声拒绝,并把对应防线的理由原样返回给智能体。

并非每个运行时都允许 Control Center 拦截每一个操作。外部智能体 CLI 会在进程 内原生地运行某些工具,那里无法设置任何门禁。与其假装并非如此,每个适配器都 带有一份诚实度矩阵:对每个适配器而言,Control Center 是否筛选工具面、是否 拦截工具调用、是否观测成果物契约、能否看到运行器自带的工具,以及进程内工具 是否受沙盒配置档的约束。

在信任某个配置之前,有两行值得一读。内置运行器声明它的进程内文件工具不受 沙盒约束——工具面和这套策略引擎是它仅有的文件系统边界。Claude Code 声明它 自己的工具调用不可拦截,因为它以 --dangerously-skip-permissions 启动; Control Center 能看到它的 mcp__* 调用,以及经由钩子的 Bash 命令行,而它 自带的读取、写入和编辑工具则在无人察觉的情况下运行。

设置界面会展示这份矩阵,因为对一个护栏系统来说,唯一不可原谅的失败,就是 声称拥有并不存在的覆盖范围。

护栏位于 设置 → 工作区 → 智能体权限 (/workspaces/<id>/settings/workspace/permissions)。该界面包含用于编写 规则的策略矩阵、策略模板、预演探针、授权审计追踪,以及适配器诚实度矩阵。

探针接受一个操作类别(或一条命令)和一个作用域,并显示是哪条规则作出的决定、 以及为什么。由于解析是纯粹且确定性的——同样的操作、同样的作用域、同样的 决定——探针给出的答案就是智能体将会得到的答案。