Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2852b57
test: cover retained cwd policy on fork and retarget paths
austinywang Sep 11, 2026
1a50bd7
fix: enforce remote restore cwd policy
austinywang Sep 14, 2026
a426b99
ci: remove forbidden bare iOS runner option
austinywang Sep 14, 2026
47b21e4
test: complete coderouter repository mock
austinywang Sep 14, 2026
0237f82
test: complete shared coderouter mocks
austinywang Sep 14, 2026
845de82
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 14, 2026
13f77b5
ci: resolve dynamic Dock shortcut actions in guard
austinywang Sep 14, 2026
d766953
fix: clear Swift warning budget after main sync
austinywang Sep 14, 2026
58a12f2
test: wire renderer callbacks in surface fixtures
austinywang Sep 15, 2026
64fd1d9
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
cb34b2a
test: stabilize renderer surface fixtures
austinywang Sep 15, 2026
55813fc
test: protect one-shot renderer registration and native opt-out
austinywang Sep 15, 2026
9dbbc21
fix: preserve one-shot renderer callback registration
austinywang Sep 15, 2026
667236d
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
3bdcbf0
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
37bdc55
docs: cover vm.scp_info socket method
austinywang Sep 15, 2026
eddc09a
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
11a7166
test: replace timing sleeps with causal synchronization
austinywang Sep 15, 2026
8ba0555
test: await dashboard socket send signals
austinywang Sep 15, 2026
76591ba
fix: use current mobile host frame timeout label
austinywang Sep 15, 2026
25d99db
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
57bcb7e
test: align catalog and group title fixtures with current contracts
austinywang Sep 15, 2026
22881fd
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
fd43275
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 15, 2026
82a0c1a
test: authorize terminal portal visibility fixtures
austinywang Sep 15, 2026
3634834
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 16, 2026
b4723f2
fix: deduplicate web repository mocks
austinywang Sep 16, 2026
0eaf554
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 16, 2026
7ff12a2
test: allow intervening Chrome flags in guide contract
austinywang Sep 16, 2026
1cafecf
Merge remote-tracking branch 'origin/main' into fix/remote-restore-lo…
austinywang Sep 16, 2026
57ddffd
test: align transfer progress fixture with SCP push
austinywang Sep 16, 2026
19ffec6
fix: complete iOS pairing prompt translations
austinywang Sep 16, 2026
7ecab85
Merge origin/main and align beta version guard
austinywang Sep 16, 2026
ac86655
fix: give app and test build entries unique project IDs
austinywang Sep 16, 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
14 changes: 10 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -783,12 +783,12 @@ jobs:
echo "::warning::Passwordless sudo unavailable; XCTest will use its default automation-mode setup"
fi

- name: Run bundled command PATH regression
- name: Run remote restore and bundled command regressions
if: ${{ matrix.shard == fromJSON(env.CMUX_APP_HOST_FOCUSED_REGRESSION_SHARD) }}
run: |
# The tolerant full-suite step accepts ordinary Swift Testing failures.
# Keep the shell-resolution integration test non-tolerant so losing
# cmux's bundled commands from PATH cannot pass a shard.
# Swift Testing assertion failures are tolerated in the full sharded
# app-host suite. Keep these focused correctness boundaries
# non-tolerant so a local cwd or missing bundled command cannot pass.
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
if ! command -v fish >/dev/null 2>&1; then
Expand All @@ -803,6 +803,12 @@ jobs:
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
CMUX_SKIP_ZIG_BUILD=1 \
-only-testing:cmuxTests/RemoteAgentRestoreWorkingDirectoryTests \
-only-testing:cmuxTests/RemoteResumeBindingTests/surfaceRestoreRecordExactNilCwdDoesNotUseCapturedFallbacks \
-only-testing:cmuxTests/RemoteResumeBindingTests/legacyPersistentAgentHookBindingWithoutCwdPolicyReattachesWithoutReplayingStartupInput \
-only-testing:cmuxTests/RemoteResumeBindingTests/authenticatedPersistentSSHRefreshOverridesStaleUnavailablePolicy \
-only-testing:cmuxTests/SessionPersistenceResumeBindingTests/unavailableSelectionErasesPersistedAgentRestoreRecipe \
-only-testing:cmuxTests/SessionPersistenceResumeBindingTests/resumeBindingSelectionChangesAutosaveFingerprint \
-only-testing:cmuxTests/CmuxBundledBinPathIntegrationTests \
test

