From c70adb8528f8feef896f40f8d5abbf6b5b6915c1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 17:40:38 +0800 Subject: [PATCH] fix(plugins): wrap malformed node proxy payloads --- CHANGELOG.md | 1 + .../codex/src/node-cli-sessions.test.ts | 31 ++++++++++++++- extensions/codex/src/node-cli-sessions.ts | 8 +++- .../transports/chrome-browser-proxy.test.ts | 39 +++++++++++++++++++ .../src/transports/chrome-browser-proxy.ts | 8 +++- 5 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 extensions/google-meet/src/transports/chrome-browser-proxy.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b09bf0f710a..45aff5f5d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - ClickClack: skip malformed realtime websocket frames instead of stopping the channel monitor on a single bad JSON event. - Browser tool: treat malformed node proxy `payloadJSON` responses as browser proxy failures instead of leaking raw JSON parser errors. - Gateway HTTP: match models, session kill, and session history route paths without trusting malformed Host headers, avoiding pre-auth 500s on those endpoints. +- Google Meet/Codex: report malformed node proxy `payloadJSON` responses with plugin-owned errors instead of leaking raw JSON parser failures. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/extensions/codex/src/node-cli-sessions.test.ts b/extensions/codex/src/node-cli-sessions.test.ts index afc710f9ef3..5406518db0b 100644 --- a/extensions/codex/src/node-cli-sessions.test.ts +++ b/extensions/codex/src/node-cli-sessions.test.ts @@ -2,10 +2,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import process from "node:process"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { CODEX_CLI_SESSIONS_LIST_COMMAND, createCodexCliSessionNodeHostCommands, + listCodexCliSessionsOnNode, resolveCodexCliResumeSpawnInvocation, } from "./node-cli-sessions.js"; @@ -148,4 +150,31 @@ describe("codex cli node sessions", () => { windowsHide: true, }); }); + + it("reports malformed node session payloadJSON with an owned error", async () => { + const runtime = { + nodes: { + list: vi.fn(async () => ({ + nodes: [ + { + nodeId: "node-1", + connected: true, + commands: [CODEX_CLI_SESSIONS_LIST_COMMAND], + }, + ], + })), + invoke: vi.fn(async () => ({ + ok: true, + payloadJSON: "{not json", + })), + }, + } as unknown as PluginRuntime; + + await expect( + listCodexCliSessionsOnNode({ + runtime, + requestedNode: "node-1", + }), + ).rejects.toThrow("Codex CLI node command returned malformed payloadJSON."); + }); }); diff --git a/extensions/codex/src/node-cli-sessions.ts b/extensions/codex/src/node-cli-sessions.ts index 957bf0498ea..901e4df4e13 100644 --- a/extensions/codex/src/node-cli-sessions.ts +++ b/extensions/codex/src/node-cli-sessions.ts @@ -624,7 +624,13 @@ function parseCodexCliSessionsListResult(raw: unknown): CodexCliSessionsListResu function unwrapNodeInvokePayload(raw: unknown): unknown { const record = isRecord(raw) ? raw : {}; if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) { - return JSON.parse(record.payloadJSON) as unknown; + try { + return JSON.parse(record.payloadJSON) as unknown; + } catch (error) { + throw new Error("Codex CLI node command returned malformed payloadJSON.", { + cause: error, + }); + } } if ("payload" in record) { return record.payload; diff --git a/extensions/google-meet/src/transports/chrome-browser-proxy.test.ts b/extensions/google-meet/src/transports/chrome-browser-proxy.test.ts new file mode 100644 index 00000000000..b334e4dd7bf --- /dev/null +++ b/extensions/google-meet/src/transports/chrome-browser-proxy.test.ts @@ -0,0 +1,39 @@ +import type { PluginRuntime } from "openclaw/plugin-sdk/plugin-runtime"; +import { describe, expect, it, vi } from "vitest"; +import { callBrowserProxyOnNode } from "./chrome-browser-proxy.js"; + +describe("Google Meet Chrome browser proxy", () => { + it("reports malformed node proxy payloadJSON with an owned error", async () => { + const invoke = vi.fn(async () => ({ + ok: true, + payloadJSON: "{not json", + })); + const runtime = { + nodes: { + invoke, + }, + } as unknown as PluginRuntime; + + await expect( + callBrowserProxyOnNode({ + runtime, + nodeId: "node-1", + method: "GET", + path: "/tabs", + timeoutMs: 100, + }), + ).rejects.toThrow("Google Meet browser proxy returned malformed payloadJSON."); + + expect(invoke).toHaveBeenCalledWith({ + nodeId: "node-1", + command: "browser.proxy", + params: { + method: "GET", + path: "/tabs", + body: undefined, + timeoutMs: 100, + }, + timeoutMs: 5_100, + }); + }); +}); diff --git a/extensions/google-meet/src/transports/chrome-browser-proxy.ts b/extensions/google-meet/src/transports/chrome-browser-proxy.ts index afdd5b67f5e..81110793ff3 100644 --- a/extensions/google-meet/src/transports/chrome-browser-proxy.ts +++ b/extensions/google-meet/src/transports/chrome-browser-proxy.ts @@ -148,7 +148,13 @@ export async function resolveChromeNode(params: { function unwrapNodeInvokePayload(raw: unknown): unknown { const record = raw && typeof raw === "object" ? (raw as Record) : {}; if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) { - return JSON.parse(record.payloadJSON); + try { + return JSON.parse(record.payloadJSON); + } catch (error) { + throw new Error("Google Meet browser proxy returned malformed payloadJSON.", { + cause: error, + }); + } } if ("payload" in record) { return record.payload;