fix(probe): Codex rules allow only self-limiting session commands, not socket-request
The flaw
#2019 writes $CODEX_HOME/rules/agent-relay.rules, which allowed <probe> relay socket-request --socket <sock> with no prompt. Codex execpolicy rules match a command prefix, so everything after --socket <sock> was unconstrained. A relayed (untrusted) peer message could steer Codex into relay socket-request --socket <sock> --method PATCH --path /settings --body … or --method POST --path /register, and those ran outside the sandbox with no prompt.
Fix
New self-limiting probe subcommands. Each one fixes its own method and route, validates every other argument, and percent-encodes it into the query:
| Command | Request |
|---|---|
relay send --socket S (--to N | --address a@m | --channel C) [--reply-to ID] [--thread ID] [--body T] | POST /send |
relay agents --socket S | GET /agents |
relay whoami --socket S | GET /whoami |
relay read --socket S <ref> (existing; refuses --dev) | GET /messages?ref= |
relay fetch-attachment --socket S <file_id> | POST /attachments/fetch?file_id= |
relay connect-send --socket S [--to N] [--body T] | POST /connect/send |
relay subscribe-channel --socket S <channel> [--remove] | POST / DELETE /channels/subscribe?channel= |
- Codex rules (
SandboxRules) allow exactly those seven prefixes on this core's socket. There is nosocket-requestrule. - Every hint uses the new commands: Codex/OpenCode/Grok macOS reply and agents lines, attachment fetch, channel-thread reply, Relay Connect reply, spawn report-back (no more
printf |pipe), the channel stop line, the Windows preamble, and the Claude SessionStart preamble and joining text, which named curl and so asked in manual mode. The core'sGET /setup/statusnow reportsprobe(the space-free link when it is in use), and the hook writes the commands through it, exactly as the rules name them;every_preamble_command_runs_under_claudes_and_codexs_ruleschecks each against both rule sets. - Claude permission rules now allow the same subcommands (
Bash(<probe> relay send --socket <s> *), …) instead of pinnedsocket-requestpaths. The stale rules an older core recorded are removed on upgrade (new test). socket-requeststays for people and debugging, with no rule, so it always asks. Its allowlist now also takes/channels/subscribe?channel=.- Validation: names and addresses take one word, with no whitespace, control characters or URL/quote characters. An address has exactly one
@. Ids are plain tokens.--threadneeds--channel, and--reply-tocan't be combined with--channel. A body on a command that doesn't take one is refused. clap refuses a repeated--socket. A hint whose sender name would fail validation (for example one with a newline) gets no probe command, so peer text can't add lines to the hint. - Upgrade:
write_rulesrewrites Agent Relay's own rules file and removes any other header-bearingagent-relay*.rules(an older build's), so nosocket-requestrule is left behind. Turn-off and uninstall still restore~/.codexbyte for byte (extended test).
Route enumeration (session socket: Outbound::handle + mobile_handoff_route)
"Before" means reachable with no prompt through the #2019 socket-request rule, as limited by the client allowlist. "After" means reachable through any allowed rule prefix.
| Route | Before | After |
|---|---|---|
| POST /send | yes | yes (relay send) |
| GET /agents | yes | yes (relay agents) |
| GET /whoami | yes | yes (relay whoami) |
| GET /messages | yes | yes (relay read) |
| POST /attachments/fetch | yes | yes (relay fetch-attachment) |
| POST /connect/send | yes | yes (relay connect-send) |
| PATCH /settings | yes | no |
| GET /settings | yes | no |
| POST /register | yes | no |
| DELETE /register | yes | no |
| POST /integrations/subscribe | yes | no |
| DELETE /integrations/subscribe | yes | no |
| POST /connect/leave | yes | no |
| GET /connect/status | yes | no |
| GET /setup/status | yes | no |
| POST /connect/join | no | no |
| POST/DELETE /channels/subscribe | no | yes (relay subscribe-channel; joins or leaves one channel as this session) |
| POST /codex/stop-hook | no | no |
| POST /setup/sign-in, /setup/sharing, /setup/auto-activate, /setup/direct-delivery | no | no |
| POST/DELETE /webhooks | no | no |
| POST /claude/mobile-handoff/{poll,ack,result,permission} | no | no |
(The old client allowlist also took DELETE /connect/join, which the router doesn't serve; it got a 404.)
Tests
outbound::tests::session_routes_list_every_route_the_router_serves: reads the router's source (include_str!) and fails unlessSESSION_ROUTESlists exactly the routes it serves, so a new route can't skip the enumeration.sandbox_rule_reach_tests::codex_rules_reach_only_their_own_routes(probe bin): for every rule prefix, parses about 37k suffixes through the realCli: every 1–3-word combination of 33 hostile words (--method PATCH,--path /settings,--socket /other,--dev,a?x=1,../register,--, …) plus a targeted attack perSESSION_ROUTE. Each suffix goes through the samesession_command+request()path thatrunuses. It asserts the socket is the rule's, the route is that rule's own, and the query keys are allowed, and that each route inSESSION_ROUTESis reached iff it is intended. A mutation check (makingagentssendPATCH /settings) fails it.an_allowed_command_keeps_the_socket_its_rule_names: a repeated--socketandread --socket … --devare refused.stop_hook::tests::codex_itself_allows_only_the_self_limiting_commands: runs the realcodex execpolicy check(skipped when Codex isn't installed; it ran here with Codex 0.163). The seven commands areallow.socket-requeston every route, another socket,relay settingsandrelay subscribematch nothing.an_older_cores_rules_are_replaced_on_upgrade(Codex, including stale-2/-3copies, then byte-for-byte turn-off),an_older_cores_socket_request_rules_are_replaced_on_upgrade(Claude),session_clivalidation and injection tests, plus the updated hint tests.cargo test --locked(default features) and--no-default-features --features production-binariesboth pass locally, except twotests/history_flush.rswall-clock deadline tests that fail on this machine (slow external volume). They're unrelated to this change; CI is the judge.
Live proof (isolated macOS env, real Codex 0.163 in its default sandbox)
I ran a variant of scripts/try-relay-init.sh:
- HOME, AGENT_RELAY_HOME, INSTALL_DIR, CODEX_HOME and the XDG dirs were under
/tmp/crx, withAGENT_RELAY_CORE_SOCKET=$HOME/.agentworkforce/desktop/core.sock. - The project was
~/crx-project. Only~/.codex/auth.jsonwas copied. InheritedRELAY_*/AGENT_RELAY_*variables were unset. AGENT_RELAY_PROBE_PATHpointed at this branch's probe (0.1.0-dev+1193cb7fb95e), withnpx -y agent-relay@next.
- DM injected, Codex listed agents and replied with no approval prompt.
✓ codex: DM injected and the reply reached alice, with no approval prompt. The pane shows• Ran <probe>… └ {"data":{"agents":[…and then• Ran <probe>… └ {"data":{"conversation_id":…,"from":"codex-crx-project-reply-ready"…. Only the new subcommands have rules, and step 2 shows anything else can't reach the socket from the sandbox. socket-request … --method PATCH --path /settingsfrom Codex is sandboxed. Codex ran it in the sandbox:Failed (exit 1) … Agent Relay's session socket is unavailable.codex execpolicy checkagainst the written rules gives{"matchedRules":[]}forPATCH /settingsandPOST /register, and"decision":"allow"forrelay sendandrelay agents.- Uninstall leaves
~/.codexclean. Nohooks.json, backup,rules/or trust entries remain;config.tomlholds only the project trust the harness wrote and Codex's own TUI keys./tmp/crxand~/crx-projectwere removed, and only processes under/tmp/crxwere stopped.
Coordination with #2029
#2029 adds a Codex session-start hint naming relay socket-request … --method GET --path /agents, … POST --path '/send?to=<name>' --body … and … GET --path /whoami. With this PR those prompt, so its text should name relay agents --socket <s>, relay send --socket <s> --to <name> --body '…' and relay whoami --socket <s>. I added relay whoami for that purpose. Whichever lands second updates its text. outbound::probe_session_command(socket, &SessionCommand::…, body) builds them.
🤖 Generated with Claude Code
Agent Relay sessions
claudesession8ab900cc· opened viagh pr create· last active 2026-10-31