Skip to main content
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:
PatternMatches
*:*:*All actions
data:read:*All data read actions
data:*:*All data domain actions
code:review:pull_requestExact 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

{
  "allowed_actions": ["*:*:*"],
  "denied_actions": [],
  "allowed_resources": ["*"],
  "denied_resources": [],
  "max_sensitivity_level": 4
}

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