RBAC policies are attached to agent and subagent tokens. They control which actions an agent can perform and which resources it can access.
Policy Structure
class RBACPolicy:
allowed_actions: list[str] # Default: ["*:*:*"]
denied_actions: list[str] # Default: []
allowed_resources: list[str] # Default: ["*"]
denied_resources: list[str] # Default: []
max_sensitivity_level: int # Default: 4 (0-4 scale)
Pattern Matching
RBAC uses fnmatch glob patterns through action_matches_pattern() from the shared taxonomy module. This supports:
| Pattern | Matches |
|---|
*:*:* | All actions |
data:read:* | All data read actions |
data:*:* | All data domain actions |
code:review:pull_request | Exact match |
data:read:user_* | Actions starting with user_ |
Actions follow the domain:operation:resource taxonomy defined in src/shared/taxonomy.py.
Evaluation Order (Deny-First)
RBAC evaluation follows a strict deny-first order:
1. Check denied_actions → if action matches any deny pattern → DENIED
2. Check allowed_actions → if action matches no allow pattern → DENIED
3. Check denied_resources → if resource matches any deny pattern → DENIED
4. Check allowed_resources → if resource matches no allow pattern → DENIED
5. Check sensitivity_level → if level exceeds max_sensitivity_level → DENIED
6. All checks pass → ALLOWED
An explicit deny always wins over an allow. There is no way to override a denied action pattern.
Examples
Full access
Read-only
Code review
{
"allowed_actions": ["*:*:*"],
"denied_actions": [],
"allowed_resources": ["*"],
"denied_resources": [],
"max_sensitivity_level": 4
}
{
"allowed_actions": ["data:read:*", "code:read:*"],
"denied_actions": ["data:write:*", "data:delete:*"],
"allowed_resources": ["repo:frontend", "repo:backend"],
"denied_resources": ["repo:infrastructure"],
"max_sensitivity_level": 2
}
{
"allowed_actions": ["code:review:*", "code:read:*"],
"denied_actions": ["code:write:*", "code:deploy:*"],
"allowed_resources": ["repo:*"],
"denied_resources": ["repo:secrets", "repo:keys"],
"max_sensitivity_level": 3
}
Permission Narrowing
When creating a subagent token, the child’s RBAC must be a strict subset of the parent’s RBAC. This is enforced by is_rbac_subset():
child ⊆ parent means:
- child.allowed_actions ⊆ parent.allowed_actions (coverage)
- child.denied_actions ⊇ parent.denied_actions (inherits denies)
- child.allowed_resources ⊆ parent.allowed_resources
- child.denied_resources ⊇ parent.denied_resources
- child.max_sensitivity_level ≤ parent.max_sensitivity_level
Example: Valid narrowing
Parent agent RBAC:
{
"allowed_actions": ["data:*:*"],
"denied_actions": ["data:delete:*"],
"max_sensitivity_level": 3
}
Valid child subagent RBAC:
{
"allowed_actions": ["data:read:*"],
"denied_actions": ["data:delete:*", "data:write:sensitive_*"],
"max_sensitivity_level": 2
}
Invalid child (would be rejected):
{
"allowed_actions": ["data:*:*", "code:*:*"],
"denied_actions": [],
"max_sensitivity_level": 4
}
This fails because:
code:*:* is not covered by parent’s data:*:*
- Parent’s
data:delete:* deny is not inherited
max_sensitivity_level: 4 exceeds parent’s 3
Integration with Event Ingestion
RBAC is enforced as a FastAPI dependency on the POST /events route:
@router.post("", response_model=EventResponse)
async def ingest_event(
event: AgentEventCreate,
request: Request,
rbac_check=Depends(enforce_rbac),
...
):
event_action = event.action
target = event.target_resource
rbac_check(event_action, target) # Raises RBACDeniedError if denied
If the request uses legacy X-API-Key auth (no token), RBAC is skipped.
Error Response
When RBAC denies an action:
{
"detail": "Action 'data:write:production_db' denied: resource 'production_db' matched deny pattern 'production_*'"
}
HTTP status: 403 Forbidden