跳至內容

護欄——每個變更操作都套用同一套政策

Control Center 的代理可以刪除檔案、推送分支、發佈 pull request,以及呼叫外部 API。這些各自屬於不同_類別_的風險,而長久以來只有其中一種——shell 指令——有政策規範。護欄正是為了統一回答一個問題而存在,適用於代理可採取的每一個變更操作,不論它由哪個執行環境或工具提出:這項操作是允許、需要核准,還是被拒絕?

政策引擎只有一個。凡是操作可能離開系統的位置,都會查詢它:內建框架的工具註冊表、伺服器端的 MCP 分派閘門,以及程式庫操作的變更。因此,寫一次的規則同時適用於內建執行環境與外部代理 CLI——不存在第二條更隱蔽的繞道。舊有的純 bash 指令政策仍然有效:規則可以改為比對指令前綴,而非操作類別,兩種形式並存於同一個儲存庫。

然而,這個引擎不能取代作業系統沙箱,沙箱也不能取代它。兩者回答不同的問題:護欄決定 Control Center 看得見的呼叫是否獲准發生,沙箱則界定隨之而來的程序可以觸及何處。主機提供後端時,cc_server 會套用沙箱——但 Windows 不提供任何後端,而 Linux 需要 bwrap 和 socat。在這些主機上,護欄就是最外層的強制執行機制,而不是一層架在底板之上的政策;在你決定要放寬到甚麼程度時,這一點值得留意。

每個變更工具都會宣告自己屬於哪些 ActionClass,這些類別來自一個由十三個效果類別組成的封閉集合。其中五個開箱即設為先詢問;其餘則允許,要改動就得靠規則。

類別 內建預設 涵蓋範圍
fileDelete 先詢問 刪除檔案(刪除形式的寫入,rm)
gitPush 先詢問 推送至 git 遠端
prCreate 先詢問 建立 pull request
prPublish 先詢問 發佈審查、合併 PR
vendorSyncWrite 先詢問 寫入外部工單供應商(Linear、Jira、GitHub 同步)
fileWriteOutsideWorktree 允許 在隔離 worktree 之外寫入檔案
gitCommit 允許 建立 git 提交
networkEgress 允許 網絡出口(網頁抓取、任意 HTTP)
secretAccess 允許 讀取密鑰或憑證
packageInstall 允許 安裝套件(npm、pip、pub、brew)
processSpawn 允許 啟動程序(shell 及任何經 shell 執行的東西)
workspaceMutation 允許 變更工作區結構(程式庫、空間、代理)
enclosureControl 允許 操控隔離環境(測試機):啟動 VM、向它傳送輸入

這八個預設允許的類別,是基於沙箱底板在其下生效的假設而選定的。主機提供後端時確實如此,但在 Windows 或缺少 bwrap 和 socat 的 Linux 機器上並非如此——所以若你想在那些環境為 fileWriteOutsideWorktree 或 packageInstall 加設閘門,就得自己寫一條規則。

這個集合是刻意封閉的。新的變更工具若不宣告任何類別,就無法通過棘輪測試,因此分類體系只能經明確而經過論證的決定才得以擴充——「分類體系失控膨脹,就是這項功能的死因。」十三個類別足以表達人們實際會寫的政策(未經詢問絕不推送、絕不在 worktree 之外刪除、一律允許讀取),同時又少得讓設定介面保持易懂。

一條規則將 (scope, ActionClass | command prefix) → allow | prompt | deny 對應起來。範圍由最具體到最概括層層嵌套:

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

判定以具體程度優先:第一個有相符規則的範圍作出決定,判定即在此停止。最嚴格只是平手時的決勝條件——用於同一範圍內兩條具體程度相同的規則之間,以及在合併單一操作所宣告的多個類別時。在同一個範圍內,指令規則以最長的指令前綴作比對。在任何地方,允許都不會壓過拒絕。

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

  • 失效即拒絕的詢問。 在沒有連接核准人的情況下,prompt 會判定為 拒絕。沒有回答的問題永遠不等於同意。
  • 最嚴格合併。 橫跨多個類別的操作,會以最嚴格的方式合併;當多個類別都要求先詢問,一次確認會全部列出。

系統刻意不設逐回合鎖存。每次工具呼叫都會從頭重新判定政策,因此同一操作重複詢問的次數,是由操作者自己選擇「記住這個決定」所限制,而非任何以單一回合為範圍的機制。

規則可以約束操作的參數,而不只是它的動作。「可以推送」與「可以推送到 feature/*」是兩種不同的宣稱,只有後者才是管控——只授權動作本身的能力閘門,正是大多數已公開代理事故的成因。

約束可以指定:

  • 路徑——glob 模式(repos/**、!**/.env)
  • ref——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)。這個畫面有撰寫規則的政策矩陣、政策範本、假設情境探測、授權稽核軌跡,以及配接器誠實矩陣。

探測會取一個操作類別(或一條指令)和一個範圍,顯示是哪條規則作出決定及其原因。由於判定是純粹而確定性的——同樣的操作、同樣的範圍,就有同樣的決定——探測得出的答案,就是代理會得到的答案。