两种安全视角:同一条命令,两套判词
出事之前有没有门可以拒绝,出事之后能不能复原模型当时看见的世界。同一条危险命令上,这两套视角会一致,也会给出相反结论。
- 同一条 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 也拼不回环境变量。
再切到沙箱拦住危险写,看同一套机制这次为什么给出一致的判词。