Skip to content

跟成熟 coding agent 學設計(3):Workspace 隔離與 path policy

2026年8月30日 1 分鐘
TL;DR Rivumi 的 disposable clone 與 SafePathPolicy 保護來源 repo;現在 `--sandbox-checks` 也能用 macOS sandbox-exec、Linux bubblewrap 或 Landlock 包住 verification command,Cloudflare 另有受限 Sandbox slice。但不同 backend 的 network policy、外部 runtime coverage 與 production hardening 仍未一致驗證。
目錄
  1. 設計問題
  2. 五家怎麼做
    1. Codex:OS 級沙箱才是最後防線
    2. Claude Code:permission 層的精細路徑解析
    3. OpenCode:project 邊界 + allow/ask/deny
    4. Pi:把邊界留給 ExecutionEnv
    5. OMP:繼承 Pi,加上 worktree 基線
  3. rivumi 的選擇與差異
  4. 學術依據
  5. 改善路線
  6. 參考資料

🌏 English version

設計問題

Coding agent 的每個檔案工具,第一個參數幾乎都是路徑。這條路徑是 LLM 生成的字串——可能拼錯、可能幻覺、也可能被 prompt injection 推著去讀 .env、寫 ~/.ssh/authorized_keys。所以每個 agent 都得回答同一組問題:

  1. 模型能不能碰到 workspace 以外的東西?
  2. 路徑校驗做在哪一層——工具程式碼、permission 系統、還是作業系統?
  3. 就算校驗被繞過,最壞情況的爆炸半徑是什麼?

這三題答案的組合,決定了一個 agent 是「方便」還是「可信」。這篇把五個成熟專案的答案攤開來比,再對照我在 rivumi 上的選擇。

五家怎麼做

Codex:OS 級沙箱才是最後防線

OpenAI Codex 的態度最明確:path 校驗可以做很多層,但真正的保證來自核心的 sandboxing 模組。macOS 上走 Apple Seatbelt——codex/codex-rs/sandboxing/src/seatbelt.rs#create_seatbelt_command_args 把工作目錄轉成 sandbox-exec 的 policy 參數;底層 policy 寫在 codex/codex-rs/sandboxing/src/seatbelt_base_policy.sbpl,第一行實質規則就是 (deny default),註解直接說明靈感來自 Chrome 的 renderer sandbox。

Linux 上走 Landlock + seccomp:codex/codex-rs/linux-sandbox/src/landlock.rs#install_filesystem_landlock_rules_on_current_thread 把整個 / 設為唯讀、只把 writable_roots 加上讀寫權限,再掛 seccomp filter 擋網路 syscall。最值得學的是 fail-closed 那一行——restrict_self() 之後檢查 RulesetStatus::NotEnforced,如果核心根本不支援 Landlock、policy 沒有真正生效,就直接回 SandboxErr::LandlockRestrict 拒絕執行。「沙箱沒生效」不等於「可以不沙箱」。

Claude Code:permission 層的精細路徑解析

Claude Code 的防線主要在工具與 permission 層。claude-code-source/src/tools/BashTool/pathValidation.ts#validatePath 對 Bash 指令裡出現的每個路徑做展開 tilde、resolve、比對 allowed working directories。有個細節很誠實:原始碼註解明說刻意不做 symlink resolve 再檢查——因為 macOS 的 /tmp/private/tmp 的 symlink,先 resolve 反而會讓危險路徑漏掉。同一個檔案裡的 checkPathConstraintsvalidateOutputRedirections 連輸出重導向都攔。

檔案工具側,FileEditTool/FileEditTool.ts#validateInput 先驗絕對路徑與 settings 檔案的特殊限制,再交給 checkWritePermissionForTool 走 permission 判定;讀取走 FileReadTool/FileReadTool.tscheckReadPermissionForTool。另外它有一個很有意思的設計:EnterWorktreeTool/EnterWorktreeTool.ts#createWorktreeForSession 可以整個 session 切進一個獨立 git worktree——用版本控制做工作區隔離,而不是硬擋路徑。

OpenCode:project 邊界 + allow/ask/deny

OpenCode 把邊界定義在 project instance 上。opencode/packages/opencode/src/project/instance-context.ts#containsPath 回答「這個路徑算不算專案內」:在 ctx.directoryctx.worktree 內就算,而且特別處理了非 git 專案 worktree 設為 / 會匹配所有絕對路徑的地雷。超出邊界的操作會觸發 tool/external-directory.ts 的 external directory 許可流程,最終由 permission/index.ts 的 allow / ask / deny 規則評估決定放行、詢問或拒絕。

Pi:把邊界留給 ExecutionEnv

badlogic/pi-mono 的做法最極簡:工具層只負責把相對路徑正規化成絕對路徑——pi-mono/packages/agent/src/harness/tools/path-utils.ts#resolveToolPath 全部委派給 pi-mono/packages/agent/src/harness/types.ts 定義的 ExecutionEnv.absolutePath() 介面。也就是說 Pi 本體不內建沙箱,邊界由宿主環境(嵌入 Pi 的應用)自己決定。這是刻意的取捨:core 保持小而可測,隔離責任外移。

