import fs from "node:fs"; import path from "node:path"; import { formatCliCommand } from "../cli/command-format.js"; import { resolveStateDir } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { callGateway } from "../gateway/call.js"; import {
listApprovedPairedDeviceRoles,
listDevicePairing,
summarizeDeviceTokens,
type DeviceAuthTokenSummary,
type DevicePairingPendingRequest,
type PairedDevice,
} from "../infra/device-pairing.js"; import type { DeviceAuthStore } from "../shared/device-auth.js"; import { normalizeDeviceAuthScopes } from "../shared/device-auth.js"; import { roleScopesAllow } from "../shared/operator-scope-compat.js"; import { note } from "../terminal/note.js"; import { sanitizeTerminalText } from "../terminal/safe-text.js";
type StoredDeviceIdentity = {
version: 1;
deviceId: string;
};
function hasNumberVersion(value: object): value is { version: number } { return"version" in value && typeof value.version === "number";
}
function isDeviceAuthStoreTokenEntry(value: unknown): value is DeviceAuthStore["tokens"][string] { return ( typeof value === "object" &&
value !== null && "token" in value && typeof value.token === "string" && "role" in value && typeof value.role === "string" && "scopes" in value &&
Array.isArray(value.scopes) &&
value.scopes.every((scope) => typeof scope === "string") && "updatedAtMs" in value && typeof value.updatedAtMs === "number"
);
}
function uniqueStrings(...items: Array<string | string[] | undefined>): string[] { const values = new Set<string>(); for (const item of items) { if (!item) { continue;
} if (Array.isArray(item)) { for (const value of item) { const trimmed = value.trim(); if (trimmed) {
values.add(trimmed);
}
} continue;
} const trimmed = item.trim(); if (trimmed) {
values.add(trimmed);
}
} return [...values];
}
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}.`;
} thrownew Error("Unsupported pending pairing issue");
}
function collectPairedRecordIssues(snapshot: DoctorPairingSnapshot): string[] { const lines: string[] = []; for (const device of snapshot.paired) { const deviceLabel = describeDevice({
deviceId: device.deviceId,
displayName: device.displayName,
clientId: device.clientId,
}); const approvedRoles = listApprovedPairedDeviceRoles(device); const approvedScopes = resolveApprovedScopes(device); if (approvedRoles.includes("operator") && approvedScopes.length === 0) {
lines.push(
`- Paired device ${deviceLabel} is missing its approved operator scope baseline. Scope upgrades can get stuck in pairing-required until the device repairs or is re-approved.`,
);
} for (const role of approvedRoles) { const token = findTokenSummary(device, role); const rotateCommand = formatCliArgs([ "openclaw", "devices", "rotate", "--device",
device.deviceId, "--role",
role,
]); if (!token) {
lines.push(
`- Paired device ${deviceLabel} has no active ${role} device token even though the role is approved. This commonly ends in pairing-required or device-token-mismatch. Rotate a fresh token with ${rotateCommand}.`,
); continue;
} if (
token.scopes.length > 0 &&
!roleScopesAllow({
role,
requestedScopes: token.scopes,
allowedScopes: approvedScopes,
})
) {
lines.push(
`- Paired device ${deviceLabel} has a ${role} token outside the approved scope baseline [${formatScopes(approvedScopes)}]. Rotate it with ${rotateCommand}.`,
);
}
}
} return lines;
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.