RBAC for AI Agents: Role-Based Access Control for Tool Permissions
Implement role-based access control for AI agents to restrict which tools each user can invoke, define permission models, enforce authorization at the tool level, and maintain audit logs for compliance.
Why AI Agents Need RBAC
Traditional RBAC controls which API endpoints a user can access. AI agent RBAC must go further because an agent acts on behalf of the user — and a single agent endpoint might expose dozens of tools with varying sensitivity levels. Without tool-level access control, every user gets access to every tool the agent has, regardless of whether they should.
Consider a customer support agent with tools for looking up orders, processing refunds, accessing customer PII, and modifying account settings. A tier-1 support representative should look up orders and process small refunds. Only supervisors should access PII or modify account settings. The agent needs to enforce these boundaries.
Defining the Permission Model
Start by modeling roles, permissions, and tool mappings:
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class Permission(Enum):
# Read permissions
READ_ORDERS = "read:orders"
READ_CUSTOMERS = "read:customers"
READ_PII = "read:pii"
READ_ANALYTICS = "read:analytics"
# Write permissions
PROCESS_REFUND_SMALL = "write:refund:small" # Up to $50
PROCESS_REFUND_LARGE = "write:refund:large" # Up to $500
MODIFY_ACCOUNT = "write:account:modify"
DELETE_ACCOUNT = "write:account:delete"
# Admin permissions
ADMIN_VIEW_LOGS = "admin:logs"
ADMIN_MANAGE_USERS = "admin:users"
@dataclass
class Role:
name: str
permissions: set[Permission]
max_refund_amount: float = 0.0
description: str = ""
# Define roles
ROLES = {
"tier1_support": Role(
name="tier1_support",
permissions={
Permission.READ_ORDERS,
Permission.READ_CUSTOMERS,
Permission.PROCESS_REFUND_SMALL,
},
max_refund_amount=50.0,
description="Basic support: view orders, small refunds",
),
"tier2_support": Role(
name="tier2_support",
permissions={
Permission.READ_ORDERS,
Permission.READ_CUSTOMERS,
Permission.READ_PII,
Permission.PROCESS_REFUND_SMALL,
Permission.PROCESS_REFUND_LARGE,
Permission.MODIFY_ACCOUNT,
},
max_refund_amount=500.0,
description="Senior support: PII access, large refunds, account edits",
),
"admin": Role(
name="admin",
permissions=set(Permission), # All permissions
max_refund_amount=10000.0,
description="Full access to all agent tools",
),
}
@dataclass
class User:
id: str
email: str
role_name: str
@property
def role(self) -> Role:
return ROLES[self.role_name]
def has_permission(self, permission: Permission) -> bool:
return permission in self.role.permissions
Tool Authorization Decorator
A decorator that checks permissions before executing any tool:
import functools
import time
from typing import Callable
class AuthorizationError(Exception):
def __init__(self, user_id: str, tool_name: str, required_permission: str):
self.user_id = user_id
self.tool_name = tool_name
self.required_permission = required_permission
super().__init__(
f"User {user_id} lacks permission {required_permission} for tool {tool_name}"
)
def require_permission(*permissions: Permission):
"""Decorator that enforces permission checks on agent tools."""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
# Extract user from the agent context
context = kwargs.get("context") or (args[0] if args else None)
if not context or not hasattr(context, "current_user"):
raise AuthorizationError("unknown", func.__name__, str(permissions))
user: User = context.current_user
missing = [p for p in permissions if not user.has_permission(p)]
if missing:
# Log the denied access attempt
audit_logger.log_denied(
user_id=user.id,
tool=func.__name__,
required=str(missing),
)
raise AuthorizationError(
user.id, func.__name__, str(missing)
)
# Log the authorized access
audit_logger.log_allowed(
user_id=user.id,
tool=func.__name__,
)
return await func(*args, **kwargs)
wrapper._required_permissions = permissions
return wrapper
return decorator
Applying RBAC to Agent Tools
@require_permission(Permission.READ_ORDERS)
async def lookup_order(context, order_id: str) -> dict:
"""Look up an order by ID."""
return await db.orders.find_one({"id": order_id})
@require_permission(Permission.READ_PII)
async def get_customer_pii(context, customer_id: str) -> dict:
"""Retrieve customer personal information including address and phone."""
return await db.customers.find_one(
{"id": customer_id},
projection={"name": 1, "email": 1, "phone": 1, "address": 1},
)
@require_permission(Permission.PROCESS_REFUND_SMALL, Permission.PROCESS_REFUND_LARGE)
async def process_refund(context, order_id: str, amount: float, reason: str) -> dict:
"""Process a refund for an order."""
user: User = context.current_user
max_allowed = user.role.max_refund_amount
if amount > max_allowed:
return {
"error": f"Refund amount ${amount} exceeds your limit of ${max_allowed}. "
"Please escalate to a supervisor."
}
return await payment_service.refund(order_id, amount, reason, approved_by=user.id)
Dynamic Tool Filtering
Instead of exposing all tools and checking permissions at call time, filter the tool list before the agent sees it:
See AI Voice Agents Handle Real Calls
Book a free demo or calculate how much you can save with AI voice automation.
class RBACToolFilter:
"""Filter available tools based on user permissions."""
def __init__(self, all_tools: list):
self.all_tools = all_tools
def get_tools_for_user(self, user: User) -> list:
"""Return only tools the user has permission to use."""
authorized_tools = []
for tool in self.all_tools:
func = tool.func if hasattr(tool, "func") else tool
required = getattr(func, "_required_permissions", None)
if required is None:
authorized_tools.append(tool)
continue
if all(user.has_permission(p) for p in required):
authorized_tools.append(tool)
return authorized_tools
# Usage with the OpenAI Agents SDK
from agents import Agent
def create_agent_for_user(user: User) -> Agent:
tool_filter = RBACToolFilter(all_tools=[
lookup_order,
get_customer_pii,
process_refund,
])
authorized_tools = tool_filter.get_tools_for_user(user)
return Agent(
name="Support Agent",
instructions=f"You are helping {user.email} (role: {user.role_name}). "
f"You only have access to the tools listed below.",
tools=authorized_tools,
)
Audit Logging
Every tool invocation — whether allowed or denied — must be logged for compliance:
import json
from datetime import datetime, timezone
class AuditLogger:
def __init__(self, log_store):
self.store = log_store
def _log(self, event_type: str, **kwargs) -> None:
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"event_type": event_type,
**kwargs,
}
self.store.append(json.dumps(entry))
def log_allowed(self, user_id: str, tool: str, **extra) -> None:
self._log("tool_access_granted", user_id=user_id, tool=tool, **extra)
def log_denied(self, user_id: str, tool: str, required: str, **extra) -> None:
self._log(
"tool_access_denied",
user_id=user_id,
tool=tool,
required_permissions=required,
**extra,
)
def log_tool_result(self, user_id: str, tool: str, success: bool, **extra) -> None:
self._log(
"tool_execution",
user_id=user_id,
tool=tool,
success=success,
**extra,
)
audit_logger = AuditLogger(log_store=[])
FAQ
Should the agent know about tools it cannot use?
No. Filter tools before passing them to the agent, not just at execution time. If the agent knows about a tool but cannot use it, it may still try to use it or mention it to the user, creating confusion. When the agent only sees authorized tools, it naturally limits its suggestions and actions to what the user can actually do.
How do I handle permission escalation during a conversation?
Implement a handoff pattern. When the agent encounters an action requiring higher permissions, it should inform the user and offer to escalate to a supervisor. In practice, this means switching to a new agent instance configured with the supervisor's tools, or queuing the action for supervisor approval. Never temporarily grant elevated permissions — this defeats the purpose of RBAC.
How granular should tool permissions be?
Match the granularity to your risk model. Read-only tools with non-sensitive data can share a single permission. Write tools that modify data or trigger external actions should each have their own permission. Tools accessing PII or financial data should have fine-grained permissions that distinguish between viewing and modifying. Start with coarse permissions and refine as you discover edge cases in production.
#RBAC #Authorization #AISafety #AccessControl #Python #AgenticAI #LearnAI #AIEngineering
CallSphere Team
Expert insights on AI voice agents and customer communication automation.
Try CallSphere AI Voice Agents
See how AI voice agents work for your industry. Live demo available -- no signup required.