Skip to content

Add Slack-button approvals for Cedar HITL approval gates #112

@scoropeza

Description

@scoropeza

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:

  1. Refactor the approve/deny handler core into cdk/src/handlers/shared/approval-decision.ts (no behavior change; passes existing tests).
  2. Add Block Kit template + new action_id branches in slack-interactions.ts.
  3. Update slack-notify.ts to use the Block Kit template for approval_requested events.
  4. 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

  • Operator who is logged into Slack via the existing link account flow can approve / deny a pending task by clicking a button in the FanOut-dispatched Slack message
  • Approval row in TaskApprovalsTable shows decided_by_channel: "slack" (new optional field — extends the existing schema additively)
  • Slack button click is rejected (with a friendly Slack ephemeral message) if the clicker is not the task owner — same 403 logic as the HTTP handler
  • CDK unit tests cover both new action_ids; CDK construct tests verify the FanOut config
  • Updated user docs section in docs/guides/USER_GUIDE.md showing the Slack approval flow

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions