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::UsageQuotaServicealready billsuser.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_accesswith: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.rbThe 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), notbun. Running the CLI unbundled withbunmishandles the WebSocket handshake and fails after the session is created. That is abunquirk, unrelated to this change. Usenode packages/cli/dist/index.js(or the released binary).
You need
- A GDK with Duo set up (running
aiFlowsMetadatashows aduo_developercapability). - A personal access token with the
apiscope. - 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:
- Run GDK in SaaS mode. Give a user a Duo seat and default namespace on group A.
- From a checkout that resolves to a project under a different root group B (readable, but no seat there), start a session.
- On
masterit 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/StageCheckchecks is acceptable.