Download src/cli/nodes-cli/register.pairing.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 8.87 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/nodes-cli/register.pairing.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/cli/nodes-cli/register.pairing.ts
-
curl -L -o register.pairing.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/cli/nodes-cli/register.pairing.ts
8.87 kB
| // Node pairing commands: list, approve, reject, remove, and rename paired nodes. | |
| import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; | |
| import type { Command } from "commander"; | |
| import { getTerminalTableWidth } from "../../../packages/terminal-core/src/table.js"; | |
| import type { OperatorScope } from "../../gateway/method-scopes.js"; | |
| import { resolveNodePairApprovalScopes } from "../../infra/node-pairing-authz.js"; | |
| import { defaultRuntime } from "../../runtime.js"; | |
| import { formatCliCommand } from "../command-format.js"; | |
| import { formatConnectionFlagReminder, getNodesTheme, runNodesCommand } from "./cli-utils.js"; | |
| import { parsePairingList } from "./format.js"; | |
| import { renderPendingPairingRequestsTable } from "./pairing-render.js"; | |
| import { | |
| callNodesGatewayCli, | |
| callNodePairApprovalGatewayCli, | |
| nodesCallOpts, | |
| resolveCliNodeId, | |
| } from "./rpc.js"; | |
| import type { NodesRpcOpts, PendingRequest } from "./types.js"; | |
| const DEFAULT_NODE_PAIR_APPROVE_SCOPES: OperatorScope[] = ["operator.pairing"]; | |
| const NODE_PAIR_APPROVE_SCOPE_SET = new Set<OperatorScope>([ | |
| "operator.pairing", | |
| "operator.write", | |
| "operator.admin", | |
| ]); | |
| function normalizeNodePairApproveScopes(scopes: unknown): OperatorScope[] { | |
| const normalized = new Set<OperatorScope>(DEFAULT_NODE_PAIR_APPROVE_SCOPES); | |
| if (!Array.isArray(scopes)) { | |
| return [...normalized]; | |
| } | |
| for (const scope of scopes) { | |
| if (typeof scope !== "string") { | |
| continue; | |
| } | |
| if (!NODE_PAIR_APPROVE_SCOPE_SET.has(scope as OperatorScope)) { | |
| continue; | |
| } | |
| normalized.add(scope as OperatorScope); | |
| } | |
| return [...normalized]; | |
| } | |
| async function resolveApproveScopesForRequest( | |
| opts: NodesRpcOpts, | |
| requestId: string, | |
| ): Promise<{ scopes: OperatorScope[] }> { | |
| let pending: PendingRequest[]; | |
| try { | |
| const result = await callNodePairApprovalGatewayCli( | |
| "node.pair.list", | |
| opts, | |
| {}, | |
| { scopes: DEFAULT_NODE_PAIR_APPROVE_SCOPES }, | |
| ); | |
| pending = parsePairingList(result).pending; | |
| } catch { | |
| return { scopes: [...DEFAULT_NODE_PAIR_APPROVE_SCOPES] }; | |
| } | |
| const pendingRequestIds = pending | |
| .map((request) => request.requestId) | |
| .filter((id): id is string => typeof id === "string" && id.length > 0); | |
| const request = pending.find((candidate) => candidate.requestId === requestId); | |
| if (!request) { | |
| throw new Error(buildUnknownNodePairRequestIdMessage(requestId, opts, pendingRequestIds)); | |
| } | |
| const declaredScopes = normalizeNodePairApproveScopes(request.requiredApproveScopes); | |
| if (declaredScopes.length > DEFAULT_NODE_PAIR_APPROVE_SCOPES.length) { | |
| return { scopes: declaredScopes }; | |
| } | |
| // Older pending requests only list requested commands; derive approval scopes from them. | |
| return { | |
| scopes: resolveNodePairApprovalScopes(request.commands) as OperatorScope[], | |
| }; | |
| } | |
| function isUnknownNodePairRequestIdError( | |
| error: unknown, | |
| ): error is Error & { gatewayCode: "INVALID_REQUEST" } { | |
| const requestError = error as (Error & { gatewayCode?: unknown }) | undefined; | |
| return ( | |
| requestError instanceof Error && | |
| requestError.name === "GatewayClientRequestError" && | |
| requestError.gatewayCode === "INVALID_REQUEST" && | |
| requestError.message === "unknown requestId" | |
| ); | |
| } | |
| function buildUnknownNodePairRequestIdMessage( | |
| requestId: string, | |
| opts: NodesRpcOpts, | |
| pendingRequestIds?: string[], | |
| ): string { | |
| const lines = [`Unknown node pairing requestId: ${requestId}`]; | |
| if (pendingRequestIds !== undefined) { | |
| if (pendingRequestIds.length > 0) { | |
| lines.push(`Pending requestIds: ${pendingRequestIds.join(", ")}`); | |
| } else { | |
| lines.push("No pending node pairing requests are currently visible."); | |
| } | |
| } | |
| lines.push(`Run ${formatCliCommand("openclaw nodes pending")} to inspect current requests.`); | |
| const connectionReminder = formatConnectionFlagReminder(opts); | |
| if (connectionReminder) { | |
| lines.push(connectionReminder); | |
| } | |
| return lines.join("\n"); | |
| } | |
| function rethrowUnknownNodePairRequestId( | |
| error: unknown, | |
| requestId: string, | |
| opts: NodesRpcOpts, | |
| ): never { | |
| if (!isUnknownNodePairRequestIdError(error)) { | |
| throw error; | |
| } | |
| // Reuse the gateway error so generic formatting does not append its raw cause. | |
| error.name = "Error"; | |
| error.message = buildUnknownNodePairRequestIdMessage(requestId, opts); | |
| throw error; | |
| } | |
| /** Register node pairing management commands. */ | |
| export function registerNodesPairingCommands(nodes: Command) { | |
| nodesCallOpts( | |
| nodes | |
| .command("pending") | |
| .description("List pending pairing requests") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("pending", async () => { | |
| const result = await callNodesGatewayCli("node.pair.list", opts, {}); | |
| const { pending } = parsePairingList(result); | |
| if (opts.json) { | |
| defaultRuntime.writeJson(pending); | |
| return; | |
| } | |
| if (pending.length === 0) { | |
| const { muted } = getNodesTheme(); | |
| defaultRuntime.log(muted("No pending pairing requests.")); | |
| return; | |
| } | |
| const { heading, warn, muted } = getNodesTheme(); | |
| const tableWidth = getTerminalTableWidth(); | |
| const now = Date.now(); | |
| const rendered = renderPendingPairingRequestsTable({ | |
| pending, | |
| now, | |
| tableWidth, | |
| theme: { heading, warn, muted }, | |
| }); | |
| defaultRuntime.log(rendered.heading); | |
| defaultRuntime.log(rendered.table); | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("approve") | |
| .description("Approve a pending pairing request") | |
| .argument("<requestId>", "Pending request id") | |
| .action(async (requestId: string, opts: NodesRpcOpts) => { | |
| await runNodesCommand("approve", async () => { | |
| const { scopes } = await resolveApproveScopesForRequest(opts, requestId); | |
| let result: unknown; | |
| try { | |
| result = await callNodePairApprovalGatewayCli( | |
| "node.pair.approve", | |
| opts, | |
| { | |
| requestId, | |
| }, | |
| { | |
| scopes, | |
| }, | |
| ); | |
| } catch (error) { | |
| rethrowUnknownNodePairRequestId(error, requestId, opts); | |
| } | |
| defaultRuntime.writeJson(result); | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("reject") | |
| .description("Reject a pending pairing request") | |
| .argument("<requestId>", "Pending request id") | |
| .action(async (requestId: string, opts: NodesRpcOpts) => { | |
| await runNodesCommand("reject", async () => { | |
| let result: unknown; | |
| try { | |
| result = await callNodesGatewayCli("node.pair.reject", opts, { | |
| requestId, | |
| }); | |
| } catch (error) { | |
| rethrowUnknownNodePairRequestId(error, requestId, opts); | |
| } | |
| defaultRuntime.writeJson(result); | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("remove") | |
| .description("Remove a paired node entry") | |
| .requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("remove", async () => { | |
| const nodeId = await resolveCliNodeId(opts, normalizeOptionalString(opts.node) ?? ""); | |
| const result = await callNodesGatewayCli("node.pair.remove", opts, { nodeId }); | |
| if (opts.json) { | |
| defaultRuntime.writeJson(result); | |
| return; | |
| } | |
| const { warn } = getNodesTheme(); | |
| defaultRuntime.log(warn(`Removed paired node ${nodeId}`)); | |
| }); | |
| }), | |
| ); | |
| nodesCallOpts( | |
| nodes | |
| .command("rename") | |
| .description("Rename a paired node (display name override)") | |
| .requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP") | |
| .requiredOption("--name <displayName>", "New display name") | |
| .action(async (opts: NodesRpcOpts) => { | |
| await runNodesCommand("rename", async () => { | |
| const name = normalizeOptionalString(opts.name) ?? ""; | |
| if (!name) { | |
| throw new Error( | |
| `--name must not be empty. Run ${formatCliCommand("openclaw nodes list")} to see paired nodes, then rerun with --name <displayName>.`, | |
| ); | |
| } | |
| const nodeId = await resolveCliNodeId(opts, normalizeOptionalString(opts.node) ?? ""); | |
| const result = await callNodesGatewayCli("node.rename", opts, { | |
| nodeId, | |
| displayName: name, | |
| }); | |
| if (opts.json) { | |
| defaultRuntime.writeJson(result); | |
| return; | |
| } | |
| const { ok } = getNodesTheme(); | |
| defaultRuntime.log(ok(`node rename ok: ${nodeId} -> ${name}`)); | |
| }); | |
| }), | |
| ); | |
| } | |