fix(slack): use unique action_ids for approval buttons

Slack requires unique action_id per button in a block. Changed from
single shared ID to per-action IDs (allow_once, allow_always, deny)
and use regex matching in the action handler.
This commit is contained in:
Kieran Klukas 2026-01-26 02:29:32 -05:00
parent 26cd21e73b
commit 07ae264704
No known key found for this signature in database
2 changed files with 42 additions and 34 deletions

View File

@ -8,7 +8,7 @@ import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
import { logDebug, logError } from "../../logger.js"; import { logDebug, logError } from "../../logger.js";
import type { RuntimeEnv } from "../../runtime.js"; import type { RuntimeEnv } from "../../runtime.js";
const EXEC_APPROVAL_ACTION_ID = "clawdbot_execapproval"; const EXEC_APPROVAL_ACTION_ID_PREFIX = "clawdbot_execapproval";
const EXEC_APPROVAL_VALUE_PREFIX = "execapproval"; const EXEC_APPROVAL_VALUE_PREFIX = "execapproval";
export type ExecApprovalRequest = { export type ExecApprovalRequest = {
@ -60,8 +60,12 @@ export function parseApprovalValue(
} }
} }
export function getExecApprovalActionId(): string { export function getExecApprovalActionIdPrefix(): string {
return EXEC_APPROVAL_ACTION_ID; return EXEC_APPROVAL_ACTION_ID_PREFIX;
}
export function matchesExecApprovalActionId(actionId: string): boolean {
return actionId.startsWith(EXEC_APPROVAL_ACTION_ID_PREFIX);
} }
function formatApprovalBlocks(request: ExecApprovalRequest) { function formatApprovalBlocks(request: ExecApprovalRequest) {
@ -121,20 +125,20 @@ function formatApprovalBlocks(request: ExecApprovalRequest) {
{ {
type: "button", type: "button",
text: { type: "plain_text", text: "✓ Allow once", emoji: true }, text: { type: "plain_text", text: "✓ Allow once", emoji: true },
action_id: EXEC_APPROVAL_ACTION_ID, action_id: `${EXEC_APPROVAL_ACTION_ID_PREFIX}_allow_once`,
value: encodeApprovalValue(request.id, "allow-once"), value: encodeApprovalValue(request.id, "allow-once"),
style: "primary", style: "primary",
}, },
{ {
type: "button", type: "button",
text: { type: "plain_text", text: "✓✓ Always allow", emoji: true }, text: { type: "plain_text", text: "✓✓ Always allow", emoji: true },
action_id: EXEC_APPROVAL_ACTION_ID, action_id: `${EXEC_APPROVAL_ACTION_ID_PREFIX}_allow_always`,
value: encodeApprovalValue(request.id, "allow-always"), value: encodeApprovalValue(request.id, "allow-always"),
}, },
{ {
type: "button", type: "button",
text: { type: "plain_text", text: "✗ Deny", emoji: true }, text: { type: "plain_text", text: "✗ Deny", emoji: true },
action_id: EXEC_APPROVAL_ACTION_ID, action_id: `${EXEC_APPROVAL_ACTION_ID_PREFIX}_deny`,
value: encodeApprovalValue(request.id, "deny"), value: encodeApprovalValue(request.id, "deny"),
style: "danger", style: "danger",
}, },

View File

@ -28,7 +28,8 @@ import { normalizeAllowList } from "./allow-list.js";
import type { MonitorSlackOpts } from "./types.js"; import type { MonitorSlackOpts } from "./types.js";
import { import {
SlackExecApprovalHandler, SlackExecApprovalHandler,
getExecApprovalActionId, getExecApprovalActionIdPrefix,
matchesExecApprovalActionId,
parseApprovalValue, parseApprovalValue,
} from "./exec-approvals.js"; } from "./exec-approvals.js";
@ -234,43 +235,46 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
( (
app as unknown as { app as unknown as {
action: ( action: (
id: string, id: string | RegExp,
handler: (args: { handler: (args: {
ack: () => Promise<void>; ack: () => Promise<void>;
body: { user?: { id?: string } }; body: { user?: { id?: string } };
action: { value?: string }; action: { action_id?: string; value?: string };
respond: (payload: { text: string; response_type?: string }) => Promise<void>; respond: (payload: { text: string; response_type?: string }) => Promise<void>;
}) => Promise<void>, }) => Promise<void>,
) => void; ) => void;
} }
).action(getExecApprovalActionId(), async ({ ack, body, action, respond }) => { ).action(
await ack(); new RegExp(`^${getExecApprovalActionIdPrefix()}_`),
const parsed = parseApprovalValue(action?.value); async ({ ack, body, action, respond }) => {
if (!parsed) { await ack();
const parsed = parseApprovalValue(action?.value);
if (!parsed) {
await respond({
text: "This approval button is no longer valid.",
response_type: "ephemeral",
});
return;
}
const decisionLabel =
parsed.action === "allow-once"
? "Allowed (once)"
: parsed.action === "allow-always"
? "Allowed (always)"
: "Denied";
await respond({ await respond({
text: "This approval button is no longer valid.", text: `Submitting decision: **${decisionLabel}**...`,
response_type: "ephemeral", response_type: "ephemeral",
}); });
return; const ok = await execApprovalHandler!.resolveApproval(parsed.approvalId, parsed.action);
} if (!ok) {
const decisionLabel = await respond({
parsed.action === "allow-once" text: "Failed to submit approval. The request may have expired or already been resolved.",
? "Allowed (once)" response_type: "ephemeral",
: parsed.action === "allow-always" });
? "Allowed (always)" }
: "Denied"; },
await respond({ );
text: `Submitting decision: **${decisionLabel}**...`,
response_type: "ephemeral",
});
const ok = await execApprovalHandler!.resolveApproval(parsed.approvalId, parsed.action);
if (!ok) {
await respond({
text: "Failed to submit approval. The request may have expired or already been resolved.",
response_type: "ephemeral",
});
}
});
} }
} }