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:可回放问出事之后能不能复原。可拒绝问出事之前有没有门。先问三个问题再决定抄哪一侧:跑在谁的机器上,处理谁的数据,失败的代价是密钥泄漏、仓库被改,还是评测不可复现。