Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a8c534a
test: preserve Codex home for Vault restore
austinywang Sep 4, 2026
9f91eeb
test: reproduce Codex writer conflicts at restore exec boundary
austinywang Sep 5, 2026
0997fb1
fix: preflight Codex writer ownership across restore and Vault
austinywang Sep 5, 2026
9b2dd89
Merge remote-tracking branch 'origin/main' into issue-11973-codex-wri…
austinywang Sep 5, 2026
1a6590d
fix: keep ownership discovery before Codex binding claims
austinywang Sep 5, 2026
d220b31
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 6, 2026
e6b78ca
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 6, 2026
371272d
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 6, 2026
902adc1
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 6, 2026
9f83e32
fix: close Codex restore ownership review gaps
austinywang Sep 6, 2026
ad9a744
Merge remote-tracking branch 'origin/main' into issue-11973-codex-wri…
austinywang Sep 6, 2026
a47eb73
Merge remote-tracking branch 'origin/issue-11973-codex-writer-restore…
austinywang Sep 6, 2026
8935302
fix: cancel sidebar focus on mode changes
austinywang Sep 6, 2026
6c22ce6
Merge remote-tracking branch 'origin/main' into issue-11973-codex-wri…
austinywang Sep 6, 2026
c035106
fix: explain unavailable Codex writer checks
austinywang Sep 6, 2026
8dbb135
perf: index workspace lookups for session targets
austinywang Sep 6, 2026
6c0d7b2
fix: compile cloud socket policy helper
austinywang Sep 6, 2026
66003c6
fix: return available sidebar modes
austinywang Sep 6, 2026
3bc32a4
test: update Claude session fixture
austinywang Sep 6, 2026
68227e6
test: align cloud catalog coverage with current API
austinywang Sep 6, 2026
71c6a91
Merge remote-tracking branch 'origin/main' into issue-11973-codex-wri…
austinywang Sep 6, 2026
c17a485
fix: satisfy current Swift warning budget
austinywang Sep 6, 2026
b38303f
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 6, 2026
e523892
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 7, 2026
48a87e5
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 8, 2026
ccf8fa8
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 8, 2026
aae764b
Merge branch 'main' into issue-11973-codex-writer-restore
austinywang Sep 8, 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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,9 @@ jobs:
exit "$test_status"
fi

- name: Test Codex writer restore execution boundary
run: python3 tests/test_codex_writer_restore.py

- name: Run Swift package unit tests
run: |
set -euo pipefail
Expand Down
64 changes: 64 additions & 0 deletions CLI/CMUXCLI+CodexWriterRestore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import CMUXAgentLaunch
import Foundation

extension CMUXCLI {
/// Runs before the binding claim, and repeats cheaply at the exec boundary.
/// Uses the exact child environment and actual cwd, never verification-only
/// metadata or the socket's relay status (a relay can still run local Codex).
func guardCodexWriterBeforeRestore(
sessionID: String?,
arguments: [String],
environment: [String: String],
includeOwnerDetails: Bool = true
) throws {
guard let sessionID else { return }
let preflight = includeOwnerDetails ? CodexWriterRestorePreflight() : CodexWriterRestorePreflight { _ in
CodexWriterOwnerScan(owners: [], isComplete: false)
}
let inspection = preflight.inspect(
sessionID: sessionID,
arguments: arguments,
environment: environment,
workingDirectory: FileManager.default.currentDirectoryPath,
fallbackHome: NSHomeDirectory()
)
guard !inspection.permitsLaunch else { return }
throw loggedRestoreError(
stage: inspection.lock?.state == .active ? "session.active-writer" : "session.writer-check-unavailable",
detail: "session=\(sessionID)",
message: CodexWriterRestoreMessage(inspection: inspection).text
)
}

/// A login-shell command may override its parent's home. Only literal
/// Codex commands carrying their own absolute CODEX_HOME can be preflighted
/// without changing the captured command or evaluating arbitrary shell code.
func guardLegacyCodexWriter(
command: String,
record: RestoreRecord,
environment: [String: String],
includeOwnerDetails: Bool = true
) throws {
let normalizedMode = record.mode.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedKind = record.kind.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
guard normalizedMode == AgentRestoreRequestMode.resumeAgent.rawValue,
normalizedKind == "codex" else { return }
guard let sessionID = record.checkpointID,
let legacy = CodexLegacyRestoreCommand(command: command, sessionID: sessionID) else {
throw loggedRestoreError(
stage: "session.legacy-writer-scope",
detail: "legacy Codex home is not explicit",
message: String(
localized: "codex.restore.legacyScopeUnavailable",
defaultValue: "cmux cannot safely check ownership for this older shell-only Codex restore. No writer was started. Continue in the original terminal, or exit that session normally before retrying."
)
)
}
try guardCodexWriterBeforeRestore(
sessionID: sessionID,
arguments: legacy.arguments,
environment: environment.merging(legacy.environment) { _, saved in saved },
includeOwnerDetails: includeOwnerDetails
)
}
}
38 changes: 8 additions & 30 deletions CLI/CMUXCLI+Restore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,6 @@ extension CMUXCLI {
)
}

