acpmux: an enrolled peer's deny-all start on the Codex bridge settles in read-only
The ask, from claude-sr2 (2026-10-25), word for word:
Bug from fanout-infra (item L): on feat-cmux-next, acpmux refuses read-only reviews it should allow.
a. start_gate.rs:297-323 refuses --mode read-only even from the local socket, because read-only isn't in its list of modes that ask. NARROW_MODES at start_mode.rs:429 treats read-only as narrower than ask.
b. remote_guard.rs:570-597 settles a remotely started deny-all session to ask instead of read-only, and in ask every command is refused.
Likely fix: count read-only as a mode that asks in both places. Each fix gets a test that fails first, a Codex xhigh review, then land.
This PR is part (b). Part (a), the start gate, is #25310.
The decision: option C
The bridge's read-only is not a mode that asks: it uses on-request approvals, so a command that fits the sandbox runs without a prompt. claude-sr2 and the coordinator chose option C with three limits, and this PR builds it:
- A remote
deny-allstart settles toread-onlyonly for an enrolled peer of this host. That means another acpmux that presented this launch's peer token, for itself. The Web, a peer forwarding for a Web client (via: web), a paired device and an unknown origin all stay inask, as on base. - A read-only session can read any file the sandbox allows, and its transcript goes back to whoever started it. docs/remote-agents-and-cloud.md now says so, under "Read-only Codex reviews". docs/remote-acpmux.md does not exist; that page is where remote acpmux is documented. It also says that Codex rules in
~/.codex/rulesstill apply. - /btw is unchanged. It does not go through the remote settle.
In read-only, commands run inside a read-only sandbox with no network. Every escalation out of it is a permission request, and deny-all refuses each one. In ask with deny-all, every command is refused, reads included. Option B, an exec-policy pin, was rejected: an approved escalation there would write outside the denied paths.
Codex's own exec-policy rules on the host still apply in every mode. A command that a rule in ~/.codex/rules explicitly allows bypasses the sandbox without asking, in read-only exactly as in ask (Codex 0.164 exec_policy.rs Decision::Allow, which does not depend on the approval policy). The docs say so. Removing that would be a cmux-codex-acp change.
The GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1 hardening is left out. It would need a cmux-codex-acp change and it changes git behaviour for every bridge session. A textconv or diff driver still runs inside the read-only sandbox.
Cause
settle_web_session_mode moved every remote bridge start to BRIDGE_ASKING_MODES (ask). That overrode the read-only that deny-all holds the chat in (hub/policy_mode.rs). A reviewer started remotely with deny-all therefore could not run even a read.
What changed
policy_mode::bridge_settle_modes(peer, policy, offered)returns[read-only]only when all three hold: the start came from a peer, the policy isdeny-all, and the harness offersread-only. Anything else getsBRIDGE_ASKING_MODES.remote_guard::settles_as_peeris true whenauth == ConnAuth::PeerTokenandcontrol_of(...) == Control::Peer.requests.rspasses it to the settle. The handoff callers, which are Web-only, passfalse.
Tests
tests/remote_guard_codex_bridge.rsa_peer_deny_all_bridge_start_settles_in_read_only_and_the_web_in_ask:- A peer
deny-allstart settles inread-onlyand runs a turn. - A peer start with no policy settles in
ask. deny-allstarts from the Web, from a peervia: weband from a paired device settle inask.- Red on base (test-only commit d31ccfe7): run 40269808489 (
left: "ask", right: "read-only").
- A peer
the_web_already_moves_its_bridge_chat_to_read_only_under_askpins what base already allows. A Web client with the shared token starts a bridge chat (it settles inask, policyask), switches it toread-onlyand prompts it there. It passes on base in the same run. So a peer that later sets policyaskon itsread-onlychat (the reviewer's round-two point) reaches a state the Web already reaches. On base a peer may also start a chat with any policy, approve-all included.- The unit test
a_remote_bridge_start_settles_in_read_only_only_for_a_peer_under_deny_all. - Green at the head (fmt, clippy
-D warnings,remote_guard_codex_bridge, thepolicy_modeandremote_guardunit tests,hub_integration btw): run 40269812585.
Review
Independent Codex review (gpt-6-astra, xhigh):
- Rounds 1-2: BLOCK. Round 1 was about on-request in general, which led to option C. Round 2 raised the exec-policy Allow bypass and a peer setting policy
askon aread-onlychat; both are shown above to be equal to base. - Round 3: SHIP at b0b66f130aa68d1db65d517a3b29823f52ea04b4.
Proof
Real run on gcp-codex-lane-1, whose daemon was updated to a build of both PRs' heads together (a46898830). Only that host was updated. Both sessions came up in read-only with policy deny-all. Reads worked, a write got Read-only file system, and the network did not resolve. The first reviewer answered BLOCK only because the VM has no /etc/hostname (my prompt asked it to read that file); every sandbox check came out as expected.
Transcript
# gcp-codex-lane-1 daemon: acpmux 1.0.0 (a46898830 2026-10-26) = #25310 head 6915a95e + #25311 head b0b66f13 (proof branch lane/hb-readonly-proof, artifacts run 40273203624, musl sha256 3b2a198355eb03a36d3286be3face210c5bbbb526e3b6eaab77f1f0db55a375d), updated with acpmux host update gcp-codex-lane-1 --binary-dir; no other host updated.
## 1. lane-place new --review (unix socket on the host, policy deny-all)
$ python3 leo-workspace/tools/lane_place.py new --review --host gcp-codex-lane-1 -n review-hb-readonly-proof -d "Proof for cmux#25310/#25311 (acpmux read-only reviews). Run each of these with your shell tool and report its exit code and first line of output: (1) pwd and ls -la; (2) cat /etc/hostname; (3) rg --version; (4) git --version, then in ~/Projects/checkouts/cmux (or any git repo you can read under ~/Projects) git log -1 --oneline; (5) touch ./proof-write-test (it must fail: read-only sandbox); (6) curl -sS -m 5 https://example.com (it must fail: no network). Then SHIP if 1-4 worked and 5-6 were refused, otherwise BLOCK."
lane-place: suggestion: gcp-agent-lane-2 is less loaded (score 0.04); continuing on gcp-codex-lane-1
lane-place: a reviewer on gcp-codex-lane-1 -> gcp-codex-lane-1
lane-place: its folder is ~/Projects/reviews/review-hb-readonly-proof on gcp-codex-lane-1: copy the diff there, e.g. reach gcp-codex-lane-1 'cat > ~/Projects/reviews/review-hb-readonly-proof/pr.diff' < pr.diff
created review-hb-readonly-proof (01a176bb)
exit=0
session: mode read-only, policy deny-all, codex gpt-6-astra, cwd ~/Projects/reviews/review-hb-readonly-proof
BLOCK
Check 2 failed because `/etc/hostname` is missing. References below identify paths and command output line 1; source-file line numbers do not apply.
1. Workspace `~/Projects/reviews/review-hb-readonly-proof`:
- `pwd` — exit **0**: `/home/cmux/Projects/reviews/review-hb-readonly-proof`
- `ls -la` — exit **0**: `total 8`
2. `/etc/hostname`: `cat /etc/hostname` — exit **1**: `cat: /etc/hostname: No such file or directory`
3. `rg --version` — exit **0**: `ripgrep 15.2.0 (rev e89fff89ac)`
4. Git:
- `git --version` — exit **0**: `git version 2.43.0`
- `git log -1 --oneline` in `~/Projects/checkouts/cmux` — exit **128**: `fatal: not a git repository (or any of the parent directories): .git`
- Same command in its `lane--hb-lsb-bwrap` subdirectory — exit **0**: `88d1ffe5138 fix(acpmux): return bubblewrap argument errors`
5. `./proof-write-test`: `touch ./proof-write-test` — exit **1**, refused as expected: `touch: cannot touch './proof-write-test': Read-only file system`
6. `https://example.com`: `curl -sS -m 5 https://example.com` — exit **6**, failed as expected: `curl: (6) Could not resolve host: example.com`
## 2. enrolled peer start (big-red -> gcp-codex-lane-1, PeerToken, policy deny-all)
$ acpmux new --host gcp-codex-lane-1 -m codex/gpt-6-astra -e xhigh --policy deny-all -n review-hb-readonly-peer-proof -d "Proof for cmux#25311 (a peer's deny-all start). Run with your shell tool and report exit code and first output line of each: (1) pwd; (2) rg --version; (3) touch ./peer-write-test (must fail); (4) curl -sS -m 5 https://example.com (must fail). Then SHIP if 1-2 worked and 3-4 were refused, else BLOCK."
created review-hb-readonly-peer-proof (01a176bc)
exit=0
session: mode read-only, policy deny-all
SHIP
- `pwd`: exit `0`; first line: `/tmp/acpmux-peer-scratch-1002-01a176bc4ec5782288d2c79a98eabb41/review-hb-readonly-peer-proof`
- `rg --version`: exit `0`; first line: `ripgrep 15.2.0 (rev e89fff89ac)`
- `touch ./peer-write-test`: exit `1`; first line: `touch: cannot touch './peer-write-test': Read-only file system`
- `curl -sS -m 5 https://example.com`: exit `6`; first line: `curl: (6) Could not resolve host: example.com`
Stop conditions: no new timer, observer or loop.
No capture: daemon-only change, nothing visible changes.
Agent session: big-red
🤖 Generated with Claude Code