From e7391fc2b6edcd46a922b50af13d751a9b716013 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 06:47:36 +0100 Subject: [PATCH] fix(gateway): ignore malformed node catalog capabilities (#79205) --- CHANGELOG.md | 1 + src/gateway/node-catalog.test.ts | 26 ++++++++++++++++++++++++++ src/gateway/node-catalog.ts | 7 +++++-- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a001e0e65b9..9a1db15cd1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -186,6 +186,7 @@ Docs: https://docs.openclaw.ai - Agents/PI: route PI-native OpenAI-compatible default streams through OpenClaw boundary-aware transports so local-compatible model runs keep API-key injection and transport policy. - Gateway/media: require authenticated owner or admin context for managed outgoing image bytes instead of trusting requester-session headers. - Doctor/gateway: avoid duplicate Node runtime warnings when the daemon install plan already selected a supported Node runtime. +- Gateway/nodes: ignore malformed non-string capability entries from live nodes instead of throwing while listing the node catalog. - Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested. - Codex app-server: close stdio stdin before force-killing the managed app-server, matching Codex single-client shutdown behavior and avoiding unsettled CLI exits after successful runs. - CLI/Codex: dispose registered agent harnesses during short-lived CLI shutdown so successful Codex-backed `agent --local` runs do not leave app-server child processes alive. diff --git a/src/gateway/node-catalog.test.ts b/src/gateway/node-catalog.test.ts index 05a1e1a4651..9c5f3d32065 100644 --- a/src/gateway/node-catalog.test.ts +++ b/src/gateway/node-catalog.test.ts @@ -289,4 +289,30 @@ describe("gateway/node-catalog", () => { }), ); }); + + it("ignores malformed node capability entries instead of throwing", () => { + const catalog = createKnownNodeCatalog({ + pairedDevices: [], + pairedNodes: [], + connectedNodes: [ + { + nodeId: "bad-node", + connId: "conn-1", + client: {} as never, + displayName: "Bad Node", + caps: ["camera", undefined], + commands: ["system.run", null], + connectedAtMs: 1, + } as never, + ], + }); + + expect(listKnownNodes(catalog)).toEqual([ + expect.objectContaining({ + nodeId: "bad-node", + caps: ["camera"], + commands: ["system.run"], + }), + ]); + }); }); diff --git a/src/gateway/node-catalog.ts b/src/gateway/node-catalog.ts index ba502bec4b7..4f0d2fcfa78 100644 --- a/src/gateway/node-catalog.ts +++ b/src/gateway/node-catalog.ts @@ -47,13 +47,16 @@ type KnownNodeCatalog = { entriesById: Map; }; -function uniqueSortedStrings(...items: Array): string[] { +function uniqueSortedStrings(...items: Array): string[] { const values = new Set(); for (const item of items) { - if (!item) { + if (!Array.isArray(item)) { continue; } for (const value of item) { + if (typeof value !== "string") { + continue; + } const trimmed = value.trim(); if (trimmed) { values.add(trimmed);