Flow workflow tokens are signed from the Agents & flows setting, not the flow setting
Summary
On self-managed, a Duo Agent Platform flow chooses the Duo Workflow Service URL from its own feature setting but signs the GenerateToken request from the Agents & flows setting. When the two disagree, the request carries a token meant for the other service. On an offline cloud license with Agentic Code Review on a self-hosted model and Agents & flows without one, a Code Review request fails with DCR4005.
Steps to reproduce
- On self-managed GitLab 19.4.1 with an offline cloud license and the GitLab Duo Agent Platform Self-Hosted add-on, point the Duo Agent Platform service URL at a self-hosted Duo Workflow Service that has authentication enabled.
- Assign a self-hosted model to Agentic Code Review. Leave Agents & flows without a self-hosted model.
- In a merge request, comment
/assign_reviewer @GitLabDuo.
Example Project
Not applicable: self-managed only.
What is the current bug behavior?
GitLabDuo replies:
⚠️ Code Review Flow could not obtain the required authentication tokens to connect to the GitLab AI Gateway and API. Please request a new review. If the issue persists, contact your administrator. Error code: DCR4005
No agent session is created. GenerateToken fails the same way when Agents & flows is unassigned or disabled.
What is the expected correct behavior?
The review starts, as it does when Agents & flows is also on a self-hosted model.
Relevant logs and/or screenshots
The Duo Workflow Service logs this for the GenerateToken call, and Rails receives UNAUTHENTICATED: Forbidden by auth provider:
[error ] Not enough segments [cloud_connector] ... exception_class=JWTError ...Possible fixes
sequenceDiagram
participant W as WorkflowContextGenerationService
participant S as FeatureSettingSelectionService
participant U as DuoWorkflow::Client.url_for
participant C as DuoWorkflowService::Client
participant T as CloudConnector::Tokens.get
participant D as Self-hosted Duo Workflow Service
W->>S: flow feature review_merge_request_dap
S-->>W: Agentic Code Review setting, self-hosted
W->>U: feature_setting is the flow setting
U-->>W: duo_agent_platform_service_url
W->>C: new with the URL
Note over W,C: Before: feature_setting not passed<br/>After: feature_setting is the flow setting
C->>T: unit primitive duo_agent_platform
Note over T: Before: nil, reads the Agents and flows row<br/>After: signs from the flow setting
T-->>C: bearer token
Note over C,T: Before: synced token, empty when offline<br/>After: instance-signed token
C->>D: GenerateToken
D-->>C: Before, offline: UNAUTHENTICATED<br/>After: workflow tokenWorkflowContextGenerationService#generate_workflow_token passes the flow's setting to url_for and secure? but not to DuoWorkflowService::Client.new (workflow_context_generation_service.rb:39-55). The client passes feature_setting: nil to Gitlab::AiGateway.headers (client.rb:190-197), so CloudConnector::Tokens.get looks up the duo_agent_platform row instead (tokens.rb:60-69). That row is not self-hosted, so it returns the synced Cloud Connector token, which an offline instance does not have.
Duo Developer and secrets false-positive detection take their URL and token from different settings in the same way when their own setting is self-hosted. On an online license the same lines send the synced token to a self-hosted Duo Workflow Service, and, for a flow with no self-hosted setting of its own while Agents & flows is self-hosted, an instance-signed token to the GitLab-hosted service. I have not reproduced those two on an instance.
Passing the resolved setting to the client fixes it, as DuoAgentPlatformProbe has done since !250313 (merged). #621674 (closed) had the same shape on the chat paths.
Two API endpoints choose the Duo Workflow Service URL without any setting. POST /api/v4/ai/duo_workflows/direct_access returns Gitlab::DuoWorkflow::Client.url as its base URL, which is the self-hosted URL whenever one is configured, next to a workflow token minted from the flow's setting. GET /api/v4/ai/duo_workflows/list_tools chooses its URL the same way and signs from Agents & flows. Both report secure?(feature_setting: nil) as the TLS flag (workflows.rb:219-227, workflows.rb:761-773).
The fix also changes which claims reach a self-hosted Duo Workflow Service. On an online license, a flow with its own self-hosted setting, where Agents & flows is not self-hosted, sends an instance-signed token in place of the synced one. Only the instance-signed token carries the instance's tool_access_policies claim (client.rb:198-204, tokens.rb:16-22). The self-hosted service copies the claim into the workflow token (server.py:1088-1095) and applies it (abstract_workflow.py:370-415). When any rule is set, the allow list minus the ask list replaces the tools the client pre-approves, the deny list joins the denied tools, and the ask list applies when the flow may ask the user. With no tool rules configured, the claim carries empty lists and nothing changes. I traced this in code and have not run it.
Environment
- GitLab 19.4.1-ee, self-managed (a caproni lab instance)
- Offline cloud license, Ultimate, with the GitLab Duo Agent Platform Self-Hosted add-on
- AI gateway and Duo Workflow Service image tag
self-hosted-v18.11.1-ee