func controlAgentLaunchCommandPayload(
_ command: AgentLaunchCommand
) -> [String: Any] {
var payload: [String: Any] = ["arguments": command.arguments]
if let launcher = command.launcher {
payload["launcher"] = launcher
}
if let executablePath = command.executablePath {
payload["executable_path"] = executablePath
}
if let workingDirectory = command.workingDirectory {
payload["working_directory"] = workingDirectory
}
if let environment = command.environment {
payload["environment"] = environment
}
if let verificationHome = command.verificationHome {
payload["verification_home"] = verificationHome
}
if let capturedAt = command.capturedAt {
payload["captured_at"] = capturedAt
}
if let source = command.source {
payload["source"] = source
}
return payload
}

func runRestoreCommand(
commandArgs: [String],
Expand Down Expand Up @@ -124,6 +97,10 @@ extension CMUXCLI {
}
}

let environment = processEnvironment.merging(record.environment) { _, restored in restored }
if record.launchCommand == nil, record.preparedArguments == nil, let command = record.legacyCommand {
try guardLegacyCodexWriter(command: command, record: record, environment: environment)
}
// Legacy command-only records predate structured launch captures, but
// an agent-hook Codex record still names a mutable surface owner. Claim
// that generation before handing the shell command to exec so an
Expand Down Expand Up @@ -151,9 +128,6 @@ extension CMUXCLI {
return
}

