兩種安全視角:同一條命令,兩套判詞
出事之前有沒有門可以拒絕,出事之後能不能復原模型當時看見的世界。同一條危險命令上,這兩套視角會一致,也會給出相反結論。
- 同一條 argv 同時交給兩套視角L場景
- 可拒絕先看 Decision 三態,用最嚴的那一檔decision.rs L9
- 審批快取的 key 帶完整 argv,不認前綴unified_exec.rs L92
- Guardian 超時或壞輸出就關閘guardian/mod.rs L11
- 平臺沙箱三套後端,Windows 關著就是 Nonemanager.rs L37
- 可回放認 JSONL 原文,SQLite 只是映像README.md L22
- fork 必須撞到真實的 TurnStartedthread_rollout_truncation.rs L187
- 失敗寫成觀察,success 仍為 truecontext.rs L351
- 代理 403 回給命令行程,迴圈繼續responses.rs L80
- 對照兩份判詞:一致還是相反L驗收
週一早上,安全同事把一段聊天記錄甩到群裡。模型昨晚連了外部 API,請求頭帶著倉庫裡的部署令牌。他問:當時模型究竟看見了什麼環境變數。你開啟會話,標題還在,點進去對不上。SQLite 裡有一行中繼資料,JSONL 缺了半截。
可回放要回答的就是這件事。出事之後,你能不能精確復現模型當時看見的世界。
Codex 把歷史寫成兩份。JSONL 是原文,只追加已經定稿的條目,不從內容裡推斷中繼資料。SQLite 是映像,給列表和檢索用。中繼資料丟了可以再抽。JSONL 丟了,恢復必須讀檔案。
出處:codex-rs/thread-store/README.md 第 22 至 28 行
然後還有一道篩選。持久化策略會丟掉流式增量、審批彈窗、警告和 MCP 啟動進度。TurnStarted 留下。你能回放 turn 邊界和完成態,回放不了當時螢幕上閃過的審批文案。
出處:codex-rs/rollout/src/policy.rs 第 86 至 105 行
fork 認的也是這條物理邊界。目標 turn 必須在有效歷史裡,檔案裡真有一條 TurnStarted,進行中的 turn 直接拒絕。投影出來的合成 ID 不能當切點。
出處:codex-rs/core/src/thread_rollout_truncation.rs 第 187 至 191 行
所以真相也經過篩選。列表能告訴你有過這個執行緒。只有 JSONL 能告訴你模型當時看見了哪幾條訊息。
原文和投影拆開,投影壞了可以重建,原文壞了現場就沒了。換一套儲存,該問的還是誰是原文。這份合同不隨語言變。
週三下午,另一臺機器上的 agent 跑完了 git reset --hard。策略檔案寫過禁止。審批彈窗那天太多,有人點了記住。沙箱開著,那條命令沒碰受保護的路徑,核心沒攔。你事後能把 rollout 完整回放一遍。回放告訴你它做過什麼,沒有在它做之前把路堵上。
可拒絕要回答的是:出事之前,有沒有一道門可以拒絕。
一條命令要從模型提議走到行程啟動,Codex 至少經過四道可以拒絕的門。execpolicy 的 Decision 只有 Allow、Prompt、Forbidden,用序取最嚴。規則檔案裡的 not_match 載入器真的跑一遍,反例被命中,會話開不起來。
出處:codex-rs/execpolicy/src/decision.rs 第 9 至 16 行
出處:codex-rs/execpolicy/src/rule.rs 第 281 至 306 行
第二道是審批。會話快取認精確 key,帶上規範化後的完整 argv、工作目錄、權限。早期常把記住同類理解成前綴快取。當前原始碼裡,npm run test 和 npm run lint 是兩把 key。這道門擋住同一條精確命令的重複彈窗。
出處:codex-rs/core/src/tools/runtimes/unified_exec.rs 第 86 至 97 行
第三道用模型換彈窗。Guardian 超時、壞輸出就關閘,只認明確的 allow 或 deny。它擋住審批疲勞。使用者本人點批准,這道門會讓路。
出處:codex-rs/core/src/guardian/mod.rs 第 1 至 12 行
第四道才是作業系統。macOS 拼 SBPL,Linux 預設 bubblewrap 加 seccomp,失敗不回退到遺留 Landlock。Windows 走受限令牌,開關關著就返回 None。行程啟動之後,出站再過代理。代理拒絕時把頭帶 x-proxy-error 的 403 回給命令行程,迴圈繼續。
出處:codex-rs/sandboxing/src/manager.rs 第 36 至 42 行
出處:codex-rs/network-proxy/src/responses.rs 第 76 至 83 行
出處:codex-rs/network-proxy/README.md 第 234 至 238 行
每一層看的東西不一樣。策略看 argv,審批看人,核心看路徑和 syscall,代理看域名。一層看不清,就停在這一層。規則只寫在人讀的檔案裡、沒有載入期例子,就會和原始碼一起漂。AGENTS.md 第 35 行還指向 mcp_connection_manager.rs,倉庫裡沒有這個檔案。
出處:AGENTS.md 第 35 行
命令退出碼非零,按直覺像錯誤。若把沙箱拒絕升級成引擎錯誤,模型看不到退出碼,只會換一條更繞的命令。代理 403 若打到引擎,整輪對話停,模型無法改域名再試。
工具層只允許兩種失敗:回餵模型,或打斷引擎。沙箱拒絕走的是成功的工具輸出。process_id 被清掉,exit_code 留在正文裡。記日誌時成功位恆為 true。失敗寫在 Exit code 那一行。
出處:codex-rs/tools/src/function_call_error.rs 第 1 至 10 行
出處:codex-rs/core/src/tools/context.rs 第 340 至 353 行
可回放要的是觀察還在。可拒絕已經在啟動前或核心裡做過了。這裡不再用錯誤把 turn 打斷。代理 403 是同一合同的網路版:命令行程讀到人話,輸出進 JSONL,模型再決定下一步。
出處:codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs 第 383 至 411 行
把工具錯和引擎錯分開,是任何 agent 迴圈都用得上的形狀。退出碼、超時、策略拒絕,預設走第一檔。只有編排自己壞了,才停整輪對話。
回放:DSH 把看見的必須能重建寫成紅線
DSH 倉庫用同一句話寫進 AGENTS.md(CLAUDE.md 是指向它的符號連結)和架構文件:凡是進模型請求的輸入,都必須能從會話日誌重建。只追加的日誌是真相,給模型看的是投影。審批策略只有 ask 和 never,沒有 Guardian,也沒有行程內出站代理。
Codex 的可回放停在定稿歷史。DSH 往前推了一步:新的模型可見輸入必須先成為一條會話事件。回放側的合同更硬,拒絕側更薄。
出處:AGENTS.md 第 107 行
出處:docs/architecture.md 第 92 至 96 行
出處:packages/interaction/user-approval/src/index.ts 第 84 至 94 行
拒絕:Grok 在啟動時裝一次隔離
Grok 用 nono 在行程啟動時裝一次 Landlock 或 Seatbelt,網路在行程級保持開啟,子行程用 seccomp 攔網。web_fetch 空名單全攔,loopback 預設放行。Codex 怕的是工具打到本機管理口。Grok 怕的是模型亂訪外網,仍給本機開發留門。
出處:crates/codegen/xai-grok-sandbox/src/lib.rs 第 8 至 12 行
出處:crates/codegen/xai-grok-tools/src/implementations/grok_build/web_fetch/ssrf.rs 第 14 至 18 行
兩份判詞何時相反
開啟上面的演示,切到帶令牌出站。推演可拒絕為什麼四道門過完仍然放行,可回放為什麼有 JSONL 也拼不迴環境變數。
再切到沙箱攔住危險寫,看同一套機制這次為什麼給出一致的判詞。