Files
openclaw/src/gateway/node-connect-reconcile.ts
Agustin Rivera a383878e97 Require re-pairing for node reconnect command upgrades (#62658)
* fix(node): require re-pairing for reconnect command upgrades

Co-authored-by: zsx <git@zsxsoft.com>

* fix(node): tighten reconnect pairing test polling

* docs(changelog): add node reconnect pairing entry

---------

Co-authored-by: zsx <git@zsxsoft.com>
Co-authored-by: Devin Robison <drobison@nvidia.com>
2026-04-07 12:48:18 -06:00

116 lines
3.3 KiB
TypeScript

import type { OpenClawConfig } from "../config/config.js";
import type {
NodePairingPairedNode,
NodePairingPendingRequest,
NodePairingRequestInput,
} from "../infra/node-pairing.js";
import {
normalizeDeclaredNodeCommands,
resolveNodeCommandAllowlist,
} from "./node-command-policy.js";
import type { ConnectParams } from "./protocol/index.js";
type PendingNodePairingResult = {
status: "pending";
request: NodePairingPendingRequest;
created: boolean;
};
export type NodeConnectPairingReconcileResult = {
nodeId: string;
effectiveCommands: string[];
pendingPairing?: PendingNodePairingResult;
};
function resolveApprovedReconnectCommands(params: {
pairedCommands: readonly string[] | undefined;
allowlist: Set<string>;
}) {
return normalizeDeclaredNodeCommands({
declaredCommands: Array.isArray(params.pairedCommands) ? params.pairedCommands : [],
allowlist: params.allowlist,
});
}
function buildNodePairingRequestInput(params: {
nodeId: string;
connectParams: ConnectParams;
commands: string[];
remoteIp?: string;
}): NodePairingRequestInput {
return {
nodeId: params.nodeId,
displayName: params.connectParams.client.displayName,
platform: params.connectParams.client.platform,
version: params.connectParams.client.version,
deviceFamily: params.connectParams.client.deviceFamily,
modelIdentifier: params.connectParams.client.modelIdentifier,
caps: params.connectParams.caps,
commands: params.commands,
remoteIp: params.remoteIp,
};
}
export async function reconcileNodePairingOnConnect(params: {
cfg: OpenClawConfig;
connectParams: ConnectParams;
pairedNode: NodePairingPairedNode | null;
reportedClientIp?: string;
requestPairing: (input: NodePairingRequestInput) => Promise<PendingNodePairingResult>;
}): Promise<NodeConnectPairingReconcileResult> {
const nodeId = params.connectParams.device?.id ?? params.connectParams.client.id;
const allowlist = resolveNodeCommandAllowlist(params.cfg, {
platform: params.connectParams.client.platform,
deviceFamily: params.connectParams.client.deviceFamily,
});
const declared = normalizeDeclaredNodeCommands({
declaredCommands: Array.isArray(params.connectParams.commands)
? params.connectParams.commands
: [],
allowlist,
});
if (!params.pairedNode) {
const pendingPairing = await params.requestPairing(
buildNodePairingRequestInput({
nodeId,
connectParams: params.connectParams,
commands: declared,
remoteIp: params.reportedClientIp,
}),
);
return {
nodeId,
effectiveCommands: declared,
pendingPairing,
};
}
const approvedCommands = resolveApprovedReconnectCommands({
pairedCommands: params.pairedNode.commands,
allowlist,
});
const hasCommandUpgrade = declared.some((command) => !approvedCommands.includes(command));
if (hasCommandUpgrade) {
const pendingPairing = await params.requestPairing(
buildNodePairingRequestInput({
nodeId,
connectParams: params.connectParams,
commands: declared,
remoteIp: params.reportedClientIp,
}),
);
return {
nodeId,
effectiveCommands: approvedCommands,
pendingPairing,
};
}
return {
nodeId,
effectiveCommands: declared,
};
}