From f19e3ab298650af331c7cbfdbecb5b9b5806aeba Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Mon, 20 Apr 2026 11:07:00 +0530 Subject: [PATCH] refactor(doctor): distill pending pairing diagnosis --- src/commands/doctor-device-pairing.ts | 189 ++++++++++++++++++-------- 1 file changed, 135 insertions(+), 54 deletions(-) diff --git a/src/commands/doctor-device-pairing.ts b/src/commands/doctor-device-pairing.ts index e52a3b19571..f1b150372e5 100644 --- a/src/commands/doctor-device-pairing.ts +++ b/src/commands/doctor-device-pairing.ts @@ -35,6 +35,48 @@ type DoctorPairingSnapshot = { paired: DoctorPairedDevice[]; }; +type PendingPairingIssue = + | { + kind: "first-time"; + pending: DevicePairingPendingRequest; + deviceLabel: string; + approveCommand: string; + inspectCommand: string; + } + | { + kind: "public-key-repair"; + pending: DevicePairingPendingRequest; + deviceLabel: string; + approveCommand: string; + inspectCommand: string; + removeCommand: string; + } + | { + kind: "role-upgrade"; + pending: DevicePairingPendingRequest; + deviceLabel: string; + approveCommand: string; + inspectCommand: string; + approvedRoles: string[]; + requestedRoles: string[]; + } + | { + kind: "scope-upgrade"; + pending: DevicePairingPendingRequest; + deviceLabel: string; + approveCommand: string; + inspectCommand: string; + approvedScopes: string[]; + requestedScopes: string[]; + } + | { + kind: "repair"; + pending: DevicePairingPendingRequest; + deviceLabel: string; + approveCommand: string; + inspectCommand: string; + }; + type StoredDeviceIdentity = { version: 1; deviceId: string; @@ -187,62 +229,101 @@ function hasPendingScopeUpgrade(params: { return false; } +function resolvePendingPairingIssue( + pending: DevicePairingPendingRequest, + paired: DoctorPairedDevice | undefined, +): PendingPairingIssue { + const deviceLabel = describeDevice({ + deviceId: pending.deviceId, + displayName: pending.displayName, + clientId: pending.clientId, + }); + const approveCommand = formatCliCommand(`openclaw devices approve ${pending.requestId}`); + const inspectCommand = formatCliCommand("openclaw devices list"); + if (!paired) { + return { + kind: "first-time", + pending, + deviceLabel, + approveCommand, + inspectCommand, + }; + } + if (paired.publicKey !== pending.publicKey) { + return { + kind: "public-key-repair", + pending, + deviceLabel, + approveCommand, + inspectCommand, + removeCommand: formatCliCommand(`openclaw devices remove ${pending.deviceId}`), + }; + } + const requestedRoles = uniqueStrings(pending.roles, pending.role); + const approvedRoles = listApprovedPairedDeviceRoles(paired); + if (requestedRoles.some((role) => !approvedRoles.includes(role))) { + return { + kind: "role-upgrade", + pending, + deviceLabel, + approveCommand, + inspectCommand, + approvedRoles, + requestedRoles, + }; + } + const approvedScopes = resolveApprovedScopes(paired); + const requestedScopes = normalizeDeviceAuthScopes(pending.scopes); + if ( + hasPendingScopeUpgrade({ + requestedRoles, + pendingScopes: requestedScopes, + approvedRoles, + approvedScopes, + }) + ) { + return { + kind: "scope-upgrade", + pending, + deviceLabel, + approveCommand, + inspectCommand, + approvedScopes, + requestedScopes, + }; + } + return { + kind: "repair", + pending, + deviceLabel, + approveCommand, + inspectCommand, + }; +} + +function formatPendingPairingIssue(issue: PendingPairingIssue): string { + switch (issue.kind) { + case "first-time": + return `- Pending device pairing request ${issue.pending.requestId} for ${issue.deviceLabel}. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`; + case "public-key-repair": + return `- Pending device repair ${issue.pending.requestId} for ${issue.deviceLabel}: the current device identity no longer matches the approved pairing record. This commonly loops on pairing-required for an already paired device. Remove the stale record with ${issue.removeCommand}, then rerun ${issue.inspectCommand} and approve with ${issue.approveCommand}.`; + case "role-upgrade": + return `- Pending role upgrade ${issue.pending.requestId} for ${issue.deviceLabel}: approved roles [${formatRoles(issue.approvedRoles)}], requested roles [${formatRoles(issue.requestedRoles)}]. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`; + case "scope-upgrade": + return `- Pending scope upgrade ${issue.pending.requestId} for ${issue.deviceLabel}: approved scopes [${formatScopes(issue.approvedScopes)}], requested scopes [${formatScopes(issue.requestedScopes)}]. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`; + case "repair": + return `- Pending device repair ${issue.pending.requestId} for ${issue.deviceLabel}: the device is already paired, but a new approval is still required before the requested auth can be used. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`; + } + throw new Error("Unsupported pending pairing issue"); +} + function collectPendingPairingIssues(snapshot: DoctorPairingSnapshot): string[] { const pairedByDeviceId = new Map(snapshot.paired.map((device) => [device.deviceId, device])); - const lines: string[] = []; - for (const pending of snapshot.pending) { - const deviceLabel = describeDevice({ - deviceId: pending.deviceId, - displayName: pending.displayName, - clientId: pending.clientId, - }); - const approveCommand = formatCliCommand(`openclaw devices approve ${pending.requestId}`); - const inspectCommand = formatCliCommand("openclaw devices list"); - const paired = pairedByDeviceId.get(pending.deviceId); - if (!paired) { - lines.push( - `- Pending device pairing request ${pending.requestId} for ${deviceLabel}. Review with ${inspectCommand}, then approve with ${approveCommand}.`, - ); - continue; - } - - if (paired.publicKey !== pending.publicKey) { - const removeCommand = formatCliCommand(`openclaw devices remove ${pending.deviceId}`); - lines.push( - `- Pending device repair ${pending.requestId} for ${deviceLabel}: the current device identity no longer matches the approved pairing record. This commonly loops on pairing-required for an already paired device. Remove the stale record with ${removeCommand}, then rerun ${inspectCommand} and approve with ${approveCommand}.`, - ); - continue; - } - - const requestedRoles = uniqueStrings(pending.roles, pending.role); - const approvedRoles = listApprovedPairedDeviceRoles(paired); - const approvedScopes = resolveApprovedScopes(paired); - const requestedScopes = normalizeDeviceAuthScopes(pending.scopes); - const roleUpgrade = requestedRoles.some((role) => !approvedRoles.includes(role)); - if (roleUpgrade) { - lines.push( - `- Pending role upgrade ${pending.requestId} for ${deviceLabel}: approved roles [${formatRoles(approvedRoles)}], requested roles [${formatRoles(requestedRoles)}]. Review with ${inspectCommand}, then approve with ${approveCommand}.`, - ); - continue; - } - if ( - hasPendingScopeUpgrade({ - requestedRoles, - pendingScopes: requestedScopes, - approvedRoles, - approvedScopes, - }) - ) { - lines.push( - `- Pending scope upgrade ${pending.requestId} for ${deviceLabel}: approved scopes [${formatScopes(approvedScopes)}], requested scopes [${formatScopes(requestedScopes)}]. Review with ${inspectCommand}, then approve with ${approveCommand}.`, - ); - continue; - } - lines.push( - `- Pending device repair ${pending.requestId} for ${deviceLabel}: the device is already paired, but a new approval is still required before the requested auth can be used. Review with ${inspectCommand}, then approve with ${approveCommand}.`, - ); - } - return lines; + return snapshot.pending.map((pending) => + formatPendingPairingIssue( + resolvePendingPairingIssue(pending, pairedByDeviceId.get(pending.deviceId)), + ), + ); } function collectPairedRecordIssues(snapshot: DoctorPairingSnapshot): string[] {