Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
22120f6
Add the Devices section to Mac Settings
sudoHG Nov 10, 2026
71e0faa
Address the first review round of the Devices section
sudoHG Nov 10, 2026
44ee60a
Keep the pairing sheet up until the person leaves it
sudoHG Nov 10, 2026
09c10e4
Address the second review round and diagnose the Devices captures
sudoHG Nov 10, 2026
b70d999
Shape the Devices section like the Backup section
sudoHG Nov 10, 2026
4f85b82
Record the button state at each Pair a Device click in E2E
sudoHG Nov 10, 2026
663d830
Record the App's mouse events and accessibility tree in the Devices E2E
sudoHG Nov 10, 2026
747ef73
Compare another Settings button, a posted click and window-scoped que…
sudoHG Nov 10, 2026
7015b42
Click replicas of the Devices row and press its button through access…
sudoHG Nov 10, 2026
986d42d
Scan accessibility hit tests around the Devices buttons in the E2E
sudoHG Nov 10, 2026
b6adc29
Record the E2E App's own log and standard error around the Devices cl…
sudoHG Nov 10, 2026
a6bf61f
Filter the E2E App log, record it on failure and click the Devices se…
sudoHG Nov 10, 2026
0f547e7
TEMP #298 diagnostics, revert before merge: print view changes and bi…
sudoHG Nov 10, 2026
f9fee03
TEMP #298 diagnostics, revert before merge: Devices first in Settings…
sudoHG Nov 10, 2026
b81ac28
Bind the pairing sheet to view state that follows the session
sudoHG Nov 10, 2026
60aa955
Pass the Devices model in from a holder that outlives Settings
sudoHG Nov 11, 2026
e310506
Stop promising that a removed device removes this Mac
sudoHG Nov 11, 2026
7562301
Rebuild the Devices controls once after the section appears
sudoHG Nov 11, 2026
4927ae1
Revert "Rebuild the Devices controls once after the section appears"
sudoHG Nov 11, 2026
3bbcf63
TEMP #298 diagnostics, revert before merge: trace what Pair a Device.…
sudoHG Nov 11, 2026
e4aa5df
TEMP #298 diagnostics, revert before merge: keep the pairing trace ap…
sudoHG Nov 11, 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
1 change: 1 addition & 0 deletions Sources/AskKeyAppKit/AppLanguage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ enum AppLanguage {
"Expired without decision",
"Open Ask Key to review pending requests.",
"Disable system authentication for read approvals",
"Pair a device with this Mac",
"Ask Key could not save an access record. Credential operations continue, and this warning will remain until recording succeeds.",
"Ask Key could not prepare Agent access. Open the app to review the vault state.",
"Ask Key could not apply this decision. Open Pending requests to retry or reject it.",
Expand Down
46 changes: 39 additions & 7 deletions Sources/AskKeyAppKit/Devices/DeviceCoordinator+Pairing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,63 +11,95 @@ extension DeviceCoordinator {
/// session is open. The open sheet keeps the management session from
/// expiring for idleness until the session ends or the sheet closes.
package func startPairing() async {
guard environment.backend != nil, pairing?.isActive != true else { return }
// T6 TEMP #298 trace lines in this function, revert before merge.
DevicesPairingTrace.log("startPairing: entry backend=\(environment.backend != nil) pairing=\(DevicesPairingTrace.describe(pairing)) "
+ "services=\(services != nil) availability=\(availability)")
guard environment.backend != nil, pairing?.isActive != true else {
DevicesPairingTrace.log("startPairing: returned at guard backend/active session")
return
}
if services == nil, availability == .loading || availability == .storageUnavailable {
_ = openStoredServices()
DevicesPairingTrace.log("startPairing: opened stored services, availability=\(availability)")
}
switch availability {
case .ready:
break
case .secureEnclaveUnavailable:
DevicesPairingTrace.log("startPairing: failed, secureEnclaveUnavailable")
return failPairing(.secureEnclaveUnavailable)
case .identityUnusable, .listRefused:
DevicesPairingTrace.log("startPairing: failed, identityUnusable/listRefused")
return failPairing(.identityUnusable)
case .storageUnavailable, .loading, .notConfigured:
DevicesPairingTrace.log("startPairing: failed, storageUnavailable (\(availability))")
return failPairing(.storageUnavailable)
}
guard managementSessionIsOpen() else {
DevicesPairingTrace.log("startPairing: failed, managementRequired")
pairing = .failed(.managementRequired)
return
}
DevicesPairingTrace.log("startPairing: preparing")
pairingGeneration += 1
let generation = pairingGeneration
pairing = .preparing
pairingFeedback = nil
pairingExpiresAt = nil
acquireExpiryHold()
do {
guard try await openServicesForPairing(generation: generation), generation == pairingGeneration,
let services else { return }
let opened = try await openServicesForPairing(generation: generation)
guard opened, generation == pairingGeneration, let services else {
DevicesPairingTrace.log("startPairing: returned after opening services, opened=\(opened) "
+ "current=\(generation == pairingGeneration) services=\(self.services != nil) pairing=\(DevicesPairingTrace.describe(pairing))")
return
}
if account != .available { await refreshAccount() }
guard generation == pairingGeneration else { return }
guard account == .available else { return failPairing(.accountUnavailable(account)) }
guard generation == pairingGeneration else {
DevicesPairingTrace.log("startPairing: returned, superseded after the account check")
return
}
guard account == .available else {
DevicesPairingTrace.log("startPairing: failed, account \(account)")
return failPairing(.accountUnavailable(account))
}
do {
_ = try await services.inbox.prepareInbox()
} catch {
DevicesPairingTrace.log("startPairing: inbox preparation threw \(error)")
guard generation == pairingGeneration else { return }
return failPairing(.iCloudUnavailable)
}
guard generation == pairingGeneration, self.services === services else { return }
guard generation == pairingGeneration, self.services === services else {
DevicesPairingTrace.log("startPairing: returned, superseded after the inbox")
return
}
let qr = try await services.messenger.startPairing(displayName: environment.displayName())
guard generation == pairingGeneration, self.services === services else {
DevicesPairingTrace.log("startPairing: returned, superseded after the messenger")
await services.messenger.cancelPairing()
return
}
pairing = .showingQRCode(text: qr.text, expiresAt: qr.qrExpiry)
pairingExpiresAt = qr.qrExpiry.addingTimeInterval(Self.sessionExtension)
startPairingPolling()
DevicesPairingTrace.log("startPairing: showing the QR code")
} catch DeviceIdentityError.secureEnclaveUnavailable {
DevicesPairingTrace.log("startPairing: threw secureEnclaveUnavailable")
guard generation == pairingGeneration else { return }
availability = .secureEnclaveUnavailable
failPairing(.secureEnclaveUnavailable)
} catch DeviceIdentityError.unusable {
DevicesPairingTrace.log("startPairing: threw unusable")
guard generation == pairingGeneration else { return }
identityBecameUnusable()
failPairing(.identityUnusable)
} catch DeviceIdentityError.keyCreationFailed {
DevicesPairingTrace.log("startPairing: threw keyCreationFailed")
guard generation == pairingGeneration else { return }
failPairing(.keyCreationFailed)
} catch {
DevicesPairingTrace.log("startPairing: threw \(error)")
guard generation == pairingGeneration else { return }
failPairing(.storageUnavailable)
}
Expand Down Expand Up @@ -141,7 +173,7 @@ extension DeviceCoordinator {
let generation = pairingGeneration
pairingFeedback = nil
do {
try await services.messenger.confirmPairing(approvalReason: Vault.devicePairingReason)
try await services.messenger.confirmPairing(approvalReason: ManagementAuthenticationAction.pairDevice.reasonKey)
} catch DeviceIdentityError.authenticationCancelled {
if generation == pairingGeneration { pairingFeedback = .authenticationCancelled }
} catch DeviceIdentityError.authenticationFailed, DeviceIdentityError.authenticationUnavailable {
Expand Down
3 changes: 2 additions & 1 deletion Sources/AskKeyAppKit/ManagementAuthenticationProcess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enum ManagementAuthenticationAction: CaseIterable {
case clearAccessRecords, eraseLibrary, revealFrozenFile
case approveRead, approveWrite, permanentlyDelete, replaceImportedCredential
case disableReadAuthentication, changeTimedAllowanceEnd, legacyRelease, legacyUnlock
case exportBackup, importBackup
case exportBackup, importBackup, pairDevice

var reasonKey: String {
switch self {
Expand All @@ -29,6 +29,7 @@ enum ManagementAuthenticationAction: CaseIterable {
case .legacyUnlock: "Unlock the AskKey vault for %@"
case .exportBackup: CredentialManagementCopy.exportBackupReason
case .importBackup: CredentialManagementCopy.importBackupReason
case .pairDevice: "Pair a device with this Mac"
}
}

Expand Down
Loading
Loading