跳到內容

護欄——為每個會變更狀態的動作提供同一套政策

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):啟動 VM、向它傳送輸入

這八個預設允許的類別,是在假設其下存在沙箱下限的前提下選定的。在主機 提供後端的地方,這層下限確實存在;但在 Windows 上,或是在缺少 bwrap 和 socat 的 Linux 機器上,它並不存在——所以如果你希望在那裡為 fileWriteOutsideWorktree 或 packageInstall 加上閘門,就必須自己寫 一條規則。

這個集合是刻意封閉的。新的會變更狀態的工具若不宣告任何類別,就無法 通過棘輪測試,因此分類法只能透過明確、經過論證的決定來擴張—— 「分類法蔓延是這個功能的死因」。十三個類別足以表達人們真正會寫的政策 (未經詢問絕不推送、絕不在工作樹之外刪除、讀取一律允許),又少到 足以讓設定介面保持易於理解。

一條規則把 (scope, ActionClass | command prefix) → allow | prompt | deny 對應成一個決定。範圍由最具體到最寬泛依序嵌套:

空間 > 代理 > 工作區 > 模式預設 > 內建預設

解析採具體優先:第一個有符合規則的範圍作出決定,解析就此停止。 最嚴格僅是平手時的裁決依據——用於同一範圍內兩條同樣具體的規則之間, 以及合併單一動作所宣告的多個類別時。在同一個範圍內,命令規則以最長的 命令前綴進行比對。在任何地方,允許都壓不過拒絕。

有兩項決定帶有硬性保證:

  • 先詢問的失效即拒絕。 在沒有核准者連線的情況下,prompt 會解析為 拒絕。沒有得到回答的問題永遠不算「是」。
  • 最嚴格組合。 一個動作若橫跨多個類別,這些類別會以最嚴格的方式 合併;當多個類別都要求先詢問時,一次確認會將它們全部列出。

這裡刻意不設每回合的鎖定機制。每一次工具呼叫都會從頭重新解析政策, 因此同一個動作反覆出現的詢問,是由操作者自己的「記住這個決定」加以 節制,而不是靠任何以回合為範圍的機制。

規則可以約束一個動作的引數,而不只是它的動詞。「可以推送」和 「可以推送到 feature/*」是兩種不同的主張,只有後者才算一種管控—— 只授權動詞的能力閘門,正是大多數已公開代理事故背後的共同形貌。

約束可以指名:

  • 路徑——glob 模式(repos/**、!**/.env)
  • refs——git ref,可用 ! 取反(['**', '!main'] 讀作「除 main 之外的任何分支」)
  • 主機——網路目的地,支援子網域萬用字元(*.internal)
  • 指令——命令前綴,依詞邊界比對
  • 上限——maxCount / maxCents,因此「最多刪除 50 個檔案」也能 表達出來

把文法保持封閉且無迴圈,會帶出三項特性,而這三項正是重點:每一條規則 都能化為一句你可以複述的句子、每一次拒絕都能指出符合的約束,而整套 政策可以被列舉——你可以在代理執行之前就看見它能做什麼。

三條要緊的規則:

  • 在同一範圍內,受約束的規則比未受約束的規則更具體,因此 「拒絕推送到 main」會勝過「允許推送」,沒有人需要去思考順序問題。
  • 限制型規則(deny/prompt)在請求觸及它所指名的任何一個值時 就會生效;寬容型規則(allow)則只有在請求完全落在其中時才 生效。若把兩者讀成同一回事,代理就能把一個無害的路徑與被禁止的路徑 湊在一起,藉此洗白後者。
  • 若某條規則指名了請求完全沒有提及的面向,這條規則既不會被套用,也 不會被略過:它會升級為一次核准。默默略過會讓每一條受保護分支 規則,對不回報自身 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 並附帶到期 時間,因此可以提早撤銷。

每一條規則都帶有一個層級:

  • advisory——允許,但記錄規則曾經符合。這是導入的路徑:先在 advisory 模式下推行一條嚴格規則,讀一個星期的稽核軌跡,看清楚它 原本會擋下什麼,然後再將它升級。少了這個層級,嚴格的政策永遠 不會被開啟。
  • soft——拒絕,但持有覆寫權限的人可以覆寫,並需記錄理由。覆寫 本身也是一次受稽核的事件。
  • hard(預設)——拒絕。沒有任何東西能覆寫它。

安裝的操作者可以釘住某些政策,任何工作區管理員都無法放寬。受管理規則 存放在伺服器端,並與每個工作區自身的規則鏈按最嚴格的方式合併—— 它們並不是位於規則鏈開頭的一個範圍,因為一條位於鏈首的受管理 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)。這個畫面包含 用來撰寫規則的政策矩陣、政策範本、假設情境探測器、授權稽核軌跡,以及 配接器誠實度矩陣。

探測器接受一個動作類別(或一條指令)與一個範圍,並顯示是哪一條規則 作出決定、以及為什麼。由於解析是純粹且確定性的——同樣的動作、同樣的 範圍、同樣的決定——探測器給出的答案,就是代理將會得到的答案。