Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f747933
test: cover undiscoverable Macs and persistent device controls
austinywang Sep 25, 2026
7e9ee16
fix: require host opt-in for automatic Mac discovery
austinywang Sep 25, 2026
1cfcca7
test: show the source Mac in workspace sidebar details
austinywang Sep 25, 2026
7df1162
fix: show source Mac names beside workspace directories
austinywang Sep 25, 2026
ce00287
test: propagate deliberate Mac terminal closure to its owner
austinywang Sep 25, 2026
b7c546c
fix: synchronize deliberate terminal closes across Mac workspaces
austinywang Sep 25, 2026
a890ba6
test: keep mixed local and Mac layouts from closing source terminals
austinywang Sep 25, 2026
2c62256
fix: require current whole-workspace ownership before remote close
austinywang Sep 25, 2026
a911301
test: cover directory and relay renewal after v6 activation
austinywang Sep 25, 2026
3affdb7
test: cover authority renewal at the v6 audit limit
austinywang Sep 25, 2026
d149a14
test: cover authority renewal at both schema audit limits
austinywang Sep 25, 2026
c0dd582
test: make mirrored close and source-label regressions deterministic
austinywang Sep 25, 2026
a473511
test(iroh-v2): reproduce leaked socket reservations blocking a user
azooz2003-bit Sep 25, 2026
09a448e
fix(iroh-v2): reclaim leaked socket reservations and classify the out…
azooz2003-bit Sep 25, 2026
515ab13
fix: isolate Mac discovery from incoming mobile hosting
austinywang Sep 25, 2026
a337c98
test: isolate Mac discovery from iOS inbound routing
austinywang Sep 25, 2026
b943865
fix: retain device sidebar provenance across disconnects
austinywang Sep 25, 2026
fdfd53c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13458…
austinywang Sep 25, 2026
7d45713
test: reproduce socket reservation reset failures
austinywang Sep 25, 2026
cfebd92
fix: harden Mac device closes and socket recovery
austinywang Sep 25, 2026
ca62c96
Merge origin/main into 13458-hide-undiscoverable-devices
austinywang Sep 25, 2026
bf8c646
fix: separate Mac hosting from iOS pairing
austinywang Sep 25, 2026
d84ef5c
Merge branch 'main' of https://github.com/manaflow-ai/cmux into 13458…
austinywang Sep 25, 2026
ccffaaa
fix: import Cloud feature policy after package move
austinywang Sep 25, 2026
e55d519
test: exercise peer opt-ins and production close teardown
austinywang Sep 25, 2026
1f56942
fix: enforce independent peer admission and indexed close ownership
austinywang Sep 25, 2026
2f8e815
Merge PR #14386 socket recovery with bounded cleanup and private diag…
austinywang Sep 25, 2026
2fd9d26
test: isolate discovery admission and verify repeated socket recovery
austinywang Sep 25, 2026
a1058f7
test: keep phone pairing off when Mac preferences are enabled
austinywang Sep 25, 2026
c25a3e3
fix: keep iOS pairing independent from Mac discoverability
austinywang Sep 25, 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 @@ -57,7 +57,7 @@ public struct IrxMacPeerAuthorization: Sendable {
device.endpointID != own.descriptor.endpointID,
identity.deviceID.lowercased() != localIdentity.deviceID.lowercased(),
peer.revision <= directory.revision else { throw Failure.identityMismatch }
guard device.metadata.pairingEnabled, device.metadata.capabilities.contains("cmux.mac-host.v1") else {
guard device.metadata.capabilities.contains("cmux.mac-host.v1") else {
throw Failure.unavailable
}
return peer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ public struct V2ControlConfiguration: Sendable {
guard baseURL.scheme == "https" || (baseURL.scheme == "http" && loopback),
baseURL.user == nil, baseURL.password == nil, baseURL.query == nil, baseURL.fragment == nil,
device.metadata.platform != .mac || device.metadata.pairingEnabled
|| device.metadata.capabilities.contains("cmux.mac-devices.v1") else {
|| device.metadata.capabilities.contains("cmux.mac-devices.v1")
|| device.metadata.capabilities.contains("cmux.mac-host.v1") else {
throw V2ControlFailure.scopeMismatch
}
self.baseURL = baseURL
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,14 @@ public final class V2InboundAdmissionAuthority: Sendable {
return clear(&current)
}
current.localRecordID = own.deviceRecordID
guard own.descriptor.metadata.pairingEnabled else { return clear(&current) }
// iOS pairing and Mac incoming hosting are independent capabilities.
// A Mac-only host keeps an inbound authority even when its legacy iOS
// pairing switch is off; the host capability is itself opt-in and the
// runtime still applies the incoming-access policy per peer.
guard own.descriptor.metadata.pairingEnabled
|| own.descriptor.metadata.capabilities.contains("cmux.mac-host.v1") else {
return clear(&current)
}
guard let directory = cache.directory else { return clear(&current) }
guard directory.teamID == host.identity.teamID, directory.nextCursor == nil,
(directory.inboundPeers?.count ?? 0) <= 4096 else { return clear(&current) }
Expand Down Expand Up @@ -220,6 +227,17 @@ public final class V2InboundAdmissionAuthority: Sendable {
for permission in peers {
let record = permission.device
let device = record.descriptor
// A cached grant cannot cross the host's current platform opt-in.
// The Mac route also retains the server's same-account/build scope.
if device.metadata.platform == .mac {
guard own.descriptor.metadata.capabilities.contains("cmux.mac-host.v1"),
device.metadata.capabilities.contains("cmux.mac-devices.v1"),
device.identity.userID == host.identity.userID,
device.identity.appNamespace == host.identity.appNamespace,
device.identity.buildTag == host.identity.buildTag else { continue }
} else {
guard own.descriptor.metadata.pairingEnabled else { continue }
}
guard device.identity.environment == host.identity.environment,
device.identity.projectID == host.identity.projectID,
device.identity.teamID == host.identity.teamID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ struct IrxMacPeerAuthorizationTests {
environment: "development", projectID: "project", teamID: "team", userID: user)
}

private func record(device: String, endpoint: String, enabled: Bool = true,
private func record(device: String, endpoint: String, enabled: Bool = true, hosting: Bool = true,
user: String = "owner", tag: String = "feature", revoked: Bool = false) -> V2DeviceRecord {
let descriptor = V2DeviceDescriptor(endpointID: endpoint, identity: identity(device: device, user: user, tag: tag),
identityGeneration: 1, metadata: V2DeviceMetadata(appVersion: "1", capabilities: ["cmux.mac-devices.v1", "cmux.mac-host.v1"],
identityGeneration: 1, metadata: V2DeviceMetadata(appVersion: "1", capabilities: ["cmux.mac-devices.v1"] + (hosting ? ["cmux.mac-host.v1"] : []),
displayName: "Mac", pairingEnabled: enabled, platform: .mac, relayURLs: ["https://relay.test"]))
return V2DeviceRecord(descriptor: descriptor, deviceRecordID: recordID, revision: 1, revoked: revoked)
}
Expand Down Expand Up @@ -46,13 +46,22 @@ struct IrxMacPeerAuthorizationTests {
#expect(authority.authorizedPeer(endpointID: endpoint) == nil)
}

@Test("Mac host authorization does not depend on iOS pairing")
func acceptsMacOnlyHostPermission() throws {
let state = cache(peer: record(device: device, endpoint: endpoint, enabled: false))
let selected = try IrxMacPeerAuthorization(deviceID: device, tag: "feature", endpointID: endpoint)
.resolve(cache: state, localIdentity: state.identity, now: Date(timeIntervalSince1970: 1001))
#expect(selected.descriptor.metadata.pairingEnabled == false)
#expect(selected.descriptor.metadata.capabilities.contains("cmux.mac-host.v1"))
}

@Test("Remote identities and disabled hosts fail closed")
func rejectsUntrustedPeer() throws {
let intent = IrxMacPeerAuthorization(deviceID: device, tag: "feature", endpointID: endpoint)
let invalid = [
record(device: device, endpoint: endpoint, tag: "other"),
record(device: device, endpoint: endpoint, user: "other"),
record(device: device, endpoint: endpoint, enabled: false),
record(device: device, endpoint: endpoint, hosting: false),
record(device: device, endpoint: endpoint, revoked: true),
record(device: local, endpoint: endpoint)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct V2InboundAdmissionAuthorityTests {
identity: V2Identity(appNamespace: platform == .mac ? "com.cmux.mac" : "dev.cmux.ios",
buildTag: "tag", deviceID: deviceID, environment: environment, projectID: project,
teamID: team, userID: platform == .mac ? "host-owner" : "permitted-teammate"),
identityGeneration: generation, metadata: V2DeviceMetadata(appVersion: "2", capabilities: [],
identityGeneration: generation, metadata: V2DeviceMetadata(appVersion: "2",
capabilities: platform == .mac ? ["cmux.mac-host.v1", "cmux.mac-devices.v1"] : [],
displayName: deviceID, pairingEnabled: true, platform: platform, relayURLs: []))
}

Expand Down Expand Up @@ -70,6 +71,61 @@ struct V2InboundAdmissionAuthorityTests {
#expect(authority.nextExpiration == nil)
}

@Test func macOnlyHostKeepsMacInboundAuthorityWhenPairingIsOff() throws {
let base = device()
let host = V2DeviceDescriptor(
endpointID: base.endpointID,
identity: base.identity,
identityGeneration: base.identityGeneration,
metadata: V2DeviceMetadata(
appVersion: base.metadata.appVersion,
capabilities: ["cmux.mac-host.v1"],
displayName: base.metadata.displayName,
pairingEnabled: false,
platform: .mac,
relayURLs: []
)
)
let mac = V2DeviceRecord(
descriptor: device(key: "b", deviceID: "peer", platform: .mac),
deviceRecordID: "peer-record",
revision: 1,
revoked: false
)
let permission = V2InboundPeerPermission(device: mac, permissionExpiresAt: timestamp + 100)
let authority = try V2InboundAdmissionAuthority(
host: host,
wallNow: { Date(timeIntervalSince1970: TimeInterval(timestamp)) },
monotonicNow: { .now }
)
#expect(authority.restore(cache(peers: [permission], outbound: [mac], host: host)))
#expect(authority.authorizedPeer(endpointID: mac.descriptor.endpointID) != nil)
}

@Test("Cached inbound permissions preserve independent Mac and iOS opt-ins",
arguments: [false, true], [false, true])
func cachedPeerPlatformIsGated(pairing: Bool, hosting: Bool) throws {
let base = device()
let host = V2DeviceDescriptor(endpointID: base.endpointID, identity: base.identity,
identityGeneration: base.identityGeneration,
metadata: V2DeviceMetadata(appVersion: "2", capabilities: hosting ? ["cmux.mac-host.v1"] : [],
displayName: "Host", pairingEnabled: pairing, platform: .mac, relayURLs: []))
let macBase = device(key: "c", deviceID: "mac-peer")
let mac = V2DeviceRecord(descriptor: V2DeviceDescriptor(endpointID: macBase.endpointID,
identity: macBase.identity, identityGeneration: macBase.identityGeneration,
metadata: V2DeviceMetadata(appVersion: "2", capabilities: ["cmux.mac-devices.v1"],
displayName: "Mac peer", pairingEnabled: false, platform: .mac, relayURLs: [])),
deviceRecordID: "mac-record", revision: 1, revoked: false)
let phone = peer()
let clock = V2AdmissionTestClock(wall: timestamp)
let authority = try V2InboundAdmissionAuthority(host: host,
wallNow: { clock.wall }, monotonicNow: { clock.monotonic })
_ = authority.restore(cache(peers: [phone,
V2InboundPeerPermission(device: mac, permissionExpiresAt: timestamp + 100)], host: host))
#expect((authority.authorizedPeer(endpointID: mac.descriptor.endpointID) != nil) == hosting)
#expect((authority.authorizedPeer(endpointID: phone.device.descriptor.endpointID) != nil) == pairing)
}

@Test(arguments: [V2Platform.mac, .ios])
func knowingAnEndpointIDNeverReplacesPermission(platform: V2Platform) throws {
let authority = try authority(V2AdmissionTestClock(wall: timestamp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import Foundation
/// `Sections/`.
public enum SettingsSectionID: String, CaseIterable, Identifiable, Sendable, Hashable {
case account
case computers
case app
case terminal
case textBox
/// Sleepy Mode screensaver + keep-awake lock.
case sleepyMode
/// Mobile pairing and sync settings.
case mobile
case computers
/// Cloud Machines: persistent cloud VM plan and entry points.
case cloudMachines
/// Iroh relay policy, custom relays, and private-network routes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ enum SettingsTaxonomyGroup: String, CaseIterable, Identifiable, Sendable {
case .browserAndFiles:
return [.browser, .browserImport]
case .remoteAndDevices:
return [.computers, .mobile, .cloudMachines, .networking]
return [.mobile, .computers, .cloudMachines, .networking]
case .keyboardAndAdvanced:
return [.globalHotkey, .keyboardShortcuts, .betaFeatures, .settingsJSON, .reset]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class SettingsSectionMountModel {
/// is an anchor inside the Browser section rather than a section of
/// its own, so it never appears here.
public static let displayOrder: [SettingsSectionID] = [
.account, .computers, .app, .terminal, .textBox, .sleepyMode, .mobile, .cloudMachines,
.account, .app, .terminal, .textBox, .sleepyMode, .mobile, .computers, .cloudMachines,
.networking, .sidebarAppearance, .customSidebars, .betaFeatures, .automation,
.computerUse, .browser, .globalHotkey, .keyboardShortcuts, .workspaceColors,
.settingsJSON, .reset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ extension SettingsWindowRoot {
)
}

slot(.computers, proxy: proxy) {
ComputersSection(hostActions: hostActions, defaultsStore: defaultsStore, catalog: catalog)
}

slot(.app, proxy: proxy) {
AppSection(
defaultsStore: defaultsStore,
Expand Down Expand Up @@ -63,6 +59,10 @@ extension SettingsWindowRoot {
MobileSection(defaultsStore: defaultsStore, catalog: catalog, hostActions: hostActions)
}

slot(.computers, proxy: proxy) {
ComputersSection(hostActions: hostActions, defaultsStore: defaultsStore, catalog: catalog)
}

slot(.cloudMachines, proxy: proxy) {
// `DisableCloud` (MDM) and the rollout gate: no Cloud pane at all
// while unavailable, not just no placeholder.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import Testing
struct SettingsSectionMountModelTests {
private let order: [SettingsSectionID] = [.account, .app, .terminal, .browser, .reset]

@Test func computersFollowsMobileInTheDetailStack() throws {
let order = SettingsSectionMountModel.displayOrder
let mobile = try #require(order.firstIndex(of: .mobile))
#expect(order[mobile + 1] == .computers)
}

@Test func displayOrderGivesEverySectionASlotExceptTheEmbeddedBrowserImport() {
let slots = Set(SettingsSectionMountModel.displayOrder)
#expect(slots.count == SettingsSectionMountModel.displayOrder.count)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ struct SettingsTaxonomyTests {
#expect(SettingsTaxonomyGroup.sidebarAndDock.sections == [.sidebarAppearance, .customSidebars])
#expect(SettingsTaxonomyGroup.agentsAndAutomation.sections == [.automation, .computerUse])
#expect(SettingsTaxonomyGroup.browserAndFiles.sections == [.browser, .browserImport])
#expect(SettingsTaxonomyGroup.remoteAndDevices.sections == [.computers, .mobile, .cloudMachines, .networking])
#expect(SettingsTaxonomyGroup.remoteAndDevices.sections == [.mobile, .computers, .cloudMachines, .networking])
#expect(
SettingsTaxonomyGroup.keyboardAndAdvanced.sections
== [.globalHotkey, .keyboardShortcuts, .betaFeatures, .settingsJSON, .reset]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/// Chooses the Mac installations that may appear in a surface directory.
/// Authenticated discovery defines membership; presence and saved rows only
/// contribute membership when the caller explicitly uses legacy pairing.
public struct SurfaceDeviceDirectoryAdmission: Sendable {
private let requiresAuthenticatedDiscovery: Bool

/// Creates the membership policy for this directory owner.
/// - Parameter requiresAuthenticatedDiscovery: Whether current authenticated hosts are required.
public init(requiresAuthenticatedDiscovery: Bool) {
self.requiresAuthenticatedDiscovery = requiresAuthenticatedDiscovery
}

/// Computes membership after source identities have been joined.
/// - Parameters:
/// - authenticated: Installations named by the authenticated Mac host directory.
/// - legacySources: Registry, presence, saved pairing, and retained row identities.
/// - local: This Mac's installation, whose physical device must remain hidden.
/// - Returns: The permitted remote installation identities.
public func admittedInstances(
authenticated: Set<SurfaceDeviceInstanceID>,
legacySources: [Set<SurfaceDeviceInstanceID>],
local: SurfaceDeviceInstanceID
) -> Set<SurfaceDeviceInstanceID> {
var result = authenticated
if !requiresAuthenticatedDiscovery {
for source in legacySources { result.formUnion(source) }
}
return result.filter { $0.isVisible(from: local) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Testing
@testable import CmuxSurfaceCatalogModel

struct SurfaceDeviceDirectoryAdmissionTests {
@Test(arguments: [false, true])
func authenticatedMembershipCannotBeRevivedByPresenceOrPairing(required: Bool) {
let local = SurfaceDeviceInstanceID(deviceID: "local", tag: "test")
let sameMac = SurfaceDeviceInstanceID(deviceID: "local", tag: "other")
let host = SurfaceDeviceInstanceID(deviceID: "host", tag: "test")
let hidden = SurfaceDeviceInstanceID(deviceID: "hidden", tag: "test")
let policy = SurfaceDeviceDirectoryAdmission(requiresAuthenticatedDiscovery: required)
let sources: [Set<SurfaceDeviceInstanceID>] = [[host, hidden], [hidden], [hidden], [hidden]]
#expect(policy.admittedInstances(authenticated: [host, local, sameMac], legacySources: sources, local: local)
== (required ? [host] : [host, hidden]))
#expect(policy.admittedInstances(authenticated: [], legacySources: sources, local: local)
== (required ? [] : [host, hidden]))
}
}
Loading
Loading