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/v1foundational flow (environment: web), gated by the flag. Ai::Catalog::GoalTemplates::Workplanports 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+WorkItemGenerateWorkplanmutation (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_plansuites); ESLint clean. - GraphQL schema/introspection/reference docs and permissions docs regenerated;
permissions:validateand 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).executeActiveRecord::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-runnerand a Docker daemon (colimaworks; 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_TAGgates job pickup on that tag, and the default-onduo_runner_restrictionsflag rejects project runners for Duo jobs viaAi::DuoWorkflow::RunnerValidator. - Point the runner's docker executor at the real Docker socket
(
runner.docker_hostingdk.yml) if/var/run/docker.sockis stale. - Make GitLab reachable from the job container: GDK's nginx binds
127.0.0.1by default, which containers can't reach. Setnginx.listen_address: 0.0.0.0(not the top-levellisten_address, which also repoints Vite'sproxy_pass) and addextra_hosts: ["gdk.test:<host-gateway-ip>"]to the runner config (the IP a container reaches the host on — e.g.192.168.5.2for colima/Lima,172.17.0.1for a Linux Docker bridge). - Trust GDK's cert inside the job: if GDK's HTTPS cert is mkcert-issued, the
duoCLI (Node) needs the mkcert root CA, not the leaf cert — mount it into the runner and setNODE_EXTRA_CA_CERTS/GIT_SSL_CAINFO/SSL_CERT_FILEingitlab-runner-config.toml. Without it,duofails withUNABLE_TO_VERIFY_LEAF_SIGNATUREbefore 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.