Draft: Add async work item workplan generation via Duo Agent Platform

What does this MR do and why?

Adds an asynchronous, server-side path for generating a work item Workplan through the Duo Agent Platform, as an alternative to the current interactive Duo Chat flow.

Today the "Generate workplan" button opens Duo Chat and streams the Planner agent from the browser. Behind the default-off duo_workplan_async_flow flag, the inline Generate button and the panel Regenerate action instead start the server-side workplan/v1 foundational flow via a new WorkItemGenerateWorkplan GraphQL mutation. The flow runs in CI, writes the plan into the Workplan widget, and the existing pending-open watcher reveals the panel once content lands. With the flag off, the chat-based behavior is unchanged.

Backend

  • New workplan/v1 foundational flow (environment: web), gated by the flag.
  • Ai::Catalog::GoalTemplates::Workplan ports the chat prompt into a backend goal template (adapted for async: open questions are recorded in the plan rather than asked interactively).
  • Ai::DuoWorkflows::GenerateWorkplanService + WorkItemGenerateWorkplan mutation (granular token: update_work_item, project boundary) → CreateAndStartWorkflowService.

Frontend

  • The inline "Generate" and panel "Regenerate" actions call the mutation instead of opening Duo Chat when the flag is on.

Feature flag

Behind duo_workplan_async_flow (type: wip, default off). Rollout: enable per project or group once the Duo Workflow Service side is ready.

Dependency

End-to-end execution requires the matching workplan/v1 flow implementation on the Duo Workflow Service (ai-gateway), the same as code_review/v1. Until then the flow is registered and triggerable but will not complete.

Testing

  • Backend: 85 RSpec examples (goal template, foundational flow, service, and the granular-token mutation request spec).
  • Frontend: 97 Jest tests (both agent_plan suites); ESLint clean.
  • GraphQL schema/introspection/reference docs and permissions docs regenerated; permissions:validate and introspection sync pass.

Local GDK testing notes

What it took to run this end-to-end in GDK

None of this affects production/GitLab.com behavior — it's what a fresh GDK is missing to actually execute the flow locally. Recording it for whoever tests this next.

1. Seed and enable the flow

workplan/v1 is a new Ai::Catalog::FoundationalFlow. A GDK seeded before this flow existed has no matching Ai::Catalog::Item, so FoundationalFlow#catalog_item resolves to nil and the mutation fails with "Workflow not enabled for this project/namespace" even though the flag is on:

Ai::Catalog::Flows::SeedFoundationalFlowsService.new(organization: group.organization).execute
item = Ai::Catalog::Item.with_foundational_flow_reference('workplan/v1').first

group.sync_enabled_foundational_flows!(group.enabled_flow_catalog_item_ids | [item.id])
project.sync_enabled_foundational_flows!(project.enabled_flow_catalog_item_ids | [item.id])

Ai::Catalog::Flows::SyncFoundationalFlowsService.new(group, current_user: user).execute
Ai::Catalog::Flows::SyncFoundationalFlowsService.new(project, current_user: user).execute

ActiveRecord::FixedItemsModel::Model caches flow definitions in a class-level @storage, so any Rails/Sidekiq process that resolved catalog_item as nil before this keeps returning nil for its lifetime — restart rails-web / rails-background-jobs (or touch foundational_flow.rb to force a dev-mode reload) after seeding.

2. Duo Workflow Service local signing key

GenerateToken failed server-side with jose.exceptions.JWSError: Unable to parse an RSA_JWK from key: None, surfaced to Rails as the generic "Could not obtain Duo Workflow token".

gitlab-ai-gateway/.env sets AIGW_SELF_SIGNED_JWT__SIGNING_KEY / __VALIDATION_KEY, but duo_workflow_service/server.py's GenerateToken reads DUO_WORKFLOW_SELF_SIGNED_JWT__SIGNING_KEY / __VALIDATION_KEY instead, which a generated .env doesn't set even though example.env does (to the same keypair). Copying those two values across and restarting duo-workflow-service fixed it.

3. A GitLab Runner, because this flow runs in CI

environment: "web" executes the flow in a CI job, not in Rails or Sidekiq — a fresh GDK has no runner at all. To get one working:

  • Install gitlab-runner and a Docker daemon (colima works; Docker Desktop's socket may be stale or absent).
  • Register an instance runner (or top-level group runner) tagged gitlab--duo — Ai::DuoWorkflows::Workflow::WORKLOAD_TAG gates job pickup on that tag, and the default-on duo_runner_restrictions flag rejects project runners for Duo jobs via Ai::DuoWorkflow::RunnerValidator.
  • Point the runner's docker executor at the real Docker socket (runner.docker_host in gdk.yml) if /var/run/docker.sock is stale.
  • Make GitLab reachable from the job container: GDK's nginx binds 127.0.0.1 by default, which containers can't reach. Set nginx.listen_address: 0.0.0.0 (not the top-level listen_address, which also repoints Vite's proxy_pass) and add extra_hosts: ["gdk.test:<host-gateway-ip>"] to the runner config (the IP a container reaches the host on — e.g. 192.168.5.2 for colima/Lima, 172.17.0.1 for a Linux Docker bridge).
  • Trust GDK's cert inside the job: if GDK's HTTPS cert is mkcert-issued, the duo CLI (Node) needs the mkcert root CA, not the leaf cert — mount it into the runner and set NODE_EXTRA_CA_CERTS / GIT_SSL_CAINFO / SSL_CERT_FILE in gitlab-runner-config.toml. Without it, duo fails with UNABLE_TO_VERIFY_LEAF_SIGNATURE before it can open the websocket to Duo Workflow Service.

4. The DWS-side flow config doesn't exist yet

Even with all of the above, DWS rejected the run with a gRPC INVALID_ARGUMENT ("Failed to load flow workplan/v1 (version 1.0.0)") that the duo CLI turns into a clean websocket close and exit 0 — the CI job goes green and the session silently produces nothing, with no error visible outside gdk tail duo-workflow-service. There is no workplan/v1 YAML under duo_workflow_service/agent_platform/v1/flows/configs/ in ai-assist, on main or in any open MR — this is the companion piece referenced in Dependency above. A draft for local testing is pushed to ai-assist branch workplan-v1-flow-config, not yet its own MR.

5. Work item linking gap (fixed in this MR)

While testing, found CreateWorkflowService#resolve_noteable_from_flow only linked a resolved noteable when it was a MergeRequest, silently dropping the WorkItem case — so workplan/v1 sessions never showed up linked to the work item they were generated from, even though WORKPLAN_NOTEABLE_RESOLVER was resolving it correctly. Fixed alongside the goal template wording.

Edited by Stefanos Xanthopoulos

Merge request reports

Loading
Loading