CMUXAgentLaunch: carry Subrouter session identity through restore
Summary
Carries a Subrouter-routed Codex session's identity through cmux restore, so a restored tab resumes through sr codex resume instead of a direct codex resume that would reach a dead Subrouter data-plane URL.
This is the package half of the Subrouter identity work in #21423, split out on current main.
- The restore-bounding parts of #21423 are separate PRs: #24206 (bounded workstream startup) and the bounded session-restore PR.
- The app-side snapshot-import and workspace-binding changes come in a follow-up.
- This PR has no Claude routing changes.
What changes
Provider evidence (SubrouterCodexResumeRouting)
- Evidence is
absent,provider(name)orambiguous, read from the captured argv, or for older captures from the capturedCODEX_HOME/config.toml. Explicit argv evidence always wins over the config. - Only top-level keys are read: parsing stops at the first
[table]or[[array]]header, and text inside"""/'''multi-line strings is ignored. - Only the exact
model_providerkey counts;model_providers.*is unrelated. - These make the evidence
ambiguous, which never routes:- an unquoted value, a quoted key or an escape;
- a top-level
profilekey; - a captured argv that selects a profile (
--profile,-p,--profile=,-c profile=).
- The config is opened (following a symlink) and the opened descriptor must be a regular file. At most 64 KiB is read from that same handle, so what is checked is what is read.
- The config is read when the restore is planned, not at capture.
Resume argv
- A capture with the wrapper-bound marker resumes through that marker (
sr/subrouter/cx codex resume). - A markerless capture that names no provider in argv, but whose config proves Subrouter, uses the fixed
sr codex resume. - A capture whose argv names
model_provider=subrouteris rerouted tosr codex resumebybuiltInKind. That reroute:- strips the captured
model_providers.subrouter.*endpoint; - keeps the update-check suppression override;
- also applies to a Codex executable with a custom name;
- requires argv[0] to match the captured executable, the same identity rule as a bound-marker capture.
- strips the captured
- The markerless reroute runs its Subrouter child through cmux's managed Codex wrapper, like a bound-marker restore: when the wrapper shim is available, the planner sets
SUBROUTER_CODEX_BINto it,CMUX_CUSTOM_CODEX_PATHto the captured custom executable (the launch environment's capturedCMUX_CUSTOM_CODEX_PATHfirst, then the captured executable path; so a custom binary such ascodex-enterpriseis kept; ambient values are never used) and the restore-launch authorization. Without a shim it still runs fixedsrand sets none of them. A declared external launcher doesn't wrap the reroute, as with the marker route. LauncherResolution.rejectedis new: a capture that claims a routed Codex launch but whose executable doesn't match its argv. The app and CLI resume callers treat it as "do not resume" rather than falling back to a direct resume.- When no executable path was recorded, argv[0] itself must name Codex.
Restore planner
- Every prepared-argv fallback for Codex (empty capture,
.resolved(nil), andbuiltInKindreturningnil) refuses prepared argv that carries private Subrouter routing. - Codex recovery always goes through the structured restore path.
Observed
-
cd Packages/macOS/CMUXAgentLaunch && swift test: 883 tests pass on the head. Each fix landed after a commit with a failing test or a compile failure. Red counts per round:Round Red before the fix First 8 issues Top-level config keys and profiles 13 issues Symlinked config 1 issue Missing executable path 1 issue Prepared fallbacks compile failure Custom Codex name and update override 2 issues Markerless reroute through the managed wrapper 3 issues External launcher wrapping the markerless reroute 2 issues -
On the head (fd33adc1cf1),
xcodebuild -scheme cmux-unit build-for-testingreports TEST BUILD SUCCEEDED; the CLI used in the live run below came from that build. -
Earlier, the app and CLI compiled at 3b1ea6c2fac, and a tagged dev build (
reload.sh --tag d24428) compiles at f31077adb42:xcodebuild -scheme cmux -configuration Debug build: BUILD SUCCEEDED;xcodebuild -scheme cmux-unit build-for-testing: TEST BUILD SUCCEEDED.
Both used a
-derivedDataPathunder/Volumes/Atelier/Code.
This includes the app's and the CLI's handling of.rejectedin their exhaustive switches. -
Live restore through the built
cmux-cliat fd33adc1cf1 (the binary reportscmux 0.65.2 (111) [fd33adc1c]),cmux restore codex <id>:- Setup: no app and no login shell. The CLI ran under
env -iwith a scratch HOME andPATH=<stubs>:/usr/bin:/bin. A socket responder served one restore record per case, and each case got a minimal rollout in its effectiveCODEX_HOME. - Stubs:
sr,codex,codex-enterprise,caffeinateand the wrapper shimcmux-codex-wrapperonly log their argv and environment, then exit. The harness aborts if anything else runs. - Case h sets
CMUX_CODEX_WRAPPER_SHIMthe way the app exports it into a terminal. Cases i and j declare acaffeinateexternal launcher (resumeArgvPrefix: ["caffeinate", "-i"]) in a projectcmux.jsonin the session's directory; j is the positive control showing the launcher does wrap a direct resume. - Result: every case exited 0 and only stub paths ran. The rerouted cases captured
model_providers.subrouter.base_url="http://127.0.0.1:59999/dead/v1"(case a also hadSUBROUTER_CODEX_BASE_URL); neither appears anywhere in the stubs' logs.
Every resume also carried
-c check_for_update_on_startup=falseafter the id.Case Capture Executed argv after the executable Routing env at exec a bound marker pair, argv -c model_provider=subrouter, no shimsrcodex resume <id>none b markerless, argv -c model_provider=subrouter, no shimsrcodex resume <id>none c no provider in argv; CODEX_HOME/config.tomlwith top-levelmodel_provider = "subrouter"srcodex resume <id>CODEX_HOME=<config home>d [profiles.x] model_provider = "subrouter"onlycodex(direct)resume <id>CODEX_HOME=<profile home>e plain Codex with an inherited raw SUBROUTER_CODEX_RESUME_COMMANDcodex(direct)resume <id>no SUBROUTER_*f no executable path, argv[0] codex, bound marker pairsrcodex resume <id>none g markerless custom-named codex-enterprise, no shimsrcodex resume <id>none (fixed sr, no wrapper available)h as g, with the app's CMUX_CODEX_WRAPPER_SHIMsrcodex resume <id>SUBROUTER_CODEX_BIN=<shim>,CMUX_CUSTOM_CODEX_PATH=<codex-enterprise>,CMUX_AGENT_RESTORE_LAUNCH=codex:<id>i markerless argv-proven, declared caffeinateexternal launchersr(not wrapped)codex resume <id>none j control: direct Codex, same declared launcher caffeinate-i resume <id>direct-restore env - Setup: no app and no login shell. The CLI ran under
Not run
-
No restore was run inside a tagged app's terminal panes. Those panes start through
login -flp, which resets HOME and PATH to the real account. The CLI run above uses the same restore code without them. -
cmuxTestswas compiled but not run locally, because the test host app opens the real Feed store. -
No tagged app was launched.
-
No end-to-end restore was run against a live Subrouter.
🤖 Generated with Claude Code