feat(billing): add CustomersDot usage quota gate for mcp/rest queries
Summary
Adds a pre-execution usage quota gate for mcp and rest source types. Before executing a query the server sends HEAD /api/v1/consumers/resolve to CustomersDot (with X-Admin-Email / X-Admin-Token credentials) and caches the decision in-process. Requests for frontend, core, and dws bypass the gate entirely.
CDot response → GKG decision → cache behaviour
| CDot status | GKG decision | gRPC status returned | Cached | TTL |
|---|---|---|---|---|
200 OK |
Allow | (query runs) | Yes | Cache-Control: max-age or 3600 s fallback |
402 Payment Required |
Deny – quota exhausted | RESOURCE_EXHAUSTED |
Yes | Cache-Control: max-age or 3600 s fallback |
403 Forbidden |
Allow (fail-open) | (query runs) | No | - |
| Any other 4xx / 5xx | Allow (fail-open) | (query runs) | No | - |
| Connection error / timeout | Allow (fail-open) | (query runs) | No | - |
Only 200 and 402 outcomes are cached (per-pod moka, max 10 000 entries, TTL ± 10 % jitter). Fail-open outcomes are never cached. Each request retries CDot.
Configuration
billing:
quota:
enabled: true # switch to enable access cut-off check
customers_dot_url: "http://localhost:5000"
api_user: "<CDOT_ADMIN_USER>"
api_token: "<CDOT_ADMIN_TOKEN>"
request_timeout_ms: 1000If api_user or api_token is empty while enabled: true, the gate disables itself with a warning to avoid silent fail-open on 401.
Steps to reproduce (local end-to-end)
Prerequisites
- GDK running (Rails on port 3000, CDot on port 5000, ClickHouse on port 9001)
- Orbit billable namespace set as per instructions here - gitlab-org/gitlab!233102 (merged).
- CDot running. Admin user created (
<CDOT_ADMIN_USER>/<CDOT_ADMIN_TOKEN>)
Create a personal access token
# In the GDK directory
mise exec -- bin/rails runner "
user = User.find_by(username: '<YOUR-GITLAB-USERNAME>')
pat = user.personal_access_tokens.create!(
name: 'gkg-quota-test',
scopes: ['api'],
expires_at: 1.day.from_now
)
puts pat.token
"Save the printed token as <YOUR-PAT>.
Start GKG with quota enabled
GKG_BILLING__QUOTA__ENABLED=true \
GKG_BILLING__QUOTA__CUSTOMERS_DOT_URL=http://localhost:5000 \
GKG_BILLING__QUOTA__API_USER=<CDOT_ADMIN_USER> \
GKG_BILLING__QUOTA__API_TOKEN=<CDOT_ADMIN_TOKEN> \
mise server:startAllow path (CDot returns 200)
Use a PAT belonging to a namespace that has an active CDot subscription:
curl -s -X POST http://localhost:3000/api/v4/orbit/query \
-H "Authorization: Bearer <PAT>" \
-H "Content-Type: application/json" \
-d '{
"query": {
"query_type": "neighbors",
"node": {
"id": "me",
"entity": "User",
"node_ids": [
22
]
},
"neighbors": {
"node": "me",
"direction": "both"
},
"limit": 100
},
"format": "raw"
}'Expected: 200 OK with query results.
Deny path (CDot returns 402)
- No subscription - the namespace has no CDot subscription at all.
- Quota exhausted - the namespace has a subscription but credits are depleted. This cannot be tested currently because we need to add to configure the
orbit_workflow_completionevent as billable on CDot.
Use a PAT belonging to a user who has as Orbit billable namespace with with no CDot subscription:
curl -s -X POST http://localhost:3000/api/v4/orbit/query \
-H "Authorization: Bearer <PAT>" \
-H "Content-Type: application/json" \
-d '{
"query": {
"query_type": "neighbors",
"node": {
"id": "me",
"entity": "User",
"node_ids": [
22
]
},
"neighbors": {
"node": "me",
"direction": "both"
},
"limit": 100
},
"format": "raw"
}'Expected: 402 Payment Required with {"code":"quota_exhausted","message":"GitLab credits exhausted"} (after the Workhorse MR merges).
Bypass path (non-metered source types)
The source_type claim in the JWT is set by Rails based on the calling context. For frontend, core, or dws callers, no CDot call is made regardless of subscription state.
Fail-open path
Point customers_dot_url at a non-existent host or a stub returning 5xx. Expect all queries to proceed.
GKG_BILLING__QUOTA__ENABLED=true \
GKG_BILLING__QUOTA__CUSTOMERS_DOT_URL=http://127.0.0.1:1 \
GKG_BILLING__QUOTA__API_USER=<CDOT_ADMIN_USER> \
GKG_BILLING__QUOTA__API_TOKEN=<CDOT_ADMIN_TOKEN> \
mise server:startExpected: queries succeed; GKG logs quota check request failed; failing open.
Related to https://gitlab.com/gitlab-org/orbit/knowledge-graph/-/work_items/487