Follow-up from PR #88 — Cedar HITL approvals work via CLI today; Slack interactivity is wired but not yet used for approvals.
Functional description
PR #88 ships a complete Cedar HITL approval flow: when an agent attempts a sensitive operation (force push, write to .env, etc.), the task pauses in AWAITING_APPROVAL status and an operator must run bgagent approve <task-id> <request-id> (or deny) to resume or fail it. The CLI works well for individual operators, but for team workflows (CI bots, on-call rotations, mobile approvals) the natural surface is Slack, not a terminal.
ABCA already has a working SlackIntegration construct (cdk/src/constructs/slack-integration.ts) with InteractionsFn handling block_actions callbacks, plus SlackNotifyFn posting task lifecycle events. The plumbing for "user clicks a button in a Slack message → Lambda processes the action → state update" already works for link_account_action and a few others. Extending it with approve_action: / deny_action: block_actions would close the HITL loop end-to-end without any new infrastructure.
User-visible impact (what's missing today):
- Operator on phone gets a Slack message: "Task abc-123 is awaiting approval (force push)." They have to switch to a terminal, run
bgagent login, run bgagent approve abc-123 req-456. Friction is high enough that approvals stall.
- CI bots can't approve via Slack workflow steps (they could call the API directly, but the Slack-button surface would be the cleanest integration).
- Any team using the dashboard for visibility but the CLI for action lives in two contexts.
With this issue resolved:
- Slack message gains "✅ Approve" / "❌ Deny" buttons.
- Click triggers
block_actions callback, Lambda extracts the user identity (already mapped via SlackUserMappingTable), calls the same approve-task / deny-task handlers the CLI uses.
- Operator never leaves Slack.
Technical context
Existing plumbing:
cdk/src/handlers/slack-interactions.ts — handles incoming block_actions payloads. Already validates Slack signatures, extracts the action_id, maps the Slack user to an ABCA user via SlackUserMappingTable. Adding new action_ids is a switch-case.
cdk/src/handlers/shared/slack-blocks.ts — Block Kit template helpers. Has taskCreated, taskCompleted, etc.; needs an approvalRequest template that includes the buttons.
cdk/src/handlers/slack-notify.ts — currently called by FanOutConsumer when an approval_requested event is dispatched. Today it posts plain text; would need to switch to the Block Kit template.
cdk/src/handlers/approve-task.ts / deny-task.ts — the existing handlers. Their interfaces (POST /tasks/{id}/approve etc.) are reusable; the Slack interactions Lambda would call into the same shared business logic via cdk/src/handlers/shared/approval-action.ts (need to extract the core from the HTTP handler into a shared module).
Net new code:
slackApprovalRequestBlocks(taskId, requestId, ruleIds) in slack-blocks.ts (~50 LOC).
handleApproveAction / handleDenyAction in slack-interactions.ts (~80 LOC each).
- Refactor approve-task.ts / deny-task.ts to extract a
processApprovalDecision(...) shared function that both the HTTP handler and the Slack handler call (~30 LOC refactor + tests).
- Wiring:
slack-notify.ts switches to the Block Kit template when event.event_type === 'approval_requested'.
Estimated effort: ~1 day for a single dev including tests + E2E validation against the dev stack.
Proposed options
Recommended path:
- Refactor the approve/deny handler core into
cdk/src/handlers/shared/approval-decision.ts (no behavior change; passes existing tests).
- Add Block Kit template + new action_id branches in
slack-interactions.ts.
- Update
slack-notify.ts to use the Block Kit template for approval_requested events.
- Add CDK + agent-side E2E test that simulates the Slack interaction flow end-to-end (the existing pattern from
cdk/test/handlers/slack-interactions.test.ts extends naturally).
Alternative considered: building a separate Slack approval handler that bypasses the HTTP path. Rejected because it would diverge the auth/audit trail — both surfaces should land in the same ApprovalsTable row with the same fields.
Acceptance criteria
Out of scope
- iOS / Android push notifications.
- Mobile-friendly approval landing page (web-form fallback for users not on Slack).
- Reassigning a stuck approval to a different user (separate issue if needed).
- Bulk approve/deny.
References
cdk/src/constructs/slack-integration.ts
cdk/src/handlers/slack-interactions.ts (existing block_actions handler)
cdk/src/handlers/shared/slack-blocks.ts (Block Kit template helpers)
cdk/src/handlers/approve-task.ts / deny-task.ts (existing approval handlers)
docs/design/CEDAR_HITL_GATES.md §6.5 (approval flow design)
- Slack Block Kit reference: https://api.slack.com/block-kit
Functional description
PR #88 ships a complete Cedar HITL approval flow: when an agent attempts a sensitive operation (force push, write to .env, etc.), the task pauses in
AWAITING_APPROVALstatus and an operator must runbgagent approve <task-id> <request-id>(ordeny) to resume or fail it. The CLI works well for individual operators, but for team workflows (CI bots, on-call rotations, mobile approvals) the natural surface is Slack, not a terminal.ABCA already has a working
SlackIntegrationconstruct (cdk/src/constructs/slack-integration.ts) withInteractionsFnhandlingblock_actionscallbacks, plusSlackNotifyFnposting task lifecycle events. The plumbing for "user clicks a button in a Slack message → Lambda processes the action → state update" already works forlink_account_actionand a few others. Extending it withapprove_action:/deny_action:block_actions would close the HITL loop end-to-end without any new infrastructure.User-visible impact (what's missing today):
bgagent login, runbgagent approve abc-123 req-456. Friction is high enough that approvals stall.With this issue resolved:
block_actionscallback, Lambda extracts the user identity (already mapped viaSlackUserMappingTable), calls the sameapprove-task/deny-taskhandlers the CLI uses.Technical context
Existing plumbing:
cdk/src/handlers/slack-interactions.ts— handles incomingblock_actionspayloads. Already validates Slack signatures, extracts the action_id, maps the Slack user to an ABCA user viaSlackUserMappingTable. Adding new action_ids is a switch-case.cdk/src/handlers/shared/slack-blocks.ts— Block Kit template helpers. HastaskCreated,taskCompleted, etc.; needs anapprovalRequesttemplate that includes the buttons.cdk/src/handlers/slack-notify.ts— currently called by FanOutConsumer when anapproval_requestedevent is dispatched. Today it posts plain text; would need to switch to the Block Kit template.cdk/src/handlers/approve-task.ts/deny-task.ts— the existing handlers. Their interfaces (POST /tasks/{id}/approveetc.) are reusable; the Slack interactions Lambda would call into the same shared business logic viacdk/src/handlers/shared/approval-action.ts(need to extract the core from the HTTP handler into a shared module).Net new code:
slackApprovalRequestBlocks(taskId, requestId, ruleIds)inslack-blocks.ts(~50 LOC).handleApproveAction/handleDenyActioninslack-interactions.ts(~80 LOC each).processApprovalDecision(...)shared function that both the HTTP handler and the Slack handler call (~30 LOC refactor + tests).slack-notify.tsswitches to the Block Kit template whenevent.event_type === 'approval_requested'.Estimated effort: ~1 day for a single dev including tests + E2E validation against the dev stack.
Proposed options
Recommended path:
cdk/src/handlers/shared/approval-decision.ts(no behavior change; passes existing tests).slack-interactions.ts.slack-notify.tsto use the Block Kit template forapproval_requestedevents.cdk/test/handlers/slack-interactions.test.tsextends naturally).Alternative considered: building a separate Slack approval handler that bypasses the HTTP path. Rejected because it would diverge the auth/audit trail — both surfaces should land in the same
ApprovalsTablerow with the same fields.Acceptance criteria
link accountflow can approve / deny a pending task by clicking a button in the FanOut-dispatched Slack messageTaskApprovalsTableshowsdecided_by_channel: "slack"(new optional field — extends the existing schema additively)docs/guides/USER_GUIDE.mdshowing the Slack approval flowOut of scope
References
cdk/src/constructs/slack-integration.tscdk/src/handlers/slack-interactions.ts(existing block_actions handler)cdk/src/handlers/shared/slack-blocks.ts(Block Kit template helpers)cdk/src/handlers/approve-task.ts/deny-task.ts(existing approval handlers)docs/design/CEDAR_HITL_GATES.md§6.5 (approval flow design)