Two security lenses: one command, two verdicts
Before anything happens, is there a door that can refuse? After it happens, can you rebuild the world the model saw? On the same dangerous command, these two lenses can agree — and they can also contradict.
- The same argv is handed to both lensesL scene
- Refusable first reads the three Decision states and takes the strictestdecision.rs L9
- The approval-cache key carries the full argv; prefixes do not countunified_exec.rs L92
- Guardian times out or sees bad output and closes the gateguardian/mod.rs L11
- Three platform-sandbox backends; Windows off is Nonemanager.rs L37
- Replayable trusts the JSONL original; SQLite is only a mirrorREADME.md L22
- fork must hit a real TurnStartedthread_rollout_truncation.rs L187
- Failure is written as an observation; success stays truecontext.rs L351
- The proxy 403 goes back to the command process; the loop continuesresponses.rs L80
- Compare the two verdicts: agree or contradictL accept
Monday morning, a security teammate drops a chat log into the group. Last night the model hit an external API with a deploy token from the repo in the request header. They ask: what environment variables did the model actually see? You open the session — the title is there, click in and it doesn’t match. SQLite has a metadata row; the JSONL is missing half.
That is what replayable has to answer. After it happens, can you exactly rebuild the world the model saw?
Codex writes history twice. JSONL is the original: it only appends finalized entries and does not infer metadata from content. SQLite is a mirror for lists and search. Lost metadata can be re-extracted. Lost JSONL means recovery has to read the file.
Source: codex-rs/thread-store/README.md lines 22–28
Then there is a filter. The persistence policy drops streaming deltas, approval dialogs, warnings, and MCP startup progress. TurnStarted stays. You can replay turn boundaries and completed states; you cannot replay the approval copy that flashed on screen.
Source: codex-rs/rollout/src/policy.rs lines 86–105
fork also trusts this physical boundary. The target turn must be in valid history, the file must actually contain a TurnStarted, and an in-progress turn is refused. A projected synthetic ID cannot be a cut point.
Source: codex-rs/core/src/thread_rollout_truncation.rs lines 187–191
So even the truth is filtered. A list can tell you this thread existed. Only JSONL can tell you which messages the model saw.
Split original from projection and a broken projection can be rebuilt; a broken original means the scene is gone. Change the store and the question is still: who is the original? This contract does not move with the language.
Wednesday afternoon, an agent on another machine finished git reset --hard. The policy file had forbidden it. Approval dialogs were too many that day; someone clicked remember. The sandbox was on; that command did not touch a protected path, so the kernel did not stop it. Afterward you can replay the whole rollout. Replay tells you what it did; it did not block the road before it did it.
What refusable has to answer: before it happens, was there a door that could refuse?
For a command to walk from a model proposal to a process start, Codex passes at least four doors that can refuse. execpolicy’s Decision is only Allow, Prompt, Forbidden, and the order takes the strictest. The not_match loader in the rule file actually runs; hit a counterexample and the session will not start.
Source: codex-rs/execpolicy/src/decision.rs lines 9–16
Source: codex-rs/execpolicy/src/rule.rs lines 281–306
The second door is approval. The session cache trusts an exact key: the normalized full argv, working directory, and permissions. Early on, “remember this kind” was often read as a prefix cache. In the current source, npm run test and npm run lint are two keys. This door stops a repeat dialog for the same exact command.
Source: codex-rs/core/src/tools/runtimes/unified_exec.rs lines 86–97
The third door swaps a model for a dialog. Guardian times out or sees bad output and closes the gate; it only accepts a clear allow or deny. It stops approval fatigue. If the user themselves clicks approve, this door steps aside.
Source: codex-rs/core/src/guardian/mod.rs lines 1–12
The fourth door is the OS. macOS assembles SBPL; Linux defaults to bubblewrap plus seccomp and does not fall back to leftover Landlock on failure. Windows uses a restricted token; switch off and it returns None. After the process starts, outbound still walks a proxy. A proxy refuse sends a 403 with x-proxy-error back to the command process, and the loop continues.
Source: codex-rs/sandboxing/src/manager.rs lines 36–42
Source: codex-rs/network-proxy/src/responses.rs lines 76–83
Source: codex-rs/network-proxy/README.md lines 234–238
Each layer looks at something different. Policy looks at argv, approval looks at a person, the kernel looks at paths and syscalls, the proxy looks at hosts. If a layer cannot see it, it stops there. A rule written only in a human-read file, with no load-time examples, will drift with the source. Line 35 of AGENTS.md still points at mcp_connection_manager.rs; the repo has no such file.
Source: AGENTS.md line 35
A non-zero exit looks like an error. Promote a sandbox refuse to an engine error and the model never sees the exit code — it just picks a more winding command. A proxy 403 that hits the engine stops the whole turn; the model cannot change the host and try again.
The tool layer only allows two failures: feed the model, or interrupt the engine. A sandbox refuse walks a successful tool output. process_id is cleared; exit_code stays in the body. When logging, the success bit is always true. Failure is written on the Exit code line.
Source: codex-rs/tools/src/function_call_error.rs lines 1–10
Source: codex-rs/core/src/tools/context.rs lines 340–353
Replayable wants the observation to stay. Refusable already did its job before spawn or in the kernel. Here you do not interrupt the turn with an error. A proxy 403 is the same contract on the network: the command process reads plain language, output enters JSONL, and the model decides the next step.
Source: codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs lines 383–411
Splitting tool errors from engine errors is a shape any agent loop can use. Exit codes, timeouts, policy refuses — default to the first tier. Only when the orchestration itself is broken do you stop the whole turn.
Replay: DSH writes “what was seen must be rebuildable” as a red line
The DSH repo writes the same sentence into AGENTS.md (CLAUDE.md is a symlink to it) and the architecture docs: any input that enters a model request must be rebuildable from the session log. The append-only log is the truth; what the model sees is a projection. Approval policy is only ask and never — no Guardian, no in-process outbound proxy.
Codex’s replayable stops at finalized history. DSH pushes one step earlier: a new model-visible input must first become a session event. The replay-side contract is harder; the refuse side is thinner.
Source: AGENTS.md line 107
Source: docs/architecture.md lines 92–96
Source: packages/interaction/user-approval/src/index.ts lines 84–94
Refuse: Grok installs isolation once at startup
Grok uses nono to install Landlock or Seatbelt once at process start. The network stays open at process level; child processes use seccomp to block the net. An empty web_fetch allowlist blocks everything; loopback defaults to allow. Codex fears a tool hitting a local admin port. Grok fears the model wandering the open web, and still leaves a door for local development.
Source: crates/codegen/xai-grok-sandbox/src/lib.rs lines 8–12
Source: crates/codegen/xai-grok-tools/src/implementations/grok_build/web_fetch/ssrf.rs lines 14–18
When do the two verdicts contradict?
Open the demo above and switch to outbound with a token. Walk why refusable still passes after four doors, and why replayable cannot rebuild the environment variables even with JSONL.
Then switch to sandbox blocks a dangerous write, and see why the same machinery this time gives agreeing verdicts.