沙箱與資安
為什麼這裡需要隔離
標題為「為什麼這裡需要隔離」的章節代理會代你讀取檔案、執行 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__* 工具則照常
通過閘門。有後端時,那些看不見的呼叫仍然發生在沙箱包裝之內,因此作業系統就是
它們周圍的邊界。沒有後端時,就什麼都沒有。每個配接器都在自己的強制執行矩陣中
說明這一點 — 請見配接器誠實度
矩陣。
推送經由 Control Center
標題為「推送經由 Control Center」的章節不存在依代理設定的權限旗標。代理能否建立提交、推送、開啟 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
是供互動式終端機與機台使用的封閉式 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,並發出相同的
事件類型 — 那裡沒有系統日誌掛鉤。這些事件會進入該次執行自己的事件串流,因此
被拒絕的系統呼叫會出現在對話紀錄中,而不只是出現在日誌裡。這套遙測只在原生
沙箱確實包裝著行程時才存在 — 在沒有後端的主機上沒有東西可觀測。它與護欄也
無關:違規來自被拒絕的系統呼叫,而從不來自規則查詢。
確認來自護欄,而不是沙箱
標題為「確認來自護欄,而不是沙箱」的章節沙箱沒有核准途徑。它啟動、執行、發出事件然後拆除;其中沒有任何地方能讓人 回答問題。當你在空間裡看到一則確認 — 核准這次推送、核准這次刪除 — 它是由 護欄引擎(或執行環境的命令政策)在動作真正到達執行環境_之前_就提出的。
這個順序是刻意的。發生在系統呼叫之後的核准只會是一筆稽核記錄,而不是一道 閘門。這也正是「失效即拒絕」規則存在於護欄之中的原因:在沒有連接任何核准者 時,判定會在任何東西執行之前就做出,而且是拒絕。
診斷代理 是唯一說明哪些內容可以手動檢查 的地方。
相關概念
標題為「相關概念」的章節- 機台與隔離艙:沙箱之上的一步 — 當工作需要一台 有核心邊界、用完即丟的完整機器,而不是一個被包裝的行程時
- 護欄:作業系統底線之上的政策層,也是核准真正 來自的地方
- 代理派送生命週期:隔離在派送流程中的位置
- 模式:模式如何收窄工具面與命令政策
- 沙箱後端:各後端與其設定