From 8672737f81b949d6beecf47973b8c631f255de50 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 04:04:27 +0100 Subject: [PATCH] fix: drop overlong slack command values --- CHANGELOG.md | 1 + extensions/slack/src/monitor/slash.test.ts | 40 ++++++++++++++++++++++ extensions/slack/src/monitor/slash.ts | 34 ++++++++++-------- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5018c98b63..20bd245f249 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Docs: https://docs.openclaw.ai - Slack/commands: truncate native command argument-menu confirmation text to Slack's dialog limit, so long plugin arg names no longer make fallback buttons render invalid Block Kit payloads. Thanks @slackapi. - Slack/exec approvals: cap native approval metadata context to Slack's element and text limits, so large approval details no longer make Slack reject the approval card. Thanks @slackapi. - Slack/commands: cap native command argument-menu fallback rows to Slack's message block limit, so large plugin choice lists no longer make Slack reject the generated menu. Thanks @slackapi. +- Slack/commands: drop fallback command argument buttons whose encoded values exceed Slack's button-value limit, so one oversized plugin choice no longer makes Slack reject the whole menu. Thanks @slackapi. - Channels/WhatsApp: require Baileys outbound message ids before marking auto-replies delivered, so transcript text and ack reactions no longer make failed group replies look sent. Fixes #49225. Thanks @TinyTb. - CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash. - Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme. diff --git a/extensions/slack/src/monitor/slash.test.ts b/extensions/slack/src/monitor/slash.test.ts index 0bbb5e7a594..846510f1d48 100644 --- a/extensions/slack/src/monitor/slash.test.ts +++ b/extensions/slack/src/monitor/slash.test.ts @@ -9,6 +9,7 @@ vi.mock("./slash-commands.runtime.js", () => { const reportLongCommand = { key: "reportlong", nativeName: "reportlong" }; const reportLongButtonCommand = { key: "reportlongbutton", nativeName: "reportlongbutton" }; const reportHugeButtonCommand = { key: "reporthugebutton", nativeName: "reporthugebutton" }; + const reportHugeValueCommand = { key: "reporthugevalue", nativeName: "reporthugevalue" }; const unsafeConfirmCommand = { key: "unsafeconfirm", nativeName: "unsafeconfirm" }; const longConfirmCommand = { key: "longconfirm", nativeName: "longconfirm" }; const statusAliasCommand = { key: "status", nativeName: "status" }; @@ -80,6 +81,9 @@ vi.mock("./slash-commands.runtime.js", () => { if (normalized === "reporthugebutton") { return reportHugeButtonCommand; } + if (normalized === "reporthugevalue") { + return reportHugeValueCommand; + } if (normalized === "unsafeconfirm") { return unsafeConfirmCommand; } @@ -134,6 +138,12 @@ vi.mock("./slash-commands.runtime.js", () => { acceptsArgs: true, args: [], }, + { + name: "reporthugevalue", + description: "ReportHugeValue", + acceptsArgs: true, + args: [], + }, { name: "unsafeconfirm", description: "UnsafeConfirm", @@ -187,6 +197,12 @@ vi.mock("./slash-commands.runtime.js", () => { })), ); } + if (params.command?.key === "reporthugevalue") { + return resolvePeriodMenu(params, [ + { value: "valid", label: "Valid" }, + { value: "x".repeat(2500), label: "Overlong" }, + ]); + } if (params.command?.key === "reportcompact") { return resolvePeriodMenu(params, baseReportPeriodChoices); } @@ -466,6 +482,7 @@ describe("Slack native command argument menus", () => { let reportLongHandler: (args: unknown) => Promise; let reportLongButtonHandler: (args: unknown) => Promise; let reportHugeButtonHandler: (args: unknown) => Promise; + let reportHugeValueHandler: (args: unknown) => Promise; let unsafeConfirmHandler: (args: unknown) => Promise; let longConfirmHandler: (args: unknown) => Promise; let agentStatusHandler: (args: unknown) => Promise; @@ -490,6 +507,11 @@ describe("Slack native command argument menus", () => { "/reporthugebutton", "/reporthugebutton", ); + reportHugeValueHandler = requireHandler( + harness.commands, + "/reporthugevalue", + "/reporthugevalue", + ); unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm"); longConfirmHandler = requireHandler(harness.commands, "/longconfirm", "/longconfirm"); agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus"); @@ -638,6 +660,24 @@ describe("Slack native command argument menus", () => { expect(actionBlocks.at(-1)?.elements).toHaveLength(5); }); + it("drops fallback buttons whose encoded values exceed Slack's button value limit", async () => { + const { respond } = await runCommandHandler(reportHugeValueHandler); + expect(respond).toHaveBeenCalledTimes(1); + const payload = respond.mock.calls[0]?.[0] as { + blocks?: Array<{ + type: string; + elements?: Array<{ text?: { text?: string }; value?: string }>; + }>; + }; + const actionBlocks = (payload.blocks ?? []).filter((block) => block.type === "actions"); + expect(actionBlocks).toHaveLength(1); + expect(actionBlocks[0]?.elements).toHaveLength(1); + expect(actionBlocks[0]?.elements?.[0]).toMatchObject({ + text: { text: "Valid" }, + }); + expect(actionBlocks[0]?.elements?.[0]?.value?.length).toBeLessThanOrEqual(2000); + }); + it("shows an overflow menu when choices fit compact range", async () => { const element = await getFirstActionElementFromCommand(reportCompactHandler); expect(element?.type).toBe("overflow"); diff --git a/extensions/slack/src/monitor/slash.ts b/extensions/slack/src/monitor/slash.ts index 9e002b38206..21d9ae777de 100644 --- a/extensions/slack/src/monitor/slash.ts +++ b/extensions/slack/src/monitor/slash.ts @@ -56,6 +56,7 @@ const SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX = 100; const SLACK_COMMAND_ARG_SELECT_OPTION_TEXT_MAX = 75; const SLACK_COMMAND_ARG_SELECT_OPTION_VALUE_MAX = 75; const SLACK_COMMAND_ARG_BUTTON_TEXT_MAX = 75; +const SLACK_COMMAND_ARG_BUTTON_VALUE_MAX = 2000; const SLACK_COMMAND_ARG_CONFIRM_TEXT_MAX = 300; const SLACK_HEADER_TEXT_MAX = 150; const SLACK_COMMAND_ARG_CHROME_BLOCKS = 3; @@ -299,21 +300,24 @@ function buildSlackCommandArgMenuBlocks(params: { }, ] : encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect - ? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map( - (choices, rowIndex) => ({ - type: "actions", - elements: choices.map((choice, colIndex) => ({ - type: "button", - action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`, - text: { - type: "plain_text", - text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_BUTTON_TEXT_MAX), - }, - value: choice.value, - confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), - })), - }), - ) + ? chunkItems( + encodedChoices.filter( + (choice) => choice.value.length <= SLACK_COMMAND_ARG_BUTTON_VALUE_MAX, + ), + SLACK_COMMAND_ARG_BUTTON_ROW_SIZE, + ).map((choices, rowIndex) => ({ + type: "actions", + elements: choices.map((choice, colIndex) => ({ + type: "button", + action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`, + text: { + type: "plain_text", + text: truncateSlackText(choice.label, SLACK_COMMAND_ARG_BUTTON_TEXT_MAX), + }, + value: choice.value, + confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }), + })), + })) : chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map( (choices, index) => ({ type: "actions",