Rich 403s + Permission Introspection — Self-Healing RBAC for Agents
2026-03-13
LOCKSTEP TRACEABILITY MATRIX --- api_endpoints:
- "GET /v1/rbac/my-permissions"
- "PATCH /v1/api-keys/:id"
sdk_methods_updated:
- "client.rbac.myPermissions()"
- "client.apiKeys.update(id, params)"
mcp_tools_updated:
- "br_my_permissions"
---
What We Built
Every 403 response from BrainstormRouter now includes structured remediation: the exact permission you need, which roles would grant it, whether you can self-service fix it, and what action to take. No more "Forbidden" dead ends.
Two new endpoints close the self-service loop. GET /v1/rbac/my-permissions returns the caller's complete permission landscape — granted permissions, denied permissions, and upgrade paths for each denied permission. PATCH /v1/api-keys/:id lets callers modify key scopes, rate limits, budgets, allowed models, and expiration without database access — with a privilege escalation guard that prevents keys from granting roles above their own level.
The computeRemediation() engine is context-aware: it distinguishes API key callers who can self-upgrade (patch_key) from those who need an admin (request_admin), checking both role hierarchy and whether the caller actually has config.write permission. JWT and mTLS callers get appropriate alternative recommendations.
Why It Matters
The previous RBAC surface earned a B+ in GTM assessment. The gap wasn't security — it was developer experience. When an AI agent hits a 403, it needs to know how to fix the problem, not just that a problem exists. A bare "Forbidden" forces the operator to read source code or open a support ticket. A structured 403 with recommended_action: "patch_key" and self_service_possible: true lets the agent self-correct in seconds.
This is the difference between an API you fight and one that teaches you. For platform teams evaluating AI gateways, self-healing permission workflows eliminate an entire category of support tickets.
How It Works
Rich 403 Response
{
"error": {
"message": "Forbidden: requires agent.profile.delete",
"type": "insufficient_permissions",
"required_permission": "agent.profile.delete",
"your_roles": ["operator"],
"roles_that_include_permission": ["admin"],
"self_service_possible": true,
"recommended_action": "patch_key",
"docs_url": "https://docs.brainstormrouter.com/api-reference/rbac"
}
}
Remediation Logic
function computeRemediation(principal, required, grantingRoles) {
const callerBest = Math.min(...principal.roles.map((r) => ROLE_HIERARCHY[r] ?? 999));
const requiredBest = Math.min(...grantingRoles.map((r) => ROLE_HIERARCHY[r] ?? 999));
if (
principal.authMethod === "api_key" &&
requiredBest >= callerBest &&
principal.permissions.has("config.write")
) {
return { self_service_possible: true, recommended_action: "patch_key" };
}
// JWT/mTLS callers can't self-service key scopes
if (principal.authMethod === "mtls" || principal.authMethod === "jwt") {
return grantingRoles.includes("agent")
? { recommended_action: "use_agent_jwt" }
: { recommended_action: "request_admin" };
}
return { self_service_possible: false, recommended_action: "request_admin" };
}
Introspection
curl -H "Authorization: Bearer br_live_..." \
https://api.brainstormrouter.com/v1/rbac/my-permissions
Returns roles, granted/denied permissions, and upgrade paths with self-service feasibility for each denied permission.
Key Self-Service
curl -X PATCH \
-H "Authorization: Bearer br_live_..." \
-d '{"scopes": ["admin"], "rate_limit_rpm": 120}' \
https://api.brainstormrouter.com/v1/api-keys/KEY_ID
Privilege escalation guard: a developer key cannot grant itself admin. Cache invalidation fires on every update (scopes, rate limits, budgets, allowed models, expiration) since all fields are part of the 30s Redis-cached auth state.
The Numbers
| Metric | Before | After |
|---|---|---|
| Permission debugging time | ~45 min (read source, ECS exec) | ~30 sec (read 403 response) |
| Self-service scope change | Impossible (requires DB access) | PATCH endpoint, <1s |
| Agent management grade | B+ | A- |
| New test cases | — | 13 (48 total RBAC tests) |
| Cache invalidation coverage | Scope changes only | All mutable fields |
Competitive Edge
No competing AI gateway (Portkey, OpenRouter, Martian) provides structured remediation in 403 responses. They return "Forbidden" or "Unauthorized" and leave the caller guessing. BrainstormRouter's approach treats every error as a teaching moment — the 403 response itself contains enough information for an AI agent to diagnose and fix the problem autonomously. Combined with permission introspection and key self-service, this creates a self-healing permission workflow that eliminates an entire class of support interactions.
Lockstep Checklist
> _You MUST check these boxes [x] and verify the corresponding files are updated BEFORE committing this log._
- [x] API Routes:
src/api/routes/rbac-introspection.ts(new),src/api/routes/keys.ts(PATCH handler added). - [x] TS SDK:
packages/sdk-ts—rbac.myPermissions(),apiKeys.update(), types exported. - [x] Python SDK:
packages/sdk-py—rbac.my_permissions()(sync + async),api_keys.update()with sentinel pattern. - [x] MCP Schemas:
br_my_permissionstool insrc/mcp/tool-manifest.tsandsrc/mcp/server.ts. - [x] Master Record: Capability reflected in
site/.well-known/agents.json(65 tools).