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
valuecarries 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)— noruntime:given, so the run uses the runtime the session last ran on (runtime seam issue). On CI that isResumeWorkflowService, a new job; on headless another workhorse call. - Replace the ephemeral via the interaction's
response_urlwith 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_urlwith 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'sflow_versiononly 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: trueon 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_assistantis not inGovernanceSurface::ALLOWLISTED_FLOWS— the background surface is allow/deny only. tool_approval_for_session_enabled?on the project or namespace gates thetool_call_approvalcapability 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_requiredhook); the runtime seam (StartRunServicewith an approval event). For CI testing, nothing else; for headless, the headless runtime. - Unblocks: the cutover — write actions in production need this.