fix: bound slack approval metadata

This commit is contained in:
Peter Steinberger
2026-04-30 03:47:12 +01:00
parent c39ca49c71
commit 329568905e
3 changed files with 70 additions and 6 deletions

View File

@@ -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.

View File

@@ -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");
});
});

View File

@@ -30,6 +30,9 @@ type SlackPendingDelivery = {
blocks: SlackBlock[];
};
const SLACK_CONTEXT_ELEMENTS_MAX = 10;
const SLACK_TEXT_OBJECT_MAX = 3000;
type SlackExecApprovalConfig = NonNullable<
NonNullable<NonNullable<OpenClawConfig["channels"]>["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,
]
: []),