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:

  1. 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" }]
    }

    candidates is sourced from the existing Analytics::KnowledgeGraph::GoverningNamespaceFinder — no new finder or model readers were added. This PORO is feature-flag agnostic; callers gate on knowledge_graph_billing.

  2. 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/query and POST /orbit/query/:name
  • ee/lib/analytics/orbit/command_interceptor.rb — query_graph, reached from both the REST agent-command endpoint and the MCP tools/call → invoke_command path

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 through auth_metadata, so a guard there would block read-only introspection that should stay open; and (2) JwtAuth.generate_token wraps its body in rescue StandardError => e and returns nil, 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:

  1. Feature.enabled?(:knowledge_graph_billing, user)
  2. user.user_preference.knowledge_graph_governing_namespace_with_fallback is nil
  3. Analytics::KnowledgeGraph::GoverningNamespaceFinder.new(user).candidates has 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 Entity with the contract as the JSON body. 422 was chosen over 402 Payment Required so 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 standard invalid_request code (-32600) with the contract under data. Clients discriminate on the app-level code field inside data, 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:

image

MCP:

image

Edited by Sharmad Nachnolkar

Merge request reports

Loading
Loading