OMP:繼承 Pi,加上 worktree 基線

oh-my-pi 作為 Pi 的 fork,沿用同一套 ExecutionEnv 路徑模型,但在 autoresearch 工作流補上了 worktree 基線:oh-my-pi/packages/coding-agent/src/autoresearch/index.ts 會記住 baseline commit,discard 時 reset worktree 回基準點,並在不在專用分支上時警告使用者 revert 安全性不完整。

rivumi 的選擇與差異

rivumi M1 的隔離策略是disposable Git workspace,兩個模組撐起來:

第一層:workspace 本身是拋棄式的固定 commit clone。 src/rivumi/runtime.py#LocalGitWorkspace.prepare 要求 base_sha 必須是完整 40 字元 SHA,先 rev-parse --verify 確認 commit 存在,再用 clone --no-hardlinks --no-checkout 複製(no-hardlinks 確保實體檔案分離),detach HEAD 後還要重新驗證 rev-parse HEAD 等於 base_sha 才肯交付。run_dir 也被禁止放在來源 repo 內部。來源 worktree 從頭到尾不被碰,最後產出的是 unstaged patch,由人類審查。

第二層:所有模型給的路徑過 src/rivumi/policy.py#SafePathPolicy.resolve 它拒絕絕對路徑、反斜線、NUL byte、.. traversal、任何段落的 .git,最後 resolve 完還要 relative_to(workspace_root) 確認沒有從 symlink 逃出去。glob 是 segment-aware 的——* 不跨目錄,只有完整的 ** segment 才會跨,避免 src/*.py 意外涵蓋 src/deep/x.py

跟五家比起來,差異很清楚,而且必須誠實講:

  • OS sandbox 已有 verification baseline,但不是整個 runtime 的通用沙箱。 --sandbox-checks 會把 run_check 與 final verification 交給 src/rivumi/runtime.py#resolve_command_sandbox:macOS 用 sandbox-exec,Linux auto 先找 bubblewrap,否則走專案內的 Landlock wrapper;明確要求的 backend 不可用時,command 以 126 fail closed。它包的是已宣告的 verification argv,不代表外部 CLI、provider transport 或所有 host process 都被同一套 policy 包住。
  • 遠端另有受限 container slice。 cloudflare/ 的 Worker/Sandbox control plane 只收 bounded text source map,不收 Git URL、archive、shell string 或 caller credential。這證明隔離邊界能搬進 container,但仍只是受限部署切片,不是 production traffic 與 hostile-code hardening 的完成證明。
  • path policy 比 Pi 嚴、比 Codex 淺。 Pi 把邊界外包,rivumi 和 OpenCode 一樣在應用層劃界,但 rivumi 多了 .git 全面禁入和 symlink escape 的 resolve 檢查——代價是這些檢查全是 Python 字串處理,理論上任何 parser bug 都是逃脫口。OS 沙箱沒有這個問題,因為核心不看字串。

學術依據

SWE-agent 團隊提出的 ACI(agent–computer interface)概念指出:介面設計直接影響 agent 的成功率與安全性,工具邊界不是實作細節而是設計變數(Yang et al., 2024)。Path policy 正是最基礎的 ACI 決策——五家都在「模型表達意圖」與「意圖被執行」之間插了一道程式碼關卡,差別只在關卡的強度。OpenAI 官方也把 sandbox 與 approval 明確列為 Codex 的兩個獨立安全控制(Codex security docs),呼應 defense in depth:應用層校驗降低事故機率,OS 沙箱限制事故傷害。

改善路線

按優先順序:

  1. 把 sandbox coverage 說清楚並持續擴大。 目前有界的是 verification command 和 Cloudflare remote slice;外部 runtime 仍各自擁有執行語意。下一步是逐 runtime 列出 filesystem、process、network 能力,沒有可強制邊界時明確 fail closed 或標為 trusted-local。
  2. 統一網路 egress policy。 bubblewrap 的 network namespace、macOS profile 與 Landlock wrapper 的能力並不完全相同;在 production 宣稱成立前,需要一致的 deny-by-default 規則和跨平台測試。
  3. 把秘密掃描補到完整 artifact 邊界。 run_check stdout/stderr 已掃描並遮罩,外部 runtime patch 也會先掃;仍需盤點 native patch、session 與其他 artifact 是否都經同一政策,並測試未涵蓋的 credential 格式。
  4. 保留現有的強項。 Pinned full-SHA、no-hardlinks、HEAD 重驗證、segment glob——這些是五家裡也不常見的嚴謹度,OS 沙箱進來之後仍值得留著當第二道牆。

一句話總結:disposable workspace 已保護來源 repo,--sandbox-checks 與 Cloudflare Sandbox 也補上兩塊可執行邊界;但「所有 runtime 都不會弄壞機器」仍需要跨平台、跨 backend 的強制與 production 驗證。

參考資料