Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8f91d6b
Add failing regression tests for layout capture of cmux-injected code…
austinywang Jul 8, 2026
160bb0c
Sanitize captured agent commands via shared sanitizer; fail loudly on…
austinywang Jul 8, 2026
9e079ee
Strip only cmux's own codex hook flags; gate node/bun unwrap on node_…
austinywang Jul 8, 2026
0398d3c
Gate node/bun agent unwrap on cmux wrapper-injected hook markers
austinywang Jul 8, 2026
e9573a9
Derive unwrap agent name from cmux marker for package entrypoints lik…
austinywang Jul 8, 2026
266668c
Move file-local codex launch helpers to file scope
austinywang Jul 8, 2026
a920f3f
Require the marker agent's package path for basename-less unwrap fall…
austinywang Jul 8, 2026
8de4c82
Replay marker-bearing native agent launches through the bare name
austinywang Jul 8, 2026
ce1da96
Fix save-dialog disclosure text views rendering blank
austinywang Jul 8, 2026
f25045b
Split save-dialog accessory regression test into its own wired test file
austinywang Jul 8, 2026
7d6ca65
Normalize project.pbxproj
austinywang Jul 8, 2026
d815bca
Merge origin/main into save layout commands
austinywang Jul 8, 2026
0bdb341
Fix save layout review findings
austinywang Jul 8, 2026
f354c9d
Tighten save layout wrapper proof
austinywang Jul 8, 2026
1138177
Merge remote-tracking branch 'origin/main' into issue-7631-save-layou…
austinywang Jul 8, 2026
6b4cce1
Add Claude session wrapper regression coverage
austinywang Jul 8, 2026
8aaea83
Recognize Claude wrapper session marker
austinywang Jul 8, 2026
7a84a1b
Use instance unwrapper for runtime agent argv
austinywang Jul 8, 2026
1699b18
Add Claude wrapper identity regression coverage
austinywang Jul 8, 2026
ad693db
Require structured Claude wrapper marker
austinywang Jul 8, 2026
5c7035b
Update Claude wrapper app test fixture
austinywang Jul 8, 2026
3de96e6
Route marker-backed Codex replays through wrapper
austinywang Jul 8, 2026
22759b2
Split JavaScript runtime launch unwrapper
austinywang Jul 8, 2026
54ccbc7
Trim action saver test file under guard budget
austinywang Jul 8, 2026
4346df8
Preserve explicit Codex executable with hook config
austinywang Jul 8, 2026
a0bfeb5
Sanitize node-hosted Codex resume captures
austinywang Jul 8, 2026
956edff
Preserve hook config for captured agent paths
austinywang Jul 8, 2026
1baeb11
Move Claude launch preservation out of sanitizer namespace
austinywang Jul 9, 2026
a7fa722
Strip cmux hook payloads from saved layout commands
austinywang Jul 9, 2026
a9eef49
Route stripped saved-layout agents through wrappers
austinywang Jul 9, 2026
3288aca
Preserve captured agent executables for saved layouts
austinywang Jul 9, 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
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,18 @@ public struct AgentForkArgv: Sendable, Equatable {
return ["claude", "--resume", sessionId, "--fork-session"] + preserved
case "codex":
let parts = commandParts(executablePath: executablePath, arguments: arguments, fallbackExecutable: "codex")
guard let preserved = AgentLaunchSanitizer.preservedArguments(kind: "codex-fork-replay", args: parts.tail) else {
guard let preserved = preservedCodexForkArguments(
args: parts.tail,
preservePromptTags: true,
stripCmuxHooks: parts.executable == "codex"
) else {
return nil
}
return [parts.executable, "fork", sessionId] + preserved
let replayExecutable = codexReplayExecutable(
capturedExecutable: parts.executable,
launchTail: parts.tail
)
return [replayExecutable, "fork", sessionId] + preserved
case "opencode":
let parts = commandParts(executablePath: executablePath, arguments: arguments, fallbackExecutable: "opencode")
guard let preserved = AgentLaunchSanitizer.preservedArguments(kind: "opencode", args: parts.tail) else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,21 @@ public enum AgentLaunchSanitizer {
var scansOptionsPastPositionals: Bool = false
var skipClaudeHookSettings: Bool = false
}
/// Returns launch arguments with non-restorable agent resume/session artifacts removed.
///
/// - Parameters:
/// - arguments: Captured argv, including the executable as element zero.
/// - launcher: The cmux launcher token associated with the argv, if any.
/// - fallbackKind: Agent kind to use when `launcher` does not select a wrapper.
/// - stripCmuxHookArguments: Remove cmux-owned hook injection even when the captured
/// executable is an absolute path. This keeps save-layout replay commands below
/// pty canonical-line limits without treating hook argv as executable identity proof.
/// - Returns: Sanitized argv, or nil when the launch form is not restorable.
public static func sanitizedLaunchArguments(
_ arguments: [String],
launcher: String,
fallbackKind: String
fallbackKind: String,
stripCmuxHookArguments: Bool = false
) -> [String]? {
guard let executable = arguments.first, !executable.isEmpty else { return nil }
var tail = Array(arguments.dropFirst())
Expand Down Expand Up @@ -66,9 +77,19 @@ public enum AgentLaunchSanitizer {
}

switch fallbackKind {
case "codex":
guard let preserved = preservedCodexLaunchArguments(args: tail) else { return nil }
case "claude":
guard let preserved = ClaudeLaunchArgumentsPreserver().preservedArguments(
args: tail,
stripCmuxHookSettings: stripCmuxHookArguments || executable == "claude"
) else { return nil }
return [executable] + preserved
case "codex":
guard let preserved = preservedCodexLaunchArguments(
args: tail,
stripCmuxHooks: stripCmuxHookArguments || executable == "codex"
) else { return nil }
let replayExecutable = codexReplayExecutable(capturedExecutable: executable, launchTail: tail)
return [replayExecutable] + preserved
case "rovodev":
guard let preserved = preservedArguments(kind: fallbackKind, args: tail) else { return nil }
return [executable, "rovodev", "run"] + preserved
Expand All @@ -79,67 +100,13 @@ public enum AgentLaunchSanitizer {
}

public static func preservedArguments(kind: String, args: [String]) -> [String]? {
func preserveCodexFork(_ preservePromptTags: Bool) -> [String]? {
func dropForkPositionals(_ args: [String], forkCommand: CodexForkCommand) -> [String] {
var result: [String] = []
var index = 0
var skippedSession = false

while index < args.count {
let arg = args[index]
if arg == "--" { break }
if index == forkCommand.forkIndex { index += 1; continue }
if index == forkCommand.sessionIndex { skippedSession = true; index += 1; continue }
if !arg.hasPrefix("-") || arg == "-" {
if skippedSession && preservePromptTags { result.append(arg) }
index += 1
continue
}

let width = optionWidth(args, index: index, policy: codexPolicy)
let end = min(args.count, index + width)
if codexPolicy.variadicOptions.contains(arg),
forkCommand.forkIndex > index,
forkCommand.forkIndex < end {
if forkCommand.forkIndex > index + 1 {
result.append(contentsOf: args[index..<forkCommand.forkIndex])
}
index = forkCommand.forkIndex
continue
}
if codexPolicy.variadicOptions.contains(arg),
forkCommand.sessionIndex > index,
forkCommand.sessionIndex < end {
if forkCommand.sessionIndex > index + 1 {
result.append(contentsOf: args[index..<forkCommand.sessionIndex])
}
index = forkCommand.sessionIndex
continue
}
result.append(contentsOf: args[index..<end])
index += width
}

return result
}

var tail = args; var preservePositionals = false
if let forkCommand = codexForkCommand(in: tail) {
tail = dropForkPositionals(tail, forkCommand: forkCommand); preservePositionals = preservePromptTags
}
var policy = codexPolicy; policy.preservePositionals = preservePositionals
if preservePositionals {
policy.nonRestorableCommands = []
}
return preserveOptions(tail, policy: policy)
}
switch kind {
case "claude":
return preserveOptions(args, policy: claudePolicy)
return ClaudeLaunchArgumentsPreserver().preservedArguments(args: args)
case "codex":
return preserveOptions(args, policy: codexPolicy)
case "codex-fork-replay": return preserveCodexFork(true)
case "codex-fork-restore": return preserveCodexFork(false)
return preservedCodexLaunchArguments(args: args)
case "codex-fork-replay": return preservedCodexForkArguments(args: args, preservePromptTags: true)
case "codex-fork-restore": return preservedCodexForkArguments(args: args, preservePromptTags: false)
case "grok":
return preserveOptions(args, policy: grokPolicy)
case "pi", "omp":
Expand Down Expand Up @@ -297,85 +264,6 @@ public enum AgentLaunchSanitizer {
return result
}

private static func preservedCodexLaunchArguments(args: [String]) -> [String]? {
if let forkCommand = codexForkCommand(in: args) {
return CodexForkLaunchCapture(args: args, forkIndex: forkCommand.forkIndex, sessionIndex: forkCommand.sessionIndex, preserveOptions: preserveOptions)
.arguments()
}
return preservedArguments(kind: "codex", args: args)
}

private struct CodexForkCommand {
let forkIndex: Int
let sessionIndex: Int
}

private static func codexForkCommand(in args: [String]) -> CodexForkCommand? {
var index = 0
while index < args.count {
let arg = args[index]
if arg == "--" {
return nil
}
if !isOptionToken(arg) || arg == "-" {
guard arg == "fork",
let sessionIndex = codexForkCommandSessionIndex(args, forkIndex: index) else {
return nil
}
return CodexForkCommand(forkIndex: index, sessionIndex: sessionIndex)
}
let width = optionWidth(args, index: index, policy: codexPolicy)
if codexPolicy.variadicOptions.contains(arg) {
let end = min(args.count, index + width)
if index + 2 < end {
for candidateIndex in (index + 2)..<end where args[candidateIndex] == "fork" {
if let sessionIndex = codexForkCommandSessionIndex(args, forkIndex: candidateIndex) {
return CodexForkCommand(forkIndex: candidateIndex, sessionIndex: sessionIndex)
}
}
}
}
index += width
}
return nil
}

private static func codexForkCommandSessionIndex(_ args: [String], forkIndex: Int) -> Int? {
var index = forkIndex + 1
while index < args.count {
let argument = args[index]
if argument == "--" {
return nil
}
if !argument.hasPrefix("-") || argument == "-" {
return looksLikeCodexSessionIdentifier(argument) ? index : nil
}
let width = optionWidth(args, index: index, policy: codexPolicy)
if codexPolicy.variadicOptions.contains(argument) {
let end = min(args.count, index + width)
if index + 2 < end {
for candidateIndex in (index + 2)..<end {
if looksLikeCodexSessionIdentifier(args[candidateIndex]) {
return candidateIndex
}
}
}
}
index += width
}
return nil
}

private static func looksLikeCodexSessionIdentifier(_ value: String) -> Bool {
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmed.count >= 20 else { return false }
if trimmed.hasPrefix("019") {
return true
}
let allowed = CharacterSet(charactersIn: "0123456789abcdefABCDEF-")
return trimmed.unicodeScalars.allSatisfy { allowed.contains($0) } && trimmed.contains("-")
}

private static func normalizedWorkingDirectory(_ value: String?) -> String? {
guard let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines),
!trimmed.isEmpty else {
Expand Down
Loading
Loading