From c869ca4bbf08730eec0520b0eef2d99210d424b6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 03:07:30 +0000 Subject: [PATCH] fix: harden discord agent cid parsing (#29013) (thanks @Jacky1n7) --- CHANGELOG.md | 1 + src/discord/monitor/agent-components.ts | 28 ++++++++---------- src/discord/monitor/monitor.test.ts | 38 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2e1a1b415..ac9f554b9ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Discord/Agent component interactions: accept Components v2 `cid` payloads alongside legacy `componentId`, and safely decode percent-encoded IDs without throwing on malformed `%` sequences. Landed from contributor PR #29013 by @Jacky1n7. Thanks @Jacky1n7. - Discord/Inbound media fallback: preserve attachment and sticker metadata when Discord CDN fetch/save fails by keeping URL-based media entries in context, with regression coverage for save failures and mixed success/failure ordering. Landed from contributor PR #28906 by @Sid-Qin. Thanks @Sid-Qin. - Docs/Docker images: clarify the official GHCR image source and tag guidance (`main`, `latest`, ``), and document that `OPENCLAW_IMAGE` skips local image builds but still uses the repo-local compose/setup flow. (#27214, #31180) Fixes #15655. Thanks @ipl31. - Agents/Model fallback: classify additional network transport errors (`ECONNREFUSED`, `ENETUNREACH`, `EHOSTUNREACH`, `ENETRESET`, `EAI_AGAIN`) as failover-worthy so fallback chains advance when primary providers are unreachable. Landed from contributor PR #19077 by @ayanesakura. Thanks @ayanesakura. diff --git a/src/discord/monitor/agent-components.ts b/src/discord/monitor/agent-components.ts index ca4029631f9..d33603697e9 100644 --- a/src/discord/monitor/agent-components.ts +++ b/src/discord/monitor/agent-components.ts @@ -406,22 +406,21 @@ export function buildAgentSelectCustomId(componentId: string): string { /** * Parse agent component data from Carbon's parsed ComponentData - * Carbon parses "key:componentId=xxx" into { componentId: "xxx" } + * Supports both legacy { componentId } and Components v2 { cid } payloads. */ +function readParsedComponentId(data: ComponentData): unknown { + if (!data || typeof data !== "object") { + return undefined; + } + return "cid" in data + ? (data as Record).cid + : (data as Record).componentId; +} + function parseAgentComponentData(data: ComponentData): { componentId: string; } | null { - if (!data || typeof data !== "object") { - return null; - } - - // Carbon parses "key:componentId=xxx" into { componentId: "xxx" } - // Components v2 / other builders may use { cid: "xxx" } (e.g. occomp:cid=xxx). - const raw = - ("cid" in data - ? (data as Record).cid - : (data as Record).componentId) ?? - (data as Record).componentId; + const raw = readParsedComponentId(data); const decodeSafe = (value: string): string => { // `cid` values may be raw (not URI-encoded). Guard against malformed % sequences. @@ -601,10 +600,7 @@ function parseDiscordComponentData( if (!data || typeof data !== "object") { return null; } - const rawComponentId = - "cid" in data - ? (data as { cid?: unknown }).cid - : (data as { componentId?: unknown }).componentId; + const rawComponentId = readParsedComponentId(data); const rawModalId = "mid" in data ? (data as { mid?: unknown }).mid : (data as { modalId?: unknown }).modalId; let componentId = normalizeComponentId(rawComponentId); diff --git a/src/discord/monitor/monitor.test.ts b/src/discord/monitor/monitor.test.ts index afa9bbd93a7..fc6899c96de 100644 --- a/src/discord/monitor/monitor.test.ts +++ b/src/discord/monitor/monitor.test.ts @@ -182,6 +182,44 @@ describe("agent components", () => { expect(reply).toHaveBeenCalledWith({ content: "✓" }); expect(enqueueSystemEventMock).toHaveBeenCalled(); }); + + it("accepts cid payloads for agent button interactions", async () => { + const button = createAgentComponentButton({ + cfg: createCfg(), + accountId: "default", + dmPolicy: "allowlist", + allowFrom: ["123456789"], + }); + const { interaction, defer, reply } = createDmButtonInteraction(); + + await button.run(interaction, { cid: "hello_cid" } as ComponentData); + + expect(defer).toHaveBeenCalledWith({ ephemeral: true }); + expect(reply).toHaveBeenCalledWith({ content: "✓" }); + expect(enqueueSystemEventMock).toHaveBeenCalledWith( + expect.stringContaining("hello_cid"), + expect.any(Object), + ); + }); + + it("keeps malformed percent cid values without throwing", async () => { + const button = createAgentComponentButton({ + cfg: createCfg(), + accountId: "default", + dmPolicy: "allowlist", + allowFrom: ["123456789"], + }); + const { interaction, defer, reply } = createDmButtonInteraction(); + + await button.run(interaction, { cid: "hello%2G" } as ComponentData); + + expect(defer).toHaveBeenCalledWith({ ephemeral: true }); + expect(reply).toHaveBeenCalledWith({ content: "✓" }); + expect(enqueueSystemEventMock).toHaveBeenCalledWith( + expect.stringContaining("hello%2G"), + expect.any(Object), + ); + }); }); describe("discord component interactions", () => {