CMUXAgentLaunch: carry Subrouter session identity through restore

DA
13d ago
+977-58

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) or ambiguous, read from the captured argv, or for older captures from the captured CODEX_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_provider key 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 profile key;
    • 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=subrouter is rerouted to sr codex resume by builtInKind. 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.
  • 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_BIN to it, CMUX_CUSTOM_CODEX_PATH to the captured custom executable (the launch environment's captured CMUX_CUSTOM_CODEX_PATH first, then the captured executable path; so a custom binary such as codex-enterprise is kept; ambient values are never used) and the restore-launch authorization. Without a shim it still runs fixed sr and sets none of them. A declared external launcher doesn't wrap the reroute, as with the marker route.
  • LauncherResolution.rejected is 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), and builtInKind returning nil) 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:

    RoundRed before the fix
    First8 issues
    Top-level config keys and profiles13 issues
    Symlinked config1 issue
    Missing executable path1 issue
    Prepared fallbackscompile failure
    Custom Codex name and update override2 issues
    Markerless reroute through the managed wrapper3 issues
    External launcher wrapping the markerless reroute2 issues
  • On the head (fd33adc1cf1), xcodebuild -scheme cmux-unit build-for-testing reports 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 -derivedDataPath under /Volumes/Atelier/Code.
    This includes the app's and the CLI's handling of .rejected in their exhaustive switches.

  • Live restore through the built cmux-cli at fd33adc1cf1 (the binary reports cmux 0.65.2 (111) [fd33adc1c]), cmux restore codex <id>:

    • Setup: no app and no login shell. The CLI ran under env -i with a scratch HOME and PATH=<stubs>:/usr/bin:/bin. A socket responder served one restore record per case, and each case got a minimal rollout in its effective CODEX_HOME.
    • Stubs: sr, codex, codex-enterprise, caffeinate and the wrapper shim cmux-codex-wrapper only log their argv and environment, then exit. The harness aborts if anything else runs.
    • Case h sets CMUX_CODEX_WRAPPER_SHIM the way the app exports it into a terminal. Cases i and j declare a caffeinate external launcher (resumeArgvPrefix: ["caffeinate", "-i"]) in a project cmux.json in 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 had SUBROUTER_CODEX_BASE_URL); neither appears anywhere in the stubs' logs.

    Every resume also carried -c check_for_update_on_startup=false after the id.

    CaseCaptureExecutedargv after the executableRouting env at exec
    abound marker pair, argv -c model_provider=subrouter, no shimsrcodex resume <id>none
    bmarkerless, argv -c model_provider=subrouter, no shimsrcodex resume <id>none
    cno provider in argv; CODEX_HOME/config.toml with top-level model_provider = "subrouter"srcodex resume <id>CODEX_HOME=<config home>
    d[profiles.x] model_provider = "subrouter" onlycodex (direct)resume <id>CODEX_HOME=<profile home>
    eplain Codex with an inherited raw SUBROUTER_CODEX_RESUME_COMMANDcodex (direct)resume <id>no SUBROUTER_*
    fno executable path, argv[0] codex, bound marker pairsrcodex resume <id>none
    gmarkerless custom-named codex-enterprise, no shimsrcodex resume <id>none (fixed sr, no wrapper available)
    has 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>
    imarkerless argv-proven, declared caffeinate external launchersr (not wrapped)codex resume <id>none
    jcontrol: direct Codex, same declared launchercaffeinate-i resume <id>direct-restore env

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.

  • cmuxTests was 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