Skip to content
Open
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
d1e6cd2
fix(vms): honor Team resume limits and resolve PR feedback
austinywang Sep 6, 2026
b0872ed
test(vms): pin seat allowance and provider fixture timestamps
austinywang Sep 6, 2026
e7484cf
test(vms): expose concurrent first-use network persistence failures
austinywang Sep 6, 2026
22ba417
fix(vms): serialize first-use network persistence per owner
austinywang Sep 6, 2026
b1f0dbd
test(vms): cover missing Freestyle stats required by resize
austinywang Sep 6, 2026
1485b49
fix(vms): expose Freestyle resource stats for real disk resize
austinywang Sep 6, 2026
1fb8c6c
Merge origin/main into Cloud VM review fixes
austinywang Sep 6, 2026
0317556
test(vms): reproduce inaccessible cmux-tui in the work-user shell
austinywang Sep 6, 2026
4bcec21
test(vms): reproduce private-home traversal failure on macOS
austinywang Sep 6, 2026
9b062fa
fix(vms): make the baked cmux-tui client accessible to ubuntu
austinywang Sep 6, 2026
59b7ff5
test(vm): quote public client fixture paths safely
austinywang Sep 6, 2026
9fa9655
test(vms): reproduce skipped public client repair on healthy attach
austinywang Sep 6, 2026
8d3a18a
fix(vms): finish public client repair after daemon readiness
austinywang Sep 6, 2026
69083b6
Merge branch 'main' of https://github.com/manaflow-ai/cmux into fix/c…
austinywang Sep 6, 2026
cf3e041
test(cloud): align fixtures and cover canonical IDs and IPv6 routes
austinywang Sep 6, 2026
914242d
fix(cloud): unify terminal identity, IPv6 URLs, and connect cancellation
austinywang Sep 6, 2026
e13a74f
Merge remote-tracking branch 'origin/main' into fix/cloud-vm-review-f…
austinywang Sep 8, 2026
7713461
Merge branch 'main' into fix/cloud-vm-review-followup
austinywang Sep 8, 2026
b09d2de
Merge branch 'main' into fix/cloud-vm-review-followup
austinywang Sep 8, 2026
703a2aa
fix(ci): align app-host tests with current targets
austinywang Sep 8, 2026
744eee1
Merge remote-tracking branch 'origin/main' into fix/cloud-vm-review-f…
austinywang Sep 8, 2026
7d7c180
Merge origin/main into fix/cloud-vm-review-followup
austinywang Sep 8, 2026
b059bf5
fix(web): typecheck devbox reachability entrypoint
austinywang Sep 8, 2026
4b7880e
test(vms): isolate guest self shim fixture
austinywang Sep 8, 2026
c8e6d3f
Merge remote-tracking branch 'origin/main' into fix/cloud-vm-review-f…
austinywang Sep 8, 2026
f7f8fae
Merge main into Cloud VM review fixes
lawrencecchen Sep 17, 2026
7605d1c
Merge main into Cloud VM review fixes
lawrencecchen Sep 19, 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
28 changes: 5 additions & 23 deletions CLI/VMRemoteWorkspaceResolver.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import Foundation

/// Pure catalog identity resolution shared by the CLI and its behavior tests.
Expand Down Expand Up @@ -329,33 +330,14 @@ struct VMRemoteWorkspaceResolver: Sendable {
return .resolved(terminalID: terminalID, tabID: tabID)
}