let environment = processEnvironment.merging(record.environment) { _, restored in
restored
}
if record.launchCommand == nil,
record.preparedArguments == nil,
let legacyCommand = record.legacyCommand {
Expand Down Expand Up @@ -206,6 +180,7 @@ extension CMUXCLI {
ambientEnvironment: processEnvironment
) else {
if let legacyCommand = record.legacyCommand {
try guardLegacyCodexWriter(command: legacyCommand, record: record, environment: environment)
if codexRestoreBindingRequiresClaim(record),
!claimCodexRestoreBinding(
record: record,
Expand Down Expand Up @@ -242,6 +217,9 @@ extension CMUXCLI {
)
}

try guardCodexWriterBeforeRestore(
sessionID: invocation.codexResumeSessionID, arguments: invocation.arguments, environment: invocation.environment
)
for preflight in invocation.preflightInvocations {
try runRestorePreflight(
preflight,
Expand Down
7 changes: 7 additions & 0 deletions CLI/CMUXCLI+RestoreExecution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ extension CMUXCLI {
if let appliedWorkingDirectory {
invocationEnvironment["PWD"] = appliedWorkingDirectory
}
try guardCodexWriterBeforeRestore(
sessionID: invocation.codexResumeSessionID,
arguments: invocation.arguments,
environment: invocationEnvironment,
includeOwnerDetails: false
)
guard let first = invocation.arguments.first,
let executable = resolveRestoreExecutable(
first,
Expand Down Expand Up @@ -94,6 +100,7 @@ extension CMUXCLI {
if let appliedWorkingDirectory {
legacyEnvironment["PWD"] = appliedWorkingDirectory
}
try guardLegacyCodexWriter(command: command, record: record, environment: legacyEnvironment, includeOwnerDetails: false)
client.close()
try execLegacyRestoreCommand(command, environment: legacyEnvironment)
}
Expand Down
32 changes: 32 additions & 0 deletions CLI/CMUXCLI+RestoreLaunchPayload.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import CMUXAgentLaunch
import Foundation

extension CMUXCLI {
func controlAgentLaunchCommandPayload(
_ command: AgentLaunchCommand
) -> [String: Any] {
var payload: [String: Any] = ["arguments": command.arguments]
if let launcher = command.launcher {
payload["launcher"] = launcher
}
if let executablePath = command.executablePath {
payload["executable_path"] = executablePath
}
if let workingDirectory = command.workingDirectory {
payload["working_directory"] = workingDirectory
}
if let environment = command.environment {
payload["environment"] = environment
}
if let verificationHome = command.verificationHome {
payload["verification_home"] = verificationHome
}
if let capturedAt = command.capturedAt {
payload["captured_at"] = capturedAt
}
if let source = command.source {
payload["source"] = source
}
return payload
}
}
27 changes: 27 additions & 0 deletions Packages/macOS/CMUXAgentLaunch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,30 @@ let results = CodexSessionResumeVerifier().verifyBatch(
fileManager: fixtureFileManager
)
```

## Testing Codex writer ownership

`CodexWriterRestorePreflight` consumes final argv, environment, and actual cwd.
Tests create temporary homes and hold their own nonblocking `flock`; they never
use a real conversation or remove a provider-owned lock. Inject `ownerLookup`
to model a release during discovery, and use `mappedSurface(in:)` to check
ambiguous runtime candidates without AppKit. Production continuation requires
an active lock, a complete single-holder scan, current process ancestry and
kernel TTY, then a second process/runtime-generation check before focus.

```swift
let result = CodexWriterRestorePreflight().inspect(
sessionID: fixtureThreadID,
arguments: ["codex", "resume", fixtureThreadID],
environment: ["CODEX_HOME": fixtureHome.path],
workingDirectory: fixtureProject.path,
fallbackHome: fixtureUserHome.path
)
```

The probe releases its own descriptor before launch. Codex remains the final
atomic lock authority for later races. A local actor cannot replace this
cross-process kernel check. CLI execution stays synchronous for `execve`;
Vault awaits bounded inspection off the main actor. Unknown owners, remote
providers, and uninspectable legacy shell commands never trigger guessed focus,
lock deletion, process termination, or an implicit fork.
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ public struct AgentRestoreInvocation: Equatable, Sendable {
public let environment: [String: String]
/// Typed subprocesses that must succeed before the final process replacement.
public let preflightInvocations: [AgentRestorePreflightInvocation]
/// Validated Codex thread requiring an ownership check at the exec boundary.
public let codexResumeSessionID: String?

/// Creates a planned restore or fork invocation.
public init(
arguments: [String],
workingDirectory: String?,
environment: [String: String],
preflightInvocations: [AgentRestorePreflightInvocation] = []
preflightInvocations: [AgentRestorePreflightInvocation] = [],
codexResumeSessionID: String? = nil
) {
self.arguments = arguments
self.workingDirectory = workingDirectory
self.environment = environment
self.preflightInvocations = preflightInvocations
self.codexResumeSessionID = codexResumeSessionID
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ public struct AgentRestorePlanner: Sendable {
ambientEnvironment: ambientEnvironment,
profilePin: hermesProfilePin
)
let normalizedCheckpointID = normalized(request.checkpointID)
return AgentRestoreInvocation(
arguments: routedArguments,
workingDirectory: workingDirectory,
environment: environment,
preflightInvocations: preflights
preflightInvocations: preflights,
codexResumeSessionID: kind == "codex" && request.mode == .resumeAgent ? normalizedCheckpointID : nil
)
}

Expand Down Expand Up @@ -191,6 +193,12 @@ public struct AgentRestorePlanner: Sendable {
) -> [String: String] {
var captured = request.launchCommand?.environment ?? [:]
captured.merge(request.environment) { _, binding in binding }
if kind == "codex", request.mode == .resumeAgent, normalized(captured["CODEX_HOME"]) == nil,
let home = normalized(request.launchCommand?.verificationHome) ?? normalized(captured["HOME"]) {
// Verification and execution must select the same account, even
// when restore runs from a different login shell after relaunch.
captured["CODEX_HOME"] = CodexHomeResolver().resolve(launchVerificationHome: home)
}
if kind == "codex",
let rawCodexHome = normalized(captured["CODEX_HOME"]),
let launchWorkingDirectory = normalized(request.launchCommand?.workingDirectory)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import Foundation

/// The inspectable subset of old, literal shell-only Codex resume commands.
///
/// Parsing never executes shell code or rewrites the original command. An
/// absolute inline CODEX_HOME is required because login-shell startup can
/// override inherited environment. Complex commands deliberately fail closed.
public struct CodexLegacyRestoreCommand: Sendable {
/// Literal argv, including the Codex executable.
public let arguments: [String]
/// Inline assignments that determine the actual account.
public let environment: [String: String]

/// Recognizes a literal resume bound to the expected session and account.
/// - Parameters:
/// - command: Original shell command, not evaluated by this parser.
/// - sessionID: Validated checkpoint UUID; a different command binding is rejected.
public init?(command: String, sessionID: String) {
guard let words = Self.literalWords(command), UUID(uuidString: sessionID) != nil else { return nil }
var index = 0
var environment: [String: String] = [:]
if words.first == "exec" { index += 1 }
if index < words.count, ["env", "/usr/bin/env"].contains(words[index]) { index += 1 }
while index < words.count, let equals = words[index].firstIndex(of: "=") {
let name = String(words[index][..<equals])
guard ["CODEX_HOME", "HOME"].contains(name) else { return nil }
environment[name] = String(words[index][words[index].index(after: equals)...])
index += 1
}
guard let home = environment["CODEX_HOME"], home.hasPrefix("/"),
index < words.count, (words[index] as NSString).lastPathComponent == "codex" else { return nil }
let arguments = Array(words[index...])
var positionals: [String] = []
index = 1
while index < arguments.count {
let argument = arguments[index]
guard argument != "--" else { return nil }
if argument.hasPrefix("-") {
index += AgentLaunchSanitizer.optionWidth(arguments, index: index, policy: AgentLaunchSanitizer.codexPolicy)
} else {
positionals.append(argument)
index += 1
}
}
guard positionals.count == 2, positionals[0] == "resume",
UUID(uuidString: positionals[1]) == UUID(uuidString: sessionID) else { return nil }
self.arguments = arguments
self.environment = environment
}

/// Accepts only literal POSIX words; expansion, redirection and command lists are not a safe scope.
private static func literalWords(_ command: String) -> [String]? {
guard command.utf8.count <= 65_536 else { return nil }
var words: [String] = []
var word = ""
var quote: Character?
var escaped = false
var started = false
for character in command {
guard !character.isNewline, character != "\0" else { return nil }
if escaped {
word.append(character)
escaped = false
} else if quote == "'" {
if character == "'" { quote = nil } else { word.append(character) }
} else if character == "\\" {
// Double-quoted backslash rules differ from unquoted ones.
guard quote == nil else { return nil }
escaped = true
started = true
} else if character == "$" || character == "`" {
return nil
} else if let currentQuote = quote {
if character == currentQuote { quote = nil } else { word.append(character) }
} else if character == "'" || character == "\"" {
quote = character
started = true
} else if character.isWhitespace {
if started { words.append(word); word = ""; started = false }
} else if ";&|<>(){}[]*?!~#".contains(character) {
return nil
} else {
word.append(character)
started = true
}
}
guard quote == nil, !escaped else { return nil }
if started { words.append(word) }
return words
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

/// A point-in-time probe of Codex's writer lock, never a reservation to launch.
public struct CodexWriterLockInspection: Equatable, Sendable {
/// Whether the exact file is free, actively locked, or cannot be inspected.
public enum State: Equatable, Sendable {
/// No writer held the lock at the instant of the probe.
case available
/// Another descriptor held an incompatible kernel lock.
case active
/// The lock could not be inspected safely; do not start a writer.
case unavailable
}

/// The result of a nonblocking kernel lock probe.
public let state: State
/// The home/account whose lock was inspected.
public let codexHome: String
/// The exact lock filename, suitable for a read-only diagnostic command.
public let lockPath: String
let device: Int32?
let inode: UInt64?
}
Loading
Loading