fix(nodes): allow removing stale paired nodes · openclaw/openclaw@7fb2a35
steipete
·
2026-04-27
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
21 | 21 | |
22 | 22 | ### Fixes |
23 | 23 | |
| 24 | +- Nodes/CLI: add `openclaw nodes remove --node <id|name|ip>` and `node.pair.remove` so stale gateway-owned node pairing records can be cleaned without hand-editing state files. Thanks @openclaw. |
24 | 25 | - Local models: default custom providers with only `baseUrl` to the Chat Completions adapter and trust loopback model requests automatically, so local OpenAI-compatible proxies receive `/v1/chat/completions` without timing out. Fixes #40024. Thanks @parachuteshe. |
25 | 26 | - Channels/message tool: surface Discord, Slack, and Mattermost `user:`/`channel:` target syntax in the shared message target schema and Discord ambiguity errors, so DM sends by numeric id stop burning retries before finding `user:<id>`. Fixes #72401. Thanks @garyd9, @hclsys, and @praveen9354. |
26 | 27 | - Agents/tools: scope tool-loop detection history to the active run when available, so scheduled heartbeat cycles no longer inherit stale repeated-call counts from previous runs. Fixes #40144. Thanks @mattbrown319. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -136,7 +136,7 @@ Stored under `~/.openclaw/devices/`:
|
136 | 136 | |
137 | 137 | ### Notes |
138 | 138 | |
139 | | -- The legacy `node.pair.*` API (CLI: `openclaw nodes pending|approve|reject|rename`) is a |
| 139 | +- The legacy `node.pair.*` API (CLI: `openclaw nodes pending|approve|reject|remove|rename`) is a |
140 | 140 | separate gateway-owned pairing store. WS nodes still require device pairing. |
141 | 141 | - The pairing record is the durable source of truth for approved roles. Active |
142 | 142 | device tokens stay bounded to that approved role set; a stray token entry |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -29,6 +29,7 @@ openclaw nodes list --last-connected 24h
|
29 | 29 | openclaw nodes pending |
30 | 30 | openclaw nodes approve <requestId> |
31 | 31 | openclaw nodes reject <requestId> |
| 32 | +openclaw nodes remove --node <id|name|ip> |
32 | 33 | openclaw nodes rename --node <id|name|ip> --name <displayName> |
33 | 34 | openclaw nodes status |
34 | 35 | openclaw nodes status --connected |
@@ -38,6 +39,7 @@ openclaw nodes status --last-connected 24h
|
38 | 39 | `nodes list` prints pending/paired tables. Paired rows include the most recent connect age (Last Connect). |
39 | 40 | Use `--connected` to only show currently-connected nodes. Use `--last-connected <duration>` to |
40 | 41 | filter to nodes that connected within a duration (e.g. `24h`, `7d`). |
| 42 | +Use `nodes remove --node <id|name|ip>` to delete a stale gateway-owned node pairing record. |
41 | 43 | |
42 | 44 | Approval note: |
43 | 45 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -39,6 +39,7 @@ openclaw nodes pending
|
39 | 39 | openclaw nodes approve <requestId> |
40 | 40 | openclaw nodes reject <requestId> |
41 | 41 | openclaw nodes status |
| 42 | +openclaw nodes remove --node <id|name|ip> |
42 | 43 | openclaw nodes rename --node <id|name|ip> --name "Living Room iPad" |
43 | 44 | ``` |
44 | 45 | |
@@ -57,6 +58,7 @@ Methods:
|
57 | 58 | - `node.pair.list` — list pending + paired nodes (`operator.pairing`). |
58 | 59 | - `node.pair.approve` — approve a pending request (issues token). |
59 | 60 | - `node.pair.reject` — reject a pending request. |
| 61 | +- `node.pair.remove` — remove a stale paired node entry. |
60 | 62 | - `node.pair.verify` — verify `{ nodeId, token }`. |
61 | 63 | |
62 | 64 | Notes: |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -369,7 +369,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
369 | 369 | </Accordion> |
370 | 370 | |
371 | 371 | <Accordion title="Node pairing, invoke, and pending work"> |
372 | | -- `node.pair.request`, `node.pair.list`, `node.pair.approve`, `node.pair.reject`, and `node.pair.verify` cover node pairing and bootstrap verification. |
| 372 | +- `node.pair.request`, `node.pair.list`, `node.pair.approve`, `node.pair.reject`, `node.pair.remove`, and `node.pair.verify` cover node pairing and bootstrap verification. |
373 | 373 | - `node.list` and `node.describe` return known/connected node state. |
374 | 374 | - `node.rename` updates a paired node label. |
375 | 375 | - `node.invoke` forwards a command to a connected node. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -49,8 +49,10 @@ Notes:
|
49 | 49 | - The device pairing record is the durable approved-role contract. Token |
50 | 50 | rotation stays inside that contract; it cannot upgrade a paired node into a |
51 | 51 | different role that pairing approval never granted. |
52 | | -- `node.pair.*` (CLI: `openclaw nodes pending/approve/reject/rename`) is a separate gateway-owned |
| 52 | +- `node.pair.*` (CLI: `openclaw nodes pending/approve/reject/remove/rename`) is a separate gateway-owned |
53 | 53 | node pairing store; it does **not** gate the WS `connect` handshake. |
| 54 | +- `openclaw nodes remove --node <id|name|ip>` deletes stale entries from that |
| 55 | + separate gateway-owned node pairing store. |
54 | 56 | - Approval scope follows the pending request's declared commands: |
55 | 57 | - commandless request: `operator.pairing` |
56 | 58 | - non-exec node commands: `operator.pairing` + `operator.write` |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -71,6 +71,30 @@ export function registerNodesPairingCommands(nodes: Command) {
|
71 | 71 | }), |
72 | 72 | ); |
73 | 73 | |
| 74 | +nodesCallOpts( |
| 75 | +nodes |
| 76 | +.command("remove") |
| 77 | +.description("Remove a paired node entry") |
| 78 | +.requiredOption("--node <idOrNameOrIp>", "Node id, name, or IP") |
| 79 | +.action(async (opts: NodesRpcOpts) => { |
| 80 | +await runNodesCommand("remove", async () => { |
| 81 | +const nodeId = await resolveNodeId(opts, normalizeOptionalString(opts.node) ?? ""); |
| 82 | +if (!nodeId) { |
| 83 | +defaultRuntime.error("--node required"); |
| 84 | +defaultRuntime.exit(1); |
| 85 | +return; |
| 86 | +} |
| 87 | +const result = await callGatewayCli("node.pair.remove", opts, { nodeId }); |
| 88 | +if (opts.json) { |
| 89 | +defaultRuntime.writeJson(result); |
| 90 | +return; |
| 91 | +} |
| 92 | +const { warn } = getNodesTheme(); |
| 93 | +defaultRuntime.log(warn(`Removed paired node ${nodeId}`)); |
| 94 | +}); |
| 95 | +}), |
| 96 | +); |
| 97 | + |
74 | 98 | nodesCallOpts( |
75 | 99 | nodes |
76 | 100 | .command("rename") |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -22,6 +22,7 @@ export function registerNodesCli(program: Command) {
|
22 | 22 | `\n${theme.heading("Examples:")}\n${formatHelpExamples([ |
23 | 23 | ["openclaw nodes status", "List known nodes with live status."], |
24 | 24 | ["openclaw nodes pairing pending", "Show pending node pairing requests."], |
| 25 | + ["openclaw nodes remove --node <id|name|ip>", "Remove a stale paired node entry."], |
25 | 26 | [ |
26 | 27 | 'openclaw nodes invoke --node <id> --command system.which --params \'{"name":"uname"}\'', |
27 | 28 | "Invoke a node command directly.", |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -424,6 +424,35 @@ describe("cli program (nodes basics)", () => {
|
424 | 424 | ); |
425 | 425 | }); |
426 | 426 | |
| 427 | +it("runs nodes remove and calls node.pair.remove", async () => { |
| 428 | +callGateway.mockImplementation(async (...args: unknown[]) => { |
| 429 | +const opts = (args[0] ?? {}) as { method?: string }; |
| 430 | +if (opts.method === "node.list") { |
| 431 | +return { |
| 432 | +nodes: [{ nodeId: "ios-node", displayName: "iOS Node", paired: true }], |
| 433 | +}; |
| 434 | +} |
| 435 | +if (opts.method === "node.pair.list") { |
| 436 | +return { |
| 437 | +pending: [], |
| 438 | +paired: [{ nodeId: "ios-node", displayName: "iOS Node" }], |
| 439 | +}; |
| 440 | +} |
| 441 | +if (opts.method === "node.pair.remove") { |
| 442 | +return { nodeId: "ios-node" }; |
| 443 | +} |
| 444 | +return { ok: true }; |
| 445 | +}); |
| 446 | + |
| 447 | +await runProgram(["nodes", "remove", "--node", "iOS Node"]); |
| 448 | +expect(callGateway).toHaveBeenCalledWith( |
| 449 | +expect.objectContaining({ |
| 450 | +method: "node.pair.remove", |
| 451 | +params: { nodeId: "ios-node" }, |
| 452 | +}), |
| 453 | +); |
| 454 | +}); |
| 455 | + |
427 | 456 | it("runs nodes invoke and calls node.invoke", async () => { |
428 | 457 | mockGatewayWithIosNodeListAnd("node.invoke", { |
429 | 458 | ok: true, |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -55,6 +55,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
55 | 55 | "node.pair.request", |
56 | 56 | "node.pair.list", |
57 | 57 | "node.pair.reject", |
| 58 | +"node.pair.remove", |
58 | 59 | "node.pair.verify", |
59 | 60 | "node.pair.approve", |
60 | 61 | "device.pair.list", |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。