Expand Down
3 changes: 2 additions & 1 deletion CLI/CMUXCLI+SessionsListForkStartupInput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ extension CMUXCLI {
let workingDirectory = sessionsListNormalized(launchCommand?.workingDirectory ?? record.cwd)
let sanitizedCommandParts = AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: commandParts,
workingDirectory: workingDirectory
workingDirectory: workingDirectory,
agentKind: agent
)
let shellCommand = agent == "codex"
? AgentResumeArgv.renderedPortableCodexResumeShellCommand(
Expand Down
3 changes: 1 addition & 2 deletions CLI/cmux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32079,8 +32079,7 @@ struct CMUXCLI {

let cwd = normalizedHookValue(workingDirectory)
let sanitizedCommandParts = AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: commandParts,
workingDirectory: cwd
from: commandParts, workingDirectory: cwd, agentKind: kind
)
let resumeCommandParts = kind == "hermes-agent"
? hermesAgentArgumentsByReplacingOpenAICodexProvider(sanitizedCommandParts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,28 @@ struct CmxConnectivityPeerSessionTests {

_ = try await first.value
_ = try await second.value

#expect(await builder.callCount() == 1)
#expect(await peer.snapshot().phase == .connected)
#expect(await peer.snapshot().connectionGeneration == 1)
}

@Test
func onePeerTraceUsesOneAliasAndOneEstablishedSessionEvent() async throws {
let request = try Self.request()
let peerID = try CmxConnectivityPeerID(request: request)
let log = DiagnosticLog(capacity: 32, role: .mobileClient)
let admitted = TestConnectivitySession(continuityID: 17)
let builder = SequencedConnectivitySessionBuilder(sessions: [admitted])
let builder = GatedConnectivitySessionBuilder(session: admitted)
let peer = CmxConnectivityPeerSession(
peerID: peerID,
buildSession: { request in try await builder.build(request) },
diagnosticLog: log
)

_ = try await peer.connectedSession(for: request)
// The gated builder parks every dial until released. Awaiting the
// dial before releasing the gate would deadlock this test.
let dial = Task { try await peer.connectedSession(for: request) }
try await Self.waitUntil { await builder.callCount() == 1 }
await builder.release()
_ = try await dial.value
await peer.releaseControl(ownerID: UUID())
await peer.invalidate()
#expect(await waitForDiagnosticProcessedCount(log, atLeast: 3))
Expand All @@ -57,7 +59,6 @@ struct CmxConnectivityPeerSessionTests {
#expect(lifecycle.compactMap(\.surface).count == lifecycle.count)
#expect(Set(lifecycle.compactMap(\.surface)).count == 1)
}

@Test
func nextControlOwnerWaitsAndReleaseClosesThePeerConnection() async throws {
let request = try Self.request()
Expand All @@ -76,7 +77,6 @@ struct CmxConnectivityPeerSessionTests {
)
let firstOwner = UUID()
let secondOwner = UUID()

_ = try await peer.acquireControl(for: request, ownerID: firstOwner)
let secondAcquire = Task {
try await peer.acquireControl(for: routeVariant, ownerID: secondOwner)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,16 @@ actor IrxAsyncLatch {
pending.forEach { $0.resume() }
}
}

enum IrxAsyncWait {
static func until(
_ condition: @escaping @Sendable () async -> Bool
) async throws {
for _ in 0..<20 {
if await condition() { return }
try await Task.sleep(for: .milliseconds(50))
}
struct TimedOut: Error {}
throw TimedOut()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ struct IrxLiveQUICTests {
)
)
await retired.connection.close(code: .explicitRedial, origin: .local)
try await Task.sleep(for: .milliseconds(150))
try await IrxAsyncWait.until { await engine.currentSession() == nil }
#expect(await engine.currentSession() == nil)
#expect(
journal.counterSnapshot()["dial-started"] ?? 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ struct IrxLivenessTests {
}
try await waitUntil { await host.probeCount == 1 }
await session.connection.setApplicationActive(false)
// Deliberately outlast the cancelled probe deadline. This represents time
// during which iOS is backgrounded and cannot perform application work.
try await Task.sleep(for: .milliseconds(250))
#expect(host.journal.counterSnapshot()["miss", default: 0] == 0)
#expect(await host.deathCount == 0)
#expect(await host.probeCount == 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ struct IrxRelayCredentialInstallerTests {
let native = RelayInstallProbe(failFirst: true)
let gate = IrxRelayCredentialRotationGate()
let generation = await gate.begin()
let releaseRetry = IrxAsyncLatch()
let installer = IrxRelayCredentialInstaller(installed: [], journal: IrxLiveTestSupport.journal(),
sleep: { _ in
await native.noteSleep()
try await Task.sleep(for: .milliseconds(100))
await releaseRetry.wait()
}, install: { try await native.install($0) })
await installer.replace(with: [credential("old")], ownership: .init(gate: gate, generation: generation))
try await waitUntil { await native.sleepCount == 1 }
await gate.invalidate()
try await Task.sleep(for: .milliseconds(200))
await releaseRetry.signal()
try await waitUntil { await native.finished == 1 }
#expect(await native.tokens == ["old"])

let current = await gate.begin()
Expand Down Expand Up @@ -135,6 +137,7 @@ private actor RelayInstallProbe {
private(set) var maximumConcurrent = 0
private(set) var tokens: [String] = []
private(set) var completed = 0
private(set) var finished = 0
private(set) var sleepCount = 0

init(failFirst: Bool = false, holdFirst: Bool = false) {
Expand All @@ -146,7 +149,10 @@ private actor RelayInstallProbe {
tokens.append(credential.token)
concurrent += 1
maximumConcurrent = max(maximumConcurrent, concurrent)
defer { concurrent -= 1 }
defer {
concurrent -= 1
finished += 1
}
let first = tokens.count == 1
if holdFirst, first { await withCheckedContinuation { releaseWaiter = $0 } }
if failFirst, first { throw Failure.unavailable }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,24 @@ actor MobileTerminalLaneCoordinator {
return
}
let id = UUID()
let hasProvider: Bool
switch configuration.mode {
case .output:
hasProvider = provider != nil
case .inputOnly:
hasProvider = inputOnlyProvider != nil || provider != nil
}
entriesByKey[key] = Entry(
id: id,
configuration: configuration,
phase: .opening,
phase: hasProvider ? .opening : .failed,
lane: nil,
task: nil,
outputReady: false
)
launch(key: key, id: id)
if hasProvider {
launch(key: key, id: id)
}
}

func resume(surfaceID: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,39 @@ public enum AgentLaunchSanitizer {
}
return false
}

/// Removes captured cwd options before an argument boundary.
///
/// - Parameters:
/// - args: The captured command arguments to sanitize.
/// - workingDirectory: The saved cwd whose matching options should be removed.
/// - agentKind: The exact built-in kind, or `nil` for a custom registration.
/// - removeAllWorkingDirectoryOptions: Whether to remove every cwd option regardless of value.
/// - Returns: Sanitized arguments while preserving content after `--`.
public static func removingSavedWorkingDirectoryOptions(
from args: [String],
workingDirectory: String?
workingDirectory: String?,
agentKind: String? = nil,
removeAllWorkingDirectoryOptions: Bool = false
) -> [String] {
guard let workingDirectory = normalizedWorkingDirectory(workingDirectory) else {
let savedWorkingDirectory = normalizedWorkingDirectory(workingDirectory)
guard removeAllWorkingDirectoryOptions || savedWorkingDirectory != nil else {
return args
}

let valueOptions: Set<String> = ["--cd", "-C", "--cwd", "--workspace", "-w"]
let optionPolicy = AgentWorkingDirectoryOptionPolicy(
agentKind: agentKind,
builtInAgentKind: agentKind
)
let valueOptions = optionPolicy.valueOptions
let unconditionallyRemovableValueOptions = optionPolicy.unconditionallyRemovableValueOptions
Comment thread
austinywang marked this conversation as resolved.
let attachedShortValueOptions = optionPolicy.attachedShortValueOptions
let optionPrefixes = valueOptions.map { "\($0)=" }
let shouldRemoveValue: (String, String) -> Bool = { option, value in
(removeAllWorkingDirectoryOptions && unconditionallyRemovableValueOptions.contains(option)) ||
savedWorkingDirectory.map {
workingDirectoryValue(value, matches: $0)
} == true
}
var result: [String] = []
var index = 0
while index < args.count {
Expand All @@ -255,15 +278,45 @@ public enum AgentLaunchSanitizer {
result.append(contentsOf: args[index...])
break
}
if valueOptions.contains(arg),
index + 1 < args.count,
workingDirectoryValue(args[index + 1], matches: workingDirectory) {
index += 2
continue
if valueOptions.contains(arg) {
guard index + 1 < args.count else {
if removeAllWorkingDirectoryOptions &&
unconditionallyRemovableValueOptions.contains(arg) {
index += 1
continue
}
result.append(arg)
index += 1
continue
}
let value = args[index + 1]
if removeAllWorkingDirectoryOptions &&
unconditionallyRemovableValueOptions.contains(arg) &&
value.hasPrefix("-") && value != "-" {
// An option-looking token cannot be a reliable cwd value.
// Remove only the incomplete cwd option so the next option
// remains available to the replayed agent command.
index += 1
continue
}
if shouldRemoveValue(arg, value) {
index += 2
continue
}
}
if let prefix = optionPrefixes.first(where: { arg.hasPrefix($0) }) {
let option = String(prefix.dropLast())
let value = String(arg.dropFirst(prefix.count))
if workingDirectoryValue(value, matches: workingDirectory) {
if shouldRemoveValue(option, value) {
index += 1
continue
}
}
if let option = attachedShortValueOptions.first(where: {
arg.count > $0.count && arg.hasPrefix($0)
}) {
let value = String(arg.dropFirst(option.count))
if shouldRemoveValue(option, value) {
index += 1
continue
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public struct AgentRestorePlanner: Sendable {
sanitizedArguments = workingDirectories.reduce(plannedArguments.values) {
AgentLaunchSanitizer.removingSavedWorkingDirectoryOptions(
from: $0,
workingDirectory: $1
workingDirectory: $1,
agentKind: kind
)
}
} else {
Expand Down
Loading
Loading