diff --git a/src/infra/exec-approval-forwarder.ts b/src/infra/exec-approval-forwarder.ts index 2fbf53ae6..c73c2e3eb 100644 --- a/src/infra/exec-approval-forwarder.ts +++ b/src/infra/exec-approval-forwarder.ts @@ -158,6 +158,30 @@ function defaultResolveSessionTarget(params: { }; } +function isSlackExecApprovalsEnabled(cfg: ClawdbotConfig, accountId?: string | null): boolean { + const slackCfg = cfg.channels?.slack as + | { + execApprovals?: { enabled?: boolean; approvers?: Array }; + accounts?: Record< + string, + { execApprovals?: { enabled?: boolean; approvers?: Array } } + >; + } + | undefined; + if (!slackCfg) return false; + // Check account-specific config first + if (accountId && slackCfg.accounts?.[accountId]?.execApprovals?.enabled) { + const approvers = slackCfg.accounts[accountId].execApprovals?.approvers; + return Array.isArray(approvers) && approvers.length > 0; + } + // Fall back to top-level config + if (slackCfg.execApprovals?.enabled) { + const approvers = slackCfg.execApprovals?.approvers; + return Array.isArray(approvers) && approvers.length > 0; + } + return false; +} + async function deliverToTargets(params: { cfg: ClawdbotConfig; targets: ForwardTarget[]; @@ -169,6 +193,10 @@ async function deliverToTargets(params: { if (params.shouldSend && !params.shouldSend()) return; const channel = normalizeMessageChannel(target.channel) ?? target.channel; if (!isDeliverableMessageChannel(channel)) return; + // Skip Slack text messages when Slack exec approval buttons are enabled + if (channel === "slack" && isSlackExecApprovalsEnabled(params.cfg, target.accountId)) { + return; + } try { await params.deliver({ cfg: params.cfg, diff --git a/src/slack/monitor/exec-approvals.ts b/src/slack/monitor/exec-approvals.ts index 5796fd8ae..cba65c03e 100644 --- a/src/slack/monitor/exec-approvals.ts +++ b/src/slack/monitor/exec-approvals.ts @@ -72,7 +72,9 @@ function formatApprovalBlocks(request: ExecApprovalRequest) { const commandText = request.request.command; const commandPreview = commandText.length > 2000 ? `${commandText.slice(0, 2000)}...` : commandText; + const expiresAtUnix = Math.floor(request.expiresAtMs / 1000); const expiresIn = Math.max(0, Math.round((request.expiresAtMs - Date.now()) / 1000)); + const fallbackTime = new Date(request.expiresAtMs).toISOString(); const contextParts: string[] = []; if (request.request.cwd) contextParts.push(`*CWD:* ${request.request.cwd}`); @@ -116,7 +118,12 @@ function formatApprovalBlocks(request: ExecApprovalRequest) { blocks.push({ type: "context", - elements: [{ type: "mrkdwn", text: `Expires in ${expiresIn}s | ID: \`${request.id}\`` }], + elements: [ + { + type: "mrkdwn", + text: `Expires (${expiresIn}s) | ID: \`${request.id}\``, + }, + ], }); blocks.push({