Stamp client-injected MCP tools in Workhorse
What does this MR do and why?
This MR changes Workhorse so it marks which MCP tools came from the client and which came from GitLab.
Here is the problem, in the order it happens:
- A user's client connects to a Duo Agent Platform flow and sends a start request.
- That start request can carry MCP tools the client got from an external MCP server the user configured locally.
- Workhorse receives the start request. It appends the MCP tools it fetched itself from MCP servers that GitLab configured.
- Workhorse forwards the merged list to the Duo Workflow Service.
- The Duo Workflow Service now sees one flat list. It cannot tell which entries came from the client and which came from GitLab.
- GitLab never saw the names of the client's tools. It cannot resolve a governance verdict for them.
- The pre-approval clamp merged on 8 September 2026. Since then, every call to such a tool hits the approval gate. A headless client has nobody to answer the prompt. The session stalls.
The fix works like this:
- Workhorse stamps a new
client_injectedprotobuf field, set totrue, on every entry that arrived in the client's start request. It does this before appending its own entries. - The stamp is set unconditionally. If a client puts its own value in that field, Workhorse overwrites it. A client cannot claim its own origin.
- Workhorse leaves its own appended entries unset. Absent reads as "governed" on the service side. A newer Workhorse talking to an older Duo Workflow Service degrades to prompting rather than to running without approval.
The existing trusted field could not be reused for this. trusted is only set for GitLab's own MCP server and for Orbit. A registered AI Catalog server carries trusted: false. Reusing trusted would have wrongly marked that server's tools as ungoverned.
This MR covers the Workhorse side only. The Duo Workflow Service side is already merged in gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!6890 (merged). The Rails side adds the dap_allow_client_injected_mcp_tools feature flag and the session field that answers it. That work is split into !255644 (merged) so it gets Rails CI coverage this branch cannot provide. That MR is still open. The feature flag ships off by default.
This branch is no longer blocked. It now pins the generated Go module gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/clients/gopb at pseudo-version v0.0.0-20260923131449-098cbc7a6bce. That version contains the client_injected field. No local replace directive is needed.
This branch was also rebased on master. Master had separately gained a loop that clears a client-claimed trusted value. Both loops now run over the client's entries: the trusted reset runs first, then the client_injected stamp. They address independent concerns.
How to reproduce the issue
The clean way to see this bug is at the unit level, by reading what Workhorse forwards to the Duo Workflow Service.
- Check out
master, without this branch. - Configure an external MCP server on a client and connect it to a Duo Agent Platform flow.
- Send a start request from the client. The request includes the MCP tools from that external server.
- Look at the list Workhorse forwards to the Duo Workflow Service. Nothing in that list distinguishes the client's entries from the entries Workhorse appended itself.
- Because the service cannot tell them apart, it has no basis to treat the client's tools differently from GitLab's own tools. Every tool call goes through the same approval gate, and a headless client has no one to answer the approval prompt, so the session stalls.
A full end-to-end reproduction needs an external MCP server configured on both the client and the Duo Workflow Service side. That setup is outside what this MR can walk through on its own.
How to test the fix
- Check out this branch.
- From the
workhorsedirectory, rungo build ./.... It succeeds. - From the
workhorsedirectory, rungo test ./internal/ai_assist/duoworkflow/.... It passes.
Two tests in the branch cover the behavior directly:
TestMarkClientInjectedsends a client entry that already sets its ownclient_injectedvalue, and asserts Workhorse overwrites it totrue. This proves a client cannot claim its own origin.- The existing
TestRunner_handleClientEventstart-request case gained new assertions: the tool that came from the client is stampedclient_injected: true, and the tool Workhorse fetched itself is left unstamped. This proves the two sources stay distinguishable in the forwarded list.
References
- Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/628633
- Duo Workflow Service side, merged: gitlab-org/modelops/applied-ml/code-suggestions/ai-assist!6890 (merged)
- Rails side, still open: !255644 (merged)
- Longer-term fix this is a stopgap for: https://gitlab.com/gitlab-org/gitlab/-/work_items/628575