/// Returns the terminal key accepted by `surface.project` from either a
/// catalog's explicit `key` or its canonical resource id. Keeping this in
/// one helper prevents callers from sending a full id where a key is
/// required and producing `machine/terminal/machine/terminal/key`.
/// Shared canonical identity policy, including exact ID precedence over stale keys.
func vmTerminalID(in resource: [String: Any], machine: String) -> String? {
if let key = (resource["key"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !key.isEmpty {
// `key` is the final path component. A complete resource id would be
// prefixed again by callers and route to a different terminal.
guard !key.contains("/") else { return vmTerminalIDFromCanonicalID(in: resource, machine: machine) }
return key
}
return vmTerminalIDFromCanonicalID(in: resource, machine: machine)
CmuxCloudTerminalIdentity(catalogResource: resource, machine: machine)?.key
}

private func vmTerminalIDFromCanonicalID(in resource: [String: Any], machine: String) -> String? {
guard let id = (resource["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else {
return nil
}
let prefix = "\(machine)/terminal/"
if id.hasPrefix(prefix) {
let key = String(id.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
return key.isEmpty ? nil : key
}
// A few older catalog producers emitted the terminal key as `id`.
// Accept it only when it has no path separators, so a different
// machine's canonical id cannot be routed to this machine.
return id.contains("/") ? nil : id
guard let id = resource["id"] as? String else { return nil }
return CmuxCloudTerminalIdentity(catalogResource: ["id": id], machine: machine)?.key
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation

/// A terminal key resolved from a catalog row for one Cloud machine.
///
/// ```swift
/// let identity = CmuxCloudTerminalIdentity(
/// catalogResource: ["id": "vm-one/terminal/term-real", "key": "stale"],
/// machine: "vm-one"
/// )
/// // identity?.key is "term-real".
/// ```
public struct CmuxCloudTerminalIdentity: Sendable, Equatable {
/// The daemon terminal key, without a machine or resource-kind prefix.
public let key: String

/// Resolves canonical identity before considering legacy catalog fields.
///
/// A full resource ID is authoritative even when its separate `key` field is
/// stale. IDs for another machine or resource kind fail closed. Older rows
/// may supply only a `key` or an unqualified terminal ID.
///
/// - Parameters:
/// - catalogResource: The terminal catalog row to resolve.
/// - machine: The machine whose daemon will receive the key.
public init?(catalogResource: [String: Any], machine: String) {
guard !machine.isEmpty else { return nil }
let id = (catalogResource["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
if let id, id.contains("/") {
let prefix = "\(machine)/terminal/"
guard id.hasPrefix(prefix) else { return nil }
let key = String(id.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
guard !key.isEmpty, !key.contains("/") else { return nil }
self.key = key
return
}
if let key = (catalogResource["key"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines),
!key.isEmpty, !key.contains("/") {
self.key = key
return
}
guard let id, !id.isEmpty else { return nil }
key = id
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,19 @@ public enum CmuxInternalHostnames {
/// sometimes works is worse than one that always does. An IPv6 literal is
/// bracketed the way every URL scheme requires.
public static func directPortURL(privateAddress: String, port: Int) -> String {
let bracketed = privateAddress.contains(":") ? "[\(privateAddress)]" : privateAddress
return "http://\(bracketed):\(port)"
"http://\(urlHost(privateAddress: privateAddress)):\(port)"
}

/// Formats an IP literal for a URL or `URLComponents.host`.
///
/// - Parameter privateAddress: An IPv4 or IPv6 literal, optionally bracketed.
/// - Returns: IPv4 unchanged, or IPv6 enclosed in exactly one pair of brackets.
public static func urlHost(privateAddress: String) -> String {
guard privateAddress.contains(":") else { return privateAddress }
if privateAddress.hasPrefix("["), privateAddress.hasSuffix("]") {
return privateAddress
}
return "[\(privateAddress)]"
}

/// Render entries as the managed block's body (no markers): one `ip host`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Testing
@testable import CmuxFoundation

struct CmuxCloudTerminalIdentityTests {
@Test func canonicalIDWinsOverAStaleKey() {
let identity = CmuxCloudTerminalIdentity(
catalogResource: ["id": "vm-one/terminal/term-real", "key": "stale-key"],
machine: "vm-one"
)
#expect(identity?.key == "term-real")
}

@Test func acceptsLegacyKeysAndUnqualifiedIDs() {
#expect(CmuxCloudTerminalIdentity(catalogResource: ["key": " term-old "], machine: "vm-one")?.key == "term-old")
#expect(CmuxCloudTerminalIdentity(catalogResource: ["id": "term-old"], machine: "vm-one")?.key == "term-old")
#expect(CmuxCloudTerminalIdentity(catalogResource: ["id": "term-old", "key": "term-explicit"], machine: "vm-one")?.key == "term-explicit")
}

@Test func malformedCanonicalIDsCannotFallBackToAKey() {
for id in ["vm-other/terminal/term-real", "vm-one/browser/term-real", "vm-one/terminal/", "vm-one/terminal/vm-one/terminal/term-real"] {
#expect(CmuxCloudTerminalIdentity(catalogResource: ["id": id, "key": "term-real"], machine: "vm-one") == nil)
}
}

@Test func rejectsKeysThatAreResourcePathsOrEmpty() {
#expect(CmuxCloudTerminalIdentity(catalogResource: ["key": "vm-one/terminal/term-real"], machine: "vm-one") == nil)
#expect(CmuxCloudTerminalIdentity(catalogResource: ["key": " "], machine: "vm-one") == nil)
#expect(CmuxCloudTerminalIdentity(catalogResource: ["key": "term-real"], machine: "") == nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ struct CmuxInternalHostnamesTests {
CmuxInternalHostnames.directPortURL(privateAddress: "fd60:1e5e:6720::3", port: 22)
== "http://[fd60:1e5e:6720::3]:22"
)
#expect(
CmuxInternalHostnames.directPortURL(privateAddress: "[fd60:1e5e:6720::3]", port: 22)
== "http://[fd60:1e5e:6720::3]:22"
)
}

@Test("A dev build's scoped block coexists with the production block; each clears only its own")
Expand Down
8 changes: 4 additions & 4 deletions Sources/Cloud/CloudMachineLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,11 @@ actor CloudMachineLink {
return .timedOut
}
defer { group.cancelAll() }
let firstLine = try await group.next()
// Cancellation closes the first-value waiter as well as the
// timeout task. Its EOF must not be reported as a client exit.
let first = try await group.next()
// Cancellation resumes the non-throwing socket waiter with nil.
// Preserve cancellation instead of reporting that wakeup as a timeout.
try Task.checkCancellation()
switch firstLine {
switch first {
case .socket(let socket)?:
return socket
case .ended?:
Expand Down
29 changes: 3 additions & 26 deletions Sources/Surfaces/CmuxTuiRemoteRouting.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxFoundation
import Foundation

/// Pure remote catalog selector and placement resolution shared by the app and CLI.
Expand Down Expand Up @@ -360,32 +361,8 @@ enum CmuxTuiRemoteRouting {
return .resolved(terminalID: terminalID, tabID: tabID)
}

/// Returns the terminal key accepted by `surface.project` from either a
/// catalog's explicit `key` or its canonical resource id. Keeping this in
/// one helper prevents callers from sending a full id where a key is
/// required and producing `machine/terminal/machine/terminal/key`.
/// Resolves the daemon key through the same canonical-identity policy as the CLI.
static func vmTerminalID(in resource: [String: Any], machine: String) -> String? {
if let key = (resource["key"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !key.isEmpty {
// `key` is the final path component. A complete resource id would be
// prefixed again by callers and route to a different terminal.
guard !key.contains("/") else { return vmTerminalIDFromCanonicalID(in: resource, machine: machine) }
return key
}
return vmTerminalIDFromCanonicalID(in: resource, machine: machine)
}

private static func vmTerminalIDFromCanonicalID(in resource: [String: Any], machine: String) -> String? {
guard let id = (resource["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else {
return nil
}
let prefix = "\(machine)/terminal/"
if id.hasPrefix(prefix) {
let key = String(id.dropFirst(prefix.count)).trimmingCharacters(in: .whitespacesAndNewlines)
return key.isEmpty ? nil : key
}
// A few older catalog producers emitted the terminal key as `id`.
// Accept it only when it has no path separators, so a different
// machine's canonical id cannot be routed to this machine.
return id.contains("/") ? nil : id
CmuxCloudTerminalIdentity(catalogResource: resource, machine: machine)?.key
}
}
83 changes: 27 additions & 56 deletions cmuxTests/CLILocalTmuxReviewRegressionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,81 +62,52 @@ extension CLINotifyProcessIntegrationRegressionTests {
XCTAssertTrue(result.stderr.contains("only supports attach"), result.stderr)
}

func testLocalTmuxAttachCommandRunsThroughGhosttyLoginShellWrapper() throws {
let root = makeLocalTmuxTestRoot("ghostty-attach-wrapper")
// The command-builder and parser implementation live in the cmux-cli target.
// These app-host tests exercise the same contracts through the bundled CLI
// process so they do not import executable-only implementation details.
func testLocalTmuxClientListingUsesPopulatedTTYTarget() throws {
let cliPath = try bundledCLIPath()
let root = makeLocalTmuxTestRoot("client-tty-target")
let fakeTmuxURL = root.appendingPathComponent("fake-tmux", isDirectory: false)
let outputURL = root.appendingPathComponent("invocation", isDirectory: false)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }

let fakeTmux = """
#!/bin/sh
printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \
"$TMUX" "$CMUX_LOCAL_TMUX" "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8" \
> "$CMUX_TEST_OUTPUT"
case "$*" in
*list-sessions*) printf 'client-tty\t$999\t99999999-9999-9999-9999-999999999999\t999\t1\n'; exit 0 ;;
*list-clients*) printf '/dev/ttys999\tclient-tty\t123\t/dev/ttys999\n'; exit 0 ;;
*display-message*) printf 'client-tty\t$999\t99999999-9999-9999-9999-999999999999\t999\n'; exit 0 ;;
*has-session*) exit 0 ;;
*) exit 0 ;;
esac
"""
try Data(fakeTmux.utf8).write(to: fakeTmuxURL)
XCTAssertEqual(chmod(fakeTmuxURL.path, 0o755), 0)

let sessionID = try XCTUnwrap(LocalTmuxSessionIdentity("$7"))
let binding = LocalTmuxSessionBinding(
sessionID: sessionID,
serverID: UUID(uuidString: "cccccccc-cccc-cccc-cccc-cccccccccccc")!,
sessionCreated: 42
)
let command = LocalTmuxCommandBuilder(
tmuxPath: fakeTmuxURL.path,
socketPath: root.appendingPathComponent("server.sock").path
).attachCommand(binding: binding)

var environment = ProcessInfo.processInfo.environment
environment["CMUX_TEST_OUTPUT"] = outputURL.path
environment["CMUX_CLI_SENTRY_DISABLED"] = "1"
environment["CMUX_LOCAL_TMUX_BIN"] = fakeTmuxURL.path
environment["CMUX_LOCAL_TMUX_STATE_DIR"] = root.path
environment.removeValue(forKey: "CMUX_SOCKET")
environment.removeValue(forKey: "CMUX_SOCKET_PATH")

let result = runProcess(
executablePath: "/bin/bash",
arguments: ["--noprofile", "--norc", "-c", "exec -l \(command)"],
executablePath: cliPath,
arguments: ["local-tmux", "list", "--json"],
environment: environment,
timeout: 10
)

XCTAssertFalse(result.timedOut, result.stderr)
XCTAssertEqual(result.status, 0, result.stderr)
let invocation = try String(contentsOf: outputURL, encoding: .utf8)
XCTAssertTrue(invocation.hasPrefix("|1|-S|"), invocation)
XCTAssertTrue(invocation.contains("attach-session -t $7"), invocation)
}

func testLocalTmuxClientListingUsesPopulatedTTYTarget() throws {
let root = makeLocalTmuxTestRoot("client-tty-target")
let fakeTmuxURL = root.appendingPathComponent("fake-tmux", isDirectory: false)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }

let fakeTmux = """
#!/bin/sh
case "$*" in
*list-clients*)
case "$*" in
*'#{client_tty}'*) printf '/dev/ttys999\\twork\\t123\\t/dev/ttys999\\n'; exit 0 ;;
*) printf '\\twork\\t123\\t/dev/ttys999\\n'; exit 0 ;;
esac
;;
*) exit 0 ;;
esac
"""
try Data(fakeTmux.utf8).write(to: fakeTmuxURL)
XCTAssertEqual(chmod(fakeTmuxURL.path, 0o755), 0)

let builder = LocalTmuxCommandBuilder(
tmuxPath: fakeTmuxURL.path,
socketPath: root.appendingPathComponent("server.sock").path
)
let result = try LocalTmuxProcessRunner(executablePath: fakeTmuxURL.path).run(
arguments: builder.listClientsArguments()
let payload = try XCTUnwrap(
JSONSerialization.jsonObject(with: Data(result.stdout.utf8)) as? [String: Any]
)

XCTAssertEqual(result.status, 0, result.stderr)
let clients = try LocalTmuxSessionListParser().clients(result.stdout)
XCTAssertEqual(clients.map(\.clientID), ["/dev/ttys999"])
let sessions = try XCTUnwrap(payload["sessions"] as? [[String: Any]])
let session = try XCTUnwrap(sessions.first)
XCTAssertEqual(session["session_name"] as? String, "client-tty")
XCTAssertEqual(session["clients"] as? Int, 1)
}

func testLocalTmuxDirectoryOverrideIsRejectedAsMissingExecutable() throws {
Expand Down
Loading
Loading