MCP authentication & rate limiting
Module: apps/mcp/src/auth.ts Token storage: agent_tokens (task 11). Hash is SHA-256 keyed by AGENT_TOKEN_PEPPER; the plaintext format is drk_at_<base64url>.
Request lifecycle
Authorization: Bearer drk_at_…
│
▼
cachedVerify (60s LRU) ──miss──▶ verifyAgentToken (Postgres)
│
│ revoked / expired / agent.status='disabled' → null → 401
▼
bumpRateLimit (Redis INCR rl:{tokenId}:{minute})
│
│ > limit → 429 + Retry-After
▼
setAuthContext({ agentId, orgId, scopes, tokenId })
│
▼
maybeUpdateLastUsed (fire-and-forget; 10% sample, forced after 5 min)Limits
| Token type | Calls / minute |
|---|---|
| Default | 60 |
Read-only (every scope ends with :read) | 600 |
The Redis key is rl:{token_id}:{floor(now/60)} with EXPIRE 60. The window rolls forward every wall-clock minute; we don't bother with sliding windows — the goal is to stop runaway agent loops, not to be precise.
Scope catalogue
Authoritative list: AGENT_SCOPES in packages/db/src/schema/agents.ts.
| Scope | Tools that require it |
|---|---|
tasks:read | get_next_actionable, get_task, … |
tasks:write | claim_task, complete_task, add_task, … |
knowledge:read | search_knowledge |
knowledge:write | store_knowledge, supersede_knowledge |
plans:read | get_plan |
plans:write | propose_plan, instantiate_template, approve_plan |
audit:read | get_audit_log |
Tools call assertScope(ctx, 'tasks:write') at the top of their handler; the error code on miss is auth.insufficient_scope.
Cache invalidation
The middleware subscribes to the Redis channel agent_token.revoked. When a revoke happens (UI or /agents/tokens/{id} API later), the publisher posts a no-op message and every MCP process flushes its token LRU. Subsequent verifications hit Postgres and observe the revoked_at column.
Dev/test bypass
NODE_ENV=testorDEV_AUTH_BYPASS=true— missingAuthorizationreturns
200 with an empty AuthContext. Tools that need a real orgId will reject on their own; this only short-circuits the middleware.
- The env validator (
packages/shared/src/env.ts) refuses to start a
production process with DEV_AUTH_BYPASS=true.
Audit
The auth middleware itself does not write to agent_audit — every tool handler does, with recordAudit({ action: tool_name, … }). This keeps the audit row attached to the business write inside the same transaction.