Approve or deny an agent's action from Slack

Why

The Slack agent runs as the user, so the guard rail is the same as in agentic web chat: an admin's tool rules decide which actions run silently and which need a human to say yes. Web chat shows that question in the chat window. Slack needs its own way to ask and to hear the answer.

Without this, the approval gate stays off and every action the admin would want to ask about is simply not available to the agent.

What this issue delivers

When the agent needs an approval, the person who asked gets a message in the thread only they can see: what the agent wants to do, and buttons. Their click continues the session from where it paused. Nothing is asked unless an admin rule requires it; with no rules configured, behaviour is unchanged.

The approval gate in the slack_assistant flow is switched on as part of this — in a way that cannot strand a session before the buttons exist.

Done when

  • With an admin "ask" rule on a tool (for example create_issue) and no rule otherwise, the agent pauses on that tool, the requesting user gets an ephemeral message in the thread with the action and buttons, and nobody else sees the buttons.
  • Approve continues the session and the agent performs the action; Deny continues it without the action. The ephemeral is replaced with the outcome.
  • A different Slack user clicking the buttons is refused.
  • If the session can no longer be continued (finished, dropped, stopped, or another run is active), the user gets a clear message instead of a silent failure.
  • With no admin rules, the agent runs exactly as before — no prompts.
  • Works on the CI runtime, and on the headless runtime once it exists, without Slack-side changes.
Proposal — for whoever implements this

Showing the request

Ai::Messaging::Adapters::Slack#on_approval_required(callback_context:, workflow:) — the hook added by the lifecycle-events issue. Read the pending approval from the session's latest ui_chat_log: the approval-request entry carries the tool name and a human-readable rendering of the call. Post with chat.postEphemeral to callback_context['user_id'] in the thread, with block actions.

Buttons: to be aligned with what agentic web chat offers. Minimum is Approve and Deny. "Approve for this session" maps to Approval.remember_approval, which DWS already honours and Rails persists (Workflow#add_tool_call_approval). A deny-with-reason modal (like the feedback one) is optional.

Idempotent: the hook runs in an at-least-once worker. Guard on persisted state (e.g. store the ephemeral's identity or the approval's tool_call_id in messaging_callback_context) so a redelivery does not post twice.

The public progress message already shows the "waiting for approval" state — the request is in ui_chat_log, which on_progress renders. No extra work.

If the ephemeral is missed (reload, mobile), the session page is the fallback. Verify that the session page offers the approve action for a non-chat flow; if not, note the gap.

Handling the click

A handler under Integrations::SlackInteractions::SlackBlockActions, registered in the EE BlockActionService handlers, following DuoFeedbackHandler:

  • Button value carries the workflow id and the decision (and the tool_call_id, so a stale click on an already-answered request is detected).
  • Map the Slack user to the GitLab user with ChatNames::FindUserService.
  • Ownership check: refuse unless workflow.user == current_user. The ephemeral is already private, but the payload is not trusted; this is defence in depth and it is what makes the same handler safe if approvals ever become visible to a channel.
  • Continue the session: Ai::DuoWorkflows::StartRunService.new(workflow, event: approval) — no runtime: given, so the run uses the runtime the session last ran on (runtime seam issue). On CI that is ResumeWorkflowService, a new job; on headless another workhorse call.
  • Replace the ephemeral via the interaction's response_url with the outcome ("Approved — continuing", "Denied").
  • Cannot continue: if the session is not in an approvable state, or the run lease is held, or the approval has already been answered, reply on response_url with a short explanation and a link to the session. No exception, no silent drop.

Turning the gate on — ordering matters

The gate is require_tool_approval: true on the slack_assistant agent component, plus on_tool_approval_request and on_tool_approval_feedback in its ui_log_events (the request node raises without them). This is an ai-assist MR.

Once it merges, any dogfood namespace with an "ask" rule pauses sessions — with no buttons if the Rails side is not there yet. Two safe orders:

  • Ship the gate as a new flow version (slack_assistant/1.1.0) and bump the registry's flow_version only when the Rails side is merged, or
  • merge the ai-assist MR last.

Either is fine; pick one and say so in the MR.

Settings that make approvals work at all

  • allow_agent_to_request_user: true on the registry entry — set in the registration issue. Without it DWS drops every "ask" rule.
  • The Slack session must resolve to a governance surface with an "ask" level. It does (web rules) as long as slack_assistant is not in GovernanceSurface::ALLOWLISTED_FLOWS — the background surface is allow/deny only.
  • tool_approval_for_session_enabled? on the project or namespace gates the tool_call_approval capability that Rails advertises to DWS; check it is on for the dogfood namespace.

Testing before the headless runtime exists

Everything above works on CI: create an "ask" rule for create_issue in the dogfood group, mention the agent with a request that creates an issue, approve from Slack. Resume on CI starts a new job (slow, but the whole loop is exercised). The lifecycle-events issue makes sure the job ending at the pause is not mistaken for a result.

start_flow is just another tool

Delegation to Duo Developer is in the toolset already (flow issue). Under governance it is a tool like any other: an admin can make it "ask", and the approval message then reads "start Duo Developer on …". Nothing special to build.

Not in this issue

  • The events and hooks the adapter listens to — lifecycle events issue.
  • Resume transport on the headless runtime — headless runtime issue (requirement 6).
  • Surfacing a delegated Duo Developer session's result back into Slack — later.

Depends on / unblocks

  • Depends on: lifecycle events (the on_approval_required hook); the runtime seam (StartRunService with an approval event). For CI testing, nothing else; for headless, the headless runtime.
  • Unblocks: the cutover — write actions in production need this.