mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
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>
This commit is contained in:
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/Ollama: honor the selected provider's `baseUrl` during streaming so multi-Ollama setups stop routing every stream to the first configured Ollama endpoint. (#61678)
|
||||
- Browser/remote CDP: retry the DevTools websocket once after remote browser restarts so healthy remote browser profiles do not fail availability checks during CDP warm-up. (#57397) Thanks @ThanhNguyxn07.
|
||||
- Browser/SSRF: treat main-frame `document` redirect hops as navigations even when Playwright does not flag them as `isNavigationRequest()`, so strict private-network blocking still stops forbidden redirect pivots before the browser reaches the internal target. (#62355) Thanks @pgondhi987.
|
||||
- Gateway/node pairing: require a fresh pairing request when a previously paired node reconnects with additional declared commands, and keep the live session pinned to the earlier approved command set until the upgrade is approved. (#62658) Thanks @eleqtrizit.
|
||||
- Gateway/auth: invalidate existing shared-token and password WebSocket sessions when the configured secret rotates, so stale authenticated sockets cannot stay attached after token or password changes. (#62350) Thanks @pgondhi987.
|
||||
- Gateway/status and containers: auto-bind to `0.0.0.0` inside Docker and Podman environments, and probe local TLS gateways over `wss://` with self-signed fingerprint forwarding so container startup and loopback TLS status checks work again. (#61818, #61935) Thanks @openperf and @ThanhNguyxn07.
|
||||
- macOS/gateway version: strip trailing commit metadata from CLI version output before semver parsing so the Mac app recognizes installed gateway versions like `OpenClaw 2026.4.2 (d74a122)` again. (#61111) Thanks @oliviareid-svg.
|
||||
|
||||
@@ -22,6 +22,16 @@ export type NodeConnectPairingReconcileResult = {
|
||||
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;
|
||||
@@ -76,6 +86,28 @@ export async function reconcileNodePairingOnConnect(params: {
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -176,7 +176,7 @@ describe("gateway node pairing authorization", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("does not pin connected node commands to the approved pairing record", async () => {
|
||||
test("requests re-pairing when a paired node reconnects with upgraded commands", async () => {
|
||||
const started = await startServerWithClient("secret");
|
||||
const pairedNode = await pairDeviceIdentity({
|
||||
name: "node-command-pin",
|
||||
@@ -226,8 +226,7 @@ describe("gateway node pairing authorization", () => {
|
||||
(entry) => entry.nodeId === pairedNode.identity.deviceId && entry.connected,
|
||||
);
|
||||
if (
|
||||
JSON.stringify(node?.commands?.toSorted() ?? []) ===
|
||||
JSON.stringify(["canvas.snapshot", "system.run"])
|
||||
JSON.stringify(node?.commands?.toSorted() ?? []) === JSON.stringify(["canvas.snapshot"])
|
||||
) {
|
||||
break;
|
||||
}
|
||||
@@ -238,7 +237,18 @@ describe("gateway node pairing authorization", () => {
|
||||
.find((entry) => entry.nodeId === pairedNode.identity.deviceId && entry.connected)
|
||||
?.commands?.toSorted(),
|
||||
JSON.stringify(lastNodes),
|
||||
).toEqual(["canvas.snapshot", "system.run"]);
|
||||
).toEqual(["canvas.snapshot"]);
|
||||
|
||||
await expect(listNodePairing()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
pending: [
|
||||
expect.objectContaining({
|
||||
nodeId: pairedNode.identity.deviceId,
|
||||
commands: ["canvas.snapshot", "system.run"],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
controlWs?.close();
|
||||
await firstClient?.stopAndWait();
|
||||
@@ -249,7 +259,7 @@ describe("gateway node pairing authorization", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("does not request repair pairing when a paired node reconnects with more commands", async () => {
|
||||
test("requests re-pairing when a commandless paired node reconnects with system.run", async () => {
|
||||
const started = await startServerWithClient("secret");
|
||||
const pairedNode = await pairDeviceIdentity({
|
||||
name: "node-command-empty",
|
||||
@@ -289,10 +299,7 @@ describe("gateway node pairing authorization", () => {
|
||||
const node = lastNodes.find(
|
||||
(entry) => entry.nodeId === pairedNode.identity.deviceId && entry.connected,
|
||||
);
|
||||
if (
|
||||
JSON.stringify(node?.commands?.toSorted() ?? []) ===
|
||||
JSON.stringify(["canvas.snapshot", "system.run"])
|
||||
) {
|
||||
if (JSON.stringify(node?.commands?.toSorted() ?? []) === JSON.stringify([])) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
@@ -300,14 +307,16 @@ describe("gateway node pairing authorization", () => {
|
||||
const repairedNode = lastNodes.find(
|
||||
(entry) => entry.nodeId === pairedNode.identity.deviceId && entry.connected,
|
||||
);
|
||||
expect(repairedNode?.commands?.toSorted(), JSON.stringify(lastNodes)).toEqual([
|
||||
"canvas.snapshot",
|
||||
"system.run",
|
||||
]);
|
||||
expect(repairedNode?.commands?.toSorted(), JSON.stringify(lastNodes)).toEqual([]);
|
||||
|
||||
await expect(listNodePairing()).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
pending: [],
|
||||
pending: [
|
||||
expect.objectContaining({
|
||||
nodeId: pairedNode.identity.deviceId,
|
||||
commands: ["canvas.snapshot", "system.run"],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user