Allow client-executed Duo flows with a seat in another namespace

What does this MR do and why?

Duo CLI moved its default from the chat flow to the interactive developer/v1 flow. A CLI session runs the flow itself, so GitLab classifies it as client-executed and authorizes it up front in FlowExecutionAuthorizer (agentic chat access + flow availability).

CreateWorkflowService#check_access then ran a second check for non-chat flows, check_duo_workflow_access → the :duo_workflow ability → allowed_to_use?(:duo_agent_platform, root_namespace: ...). That check is bound to the visited project's root namespace and has no seat-anywhere fallback. So a user with a valid Duo seat in one namespace (for example a community contributor whose seat is on gitlab-community) got a 403 when running the CLI in a project under another namespace (for example gitlab-org), even though the legacy chat path allowed it and billing already routes credits to their own namespace.

This MR skips the redundant access check for client-executed runs and relies on the upstream FlowExecutionAuthorizer decision. It restores the pre-developer/v1 behavior: a locally-run flow needs a Duo seat plus flow availability, not a seat on the visited project's namespace.

  • No billing change: Ai::UsageQuotaService already bills user.governing_namespace, which falls back to the user's own default Duo namespace.
  • No change for background / GitLab-executed runs: they still go through check_duo_workflow_access with :create_duo_workflow_for_ci.

Root-cause analysis and options considered are in the issue.

Closes #623975 (closed)

Behavior change to flag for review

For a client-executed run, the service no longer independently checks the :duo_agent_platform entitlement or StageCheck.available?(:duo_workflow). This is intentional: those checks belong to the background (unattended) path, and a client-executed run is governed like chat. Looping in the governance team to confirm we are not missing anything.

How to test

1. Automated (the regression guard)

bundle exec rspec ee/spec/requests/api/ai/duo_workflows/workflows_spec.rb ee/spec/services/ai/duo_workflows/create_workflow_service_spec.rb

The new case "when the Duo seat is in another namespace" returns 403 on master and 201 with this change — the bug, captured as a test.

2. Manual: run the real Duo CLI against a local GDK

This checks that a developer/v1 session started from the CLI reaches the backend and runs.

⚠️ Use the packaged CLI (node), not bun. Running the CLI unbundled with bun mishandles the WebSocket handshake and fails after the session is created. That is a bun quirk, unrelated to this change. Use node packages/cli/dist/index.js (or the released binary).

You need

  • A GDK with Duo set up (running aiFlowsMetadata shows a duo_developer capability).
  • A personal access token with the api scope.
  • A folder whose git remote points at a Duo-enabled project on the GDK.

Run

cd <gitlab-lsp checkout>
GITLAB_BASE_URL=http://gdk.test:3000 \
GITLAB_TOKEN=<your PAT> \
node packages/cli/dist/index.js run --goal "Reply with exactly one word: pong" --cwd <your folder>

Expect: the workflow is created (POST /workflows → 201), the WebSocket opens, and the run ends with Workflow completed successfully and a pong reply.

3. Reproduce the original 403 (optional — needs SaaS mode)

The namespace-scoped seat check only applies on SaaS, so the cross-namespace 403 only shows up with SaaS simulation:

  1. Run GDK in SaaS mode. Give a user a Duo seat and default namespace on group A.
  2. From a checkout that resolves to a project under a different root group B (readable, but no seat there), start a session.
  3. On master it is refused ("not available for this namespace"); with this change it starts, and credits go to group A.

MR acceptance checklist

  • Confirmed with the governance team that dropping the client-executed :duo_agent_platform / StageCheck checks is acceptable.
Edited by Thomas Schmidt

Merge request reports

Loading
Loading