diff --git a/CHANGELOG.md b/CHANGELOG.md index e6825b6d352..95b47d6fb49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai - Slack/interactive replies: keep rendered buttons and selects within Slack Block Kit value and count limits, and align command argument select values with Slack's option limit, so overlong agent-authored choices no longer make Slack reject the whole block payload. Thanks @slackapi. - Slack/interactive replies: drop overlong Block Kit button URLs while preserving valid callback values, so malformed link buttons no longer make Slack reject the whole interactive reply. Thanks @slackapi. - 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. - 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/approval-handler.runtime.test.ts b/extensions/slack/src/approval-handler.runtime.test.ts index 13c89664509..5e53fa1b3de 100644 --- a/extensions/slack/src/approval-handler.runtime.test.ts +++ b/extensions/slack/src/approval-handler.runtime.test.ts @@ -113,4 +113,52 @@ describe("slackApprovalNativeRuntime", () => { (payload.blocks as Array<{ type?: string }>).some((block) => block.type === "actions"), ).toBe(false); }); + + it("keeps pending metadata context within Slack Block Kit limits", async () => { + const payload = (await slackApprovalNativeRuntime.presentation.buildPendingPayload({ + cfg: {} as never, + accountId: "default", + context: { + app: {} as never, + config: {} as never, + }, + request: { + id: "req-1", + request: { + command: "echo hi", + }, + createdAtMs: 0, + expiresAtMs: 60_000, + }, + approvalKind: "exec", + nowMs: 0, + view: { + approvalKind: "exec", + approvalId: "req-1", + commandText: "echo hi", + metadata: Array.from({ length: 12 }, (_entry, index) => ({ + label: `Metadata ${index + 1}`, + value: index === 0 ? "x".repeat(3100) : `value-${index + 1}`, + })), + actions: [ + { + decision: "allow-once", + label: "Allow Once", + command: "/approve req-1 allow-once", + style: "success", + }, + ], + } as never, + })) as SlackPayload; + + const contextBlock = (payload.blocks as Array<{ type?: string; elements?: unknown[] }>).find( + (block) => block.type === "context", + ); + const elements = contextBlock?.elements as Array<{ text?: string }> | undefined; + + expect(elements).toHaveLength(10); + expect(elements?.[0]?.text).toHaveLength(3000); + expect(elements?.[0]?.text?.endsWith("…")).toBe(true); + expect(elements?.at(-1)?.text).toBe("…+3 more"); + }); }); diff --git a/extensions/slack/src/approval-handler.runtime.ts b/extensions/slack/src/approval-handler.runtime.ts index 500385d3108..42404e239ef 100644 --- a/extensions/slack/src/approval-handler.runtime.ts +++ b/extensions/slack/src/approval-handler.runtime.ts @@ -30,6 +30,9 @@ type SlackPendingDelivery = { blocks: SlackBlock[]; }; +const SLACK_CONTEXT_ELEMENTS_MAX = 10; +const SLACK_TEXT_OBJECT_MAX = 3000; + type SlackExecApprovalConfig = NonNullable< NonNullable["slack"]>["execApprovals"] >; @@ -80,6 +83,21 @@ function buildSlackMetadataLines(metadata: readonly { label: string; value: stri return metadata.map((item) => formatSlackMetadataLine(item.label, item.value)); } +function buildSlackMetadataContextElements(metadata: readonly { label: string; value: string }[]) { + const lines = buildSlackMetadataLines(metadata); + const visibleLines = + lines.length > SLACK_CONTEXT_ELEMENTS_MAX + ? [ + ...lines.slice(0, SLACK_CONTEXT_ELEMENTS_MAX - 1), + `…+${lines.length - (SLACK_CONTEXT_ELEMENTS_MAX - 1)} more`, + ] + : lines; + return visibleLines.map((line) => ({ + type: "mrkdwn" as const, + text: truncateSlackMrkdwn(line, SLACK_TEXT_OBJECT_MAX), + })); +} + function resolveSlackApprovalDecisionLabel( decision: "allow-once" | "allow-always" | "deny", ): string { @@ -104,7 +122,7 @@ function buildSlackPendingApprovalText(view: ExecApprovalPendingView): string { } function buildSlackPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBlock[] { - const metadataLines = buildSlackMetadataLines(view.metadata); + const metadataElements = buildSlackMetadataContextElements(view.metadata); const interactiveBlocks = resolveSlackReplyBlocks({ text: "", @@ -125,14 +143,11 @@ function buildSlackPendingApprovalBlocks(view: ExecApprovalPendingView): SlackBl text: `*Command*\n${buildSlackCodeBlock(truncateSlackMrkdwn(view.commandText, 2600))}`, }, }, - ...(metadataLines.length > 0 + ...(metadataElements.length > 0 ? [ { type: "context", - elements: metadataLines.map((line) => ({ - type: "mrkdwn" as const, - text: line, - })), + elements: metadataElements, } satisfies SlackBlock, ] : []),