Skip to content

Commit f19e3ab

Browse files
committed
refactor(doctor): distill pending pairing diagnosis
1 parent f96eca4 commit f19e3ab

1 file changed

Lines changed: 135 additions & 54 deletions

File tree

src/commands/doctor-device-pairing.ts

Lines changed: 135 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,48 @@ type DoctorPairingSnapshot = {
3535
paired: DoctorPairedDevice[];
3636
};
3737

38+
type PendingPairingIssue =
39+
| {
40+
kind: "first-time";
41+
pending: DevicePairingPendingRequest;
42+
deviceLabel: string;
43+
approveCommand: string;
44+
inspectCommand: string;
45+
}
46+
| {
47+
kind: "public-key-repair";
48+
pending: DevicePairingPendingRequest;
49+
deviceLabel: string;
50+
approveCommand: string;
51+
inspectCommand: string;
52+
removeCommand: string;
53+
}
54+
| {
55+
kind: "role-upgrade";
56+
pending: DevicePairingPendingRequest;
57+
deviceLabel: string;
58+
approveCommand: string;
59+
inspectCommand: string;
60+
approvedRoles: string[];
61+
requestedRoles: string[];
62+
}
63+
| {
64+
kind: "scope-upgrade";
65+
pending: DevicePairingPendingRequest;
66+
deviceLabel: string;
67+
approveCommand: string;
68+
inspectCommand: string;
69+
approvedScopes: string[];
70+
requestedScopes: string[];
71+
}
72+
| {
73+
kind: "repair";
74+
pending: DevicePairingPendingRequest;
75+
deviceLabel: string;
76+
approveCommand: string;
77+
inspectCommand: string;
78+
};
79+
3880
type StoredDeviceIdentity = {
3981
version: 1;
4082
deviceId: string;
@@ -187,62 +229,101 @@ function hasPendingScopeUpgrade(params: {
187229
return false;
188230
}
189231

190-
function collectPendingPairingIssues(snapshot: DoctorPairingSnapshot): string[] {
191-
const pairedByDeviceId = new Map(snapshot.paired.map((device) => [device.deviceId, device]));
192-
const lines: string[] = [];
193-
for (const pending of snapshot.pending) {
194-
const deviceLabel = describeDevice({
195-
deviceId: pending.deviceId,
196-
displayName: pending.displayName,
197-
clientId: pending.clientId,
198-
});
199-
const approveCommand = formatCliCommand(`openclaw devices approve ${pending.requestId}`);
200-
const inspectCommand = formatCliCommand("openclaw devices list");
201-
const paired = pairedByDeviceId.get(pending.deviceId);
202-
if (!paired) {
203-
lines.push(
204-
`- Pending device pairing request ${pending.requestId} for ${deviceLabel}. Review with ${inspectCommand}, then approve with ${approveCommand}.`,
205-
);
206-
continue;
207-
}
208-
209-
if (paired.publicKey !== pending.publicKey) {
210-
const removeCommand = formatCliCommand(`openclaw devices remove ${pending.deviceId}`);
211-
lines.push(
212-
`- 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}.`,
213-
);
214-
continue;
215-
}
232+
function resolvePendingPairingIssue(
233+
pending: DevicePairingPendingRequest,
234+
paired: DoctorPairedDevice | undefined,
235+
): PendingPairingIssue {
236+
const deviceLabel = describeDevice({
237+
deviceId: pending.deviceId,
238+
displayName: pending.displayName,
239+
clientId: pending.clientId,
240+
});
241+
const approveCommand = formatCliCommand(`openclaw devices approve ${pending.requestId}`);
242+
const inspectCommand = formatCliCommand("openclaw devices list");
243+
if (!paired) {
244+
return {
245+
kind: "first-time",
246+
pending,
247+
deviceLabel,
248+
approveCommand,
249+
inspectCommand,
250+
};
251+
}
252+
if (paired.publicKey !== pending.publicKey) {
253+
return {
254+
kind: "public-key-repair",
255+
pending,
256+
deviceLabel,
257+
approveCommand,
258+
inspectCommand,
259+
removeCommand: formatCliCommand(`openclaw devices remove ${pending.deviceId}`),
260+
};
261+
}
262+
const requestedRoles = uniqueStrings(pending.roles, pending.role);
263+
const approvedRoles = listApprovedPairedDeviceRoles(paired);
264+
if (requestedRoles.some((role) => !approvedRoles.includes(role))) {
265+
return {
266+
kind: "role-upgrade",
267+
pending,
268+
deviceLabel,
269+
approveCommand,
270+
inspectCommand,
271+
approvedRoles,
272+
requestedRoles,
273+
};
274+
}
275+
const approvedScopes = resolveApprovedScopes(paired);
276+
const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
277+
if (
278+
hasPendingScopeUpgrade({
279+
requestedRoles,
280+
pendingScopes: requestedScopes,
281+
approvedRoles,
282+
approvedScopes,
283+
})
284+
) {
285+
return {
286+
kind: "scope-upgrade",
287+
pending,
288+
deviceLabel,
289+
approveCommand,
290+
inspectCommand,
291+
approvedScopes,
292+
requestedScopes,
293+
};
294+
}
295+
return {
296+
kind: "repair",
297+
pending,
298+
deviceLabel,
299+
approveCommand,
300+
inspectCommand,
301+
};
302+
}
216303

217-
const requestedRoles = uniqueStrings(pending.roles, pending.role);
218-
const approvedRoles = listApprovedPairedDeviceRoles(paired);
219-
const approvedScopes = resolveApprovedScopes(paired);
220-
const requestedScopes = normalizeDeviceAuthScopes(pending.scopes);
221-
const roleUpgrade = requestedRoles.some((role) => !approvedRoles.includes(role));
222-
if (roleUpgrade) {
223-
lines.push(
224-
`- Pending role upgrade ${pending.requestId} for ${deviceLabel}: approved roles [${formatRoles(approvedRoles)}], requested roles [${formatRoles(requestedRoles)}]. Review with ${inspectCommand}, then approve with ${approveCommand}.`,
225-
);
226-
continue;
227-
}
228-
if (
229-
hasPendingScopeUpgrade({
230-
requestedRoles,
231-
pendingScopes: requestedScopes,
232-
approvedRoles,
233-
approvedScopes,
234-
})
235-
) {
236-
lines.push(
237-
`- Pending scope upgrade ${pending.requestId} for ${deviceLabel}: approved scopes [${formatScopes(approvedScopes)}], requested scopes [${formatScopes(requestedScopes)}]. Review with ${inspectCommand}, then approve with ${approveCommand}.`,
238-
);
239-
continue;
240-
}
241-
lines.push(
242-
`- 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}.`,
243-
);
304+
function formatPendingPairingIssue(issue: PendingPairingIssue): string {
305+
switch (issue.kind) {
306+
case "first-time":
307+
return `- Pending device pairing request ${issue.pending.requestId} for ${issue.deviceLabel}. Review with ${issue.inspectCommand}, then approve with ${issue.approveCommand}.`;
308+
case "public-key-repair":
309+
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}.`;
310+
case "role-upgrade":
311+
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}.`;
312+
case "scope-upgrade":
313+
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}.`;
314+
case "repair":
315+
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}.`;
244316
}
245-
return lines;
317+
throw new Error("Unsupported pending pairing issue");
318+
}
319+
320+
function collectPendingPairingIssues(snapshot: DoctorPairingSnapshot): string[] {
321+
const pairedByDeviceId = new Map(snapshot.paired.map((device) => [device.deviceId, device]));
322+
return snapshot.pending.map((pending) =>
323+
formatPendingPairingIssue(
324+
resolvePendingPairingIssue(pending, pairedByDeviceId.get(pending.deviceId)),
325+
),
326+
);
246327
}
247328

248329
function collectPairedRecordIssues(snapshot: DoctorPairingSnapshot): string[] {

0 commit comments

Comments
 (0)