Post Duo tool approval requests to Slack

What does this MR do and why?

Second of three MRs that let a user approve or deny a paused Duo agent's tool call from Slack.

When a session pauses in tool_call_approval_required, the Slack adapter posts a threaded message with a Review button. Clicking it opens a modal listing the pending tool calls and their arguments, with an Approve / Deny choice. Submitting the modal does nothing yet — that is the next MR.

This MR

  • Slack#on_approval_required posts a persistent message naming the tools plus one Review button (value: <workflow_id>:<fingerprint>). Arguments never reach the channel.
  • pending_approval and pending_approval_ts are stored in the callback context so a redelivery does not post twice and the message can be updated later.
  • DuoToolApprovalModal builds the modal (calls with arguments, Approve / Deny radio, Submit) and the close-only notices for non-owners, unlinked accounts, and stale requests. Arguments are truncated per value so a long value cannot hide a later key.
  • DuoApprovalReviewHandler handles the Review click and opens the view matching the clicker. Read-only.

An ephemeral was dropped because it disappears on reload with no way back into the session. The message is public, so it only names the tools; arguments render in the modal once the viewer is known. Ownership is enforced again on submit.

Next MR

Handles the modal view_submission: re-checks ownership and update_duo_workflow, verifies the fingerprint, resumes the run, records audit and tracking events, updates the channel message via pending_approval_ts, and returns a confirmation view.

Behind the default-off slack_duo_api_flow flag. CallbackWorker already dispatches on_approval_required when a session pauses (!255163 (merged)), so with the flag on this is live once merged; resuming the run still waits for #628435 (closed).

Stack:

  1. Foundations — !255536 (merged)
  2. This MR — post the request and open the review modal
  3. Resolve the decision — !255538 (merged)

Split out of !254849 (closed).

References

Demo

Modal Demo
not allowed state
Screenshot_2026-09-16_at_08.15.32

How to set up and validate locally

Specs:

bin/rspec ee/spec/services/ai/messaging/adapters/slack_spec.rb \
  ee/spec/lib/integrations/slack_interactions/duo_tool_approval_modal_spec.rb \
  ee/spec/services/integrations/slack_interactions/slack_block_actions/duo_approval_review_handler_spec.rb \
  ee/spec/services/ee/integrations/slack_interactions/block_action_service_spec.rb

The demo above was seeded: a session already paused in tool_call_approval_required with hand-written request entries, and the hook called directly. A real run pausing for approval now reaches the same code through CallbackWorker.

Prerequisites: GitLab for Slack app installed on the GDK (a SlackIntegration with bot token), your Slack user linked to a GitLab user (ChatName), and Slack interactions reaching the GDK (tunnel or Socket Mode) for the Review click.

  1. Post the approval message from rails console:

    Feature.enable(:slack_duo_api_flow)
    
    team_id, channel, slack_user = 'T…', 'C…', 'U…' # workspace, channel, linked Slack user
    owner   = ChatName.find_by(team_id: team_id, chat_id: slack_user).user
    project = Project.find_by_full_path('gitlab-org/gitlab-shell')
    
    wf = Ai::DuoWorkflows::Workflow.new(user: owner, project: project, workflow_definition: 'chat', goal: 'demo')
    wf.save!(validate: false)
    wf.update_column(:status, Ai::DuoWorkflows::Workflow.state_machines[:status].states[:tool_call_approval_required].value)
    
    Ai::DuoWorkflows::Checkpoint.create!(
      workflow: wf, project: project, thread_ts: Gitlab::Utils.uuid_v7, parent_ts: Gitlab::Utils.uuid_v7,
      metadata: { 'source' => 'demo' },
      checkpoint: { 'channel_values' => { 'ui_chat_log' => [
        { 'message_type' => 'request', 'message_id' => 'toolu_1',
          'content' => 'Tool create_issue requires approval. Please confirm if you want to proceed.',
          'tool_info' => { 'name' => 'create_issue', 'args' => { 'title' => 'Demo', 'confidential' => true } } },
        { 'message_type' => 'request', 'message_id' => 'toolu_2',
          'content' => 'Tool run_command requires approval. Please confirm if you want to proceed.',
          'tool_info' => { 'name' => 'run_command', 'args' => { 'program' => 'git', 'args' => ['push', '--force'] } } }
      ] } }
    )
    
    ctx = { 'adapter' => 'slack', 'team_id' => team_id, 'channel_id' => channel, 'user_id' => slack_user, 'workflow_id' => wf.id }
    Ai::Messaging::Adapters::Slack.from_callback_context(ctx).on_approval_required(callback_context: ctx, workflow: wf)
    wf.reload.messaging_callback_context # => pending_approval + pending_approval_ts

    A message naming the two tools with a Review button appears in the channel.

  2. Click Review in Slack. As the session owner you get the tool calls and the Approve / Deny input; as another linked user the not-owner notice; with an unlinked Slack account the not-linked notice. Seed a second session with a different owner to see the non-owner path.

message_id on each request and a real Checkpoint are both required: PendingToolApproval.for reads the latest checkpoint and returns nil without them.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Jannik Lehmann

Merge request reports

Loading
Loading