OpenAI Codex · 兩種安全視角

兩種安全視角:同一條命令,兩套判詞

出事之前有沒有門可以拒絕,出事之後能不能復原模型當時看見的世界。同一條危險命令上,這兩套視角會一致,也會給出相反結論。

課程目標讀完能説清兩件事。可回放要的是模型當時看見的世界還能從日誌裏拼回來。可拒絕要的是命令跑起來之前,策略、審批、沙箱、代理裏至少有一道門能説不。同一條命令上,它們在哪裏一致,在哪裏互相讓路。
先玩一遍 · 同一條命令,兩套法官
同一個危險操作,兩套安全視角各自怎麼判
事故
策略寫過 Forbidden。看兩邊會不會給出相反結論。
git reset --hard HEAD~3
可回放審理中
問的是:出事之後,現場還能不能拼回來。
可拒絕審理中
問的是:出事之前,有沒有一道門能説不。
兩份判詞還沒對上。
邏輯軌跡 · 動畫每一步對應源碼裏的哪一段
  1. 同一條 argv 同時交給兩套視角L場景
  2. 可拒絕先看 Decision 三態,用最嚴的那一檔decision.rs L9
  3. 審批緩存的 key 帶完整 argv,不認前綴unified_exec.rs L92
  4. Guardian 超時或壞輸出就關閘guardian/mod.rs L11
  5. 平台沙箱三套後端,Windows 關着就是 Nonemanager.rs L37
  6. 可回放認 JSONL 原文,SQLite 只是鏡像README.md L22
  7. fork 必須撞到真實的 TurnStartedthread_rollout_truncation.rs L187
  8. 失敗寫成觀察,success 仍為 truecontext.rs L351
  9. 代理 403 回給命令進程,循環繼續responses.rs L80
  10. 對照兩份判詞:一致還是相反L驗收
點播放,看同一條命令在兩套視角下怎麼被判。
兩份判詞
缺口在哪
換一條命令切到另外兩起事故,看一致和相反會不會換邊。
教學示意:命令文本為課程化樣例,演示不調用真實 shell,也不連真實代理。邏輯軌跡右側行號對應 openai/codex 倉庫 commit 4f39251a01。
思路一 · 出事之後,現場還在不在
它解決什麼問題

週一早上,安全同事把一段聊天記錄甩到羣裏。模型昨晚連了外部 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 能告訴你模型當時看見了哪幾條消息。

DSH:先落成事件,再投影給模型 模型可見輸入 寫入會話日誌 deriveMessages 發給模型 Codex:先發給模型,定稿後再落原文 組裝上下文 發給模型 JSONL 原文 SQLite 鏡像 審批過程可能被策略丟掉
教學化結構圖:兩家都有原文和投影,DSH 把落事件放在發給模型之前。
為什麼長期成立

原文和投影拆開,投影壞了可以重建,原文壞了現場就沒了。換一套存儲,該問的還是誰是原文。這份合同不隨語言變。

思路二 · 出事之前,任何一道門都可以説不
它解決什麼問題

週三下午,另一台機器上的 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 testnpm 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 行

模型提出 argv execpolicy 審批與緩存 沙箱 出站代理 Forbidden deny 或 Abort 啓動失敗 進程收到 403 任何一道都可以拒絕。代理擋的是出站,不是 argv。 DNS rebinding 這一層自己承認防不住。
教學化結構圖:四道門加一道出站漏斗,每一道只鎖住一類風險。

出處: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.mdCLAUDE.md 是指向它的符號連結)和架構文檔:凡是進模型請求的輸入,都必須能從會話日誌重建。只追加的日誌是真相,給模型看的是投影。審批策略只有 asknever,沒有 Guardian,也沒有進程內出站代理。

Codex 的可回放停在定稿歷史。DSH 往前推了一步:新的模型可見輸入必須先成為一條會話事件。回放側的合同更硬,拒絕側更薄。

出處:AGENTS.md 第 107 行

出處:docs/architecture.md 第 92 至 96 行

出處:packages/interaction/user-approval/src/index.ts 第 84 至 94 行

兩側均已核對源碼 · 2026-08-22 · DSH · Model-visible ⟺ logged

拒絕: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 行

兩側均已核對源碼 · 2026-08-22
課堂練習
01

兩份判詞何時相反

打開上面的演示,切到帶令牌出站。推演可拒絕為什麼四道門過完仍然放行,可回放為什麼有 JSONL 也拼不迴環境變數。

再切到沙箱攔住危險寫,看同一套機制這次為什麼給出一致的判詞。

Takeaway:可回放問出事之後能不能復原。可拒絕問出事之前有沒有門。先問三個問題再決定抄哪一側:跑在誰的機器上,處理誰的數據,失敗的代價是密鑰泄漏、倉庫被改,還是評測不可復現。