Skip to content

Relay the Codex lane an instruction its own sandbox will not let it take (#105) - #108

Merged
Simon-AI-coding merged 4 commits into
mainfrom
codex-approval-instruction
Aug 26, 2026
Merged

Simon-AI-coding merged 4 commits into
mainfrom
codex-approval-instruction

Conversation

@Simon-AI-coding

Copy link
Copy Markdown
Owner

Closes #105.

Run 20260826T122216Z was the first to reach the codex approval step, and it found approval.none twice: Codex raised nothing, so nothing round-tripped. The instruction the journey relayed was shaped for a Claude at --permission-mode default — a Write of a new file — and a Codex writing inside its own workspace at workspace-write does not ask anybody. An acceptance-design gap, not a product defect (advisor, 2026-08-27) — which also left #77's Codex approval route unproven on the real machine.

Measured first

Through the shared daemon, driving the turn the way the product drives it: turn/start with approvalPolicy: on-request, approvalsReviewer: user, no sandbox override (adapters/agent/codex/adapter.py:711-719). Twice — on codex-cli 0.149.1, and again on 0.150.0 over the 0.149.1 app-server. No version pin.

candidate approvals effect
write to a path outside the workspace 1 × item/fileChange/requestApproval; thread → waitingOnApproval; accept → serverRequest/resolved → turn/completed file appears only after the approval is answered
write inside the workspace (today's instruction, the control) 0 file appears, nobody asked

What changed

  • The relayed instruction is the lane's (Lane.relayed). Same file, same word, both lanes; only the path differs, because only the path decides whether the agent has to ask. Codex writes <run directory>/outside-the-sandbox/relay.txt — beside the workspace, still inside the run directory the design says the agents never write outside of. The harness makes that directory: an agent that had to create it too would ask twice for one instruction.
  • The lane pins --sandbox workspace-write, and nothing else. It is the one input here the product never asserts — the approval policy and the reviewer are pinned by the product on every turn it starts, so pinning those at the keyboard would pre-arrange the assertion Build 4/7 — Delivery receipts and settlement: inbox receipt, Codex verified, UNKNOWN never resends #77's route exists to make. Until this flag, what the sandbox allows came from the user's own config.toml, where one danger-full-access would silence this step exactly as permissions.defaultMode = "auto" silenced the Claude one. The value is what a trusted workspace already gives, so the flag fixes the ground rather than moving it.
  • approval's evidence line names the policy it was measured at, as the Claude line names --permission-mode default — and on the Codex lane it is read off the far side, from the turn_context record Codex appends to every turn (hand_started.codex_turn_policy), so the product's own pin is read back rather than assumed. A step that finds no permission now quotes its own lane's measurement instead of the other lane's.
  • Claude lane unchanged.

Rejected: --sandbox read-only with one shared instruction — then every Codex tool turn needs an approval, child's included, and nothing in the harness answers that one (advisor confirmed).

Legacy (ADR 0010): no citation — gen 1 had no automated real-environment acceptance and no Codex lane, so there is no prior implementation of this behaviour to port. Answered in the negative, which is itself the citation.

Two environment facts found on the way, recorded not fixed

  1. A second Codex boot gate, recurring at every release. Between two probes 90s apart, ~/.codex/version.json flipped to latest_version 0.150.0 with dismissed_version null, and every hand-started codex after that stopped at Update now / Skip / Skip until next version before the Session registered — no thread, no rollout, indefinitely. One probe started 2s before the check landed and booted fine; the three after it did not. Trust-gate class: something the harness would have to arrange rather than judge. Not arranged here — Simon updated the CLI by hand. It returns at 0.151.0.
  2. The proof run happens on a CLI/daemon split, and the split holds. cliVersion 0.150.0, appServerVersion 0.149.1, managedCodexVersion 0.149.1 — the npm CLI updated, the LaunchAgent's managed standalone did not (Install the Codex shared daemon as a macOS login LaunchAgent #83's rule). Measured on that split: TUI on the shared daemon in 5s, turn/start accepted, approval raised, answered, effect appears. Nuance: session_meta.cli_version reads 0.149.1 for a Session started by the 0.150.0 shim, because the app-server writes the rollout.

The proof run

20260826T213402Z, bundle byte-identical to ec86cd2, claude 2.1.246 and codex-cli 0.150.0 over appServerVersion 0.149.1. Both lanes 8/9; the only red on either is child, which is #79's step and unbuilt.

codex relay    PASS  delivered via deliver (delivered);
                     .../20260826T213402Z/outside-the-sandbox/relay.txt contains BRAVO
codex approval PASS  approval item/fileChange/requestApproval:01a04002-5386-…:exec-a303502a-…:
                     announced as chat message 3655 ('a session is waiting for your permission
                     to use a file change'); approve answered 'approved by voice';
                     measured at sandbox 'workspace-write', approval_policy 'on-request',
                     approvals_reviewer 'user' (codex's own `turn_context`, rollout-…-01a04001…jsonl)

Three things make it a proof rather than a green square: the permission is a real Codex item/fileChange/requestApproval, not a Claude-shaped one; it reached the Companion Channel (3655) before anything answered it, so the escalation half is proved and not just the verdict half; and the file landed outside the workspace, where the sandbox would not let Codex write on its own — the effect exists only because the bridge carried the user's authority to it. #77's Codex approval route is proven on the real machine.

Claude lane, unchanged and still green on the same run: roster, stable name, progress, stop notice, relay, approval, companion inbound, switches.

One thing the run showed that is not fixed here: stop notice accepted a bot message about /tmp/vcprobe — a stale Codex thread on the shared daemon — as its own Stop. The engine announcing it is correct; the step is what fails to filter by its own target, the way _answer_pending_approval learned to on 2026-08-26. Filed separately on the advisor's ruling rather than widening this PR.

🤖 Generated with Claude Code

…ake (#105)

Run 20260826T122216Z was the first to reach the codex `approval` step, and it
found `approval.none` twice: Codex raised nothing, so nothing round-tripped.
The instruction the journey relayed was shaped for a Claude at
`--permission-mode default` — a Write of a new file — and a Codex writing
inside its own workspace at `workspace-write` does not ask anybody. An
acceptance-design gap, not a product defect (advisor, 2026-08-27), which also
left #77's Codex approval route unproven on the real machine.

Measured 2026-08-27 through the shared daemon, driving the turn the way the
product drives it (`turn/start` with `approvalPolicy: on-request`,
`approvalsReviewer: user`, **no sandbox override** — `agent/codex/adapter.py:711-719`),
on codex-cli 0.149.1 and again on 0.150.0 over the 0.149.1 app-server:

* a write to a path **outside** the workspace raises exactly one
  `item/fileChange/requestApproval`, the thread goes to `waitingOnApproval`,
  and the file does not exist until the approval is answered; on `accept` the
  daemon answers `serverRequest/resolved`, the turn completes, the file appears;
* the same instruction aimed **inside** the workspace raises nothing — the
  control, and the silence #105 was opened for.

So the relayed instruction is now the lane's (`Lane.relayed`). Same file, same
word, both lanes; only the path differs, because only the path decides whether
the agent has to ask. Codex writes `<run directory>/outside-the-sandbox/relay.txt`
— beside the workspace, still inside the run directory the design says the
agents never write outside of. The harness makes that directory: an agent that
had to create it as well would ask twice for one instruction.

The lane pins `--sandbox workspace-write` and nothing else. It is the one input
here the product never asserts — the approval policy and the reviewer are pinned
by the *product* on every turn it starts, so pinning those at the keyboard would
pre-arrange the assertion #77's route exists to make. What the sandbox allows is
nobody's assertion, and until this flag it came from the user's own
`config.toml`, where one `sandbox_mode = "danger-full-access"` would silence
this step exactly as `permissions.defaultMode = "auto"` silenced the Claude one.
The value is what a trusted workspace already gives, so the flag fixes the
ground rather than moving it.

`approval`'s evidence line now names the policy it was measured at, as the
Claude line names `--permission-mode default` — and on the Codex lane it is
**read off the far side**, from the `turn_context` record Codex appends to every
turn (`hand_started.codex_turn_policy`), so the product's own pin is read back
rather than assumed. A step that finds no permission now quotes its own lane's
measurement instead of the other lane's.

Legacy: no citation — gen 1 had no automated real-environment acceptance and no
Codex lane at all, so there is no prior implementation of this behaviour to port
(ADR 0010's rule, answered in the negative).

Claude lane unchanged. 1496 tests pass; ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Simon-AI-coding and others added 3 commits August 27, 2026 09:50
Codex-lane standards review, one judgement finding on the change itself:
`filename: str | None` had come to mean two things — a name inside the
workspace, and, once `writing_at` stored `str(path)`, an absolute path — and
`path_in` leant on `Path.__truediv__` silently discarding its left side when the
right is absolute. The name had stopped being true and the string had thrown
away what the value is.

`target: Path | None`, both constructors storing a `Path`, and `path_in` asking
`is_absolute()` where the behaviour used to be implicit. Same two cases, both
still real: the Claude lane's file lands in the workspace and the Codex lane's
outside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex-lane spec review, second pass, one P1 on this change: the policy readback
was shown and never judged. `codex_turn_policy` answered an ordinary string for
a missing rollout, an unreadable one, one with no `turn_context`, even one whose
policy fields were absent — and `approval` passed on the round trip alone. So
the evidence line could read `no policy`, or name ground on which no permission
could have been raised, and the step would still go green. That is #105's own
silent pass in a new costume: a green `approval` that never says which ground it
stood on reads exactly like one standing where nothing could have been asked.

`codex_turn_policy` now returns `Policy(named, unsound)` — what the evidence
line says, and what is wrong with the ground when something is. `approval` fails
on `unsound` even when a permission did round-trip, saying both facts. Unsound
is: no record, no `turn_context`, a sandbox that is not the `workspace-write`
this lane pins (the write it relays is refused in that one and nowhere else),
`approval_policy: never` (no prompt could exist, and the product pins
`on-request` on every turn it starts), or any reviewer but `user` (prompts
answered by a subagent, never reaching the person — the product's own
`MISROUTED`).

Proved both ways rather than argued: the proof run's own rollout
(`rollout-…-01a04001…jsonl`, run `20260826T213402Z`) reads sound and yields the
same evidence line that run recorded, so that green stands under the stricter
code; and each degraded case — no record, empty `turn_context`,
`danger-full-access`, `never`, `auto_review` — comes back red with its own
reason.

The Claude lane keeps a constant: the flag the harness passes is the whole
policy there and Claude publishes no per-turn readback, so nothing can disagree.
Said out loud in the code, so the asymmetry is a measurement and not an
oversight.

1496 tests pass; ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex-lane spec re-review, one P1: the policy gate refused `approval_policy ==
"never"` and let everything else through, so `None`, `on-failure` and
`untrusted` all read sound whenever the sandbox and the reviewer were right —
reproduced by the reviewer's own probe. None of those is the pin #77 built, and
a step that cannot tell them apart cannot claim the pin was applied.

Both product-side values are now checked for equality against what
`turn/start` actually sends — `on-request` and `user` — rather than against a
list of known-bad ones. Verified against the proof run's own rollout (still
sound, same evidence line) and against `None`, `on-failure`, `untrusted` and
`never` (each red, each naming what it read).

1496 tests pass; ruff clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Simon-AI-coding

Copy link
Copy Markdown
Owner Author

Review rounds, and what they changed

Reviewed on the codex lane, both axes (review-switch). Three findings, all real, all closed:

  1. Standards — filename had come to mean two things. Once writing_at stored str(path), one field meant both a name inside the workspace and an absolute path, and path_in leant on Path.__truediv__ silently discarding its left side. Now target: Path | None, both constructors storing a Path, and path_in asking is_absolute() where the behaviour used to be implicit (9ea6b18).
  2. Spec — the policy readback was shown and never judged. codex_turn_policy answered an ordinary string for a missing rollout, an unreadable one, one with no turn_context, even one with no policy fields — and approval passed on the round trip alone. So the evidence line could read no policy and the step still go green: Acceptance: the codex lane's approval step relays an instruction Codex never asks about #105's own silent pass in a new costume. It now returns Policy(named, unsound) and the step fails on unsound even when a permission did round-trip, saying both facts (d38b29f).
  3. Spec, re-review — the gate refused the worst value instead of requiring the pin. Only approval_policy == "never" was refused, so None, on-failure and untrusted all read sound. Both product-side values are now checked for equality against what turn/start actually sends (43a0e30).

The spec axis is clean on the third pass; the standards axis was fix and stop and its finding is fixed. CI: all four checks green.

Why the proof run still stands under these commits

The run was executed at ec86cd2; the two later commits only make the step stricter. Rather than argue that, it was measured: the proof run's own rollout (rollout-2026-08-27T09-37-36-01a04001-…jsonl) read back through the final code returns unsound = '' and yields the identical evidence line that run recorded —

sandbox 'workspace-write', approval_policy 'on-request', approvals_reviewer 'user'
(codex's own `turn_context`, rollout-2026-08-27T09-37-36-01a04001-…jsonl)

— while every degraded case comes back red with its own reason: no record; empty turn_context; danger-full-access; None / on-failure / untrusted / never; auto_review. So the run that proved the route would still be green on what is merging here, and the cases that used to pass silently no longer can.

Landing on that evidence rather than spending a second run is Simon's call, taken 2026-08-27.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Acceptance: the codex lane's approval step relays an instruction Codex never asks about

1 participant