Add governing namespace error contract and Rails pre-check guard
What does this MR do?
Orbit attributes query usage to the root_namespace_id claim in the JWT Rails mints for every Orbit query. On SaaS, a user who belongs to more than one eligible (paid + Orbit-enabled) namespace has nothing to put in that claim until they pick one in user preferences. Today the claim goes out as null and the Orbit server rejects the query with an opaque internal error (gRPC 13).
This MR adds:
-
Analytics::KnowledgeGraph::GoverningNamespaceError— a shared, stable error-contract PORO:{ "code": "billing_namespace_required", "message": "Select a billing namespace for Orbit to attribute usage.", "deep_link": "https://gitlab.example.com/-/profile/preferences", "candidates": [{ "id": 123, "name": "group-a" }] }candidatesis sourced from the existingAnalytics::KnowledgeGraph::GoverningNamespaceFinder— no new finder or model readers were added. This PORO is feature-flag agnostic; callers gate onknowledge_graph_billing. -
A Rails pre-check at the shared Orbit query chokepoint that raises a typed error carrying that contract before the JWT is minted, instead of letting the query fail with an opaque server-side error. The REST and MCP endpoints render the contract instead.
Everything is behind the knowledge_graph_billing flag (type wip, default_enabled: false). With the flag off — or when a governing namespace resolves, or the user has 0 or 1 candidates — behaviour is unchanged.
Related to https://gitlab.com/gitlab-org/gitlab/-/issues/622338
Chokepoint placement
The guard sits as the first line of Gitlab::Workhorse.send_orbit_query
(ee/lib/ee/gitlab/workhorse.rb), via
Analytics::KnowledgeGraph::GoverningNamespaceGuard.ensure_selected!(user).
send_orbit_query has exactly three production callers:
ee/lib/api/orbit/data.rb—POST /orbit/queryandPOST /orbit/query/:nameee/lib/analytics/orbit/command_interceptor.rb—query_graph, reached from both the REST agent-command endpoint and the MCPtools/call→invoke_commandpath
That set is exactly the query-executing paths and nothing else, so one guard covers REST, MCP, the frontend and any future caller with no per-endpoint duplication.
Two alternatives were considered and rejected:
GrpcClient#execute_query— has zero production callers today, so guarding it would cover nothing.Analytics::KnowledgeGraph::JwtAuth— tempting as the sole JWT minter, but (1) every introspection path (schema, DSL, cluster health, graph status, tool/command listing) also mints a JWT throughauth_metadata, so a guard there would block read-only introspection that should stay open; and (2)JwtAuth.generate_tokenwraps its body inrescue StandardError => eand returnsnil, so a raise there would be swallowed and degrade into a missing auth header rather than an actionable error.
Guard conditions
Analytics::KnowledgeGraph::GoverningNamespaceGuard::RequiredError is raised only when all
three hold:
Feature.enabled?(:knowledge_graph_billing, user)user.user_preference.knowledge_graph_governing_namespace_with_fallbackisnilAnalytics::KnowledgeGraph::GoverningNamespaceFinder.new(user).candidateshas more than one entry
The single-candidate case stays silent on purpose — the existing fallback already attributes usage to the only eligible namespace. The finder returns no candidates off SaaS, so self-managed is unaffected.
How each client renders it
- REST (
ee/lib/api/orbit/data.rb) —422 Unprocessable Entitywith the contract as the JSON body. 422 was chosen over402 Payment Requiredso that "billing quota exhausted" and "namespace not configured" stay distinguishable in monitoring; 422 is already used in this file and doesn't collide with the existing 404 (not enabled) / 403 (no license, no enabled namespaces) meanings. - MCP (
ee/lib/api/orbit/mcp.rb) — a JSON-RPC error entry reusing the standardinvalid_requestcode (-32600) with the contract underdata. Clients discriminate on the app-levelcodefield insidedata, not on the JSON-RPC code. This covers all MCP clients, third-party and GitLab-managed alike — no separate MR needed for MCP.
Screenshots
REST:
MCP:

