openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
699 lines (698 loc) • 28.4 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty, c as normalizeOptionalString, f as normalizeStringifiedOptionalString } from "./string-coerce-mnp54Vah.js";
import { t as sanitizeForLog } from "./ansi-BI9w76Cm.js";
import { t as formatCliCommand } from "./command-format-CKGmlpAQ.js";
import { r as theme } from "./theme-vjDs9tao.js";
import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js";
import { n as defaultRuntime } from "./runtime-B4lgFmsS.js";
import { i as isLoopbackHost } from "./net-DTe7AQiu.js";
import { i as GATEWAY_CLIENT_NAMES, r as GATEWAY_CLIENT_MODES } from "./client-info-CcqJJIan.js";
import { d as formatGatewayTransportErrorJson, i as buildGatewayConnectionDetails, o as callGateway } from "./call-B5-GYOlf.js";
import { n as normalizeDeviceAuthScopes } from "./device-auth-C-STNejO.js";
import { d as readConnectPairingRequiredMessage } from "./connect-error-details-BXqba0zp.js";
import { r as PAIRING_SCOPE, t as ADMIN_SCOPE } from "./operator-scopes-CS3xdS-V.js";
import "./method-scopes-BtdN3y7s.js";
import { n as formatTimeAgo } from "./format-relative-Bjc3l98W.js";
import { r as withProgress } from "./progress-CN3xUiCO.js";
import { n as renderTable, t as getTerminalTableWidth } from "./table-CpFtGZut.js";
import { g as summarizeDeviceTokens, i as formatDevicePairingForbiddenMessage, l as listDevicePairing, n as approveDevicePairing } from "./device-pairing-Bcmp5CuV.js";
import { n as parseTimeoutMsWithFallback } from "./parse-timeout-DZMZaF9B.js";
//#region src/shared/device-pairing-access.ts
function normalizeRoleList(...items) {
const roles = /* @__PURE__ */ new Set();
for (const item of items) {
if (!item) continue;
if (Array.isArray(item)) {
for (const role of item) {
const trimmed = role.trim();
if (trimmed) roles.add(trimmed);
}
continue;
}
const trimmed = item.trim();
if (trimmed) roles.add(trimmed);
}
return [...roles].toSorted();
}
function includesAll(allowed, requested) {
const allowedSet = new Set(allowed);
return requested.every((value) => allowedSet.has(value));
}
/** Normalizes requested roles/scopes from pending pairing records, including legacy singular role. */
function summarizePendingDeviceAccess(request) {
return {
roles: normalizeRoleList(request.roles, request.role),
scopes: normalizeDeviceAuthScopes(request.scopes)
};
}
/** Summarizes currently approved device access, excluding roles whose tokens are revoked. */
function summarizeApprovedDeviceAccess(device) {
const approvedRoles = normalizeRoleList(device.roles, device.role);
const tokenList = Array.isArray(device.tokens) ? device.tokens : device.tokens ? Object.values(device.tokens) : void 0;
return {
roles: tokenList === void 0 ? approvedRoles : normalizeRoleList(tokenList.filter((token) => !token.revokedAtMs).flatMap((token) => token.role ?? [])).filter((role) => approvedRoles.includes(role)),
scopes: normalizeDeviceAuthScopes(device.scopes)
};
}
/** Classifies a pending pairing request as new pairing, role upgrade, scope upgrade, or re-approval. */
function resolvePendingDeviceApprovalState(request, paired) {
const requested = summarizePendingDeviceAccess(request);
const approved = paired ? summarizeApprovedDeviceAccess(paired) : null;
if (!approved) return {
kind: "new-pairing",
requested,
approved: null
};
if (!includesAll(approved.roles, requested.roles)) return {
kind: "role-upgrade",
requested,
approved
};
if (!includesAll(approved.scopes, requested.scopes)) return {
kind: "scope-upgrade",
requested,
approved
};
return {
kind: "re-approval",
requested,
approved
};
}
//#endregion
//#region src/cli/devices-cli.runtime.ts
const FALLBACK_NOTICE = "Direct scope access failed; using local fallback.";
const DEFAULT_DEVICES_TIMEOUT_MS = 1e4;
const FALLBACK_STATE_MISMATCH_MESSAGE = "Gateway requires device pairing, but local fallback pairing state does not contain the gateway request.";
const OPERATOR_ROLE = "operator";
const OPERATOR_SCOPE_PREFIX = "operator.";
const KNOWN_NON_ADMIN_OPERATOR_SCOPES = new Set([
"operator.approvals",
"operator.pairing",
"operator.read",
"operator.talk.secrets",
"operator.write"
]);
const callGatewayCli = async (method, opts, params, callOpts) => withProgress({
label: `Devices ${method}`,
indeterminate: true,
enabled: opts.json !== true
}, async () => await callGateway({
url: opts.url,
token: opts.token,
password: opts.password,
method,
params,
timeoutMs: parseTimeoutMsWithFallback(opts.timeout, DEFAULT_DEVICES_TIMEOUT_MS),
clientName: GATEWAY_CLIENT_NAMES.CLI,
mode: GATEWAY_CLIENT_MODES.CLI,
scopes: callOpts?.scopes
}));
function normalizeErrorMessage(error) {
if (error instanceof Error) return error.message;
return String(error);
}
function isDevicePairingApprovalDenied(error) {
return normalizeLowercaseStringOrEmpty(normalizeErrorMessage(error)).includes("device pairing approval denied");
}
function resolveLocalPairingFallback(opts, error) {
const details = readConnectPairingRequiredMessage(normalizeLowercaseStringOrEmpty(normalizeErrorMessage(error)));
if (!details) return null;
if (typeof opts.url === "string" && opts.url.trim().length > 0) return null;
const connection = buildGatewayConnectionDetails();
if (connection.urlSource !== "local loopback") return null;
try {
return isLoopbackHost(new URL(connection.url).hostname) ? { details } : null;
} catch {
return null;
}
}
function buildFallbackStateMismatchError(details) {
return new Error([
details.requestId ? `${FALLBACK_STATE_MISMATCH_MESSAGE} Missing requestId: ${details.requestId}.` : FALLBACK_STATE_MISMATCH_MESSAGE,
"The running gateway is probably using a different OPENCLAW_PROFILE or OPENCLAW_STATE_DIR than this CLI.",
"Rerun with the same profile/state-dir as the gateway, or pass --token/--password so the CLI can approve through the gateway."
].join("\n"));
}
function assertLocalFallbackMatchesGatewayRequest(details, list) {
const requestId = normalizeOptionalString(details.requestId);
if (!requestId) return;
if (!(list.pending ?? []).some((request) => normalizeOptionalString(request.requestId) === requestId)) throw buildFallbackStateMismatchError(details);
}
function redactLocalPairedDevice(device) {
const { tokens, ...rest } = device;
return {
...rest,
tokens: summarizeDeviceTokens(tokens)
};
}
async function listPairingWithFallback(opts) {
try {
return parseDevicePairingList(await callGatewayCli("device.pair.list", opts, {}));
} catch (error) {
const fallback = resolveLocalPairingFallback(opts, error);
if (!fallback) throw error;
const local = await listDevicePairing();
const list = {
pending: local.pending,
paired: local.paired.map((device) => redactLocalPairedDevice(device))
};
assertLocalFallbackMatchesGatewayRequest(fallback.details, list);
if (opts.json !== true) defaultRuntime.log(theme.warn(FALLBACK_NOTICE));
return list;
}
}
async function approvePairingWithFallback(opts, requestId) {
const { scopes, originalRequest } = await resolveApprovePairingGatewayContext(opts, requestId);
try {
return await callGatewayCli("device.pair.approve", opts, { requestId }, scopes ? { scopes } : void 0);
} catch (error) {
if (isDevicePairingApprovalDenied(error) && !scopes?.includes("operator.admin")) return await callGatewayCli("device.pair.approve", opts, { requestId }, { scopes: [ADMIN_SCOPE] });
const fallback = resolveLocalPairingFallback(opts, error);
if (!fallback) throw error;
const gatewayRequestId = normalizeOptionalString(fallback.details.requestId);
if (gatewayRequestId && gatewayRequestId !== requestId) {
const local = await listDevicePairing();
const localList = {
pending: local.pending,
paired: local.paired.map((device) => redactLocalPairedDevice(device))
};
const replacement = findSameDeviceReplacementRequest({
originalRequest,
originalRequestId: requestId,
gatewayRequestId,
pending: localList.pending,
paired: localList.paired
});
if (replacement) {
const approved = await approveDevicePairing(replacement.requestId, { callerScopes: ["operator.admin"] });
if (!approved) return null;
if (approved.status === "forbidden") throw new Error(formatDevicePairingForbiddenMessage(approved), { cause: error });
if (opts.json !== true) {
defaultRuntime.log(theme.warn(`Pending request ${sanitizeForLog(requestId)} was replaced by same-device repair ${sanitizeForLog(replacement.requestId)}; approving latest compatible request.`));
defaultRuntime.log(theme.warn(FALLBACK_NOTICE));
}
return {
requestId: replacement.requestId,
resolved: {
kind: "same-device-replacement",
requestedRequestId: requestId,
approvedRequestId: replacement.requestId
},
device: redactLocalPairedDevice(approved.device)
};
}
const hasOriginalPending = Boolean(findPendingRequestById(localList.pending, requestId));
const hasGatewayPending = Boolean(findPendingRequestById(localList.pending, gatewayRequestId));
if (!hasOriginalPending && !hasGatewayPending) return null;
throw buildFallbackStateMismatchError(fallback.details);
}
const approved = await approveDevicePairing(requestId, { callerScopes: ["operator.admin"] });
if (!approved) {
if (gatewayRequestId && gatewayRequestId === requestId) throw buildFallbackStateMismatchError(fallback.details);
return null;
}
if (approved.status === "forbidden") throw new Error(formatDevicePairingForbiddenMessage(approved), { cause: error });
if (opts.json !== true) defaultRuntime.log(theme.warn(FALLBACK_NOTICE));
return {
requestId,
device: redactLocalPairedDevice(approved.device)
};
}
}
function parseDevicePairingList(value) {
const obj = typeof value === "object" && value !== null ? value : {};
return {
pending: Array.isArray(obj.pending) ? obj.pending : [],
paired: Array.isArray(obj.paired) ? obj.paired : []
};
}
function normalizeDeviceRoles(request) {
const roles = /* @__PURE__ */ new Set();
for (const role of request.roles ?? []) {
const normalized = normalizeOptionalString(role);
if (normalized) roles.add(normalized);
}
const role = normalizeOptionalString(request.role);
if (role) roles.add(role);
return [...roles];
}
function normalizeOperatorScopes(scopes) {
return normalizeDeviceAuthScopes(scopes).filter((scope) => scope.startsWith(OPERATOR_SCOPE_PREFIX));
}
function findPendingRequestById(pending, requestId) {
const normalizedRequestId = normalizeOptionalString(requestId);
if (!normalizedRequestId) return null;
return pending?.find((request) => normalizeOptionalString(request.requestId) === normalizedRequestId) ?? null;
}
function hasExactRoleMatch(original, replacement) {
const originalRoles = normalizeDeviceRoles(original);
const replacementRoles = normalizeDeviceRoles(replacement);
if (originalRoles.length !== replacementRoles.length) return false;
const replacementRoleSet = new Set(replacementRoles);
return originalRoles.every((role) => replacementRoleSet.has(role));
}
function hasCompatibleClientMetadata(original, replacement) {
const originalClientId = normalizeOptionalString(original.clientId);
const replacementClientId = normalizeOptionalString(replacement.clientId);
if (originalClientId && replacementClientId && originalClientId !== replacementClientId) return false;
const originalClientMode = normalizeOptionalString(original.clientMode);
const replacementClientMode = normalizeOptionalString(replacement.clientMode);
return !(originalClientMode && replacementClientMode && originalClientMode !== replacementClientMode);
}
function resolveOriginalReplacementScopes(original, paired) {
const requestedScopes = normalizeDeviceAuthScopes(original.scopes);
const inferredOperatorScopes = resolvePendingOperatorApprovalScopes(original, paired);
return uniqueStrings([...requestedScopes, ...inferredOperatorScopes]);
}
function replacementScopesCoverOriginal(original, replacement, paired) {
const originalScopes = resolveOriginalReplacementScopes(original, paired);
const replacementScopes = normalizeDeviceAuthScopes(replacement.scopes);
const replacementScopeSet = new Set(replacementScopes);
if (!originalScopes.every((scope) => replacementScopeSet.has(scope))) return false;
return replacementScopes.every((scope) => originalScopes.includes(scope) || scope === "operator.pairing");
}
function findSameDeviceReplacementRequest(params) {
const originalRequestId = normalizeOptionalString(params.originalRequestId);
if (!params.originalRequest || !originalRequestId) return null;
if (normalizeOptionalString(params.originalRequest.requestId) !== originalRequestId) return null;
const replacement = findPendingRequestById(params.pending, params.gatewayRequestId);
if (!replacement) return null;
const originalDeviceId = normalizeOptionalString(params.originalRequest.deviceId);
const replacementDeviceId = normalizeOptionalString(replacement.deviceId);
if (!originalDeviceId || originalDeviceId !== replacementDeviceId) return null;
const originalPublicKey = normalizeOptionalString(params.originalRequest.publicKey);
const replacementPublicKey = normalizeOptionalString(replacement.publicKey);
if (!originalPublicKey || !replacementPublicKey || originalPublicKey !== replacementPublicKey) return null;
if (!hasExactRoleMatch(params.originalRequest, replacement)) return null;
if (!hasCompatibleClientMetadata(params.originalRequest, replacement)) return null;
const pairedByDeviceId = indexPairedDevices(params.paired);
const originalPaired = lookupPairedDevice(pairedByDeviceId, params.originalRequest);
const replacementPaired = lookupPairedDevice(pairedByDeviceId, replacement);
if (!replacementScopesCoverOriginal(params.originalRequest, replacement, originalPaired)) return null;
if (replacement.isRepair !== true && (!originalPaired || !replacementPaired)) return null;
return replacement;
}
function resolvePairedOperatorScopes(paired) {
const operatorToken = paired?.tokens?.find((token) => {
return normalizeOptionalString(token.role) === OPERATOR_ROLE && !token.revokedAtMs;
});
return normalizeOperatorScopes(operatorToken?.scopes ?? paired?.scopes);
}
function resolvePendingOperatorApprovalScopes(request, paired) {
if (!normalizeDeviceRoles(request).includes(OPERATOR_ROLE)) return [];
const requestedScopes = normalizeOperatorScopes(request.scopes);
return requestedScopes.length > 0 ? requestedScopes : resolvePairedOperatorScopes(paired);
}
function isKnownNonAdminOperatorScope(scope) {
return KNOWN_NON_ADMIN_OPERATOR_SCOPES.has(scope);
}
function resolveApprovePairingScopesForRequest(request, paired) {
const operatorScopes = resolvePendingOperatorApprovalScopes(request, paired);
if (operatorScopes.length === 0) return;
if (operatorScopes.includes("operator.admin")) return [ADMIN_SCOPE];
const out = new Set([PAIRING_SCOPE]);
for (const scope of operatorScopes) {
if (!isKnownNonAdminOperatorScope(scope)) return [ADMIN_SCOPE];
out.add(scope);
}
return [...out];
}
async function resolveApprovePairingGatewayContext(opts, requestId) {
try {
const list = await listPairingWithFallback(opts);
const request = findPendingRequestById(list.pending, requestId);
if (!request) return {
originalRequest: null,
scopes: void 0
};
return {
originalRequest: request,
scopes: resolveApprovePairingScopesForRequest(request, lookupPairedDevice(indexPairedDevices(list.paired), request))
};
} catch {
return {
originalRequest: null,
scopes: void 0
};
}
}
function selectLatestPendingRequest(pending) {
if (!pending?.length) return null;
return pending.reduce((latest, current) => {
const latestTs = typeof latest.ts === "number" ? latest.ts : 0;
return (typeof current.ts === "number" ? current.ts : 0) > latestTs ? current : latest;
});
}
function formatTokenSummary(tokens) {
if (!tokens || tokens.length === 0) return "none";
return tokens.map((t) => `${sanitizeForLog(t.role)}${t.revokedAtMs ? " (revoked)" : ""}`).toSorted((a, b) => a.localeCompare(b)).join(", ");
}
function formatPendingDeviceIdentity(request) {
const displayName = normalizeOptionalString(request.displayName);
if (displayName) return sanitizeForLog(displayName);
return sanitizeForLog(normalizeOptionalString(request.deviceId) ?? "");
}
function formatAccessSummary(access) {
if (!access) return "none";
return `roles: ${access.roles.length > 0 ? access.roles.map((role) => sanitizeForLog(role)).join(", ") : "none"}; scopes: ${access.scopes.length > 0 ? access.scopes.map((scope) => sanitizeForLog(scope)).join(", ") : "none"}`;
}
function formatPendingApprovalKind(kind) {
switch (kind) {
case "new-pairing": return "new pairing";
case "role-upgrade": return "role upgrade";
case "scope-upgrade": return "scope upgrade";
case "re-approval": return "re-approval";
}
throw new Error("unsupported pending approval kind");
}
function indexPairedDevices(paired) {
const out = /* @__PURE__ */ new Map();
for (const device of paired ?? []) {
const deviceId = normalizeOptionalString(device.deviceId);
if (deviceId) out.set(deviceId, device);
}
return out;
}
function lookupPairedDevice(pairedByDeviceId, request) {
const normalizedDeviceId = normalizeOptionalString(request.deviceId);
if (!normalizedDeviceId) return;
const paired = pairedByDeviceId.get(normalizedDeviceId);
if (!paired) return;
const requestPublicKey = normalizeOptionalString(request.publicKey);
const pairedPublicKey = normalizeOptionalString(paired.publicKey);
if (requestPublicKey && pairedPublicKey && requestPublicKey !== pairedPublicKey) return;
return paired;
}
function quoteCliArg(value) {
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
return `'${value.replaceAll("'", "'\\''")}'`;
}
function buildExplicitApproveCommand(opts, requestId) {
const args = [
"openclaw",
"devices",
"approve",
requestId
];
const url = normalizeOptionalString(opts.url);
if (url) args.push("--url", url);
const timeout = normalizeOptionalString(opts.timeout);
if (timeout && timeout !== String(DEFAULT_DEVICES_TIMEOUT_MS)) args.push("--timeout", timeout);
if (opts.json === true) args.push("--json");
return args.map(quoteCliArg).join(" ");
}
function formatAuthFlagReminder(opts) {
const flags = [];
if (normalizeOptionalString(opts.token)) flags.push("--token");
if (normalizeOptionalString(opts.password)) flags.push("--password");
if (flags.length === 0) return "";
return `Reuse the same ${flags.join("/")} option${flags.length === 1 ? "" : "s"} when rerunning.`;
}
function resolveRequiredDeviceRole(opts) {
const deviceId = normalizeStringifiedOptionalString(opts.device) ?? "";
const role = normalizeStringifiedOptionalString(opts.role) ?? "";
if (deviceId && role) return {
deviceId,
role
};
defaultRuntime.error(`--device and --role are required. Run ${formatCliCommand("openclaw devices list")} to choose a paired device.`);
defaultRuntime.exit(1);
return null;
}
async function runDevicesListCommand(opts) {
let list;
try {
list = await listPairingWithFallback(opts);
} catch (error) {
if (opts.json) {
const payload = formatGatewayTransportErrorJson(error);
if (payload) {
defaultRuntime.writeJson(payload);
defaultRuntime.exit(1);
return;
}
}
throw error;
}
const pairedByDeviceId = indexPairedDevices(list.paired);
if (opts.json) {
defaultRuntime.writeJson(list);
return;
}
if (list.pending?.length) {
const tableWidth = getTerminalTableWidth();
defaultRuntime.log(`${theme.heading("Pending")} ${theme.muted(`(${list.pending.length})`)}`);
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Request",
header: "Request",
minWidth: 10
},
{
key: "Device",
header: "Device",
minWidth: 16,
flex: true
},
{
key: "Requested",
header: "Requested",
minWidth: 20,
flex: true
},
{
key: "Approved",
header: "Approved",
minWidth: 20,
flex: true
},
{
key: "Age",
header: "Age",
minWidth: 8
},
{
key: "Status",
header: "Status",
minWidth: 12
}
],
rows: list.pending.map((req) => {
const approval = resolvePendingDeviceApprovalState(req, lookupPairedDevice(pairedByDeviceId, req));
const statusParts = [formatPendingApprovalKind(approval.kind)];
if (req.isRepair) statusParts.push("repair");
return {
Request: req.requestId,
Device: `${formatPendingDeviceIdentity(req)}${req.remoteIp ? ` · ${sanitizeForLog(req.remoteIp)}` : ""}`,
Requested: formatAccessSummary(approval.requested),
Approved: formatAccessSummary(approval.approved),
Age: typeof req.ts === "number" ? formatTimeAgo(Date.now() - req.ts) : "",
Status: statusParts.join(", ")
};
})
}).trimEnd());
}
if (list.paired?.length) {
const tableWidth = getTerminalTableWidth();
defaultRuntime.log(`${theme.heading("Paired")} ${theme.muted(`(${list.paired.length})`)}`);
defaultRuntime.log(renderTable({
width: tableWidth,
columns: [
{
key: "Device",
header: "Device",
minWidth: 16,
flex: true
},
{
key: "Roles",
header: "Roles",
minWidth: 12,
flex: true
},
{
key: "Scopes",
header: "Scopes",
minWidth: 12,
flex: true
},
{
key: "Tokens",
header: "Tokens",
minWidth: 12,
flex: true
},
{
key: "IP",
header: "IP",
minWidth: 12
}
],
rows: list.paired.map((device) => ({
Device: sanitizeForLog(device.displayName || device.deviceId),
Roles: device.roles?.length ? device.roles.map((role) => sanitizeForLog(role)).join(", ") : "",
Scopes: device.scopes?.length ? device.scopes.map((scope) => sanitizeForLog(scope)).join(", ") : "",
Tokens: formatTokenSummary(device.tokens),
IP: device.remoteIp ? sanitizeForLog(device.remoteIp) : ""
}))
}).trimEnd());
}
if (!list.pending?.length && !list.paired?.length) defaultRuntime.log(theme.muted("No device pairing entries."));
}
async function runDevicesRemoveCommand(deviceId, opts) {
const trimmed = deviceId.trim();
if (!trimmed) {
defaultRuntime.error(`deviceId is required. Run ${formatCliCommand("openclaw devices list")} to choose a paired device.`);
defaultRuntime.exit(1);
return;
}
const result = await callGatewayCli("device.pair.remove", opts, { deviceId: trimmed });
if (opts.json) {
defaultRuntime.writeJson(result);
return;
}
defaultRuntime.log(`${theme.warn("Removed")} ${theme.command(trimmed)}`);
}
async function runDevicesClearCommand(opts) {
if (!opts.yes) {
defaultRuntime.error("Refusing to clear pairing table without --yes");
defaultRuntime.exit(1);
return;
}
const list = parseDevicePairingList(await callGatewayCli("device.pair.list", opts, {}));
const removedDeviceIds = [];
const rejectedRequestIds = [];
const paired = Array.isArray(list.paired) ? list.paired : [];
for (const device of paired) {
const deviceId = normalizeOptionalString(device.deviceId) ?? "";
if (!deviceId) continue;
await callGatewayCli("device.pair.remove", opts, { deviceId });
removedDeviceIds.push(deviceId);
}
if (opts.pending) {
const pending = Array.isArray(list.pending) ? list.pending : [];
for (const req of pending) {
const requestId = normalizeOptionalString(req.requestId) ?? "";
if (!requestId) continue;
await callGatewayCli("device.pair.reject", opts, { requestId });
rejectedRequestIds.push(requestId);
}
}
if (opts.json) {
defaultRuntime.writeJson({
removedDevices: removedDeviceIds,
rejectedPending: rejectedRequestIds
});
return;
}
defaultRuntime.log(`${theme.warn("Cleared")} ${removedDeviceIds.length} paired device${removedDeviceIds.length === 1 ? "" : "s"}`);
if (opts.pending) defaultRuntime.log(`${theme.warn("Rejected")} ${rejectedRequestIds.length} pending request${rejectedRequestIds.length === 1 ? "" : "s"}`);
}
async function runDevicesApproveCommand(requestId, opts) {
let pairingList = null;
let resolvedRequestId = requestId?.trim();
const usingImplicitSelection = !resolvedRequestId || Boolean(opts.latest);
let selectedRequest = null;
if (usingImplicitSelection) {
pairingList = await listPairingWithFallback(opts);
selectedRequest = selectLatestPendingRequest(pairingList.pending);
resolvedRequestId = selectedRequest?.requestId?.trim();
}
if (!resolvedRequestId) {
defaultRuntime.error("No pending device pairing requests to approve");
defaultRuntime.exit(1);
return;
}
if (usingImplicitSelection) {
const req = selectedRequest;
const approval = resolvePendingDeviceApprovalState(req, lookupPairedDevice(indexPairedDevices(pairingList?.paired), req));
const approveCommand = buildExplicitApproveCommand(opts, req.requestId);
const authReminder = formatAuthFlagReminder(opts);
if (opts.json) {
defaultRuntime.writeJson({
selected: req,
approvalState: {
kind: approval.kind,
requested: approval.requested,
approved: approval.approved
},
approveCommand,
requiresAuthFlags: {
token: Boolean(normalizeOptionalString(opts.token)),
password: Boolean(normalizeOptionalString(opts.password))
}
});
defaultRuntime.exit(1);
return;
}
defaultRuntime.log(`${theme.warn("Selected pending device request")} ${theme.command(req.requestId)}`);
defaultRuntime.log(` Device: ${formatPendingDeviceIdentity(req)}`);
defaultRuntime.log(` Requested: ${formatAccessSummary(approval.requested)}`);
if (approval.approved) defaultRuntime.log(` Approved: ${formatAccessSummary(approval.approved)}`);
if (req.remoteIp) defaultRuntime.log(` IP: ${sanitizeForLog(req.remoteIp)}`);
switch (approval.kind) {
case "scope-upgrade":
defaultRuntime.log(" Note: Already paired. Requested scopes exceed the current approval, so reconnect stays blocked until you approve this upgrade.");
break;
case "role-upgrade":
defaultRuntime.log(" Note: Already paired. Requested role exceeds the current approval, so reconnect stays blocked until you approve this upgrade.");
break;
case "re-approval":
defaultRuntime.log(" Note: Already paired. Approval-bound device details changed, so OpenClaw created a fresh request instead of silently reusing the old approval.");
break;
case "new-pairing":
defaultRuntime.log(" Note: First-time device pairing request.");
break;
}
defaultRuntime.error(`Approve this exact request with: ${approveCommand}`);
if (authReminder) defaultRuntime.error(authReminder);
defaultRuntime.exit(1);
return;
}
const result = await approvePairingWithFallback(opts, resolvedRequestId);
if (!result) {
defaultRuntime.error("unknown requestId");
defaultRuntime.exit(1);
return;
}
if (opts.json) {
defaultRuntime.writeJson(result);
return;
}
const resultRequestId = result?.requestId;
const approvedRequestId = typeof resultRequestId === "string" && resultRequestId.trim().length > 0 ? resultRequestId : resolvedRequestId;
const deviceId = result?.device?.deviceId;
defaultRuntime.log(`${theme.success("Approved")} ${theme.command(deviceId ?? "ok")} ${theme.muted(`(${approvedRequestId})`)}`);
}
async function runDevicesRejectCommand(requestId, opts) {
const result = await callGatewayCli("device.pair.reject", opts, { requestId });
if (opts.json) {
defaultRuntime.writeJson(result);
return;
}
const deviceId = result?.deviceId;
defaultRuntime.log(`${theme.warn("Rejected")} ${theme.command(deviceId ?? "ok")}`);
}
async function runDevicesRotateCommand(opts) {
const required = resolveRequiredDeviceRole(opts);
if (!required) return;
const result = await callGatewayCli("device.token.rotate", opts, {
deviceId: required.deviceId,
role: required.role,
scopes: Array.isArray(opts.scope) ? opts.scope : void 0
});
defaultRuntime.writeJson(result);
}
async function runDevicesRevokeCommand(opts) {
const required = resolveRequiredDeviceRole(opts);
if (!required) return;
const result = await callGatewayCli("device.token.revoke", opts, {
deviceId: required.deviceId,
role: required.role
});
defaultRuntime.writeJson(result);
}
//#endregion
export { runDevicesApproveCommand, runDevicesClearCommand, runDevicesListCommand, runDevicesRejectCommand, runDevicesRemoveCommand, runDevicesRevokeCommand, runDevicesRotateCommand };