Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f25f251
CMUXAgentLaunch: carry Subrouter session identity through restore
danielraffel Oct 23, 2026
df6aafe
Route a markerless Codex restore only when its config proves Subrouter
danielraffel Oct 23, 2026
7a0cc0f
Drop Subrouter helpers that nothing in this change calls
danielraffel Oct 23, 2026
16a9016
Handle the rejected launcher resolution in both resume callers
danielraffel Oct 23, 2026
d7163cb
test: a bound Subrouter capture without an executable path routes
danielraffel Oct 23, 2026
defc60a
Route a bound Subrouter capture that recorded no executable path
danielraffel Oct 23, 2026
1e91e62
Drop the outer-launcher prefix replay from the Codex restore planner
danielraffel Oct 23, 2026
2fffbe1
test: Codex config evidence reads only top-level keys, and a profile …
danielraffel Oct 23, 2026
be586e6
Read only top-level model_provider from a captured Codex config
danielraffel Oct 23, 2026
492d582
test: a symlinked captured Codex config is read; directories and over…
danielraffel Oct 23, 2026
d912bde
Read a captured Codex config through its symlink, bounded, from the o…
danielraffel Oct 23, 2026
9226b21
test: every prepared Codex fallback refuses private Subrouter routing
danielraffel Oct 23, 2026
72a4413
Fail closed on private Subrouter routing at every prepared Codex fall…
danielraffel Oct 23, 2026
30487f3
test: a markerless Subrouter reroute keeps the update override for a …
danielraffel Oct 23, 2026
68b80d8
Reroute a markerless Subrouter Codex capture with its custom name and…
danielraffel Oct 23, 2026
3b1ea6c
test: exercise a config-proven legacy Codex capture through the resto…
danielraffel Oct 23, 2026
3c1cf55
test: a markerless reroute needs argv[0] to match the captured execut…
danielraffel Oct 23, 2026
648b955
Apply the captured-executable check before a markerless Subrouter rer…
danielraffel Oct 23, 2026
f31077a
test: a stale marker pair with no provider evidence plans direct Codex
danielraffel Oct 23, 2026
548d608
test: a markerless Subrouter Codex restore runs its child through the…
danielraffel Oct 24, 2026
c42e434
Run a markerless Subrouter Codex restore's child through the managed …
danielraffel Oct 24, 2026
f2bd82c
test: an external launcher doesn't wrap a markerless Subrouter Codex …
danielraffel Oct 24, 2026
fd33adc
Keep an external launcher from wrapping a markerless Subrouter Codex …
danielraffel Oct 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36014,6 +36014,10 @@ struct CMUXCLI {
arguments: launchCommand?.arguments ?? [],
observedPermissionMode: observedPermissionMode
)
case .rejected:
// See RestorableAgentSession: a mismatched routed Codex record is
// not resumed directly.
return nil
}

guard let argv, !argv.isEmpty else { return nil }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ import Foundation

