Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
225deb9
Add regression for attach ticket Stack auth hot path
Jun 26, 2026
7ad9f2a
Use attach tickets on mobile reconnect hot path
Jun 26, 2026
4a33b9e
Refresh Swift file length budget
Jun 26, 2026
afb6151
Merge remote-tracking branch 'origin/main' into issue-6151-
Jun 26, 2026
a40c9c8
Update delete verifier paired Mac store for attach tokens
Jun 26, 2026
1e2f28b
Log invalid durable attach tickets
Jun 26, 2026
ca40cbd
Fix sync store paired Mac test double
Jun 26, 2026
d1a0619
Keep sync store test under file length budget
Jun 26, 2026
bfd854d
Merge remote-tracking branch 'origin/main' into issue-6151-
Jun 26, 2026
d4bcf71
Allow zsh path normalization in wrapper test
Jun 27, 2026
d1dd9e5
Merge remote-tracking branch 'origin/main' into issue-6151-
Jun 27, 2026
310e2df
Split paired Mac store persistence helpers
Jun 27, 2026
a61c78e
Stabilize terminal focus recovery test geometry
Jun 27, 2026
d1bd1b8
Document attach ticket preservation contract
Jun 27, 2026
40b54d0
Protect local attach tickets from encoding
Jun 27, 2026
5167180
Add attach-token host authorization regression
Jun 27, 2026
28cec4e
Accept attach tickets in host authorization
Jun 27, 2026
98440ee
Add stale attach-token reconnect regression
Jun 27, 2026
2348ddb
Retry stale durable attach tickets
Jun 27, 2026
1252a40
Add expired attach-token fallback regression
Jun 27, 2026
c4c497f
Allow Stack fallback for expired attach tokens
Jun 27, 2026
fb6e3d5
Address attach-token review gaps
Jun 27, 2026
5f81254
Update sync store paired Mac test double
Jun 27, 2026
67b2cbb
Add scoped workspace-list auth regression
Jun 27, 2026
7bfa339
Enforce scoped workspace-list attach auth
Jun 27, 2026
8225ada
Address scoped attach review feedback
Jun 27, 2026
c72bb1c
Retry secondary durable attach tickets
Jun 27, 2026
fa6f240
Tighten attach ticket bearer scope
Jun 27, 2026
1ecc5ea
Persist durable attach token scope
Jun 27, 2026
55c59ac
Redact secondary client auth errors
Jun 27, 2026
97ec498
Require workspace scope for terminal list tickets
Jun 27, 2026
55625ba
Seed scoped durable reconnect test token
Jun 27, 2026
afc85fe
Add attach ticket auth scope regressions
Jun 27, 2026
f068f50
Authorize attach token mouse and group scopes
Jun 27, 2026
d9f44c5
Split attach token scope regressions
Jun 27, 2026
2fb96f7
Address attach auth policy review
Jun 27, 2026
1b20e24
Gate mobile glass APIs to new SDKs
Jun 27, 2026
87cc999
Store mobile attach tokens in device keychain
Jun 27, 2026
b574831
Delete legacy attach token secrets after team claim
Jun 27, 2026
aad525e
Move attach token service naming into keychain store
Jun 27, 2026
ab2a428
Add event subscription attach token regressions
Jun 27, 2026
540c57e
Authorize event subscriptions with mac attach tokens
Jun 27, 2026
68d7c8d
Add attach token upsert rollback regression
Jun 27, 2026
d5a9690
Roll back attach token secrets on upsert failure
Jun 27, 2026
1fe2615
Add attach token reentrant delete regression
Jun 27, 2026
63cd875
Make attach token secret writes non-reentrant
Jun 27, 2026
0fdfa14
Merge remote-tracking branch 'origin/main' into issue-6151-
Jun 27, 2026
453b353
Bound attach token store validation work
Jun 27, 2026
1807183
Preserve attach tokens when secret refresh fails
Jun 27, 2026
9f57ab3
Harden attach token fallback paths
Jun 27, 2026
32c55a1
Satisfy attach token review policy
Jun 27, 2026
95ae030
Scope paired Mac store loggers
Jun 27, 2026
7b33ac1
Redact secondary client error logs
Jun 27, 2026
ea2ff3c
Add attach token scope expansion regression
Jun 27, 2026
01e81e2
Constrain attach token resource scope recording
Jun 27, 2026
8069d9a
Use internal attach auth test seam
Jun 27, 2026
dea5c1f
Add failed attach token copy regression
Jun 27, 2026
45dd612
Preserve attach token state on failed legacy copy
Jun 27, 2026
22af0a4
Add scoped durable reconnect regression
Jun 27, 2026
660c944
Prefer scoped durable reconnect requests
Jun 27, 2026
355fb8d
Add build scoped attach token regression
Jun 27, 2026
86ad431
Preserve build scoped attach tokens
Jun 27, 2026
dc7e44b
Gate iOS glass APIs for older SDKs
Jun 27, 2026
79133da
Move mobile glass container compatibility to support
Jun 27, 2026
d7ee9ca
Isolate chat attach button construction
Jun 27, 2026
e910ac7
Add legacy attach fallback message regression
Jun 27, 2026
eaeb2ff
Broaden legacy attach fallback detection
Jun 27, 2026
79f58e0
Avoid nonisolated photos picker label construction
Jun 27, 2026
99458b1
Fix iOS simulator isolation and test cleanup
Jun 27, 2026
dfbb6ed
Use terminal hardware key resolver instance
Jun 27, 2026
b7eb2b0
Bind attach tokens to current Mac account
Jun 27, 2026
b5061ab
Mark attach token resource recording async
Jun 27, 2026
83ebcb1
Fix iOS terminal surface actor isolation
Jun 27, 2026
e692628
Await attach token resource recording in tests
Jun 27, 2026
6cdf7a9
Avoid isolated deinit in mobile shell
Jun 27, 2026
e14afcd
Avoid sending notification settings across actors
Jun 27, 2026
a45e46c
Use instance notification status helper
Jun 27, 2026
5c4771a
Test legacy attach token claim cleanup
Jun 27, 2026
188b4f6
Delete claimed legacy attach token secrets
Jun 27, 2026
d78507e
Test accountless attach token rejection
Jun 27, 2026
31fc0f8
Reject accountless attach token authorization
Jun 27, 2026
24815d5
Test same-account attach token hot path
Jun 27, 2026
168877e
Use same-account attach token hot path safely
Jun 27, 2026
4d445d4
Fix iOS attach token feature tests
Jun 27, 2026
5abe3ca
Test scoped attach token workspace listing
Jun 27, 2026
4d5370e
Restrict scoped attach token workspace lists
Jun 27, 2026
e1cb45b
Address attach auth review findings
Jun 27, 2026
d0babe8
Fix scoped attach ticket refresh fallback
Jun 27, 2026
4287fbd
Update scoped attach feature test expectations
Jun 27, 2026
067eb18
Avoid auth work for non-create mobile RPCs
Jun 27, 2026
9e3098d
Restrict terminal scoped tickets from workspace mutations
Jun 27, 2026
4b2e218
Update scoped attach feature expectations
Jun 27, 2026
b26d220
Fix workspace create attach scope test data
Jun 27, 2026
8a94edb
Merge remote-tracking branch 'origin/main' into issue-6151-
austinywang Jul 4, 2026
19a14c6
Guard compact mobile glass helpers
austinywang Jul 4, 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
12 changes: 6 additions & 6 deletions .github/swift-file-length-budget.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
9497 cmuxTests/CLINotifyProcessIntegrationRegressionTests.swift
8016 CLI/cmux_open.swift
7952 Sources/Panels/BrowserPanelView.swift
7366 cmuxTests/WorkspaceUnitTests.swift
7368 cmuxTests/WorkspaceUnitTests.swift
7218 cmuxTests/WorkspaceRemoteConnectionTests.swift
6895 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
6968 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
6317 cmuxTests/SessionPersistenceTests.swift
6217 cmuxTests/GhosttyConfigTests.swift
6183 Sources/TabManager.swift
Expand All @@ -39,7 +39,7 @@
2565 Sources/Panels/CmuxWebView.swift
2546 cmuxTests/WorkspaceManualUnreadTests.swift
2524 cmuxTests/CommandPaletteSearchEngineTests.swift
2395 Sources/Mobile/MobileHostService.swift
2424 Sources/Mobile/MobileHostService.swift
2328 cmuxTests/CJKIMEInputTests.swift
2242 Sources/TerminalNotificationStore.swift
2233 Sources/TerminalWindowPortal.swift
Expand Down Expand Up @@ -69,8 +69,8 @@
1373 cmuxTests/AppDelegateIssue2907RoutingTests.swift
1363 Sources/CMUXInstalledExtensionSidebarHostView.swift
1360 Sources/Feed/FeedButtonStyleDebugWindowController.swift
1328 cmuxTests/MobileHostAuthorizationTests.swift
1295 Packages/macOS/CmuxTerminalCore/Sources/CmuxTerminalCore/Config/GhosttyConfig.swift
1295 cmuxTests/MobileHostAuthorizationTests.swift
1285 cmuxUITests/SidebarHelpMenuUITests.swift
1277 cmuxTests/RestorableAgentSessionIndexTests.swift
1258 Sources/Feed/FeedCoordinator.swift
Expand Down Expand Up @@ -112,7 +112,6 @@
824 Sources/MainWindowFocusController.swift
813 Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
810 Packages/macOS/CmuxSwiftRender/Tests/CmuxSwiftRenderTests/SwiftViewInterpreterTests.swift
803 Packages/iOS/CmuxMobilePairedMac/Sources/CmuxMobilePairedMac/MobilePairedMacStore.swift
802 Sources/WorkspaceContentView.swift
797 Sources/ClosedItemHistory.swift
779 cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
Expand Down Expand Up @@ -191,8 +190,8 @@
560 cmuxTests/CLISSHPTYResizeInputTests.swift
558 Packages/macOS/CmuxGit/Sources/CmuxGit/Parsing/GitMetadataService+Config.swift
552 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/BrowserSection.swift
551 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
549 Sources/Panels/BrowserAutomation.swift
547 Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/BackingUpPairedMacStore.swift
541 Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
540 Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
539 CLI/CMUXCLI+Themes.swift
Expand All @@ -206,6 +205,7 @@
528 cmuxUITests/AutomationSocketUITests.swift
527 CLI/CLISocketPathResolver.swift
527 cmuxTests/BrowserHTTPBasicAuthPromptTests.swift
526 Packages/iOS/CmuxMobilePairedMac/Tests/CmuxMobilePairedMacTests/MobilePairedMacStoreTests.swift
526 Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamStore.swift
525 Packages/macOS/CmuxSettings/Sources/CmuxSettings/SocketControl/SocketControlSettings.swift
524 CLI/CMUXCLI+AutoNaming.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import Foundation
/// expiry. It does keep non-secret pairing context: the Mac account email,
/// shared pairing compatibility level, and app version/build, so the phone can
/// fail fast on an account mismatch and warn before continuing across
/// compatibility skew. A pairing QR never expires;
/// the owner's Stack access token is the
/// host's sole authorization gate (`MobileHostService.authorizationError(for:)`),
/// so ticket age authorizes nothing.
/// compatibility skew. A pairing QR never expires and carries no bearer token;
/// token-bearing RPC tickets are delivered through the direct RPC payload, not
/// through the compact QR URL.
///
/// Compatibility:
/// - New decoders accept both grammars: ``CmxAttachTicketInput`` routes a
Expand Down Expand Up @@ -45,9 +44,9 @@ public struct CmxAttachTicketCompactCoder: Sendable {
/// Encode a ticket into the compact JSON grammar.
///
/// Any `authToken`, `macDisplayName`, and `expiresAt` on the ticket are
/// intentionally not encoded: the token never authorizes anything on the
/// host (Stack auth is the sole gate), the name is read post-handshake
/// from `mobile.host.status`, and a pairing QR never expires.
/// intentionally not encoded: QR/deep-link URLs are not bearer credentials,
/// the name is read post-handshake from `mobile.host.status`, and a pairing
/// QR never expires.
public func encode(_ ticket: CmxAttachTicket) throws -> Data {
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import Foundation
/// account/build context to check before dialing. The account value is the
/// opaque Stack user id, never the email itself. Everything else the earlier
/// grammars carried has a better channel or no reason to exist:
/// - **No auth token.** The owner's Stack access token is the host's sole
/// authorization gate; a token in the QR authorized nothing and made the
/// code look like a leaked credential.
/// - **No auth token.** A QR URL is not a bearer credential; token-bearing RPC
/// tickets are returned through the direct attach-ticket payload.
/// - **No expiry.** Ticket age authorizes nothing, so a code that sat on
/// screen for an hour still pairs.
/// - **No display name, no device id.** Both arrive post-handshake from
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,10 +214,9 @@ public struct CmxAttachTicket: Codable, Equatable, Sendable {
public let macAppBuild: String?
public let routes: [CmxAttachRoute]
/// When the ticket's attach token stops being usable, or `nil` for tickets
/// that never expire (the pairing QR carries no token and no expiry; Stack
/// auth is the host's sole authorization gate). Expiry is data for the
/// token consumers (`MobileCoreRPCClient`, the host's ticket store), not a
/// structural validity condition; see ``isExpired(at:)``.
/// that never expire (the pairing QR carries no token and no expiry). Expiry
/// is data for the token consumers (`MobileCoreRPCClient`, the host's ticket
/// store), not a structural validity condition; see ``isExpired(at:)``.
public let expiresAt: Date?
public let authToken: String?

Expand Down Expand Up @@ -290,9 +289,9 @@ public struct CmxAttachTicket: Codable, Equatable, Sendable {
}

/// Structural validity only. Expiry is intentionally NOT validated here:
/// a scanned pairing QR must keep working however long it sat on screen
/// (the host authorizes by Stack account, not by ticket age). Token-based
/// consumers check ``isExpired(at:)`` where the token is actually used.
/// a scanned pairing QR must keep working however long it sat on screen.
/// Token-based consumers check ``isExpired(at:)`` where the token is
/// actually used.
public func validate() throws {
guard version == Self.currentVersion else {
throw CmxAttachTicketError.unsupportedVersion(version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ private func legacyDecoder() -> JSONDecoder {
// Routes round-trip losslessly even with custom ids ("ws" differs from
// the synthesized "websocket", so it is carried verbatim).
#expect(decoded.routes == ticket.routes)
// Dropped by design: the auth token never authorizes anything, the name
// arrives via `mobile.host.status`, and a pairing QR never expires.
// Dropped by design: QR URLs are not bearer credentials, the name arrives
// via `mobile.host.status`, and a pairing QR never expires.
#expect(decoded.authToken == nil)
#expect(decoded.macDisplayName == nil)
#expect(decoded.expiresAt == nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ let sortKey: @Sendable (SyncWireRecord) -> Double = { DeviceSyncFacade.sortKey(f
actor FakePairedStore: MobilePairedMacStoring {
var macs: [MobilePairedMac]
init(macs: [MobilePairedMac]) { self.macs = macs }
func upsert(macDeviceID: String, displayName: String?, routes: [CmxAttachRoute], markActive: Bool, stackUserID: String?, teamID: String?, now: Date) async throws {}
func upsert(macDeviceID: String, displayName: String?, routes: [CmxAttachRoute], attachToken: String?, attachTokenExpiresAt: Date?, markActive: Bool, stackUserID: String?, teamID: String?, now: Date) async throws {}
func loadAll(stackUserID: String?, teamID: String?) async throws -> [MobilePairedMac] {
guard let stackUserID else { return macs }
return macs.filter { $0.stackUserID == stackUserID }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ public import Foundation

/// A Mac paired with this iOS device, persisted across launches.
///
/// Auth tokens are never persisted, only enough to re-mint a fresh attach
/// ticket via the StackAuth-authenticated manual host flow on next launch.
/// The attach token is local-only durable reconnect state; backup sync deliberately
/// omits it so only this device can redeem the ticket without a Stack round trip.
public struct MobilePairedMac: Codable, Equatable, Sendable, Identifiable {
/// Stable identifier of the paired Mac device.
public var macDeviceID: String
/// Human-readable name of the Mac, if the pairing payload supplied one.
public var displayName: String?
/// Attach routes advertised by the Mac, ordered by priority (lowest first).
public var routes: [CmxAttachRoute]
/// Local-only attach ticket secret used for fast reconnect while unexpired.
public var attachToken: String? = nil
/// Expiration time for ``attachToken``.
public var attachTokenExpiresAt: Date? = nil
/// When this pairing was first recorded.
public var createdAt: Date
/// When this pairing was last refreshed or used.
Expand Down Expand Up @@ -40,6 +44,20 @@ public struct MobilePairedMac: Codable, Equatable, Sendable, Identifiable {
/// The Mac device identifier doubles as the stable `Identifiable` id.
public var id: String { macDeviceID }

private enum CodingKeys: String, CodingKey {
case macDeviceID
case displayName
case routes
case createdAt
case lastSeenAt
case isActive
case stackUserID
case teamID
case customName
case customColor
case customIcon
}

/// The name to show: the user's custom override if set, else the Mac-reported
/// name, else the device id.
public var resolvedName: String {
Expand All @@ -53,6 +71,8 @@ public struct MobilePairedMac: Codable, Equatable, Sendable, Identifiable {
/// - macDeviceID: Stable identifier of the paired Mac device.
/// - displayName: Optional human-readable Mac name.
/// - routes: Attach routes advertised by the Mac.
/// - attachToken: Local-only attach ticket secret for fast reconnect.
/// - attachTokenExpiresAt: Expiration time for `attachToken`.
/// - createdAt: When the pairing was first recorded.
/// - lastSeenAt: When the pairing was last refreshed.
/// - isActive: Whether this pairing is currently active for its scope.
Expand All @@ -61,6 +81,8 @@ public struct MobilePairedMac: Codable, Equatable, Sendable, Identifiable {
macDeviceID: String,
displayName: String?,
routes: [CmxAttachRoute],
attachToken: String? = nil,
attachTokenExpiresAt: Date? = nil,
createdAt: Date,
lastSeenAt: Date,
isActive: Bool,
Expand All @@ -73,6 +95,8 @@ public struct MobilePairedMac: Codable, Equatable, Sendable, Identifiable {
self.macDeviceID = macDeviceID
self.displayName = displayName
self.routes = routes
self.attachToken = attachToken
self.attachTokenExpiresAt = attachTokenExpiresAt
self.createdAt = createdAt
self.lastSeenAt = lastSeenAt
self.isActive = isActive
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import SQLite3

extension MobilePairedMacStore {
/// Open the SQLite connection and set connection pragmas. `nonisolated`
/// `static` so the actor's synchronous initializer can build the handle
/// without hopping isolation. Opened with `SQLITE_OPEN_FULLMUTEX` so SQLite
/// serializes access internally; the actor adds an outer serialization layer.
/// Schema migration runs lazily on first store access via `ensureReady()`.
nonisolated static func openConnection(path: String) throws -> OpaquePointer {
var handle: OpaquePointer?
let flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX
let rc = sqlite3_open_v2(path, &handle, flags, nil)
guard rc == SQLITE_OK, let handle else {
if let handle { sqlite3_close_v2(handle) }
throw MobilePairedMacStoreError.openFailed(rc)
}
for pragma in ["PRAGMA foreign_keys = ON;", "PRAGMA journal_mode = WAL;"] {
let prc = sqlite3_exec(handle, pragma, nil, nil, nil)
guard prc == SQLITE_OK else {
sqlite3_close_v2(handle)
throw MobilePairedMacStoreError.stepFailed(prc, "")
}
}
return handle
}
}
Loading
Loading