Pre-approve GitLab MCP server read tools in the token claim

What does this MR do and why?

Fixes #607151: every tool served by GitLab's own MCP server started asking for approval on each call.

The Duo Workflow Service treats the tool_access_policies claim's allow list as a ceiling_merge_jwt_governance_claims replaces the pre-approved list Workhorse sent with the claim's allow list outright. GitLab's MCP tools were never in that claim, so all of them fell through to "ask". On top of that, Workhorse addresses these tools as gitlab_<tool> (prefixedName := s.name + "_" + tool.Name), the way Orbit tools are orbit_<tool>, so bare names in the claim would not have matched anyway.

The fix forwards Workhorse's pre-approved list into the claim, prefixed. It is derived rather than curated: GITLAB_PREAPPROVED_TOOLS minus GITLAB_TOOL_ALIASES. The one name not forwarded is gitlab_search, a tool_aliases entry rather than a served tool, so a prefixed form of it would match nothing.

Orbit hit the same ceiling and was fixed the same way earlier; this is the GitLab half. Extending ai_tool_rules governance to MCP tools is #606073 and lands separately behind a flag.

Two things reviewers should know

A Deny does not reach the prefixed MCP spelling. Rules are stored and emitted under the catalog name (get_job_logs), which never matches the prefixed MCP name (gitlab_get_job_log). So pre-approving these lets the claim outlive a Deny. That was already true before the ceiling landed, and forwarding all of them was the explicit call on #606073 — the alternative left part of the regression open. #606073 fixes it properly by putting MCP names in the registry.

This is also why the /ws endpoint does not subtract resolved deny/ask from the static append: the two sides never use the same spelling, so there is nothing to remove.

The claim's allow list is only read by the legacy chat flow. _preapproved_tools is read in workflows/chat/workflow.py only; nothing under agent_platform/ reads it, and agentic_chat/v1 is served from there. agentic_chat_flow_registry_migration is wip and off by default, so chat should be on the legacy definition and this fix lands — but if that flag flips before the allow side is wired into the v1 flow, the prompts return. Deny is unaffected either way. Raised on #606073 for the transition.

References

  • Fixes #607151
  • Governance follow-up: #606073 (targeted on this branch)
  • Deriving the list from readOnlyHint instead: #596687 (closed), open MR !231431 (merged) — it also picks up get_merge_request_conflicts and get_work_item_types, which Workhorse never pre-approved so they are out of scope here

Screenshots or screen recordings

Verified on GDK against the real minted claim. The service's contribution to the claim's allow list goes from 0 to 15 gitlab_-prefixed names. Read-only tools move to allow; manage_pipeline and every write tool stay on ask.

Before — read-only search asks for approval

Screenshot 2026-07-30 at 10.20.44 PM.png

After — same request, no approval card

Screenshot 2026-07-30 at 10.16.09 PM.png

After — a write tool still asks

Screenshot 2026-07-30 at 10.18.31 PM.png

How to set up and validate locally

Feature.enable(:mcp_client, User.find_by(username: '<you>'))
Group.find_by_full_path('<group>').ai_settings.update!(duo_workflow_mcp_enabled: true)

svc = Ai::DuoWorkflows::McpConfigService.new(User.find_by(username: '<you>'), 'tok', workflow_definition: 'chat')
svc.preapproved_tool_names.grep(/\Agitlab_/)   # before: []   after: 15 names

agentic_chat_flow_registry_migration must be off. End to end: ask Duo Chat to search the project — an approval card before, none after. Start a new session when switching, a resumed one carries the old token.

MR acceptance checklist

This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Rahul Barnwal

Merge request reports

Loading