/// Builds shell-free restore and fork invocations from structured persisted records.
public struct AgentRestorePlanner: Sendable {
/// The prepared argv as a restore plan, or `nil`. Prepared argv is not
/// independent provenance: a legacy or deferred Codex record can carry
/// Subrouter config (including a dead localhost data-plane URL) without
/// the trusted marker and launch-bound argv that rebuild `sr codex
/// resume`. Replaying it directly would silently change transport after
/// quit/reopen, so every prepared fallback for Codex fails closed on it.
static func preparedFallback(kind: String, preparedArguments: [String]?) -> ([String], Bool)? {
guard let preparedArguments else { return nil }
if kind == "codex",
SubrouterCodexResumeRouting().removingPrivateRoutingArguments(from: preparedArguments)
!= preparedArguments {
return nil
}
return (preparedArguments, false)
}

private static let claudeAuthSelectionEnvironmentKeys: Set<String> = [
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
Expand Down Expand Up @@ -202,6 +218,8 @@ public struct AgentRestorePlanner: Sendable {
if request.mode == .resumeAgent,
routedClaudeResume == nil,
let checkpointID = normalized(request.checkpointID),
!(kind == "codex" && subrouterCodexRoutedPrefix(request: request, checkpointID: checkpointID)
.map { routedArguments.starts(with: $0) } == true),
!AgentResumeArgv().resumeRoutesThroughOwnedLauncher(
launcher: request.launchCommand?.launcher,
sessionId: checkpointID,
Expand Down Expand Up @@ -378,8 +396,8 @@ public struct AgentRestorePlanner: Sendable {
// typed restore plan. Use it before launcher or built-in synthesis
// when the captured argv is empty, so a rejected capture cannot be
// replaced by a guessed command for the provider kind.
if (launch?.arguments.isEmpty ?? true), let preparedArguments {
return (preparedArguments, false)
if (launch?.arguments.isEmpty ?? true), preparedArguments != nil {
return Self.preparedFallback(kind: kind, preparedArguments: preparedArguments)
}
switch AgentResumeArgv().launcherResolution(
launcher: launch?.launcher,
Expand All @@ -392,7 +410,9 @@ public struct AgentRestorePlanner: Sendable {
if let arguments {
return (arguments, true)
}
return preparedArguments.map { ($0, false) }
return Self.preparedFallback(kind: kind, preparedArguments: preparedArguments)
case .rejected:
return nil
case .passthrough:
if let arguments = AgentResumeArgv().builtInKind(
kind: kind,
Expand All @@ -403,7 +423,7 @@ public struct AgentRestorePlanner: Sendable {
) {
return (arguments, true)
}
return preparedArguments.map { ($0, false) }
return Self.preparedFallback(kind: kind, preparedArguments: preparedArguments)
}
case .forkAgent:
if let preparedArguments {
Expand Down Expand Up @@ -541,6 +561,31 @@ public struct AgentRestorePlanner: Sendable {
}
}

/// The `sr` prefix a routed Codex resume starts with: the bound marker's
/// command, the stable launcher for a config-proven capture, or, for an
/// older capture whose argv names Subrouter without the marker, the fixed
/// `sr codex resume` that `AgentResumeArgv` reroutes it to. Either way the
/// Subrouter child has to run through cmux's wrapper.
private func subrouterCodexRoutedPrefix(request: AgentRestoreRequest, checkpointID: String) -> [String]? {
let routing = SubrouterCodexResumeRouting()
let launcher = request.launchCommand?.launcher
let launchArguments = request.launchCommand?.arguments ?? []
if let prefix = routing.resumeArguments(
launcher: launcher,
sessionID: checkpointID,
launchArguments: launchArguments,
environment: request.launchCommand?.environment
) {
return prefix
}
let normalizedLauncher = launcher?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard normalizedLauncher == nil || normalizedLauncher == "codex",
routing.providerEvidence(in: launchArguments, launcher: "codex") == .provider("subrouter") else {
return nil
}
return ["sr", "codex", "resume", checkpointID]
}

private func routeManagedWrapper(
arguments: [String],
request: AgentRestoreRequest,
Expand All @@ -556,12 +601,7 @@ public struct AgentRestorePlanner: Sendable {

if kind == "codex",
let checkpointID = normalized(request.checkpointID),
let routedPrefix = SubrouterCodexResumeRouting().resumeArguments(
launcher: request.launchCommand?.launcher,
sessionID: checkpointID,
launchArguments: request.launchCommand?.arguments ?? [],
environment: request.launchCommand?.environment
),
let routedPrefix = subrouterCodexRoutedPrefix(request: request, checkpointID: checkpointID),
arguments.starts(with: routedPrefix),
let wrapperShim = normalized(environment[restoreLaunch.wrapperShimEnvironmentKey]),
isExecutableFile(wrapperShim) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,10 @@ public struct AgentResumeArgv: Sendable, Equatable {
/// The launcher is a cmux wrapper; the associated value is its resume argv, or `nil` when the
/// wrapper has no resumable form (e.g. one-shot `omx`/`omc`).
case resolved([String]?)
/// The record claims a routed Codex launch but its executable identity
/// does not match the captured argv. Do not fall through to a direct
/// resume that could retain Subrouter provider settings.
case rejected
/// The launcher is a plain agent executable; fall through to ``builtInKind(kind:sessionId:executablePath:arguments:)``.
case passthrough
}
Expand All @@ -412,7 +416,16 @@ public struct AgentResumeArgv: Sendable, Equatable {
arguments: [String],
environment: [String: String]? = nil
) -> LauncherResolution {
if let routed = SubrouterCodexResumeRouting().resumeArguments(
let normalizedLauncher = launcher?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if normalizedLauncher == "codex",
!capturedCodexExecutableMatches(arguments: arguments, executablePath: executablePath),
SubrouterCodexResumeRouting().capturedEnvironment(in: environment).isEmpty == false {
return .rejected
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
let capturedExecutableMatches = normalizedLauncher != "codex" ||
capturedCodexExecutableMatches(arguments: arguments, executablePath: executablePath)
if capturedExecutableMatches,
let routed = SubrouterCodexResumeRouting().resumeArguments(
launcher: launcher,
sessionID: sessionId,
launchArguments: arguments,
Expand Down Expand Up @@ -466,6 +479,17 @@ public struct AgentResumeArgv: Sendable, Equatable {
}
}

private func capturedCodexExecutableMatches(arguments: [String], executablePath: String?) -> Bool {
guard let first = arguments.first else { return false }
let argvExecutable = URL(fileURLWithPath: first).lastPathComponent
// Older captures record no executable path; argv[0] is then the only
// identity evidence, and it has to name Codex itself.
guard let executablePath, !executablePath.isEmpty else {
return argvExecutable == "codex"
}
return argvExecutable == URL(fileURLWithPath: executablePath).lastPathComponent
}

/// Builds the resume argv for a built-in agent kind, or `nil` if the kind is unknown or its launch
/// arguments cannot be preserved.
///
Expand Down Expand Up @@ -502,6 +526,26 @@ public struct AgentResumeArgv: Sendable, Equatable {
preservePromptTags: false,
stripCmuxHooks: parts.executable == "codex"
) else { return nil }
// Older bindings can prove that the captured Codex invocation used
// Subrouter but lack the paired launch marker. Never replay the
// captured `model_providers.subrouter.*` argv in that case: it may
// contain a dead per-process localhost endpoint. Re-enter through
// `sr` so the provider allocates a fresh route on resume.
// The kind is known to be Codex, so a custom executable name
// (e.g. `codex-enterprise`) still counts as a Codex invocation.
if SubrouterCodexResumeRouting().providerEvidence(
in: [parts.executable] + parts.tail,
launcher: "codex"
) == .provider("subrouter") {
// Same identity rule as a bound-marker capture: argv[0] has to
// be the captured executable, or this is not that launch.
guard capturedCodexExecutableMatches(arguments: arguments, executablePath: executablePath) else {
return nil
}
let clean = SubrouterCodexResumeRouting().removingRoutingArguments(from: preserved)
return ["sr", "codex", "resume", sessionId]
+ codexResumeConfigOverrides(preserved: clean) + clean
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
let replayExecutable = codexReplayExecutable(
capturedExecutable: parts.executable,
launchTail: parts.tail
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ public struct AgentRecoveryCandidate: Equatable, Sendable {
}

func launcherResumeArguments(isReadableFile: @escaping (String) -> Bool) -> [String]? {
guard let launchCommand,
// Codex recovery must use the structured cmux restore path. A raw
// launcher prefix can bypass provider routing, writer-lock checks, and
// restore admission, which can strand a session as "open elsewhere"
// or resurrect it under the wrong provider.
guard kind != "codex",
let launchCommand,
launchCommand.externalLauncher == nil,
!routesThroughSubrouter,
let prefix = launchCommand.launcherPrefix,
Expand Down
Loading
Loading