跳至內容

沙箱與保安

代理會代你讀取檔案、執行 shell 指令,並呼叫外部 API。你真正在意的失敗很少出於惡意 —— 而是代理看錯路徑、改寫了你的工作副本,或推送了一條你還沒審查完的分支。所以產品必須回答的問題不是「代理是否可信」,而是「它出錯時的波及範圍有多大」。

Control Center 以分層的方式回答這個問題,而這些分層刻意彼此獨立。缺了一層,其餘各層仍然成立。這點很重要,因為最外層 —— 作業系統沙箱 —— 是唯一有條件的:它取決於主機是否提供後端,而在 Windows 上沒有任何主機提供。

無論有無沙箱,在任何主機上,都有五件事約束著代理執行。它們全都不是核心邊界;但它們全都真實有效。當主機提供後端時,作業系統沙箱會置於它們之上,至於這帶來什麼改變,下方「沙箱增添什麼、又止於何處」一節會說明。

它自己的寫入時複製 worktree。 每個對話都會為每個已連結的程式庫佈建自己的工作副本,透過內建的 rift 原生執行檔以寫入時複製方式複製,而非逐位元組複製,因此隔離便宜得足以作為預設。你加入工作區的已註冊程式庫,並不是代理實際工作的目錄 —— 兩個碰觸同一程式庫的對話看不見彼此的修改,而兩者都沒有在編輯你的工作副本。

這裡的粒度值得說得精確:worktree 以對話為單位,而非以代理為單位。 同一個對話中的兩個代理會得到各自不同的覆蓋層工作目錄,而這些目錄 共用 該對話的 repos/ —— 這正是重點,因為它們正在協作同一項變更 —— 而它們各自的私人代理目錄則保持分開。凡 rift 有出貨的地方,寫入時複製就是唯一的後端:無法 reflink 的檔案系統,或資料目錄與程式庫位於不同磁碟區,都會令佈建失敗,而不是退回其他方式。Windows 是例外 —— 那裡沒有寫入時複製後端,所以後端是 git worktree,而不是悄然降級。

內建執行框架檔案工具中的路徑限制。 執行框架的 read、write、edit、apply_patch、search、find 與 search_files 工具,會以該次執行的工作目錄加上共用根目錄 —— 對話的 repos/ 目錄(worktree 所在之處),以及空間的附件目錄(若存在)—— 來解析每一個路徑,並拒絕任何落在範圍以外的。絕對路徑只有在原本就位於範圍內時才被接受。worktree 內指向外部的符號連結也會被解析並拒絕。這就是即使完全不涉及沙箱,代理也無法 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 工具是另一個需要說清楚的地方。它的指令會先對照模式的指令政策接受評估,然後在已淨化的環境中、以 worktree 作為工作目錄被啟動。若有後端存在,該次啟動會被包裝,因此是沙箱阻止 shell 指令寫入 worktree 之外;若沒有後端 —— Windows,或沒有 bwrap 與 socat 的 Linux —— 指令政策就是指令與磁碟其餘部分之間的唯一屏障。在 chat 模式中,政策會硬性拒絕一份固定清單(權限提升、破壞系統與磁碟、逃逸容器與命名空間),並就遠端狀態的變更(git push、gh pr create、套件發佈與安裝)彈出提示。兩份清單都沒有列的,則會執行。shell 管線、鏈式指令與子 shell 都會被解析,每個子指令分別評估,並以最嚴格的判定為準。

Claude Code 作為配接器使用時,是沙箱承擔最多重量的一種情況。Control Center 以 --dangerously-skip-permissions 啟動它,因為非互動式的 claude -p 否則會永遠卡在自己的核准提示上。它的讀取、寫入與編輯呼叫都在自己的行程內發生,Control Center 從不會看見。它的 Bash 呼叫會在執行前送到伺服器上的一個鉤子,由鉤子把命令列分類並詢問對應的規則;Control Center 本身提供的 mcp__* 工具則照常通過閘門。若有後端存在,那些看不見的呼叫仍然在沙箱包裝之內發生,因此作業系統就是圍住它們的界線。若沒有後端,就甚麼都沒有。每個配接器都會在其強制執行矩陣中說明這一點 —— 請見配接器誠實矩陣。

不存在按代理設定的權限旗標。代理能否建立提交、推送、開啟 pull request、寫入追蹤器或存取網絡,由護欄決定 —— 允許、先詢問或拒絕 —— 無論由哪個配接器運行代理,都適用同樣的規則。

推送是即使面對試圖繞過規則的代理也必須守住的動作,所以它是靠代理手中持有甚麼來強制執行,而不是靠它執行了甚麼。設有 GitHub App 時,在 GitHub worktree 中的執行會取得一個限定單一程式庫的安裝權杖,它能讀取內容、處理 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: …)

閘道只會把推送轉送到 worktree 自己的程式庫,或與工作區連結的 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 是用於互動式終端機與測試機的隔離式 VM 後端,而不是 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,並發出同一種事件類型 —— 沒有系統日誌截取。這些會送達該次執行自己的事件串流,因此被拒的系統呼叫會出現在對話紀錄中,而不只是在日誌裡。遙測只在原生沙箱真正包裝著行程時才存在 —— 在沒有後端的主機上,沒有東西可觀察。它也與護欄無關:違規來自被拒的系統呼叫,永遠不會來自規則查詢。

沙箱沒有核准路徑。它啟動、執行、發出事件然後拆除;它裡面沒有任何人類可以回答問題的地方。當你在空間中看到一個確認提示 —— 核准這次推送、核准這次刪除 —— 它是由護欄引擎(或執行框架的指令政策)在動作根本尚未到達執行階段 之前 提出的。

這個順序是刻意的。在系統呼叫之後才發生的核准,會是一筆稽核記錄,而不是一道閘門。這也是失敗即拒絕的規則存在於護欄之中的原因:在沒有連接核准人時,決定會在任何東西執行之前作出,而且是一項拒絕。

診斷代理是唯一說明哪些項目可以手動檢查的地方。

  • 測試機與隔離環境:沙箱之上的一步 —— 當工作需要一部完整、用完即棄、帶核心邊界的機器,而不是一個受包裝的行程
  • 護欄:作業系統底線之上的政策層,也是核准真正來自的地方
  • 代理派發生命週期:隔離在派發流程中佔的位置
  • 模式:模式如何收窄工具表面與指令政策
  • 沙箱後端參考:各後端及其設定