Day25 做了一個刪除閘門,Day26 量到它擋錯了 8 次、也漏掉了它看不懂的寫法。那個閘門有一個本質限制,我必須講清楚:
它是「政策」,不是「邊界」。
它靠比對指令字串來判斷該不該擋。Day26 的 v2 補掉了 find -delete,但同一批執行裡,agent 隨手寫的 find ... -print0 | xargs -0 rm -rf 就直接穿過去了。而後面還排著 sh -c、python -c "shutil.rmtree(...)"、make clean、git clean -xdf——每補一條規則,誤擋正確指令的機率就上升一分,而繞路的寫法是無窮的。
真正的邊界只有一種:讓 agent 連「碰到」那些檔案的能力都沒有。那就是沙箱。
今天先看 Pi 怎麼把沙箱做成一個「可以換掉的零件」。
Day19 引用過 Pi 安全文件裡那句話,這裡值得再看一次:
Pi does not include a built-in sandbox... A partial in-process sandbox would be easy to misunderstand as a security boundary.
Pi 不內建沙箱,理由不是做不到,而是做半套比不做更危險——使用者會以為自己被保護了。
它的替代方案是:把「工具實際在哪裡執行」抽象成一個介面,讓你自己換掉。
ExecutionEnv 的定義短得讓人意外:
/** Filesystem and process execution environment used by the harness. */
export interface ExecutionEnv extends FileSystem, Shell {}
就是「檔案系統」加「跑指令」。Shell 只有兩個方法(exec 和 cleanup),FileSystem 有十幾個:
cwd / absolutePath / joinPath / canonicalPath / exists / fileInfo / listDir
readTextFile / readTextLines / readBinaryFile
writeFile / appendFile / renameFile / remove / createDir
createTempDir / createTempFile / cleanup
Day12 拆的那四個內建工具,沒有一個直接碰 node:fs。 read 呼叫的是 env.readTextFile(),bash 呼叫的是 env.exec()。工具只認識這個介面。
這就是整個沙箱設計的關鍵:換掉 env,工具的程式碼一行都不用改,但它們做事的地點全部換了。
這個介面有幾個細節,任何要做遠端/隔離執行的人都會用到:
一、每個操作都回傳 Result,不丟例外。
readTextFile(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
為什麼?因為當 env 可能是「另一台虛擬機」或「遠端容器」時,失敗是正常路徑而不是異常:連線斷掉、VM 沒起來、路徑在對面不存在。用回傳值強制每個呼叫點處理失敗,比散落的 try/catch 可靠。
二、錯誤碼與作業系統無關。
type FileErrorCode = "aborted" | "not_found" | "permission_denied"
| "not_directory" | "is_directory" | "invalid" | "not_supported" | "unknown";
工具不需要去解析 ENOENT、EACCES 這些平台字串,也不用管對面是 Linux 容器還是 Windows 主機。抽象層要抽的不只是操作,還有錯誤。 這是很多自製抽象層會漏掉的一半。
三、每個方法都吃 abortSignal。
使用者按 Ctrl+C 的時候,在遠端跑的 grep 也要停下來。取消能力必須一路傳到最底層,不能只在最上面做。
四、連暫存檔都在介面裡。createTempDir / createTempFile 是 env 的一部分,而不是呼叫 os.tmpdir()。理由很實際:如果工具在容器裡跑,暫存檔卻建在主機上,那個檔案在容器裡根本看不到。一旦你有了執行環境的概念,所有「檔案在哪裡」的假設都要跟著搬家。
Pi 的文件把可行做法分成兩類,很值得對照著看:
| 方式 | 被隔離的是 | 代價 |
|---|---|---|
| Gondolin extension | 只有工具執行(透過換掉 env,路由進 Linux micro-VM) | 需要 QEMU;只覆蓋內建工具 |
| Plain Docker | 整個 pi 程序 |
憑證要進容器;設定與 session 也在容器裡 |
| OpenShell | 整個程序,外加政策控制 | 需要 gateway |
Gondolin 是「agent 的腦留在主機上,手伸進 VM 裡做事」;Docker 是「連人帶腦整個關進去」。
這裡有一個容易踩雷的細節,文件寫得很清楚:
Extensions run wherever the
piprocess runs.
也就是說,用 Gondolin 這種「只路由工具」的方案時,你自己寫的 extension 工具還是在主機上跑。Day25 那個閘門是 extension,所以它在主機端;但如果我寫的是一個會刪檔案的自訂工具,它不會被 Gondolin 關進 VM。隔離的邊界在哪裡,要能講得出來,否則就是假的安全感。
原本大綱寫的是「host vs Docker vs Gondolin 三方比較」。實際做下去發現 Gondolin 在這台機器上跑不了:它是 Linux micro-VM,需要 QEMU,而我在 Windows 上;npm 上的 runner 套件也是 Linux-only。
所以明天改成兩方比較:host vs Docker。這不是偷懶,而是一個對讀者有用的資訊——在 Windows 上做 agent 隔離,Docker 幾乎是唯一現實的選項。
Docker 這條路也有它自己的麻煩,而且正好呼應「隔離的代價」這個主題:容器裡的 Pi 需要憑證才能呼叫模型。官方文件提供兩種做法,一種是把 API key 傳進容器,一種是掛載主機的設定目錄——而後者等於把主機的登入憑證交給容器。
隔離不是免費的:你關住 agent 的同時,也要把它需要的東西一件一件送進去,而每送一件,邊界就少一分。
Day28 實際做:把量測台的任務放進 Docker 容器跑,跟主機上跑的結果比較成功率、成本與時間,並且把「憑證怎麼進容器」這件事處理掉。