Permissions¶
The permission system provides fine-grained access control for file operations and shell commands. It uses pattern-based rules to allow, deny, or require approval for specific operations.
Quick Start¶
from pydantic_ai_backends import LocalBackend
from pydantic_ai_backends.permissions import DEFAULT_RULESET
# Use the default safe ruleset
backend = LocalBackend(
root_dir="/workspace",
permissions=DEFAULT_RULESET,
)
# Reads are allowed (except secrets)
content = backend.read("app.py") # Works
# Writes require approval (in sync context, denied by default)
result = backend.write("output.txt", "data") # Denied without callback
Permission Actions¶
Each operation can result in one of three actions:
| Action | Behavior |
|---|---|
allow |
Operation proceeds immediately |
deny |
Operation is blocked with an error |
ask |
Requires user approval via callback |
Pre-configured Presets¶
DEFAULT_RULESET¶
Safe defaults for development environments:
- Read: Allowed, except for secrets (
.env,.pem,credentials, etc.) - Write/Edit: Requires approval
- Execute: Requires approval, dangerous commands blocked
- Glob/Grep/Ls: Allowed
from pydantic_ai_backends.permissions import DEFAULT_RULESET
backend = LocalBackend(root_dir="/workspace", permissions=DEFAULT_RULESET)
PERMISSIVE_RULESET¶
For trusted environments where most operations should succeed:
- Read: Allowed, except secrets
- Write/Edit: Allowed, except secrets and system files
- Execute: Allowed, dangerous commands blocked
- Glob/Grep/Ls: Allowed
from pydantic_ai_backends.permissions import PERMISSIVE_RULESET
backend = LocalBackend(root_dir="/workspace", permissions=PERMISSIVE_RULESET)
READONLY_RULESET¶
For read-only access:
- Read: Allowed, except secrets
- Write/Edit/Execute: Denied
- Glob/Grep/Ls: Allowed
from pydantic_ai_backends.permissions import READONLY_RULESET
backend = LocalBackend(root_dir="/workspace", permissions=READONLY_RULESET)
STRICT_RULESET¶
Everything requires explicit approval:
- All operations: Require approval
- Secrets: Denied
from pydantic_ai_backends.permissions import STRICT_RULESET
backend = LocalBackend(root_dir="/workspace", permissions=STRICT_RULESET)
Custom Rulesets¶
Using create_ruleset()¶
Quick factory for common configurations:
from pydantic_ai_backends.permissions import create_ruleset
# Allow reads and writes, but ask for execute
ruleset = create_ruleset(
allow_read=True,
allow_write=True,
allow_execute=False, # Will require approval
deny_secrets=True, # Block access to sensitive files
)
Full Custom Configuration¶
For complete control, build a PermissionRuleset:
from pydantic_ai_backends.permissions import (
PermissionRuleset,
OperationPermissions,
PermissionRule,
)
custom_permissions = PermissionRuleset(
default="deny", # Global default for unconfigured operations
read=OperationPermissions(
default="allow",
rules=[
# Deny access to secrets
PermissionRule(
pattern="**/.env*",
action="deny",
description="Protect environment files",
),
PermissionRule(
pattern="**/secrets/**",
action="deny",
description="Protect secrets directory",
),
],
),
write=OperationPermissions(
default="ask",
rules=[
# Auto-allow Python files
PermissionRule(pattern="**/*.py", action="allow"),
# Auto-allow markdown
PermissionRule(pattern="**/*.md", action="allow"),
],
),
execute=OperationPermissions(
default="deny",
rules=[
# Allow safe git commands
PermissionRule(pattern="git status", action="allow"),
PermissionRule(pattern="git diff*", action="allow"),
PermissionRule(pattern="git log*", action="allow"),
# Allow Python execution
PermissionRule(pattern="python *", action="allow"),
PermissionRule(pattern="pytest *", action="allow"),
],
),
# Allow all search operations
glob=OperationPermissions(default="allow"),
grep=OperationPermissions(default="allow"),
ls=OperationPermissions(default="allow"),
)
Pattern Syntax¶
Patterns use glob-style matching:
| Pattern | Matches |
|---|---|
* |
Any characters except / |
** |
Any characters including / (recursive) |
? |
Any single character |
[abc] |
Any character in the set |
[!abc] |
Any character not in the set |
Examples¶
# Match .env files anywhere
PermissionRule(pattern="**/.env", action="deny")
# Match all .env* files (including .env.local, .env.production)
PermissionRule(pattern="**/.env*", action="deny")
# Match files in secrets directory
PermissionRule(pattern="**/secrets/**", action="deny")
# Match specific command
PermissionRule(pattern="rm -rf *", action="deny")
# Match command prefix
PermissionRule(pattern="git *", action="allow")
Ask Callback¶
When a permission rule resolves to "ask", the system invokes an AskCallback to determine whether the operation should proceed. The callback is an async function with the following signature:
from pydantic_ai_backends.permissions import AskCallback
# Type: Callable[[PermissionOperation, str, str], Awaitable[bool]]
# operation target reason
CLI Approval¶
For interactive CLI applications, prompt the user directly:
async def cli_approval(
operation: str, # "read", "write", "edit", "execute", etc.
target: str, # Path or command
reason: str, # Why approval is needed
) -> bool:
response = input(f"Allow {operation} on '{target}'? [y/N] ")
return response.lower() == "y"
backend = LocalBackend(
root_dir="/workspace",
permissions=DEFAULT_RULESET,
ask_callback=cli_approval,
)
Web App Approval¶
For web applications, you might store pending approvals and wait for a user response:
import asyncio
# In-memory store for pending approvals
pending_approvals: dict[str, asyncio.Future[bool]] = {}
async def web_approval(operation: str, target: str, reason: str) -> bool:
"""Send approval request to frontend via WebSocket, wait for response."""
approval_id = f"{operation}:{target}"
future: asyncio.Future[bool] = asyncio.get_event_loop().create_future()
pending_approvals[approval_id] = future
# Your WebSocket/SSE notification logic here
await notify_frontend(operation, target, reason)
try:
# Wait up to 60 seconds for user to respond
return await asyncio.wait_for(future, timeout=60.0)
except asyncio.TimeoutError:
return False # Deny on timeout
finally:
pending_approvals.pop(approval_id, None)
Auto-Approve with Logging¶
For trusted environments where you want to allow everything but keep an audit trail:
import logging
logger = logging.getLogger("permissions")
async def auto_approve_with_logging(
operation: str, target: str, reason: str
) -> bool:
logger.info(f"Auto-approved: {operation} on '{target}' ({reason})")
return True
backend = LocalBackend(
root_dir="/workspace",
permissions=STRICT_RULESET, # Everything asks
ask_callback=auto_approve_with_logging,
)
Conditional Approval¶
Implement logic that auto-approves some operations and denies others:
# Auto-approve writes to temp directories, deny everything else
async def conditional_approval(
operation: str, target: str, reason: str
) -> bool:
if operation == "write" and target.startswith("/temp/"):
return True
if operation == "execute" and target.startswith("python "):
return True
return False
Ask Fallback¶
When a permission rule resolves to "ask" but no ask_callback is provided (or the callback returns False), the AskFallback setting controls what happens.
There are two fallback modes:
| Fallback | Behavior |
|---|---|
"deny" |
Raises PermissionDeniedError (operation silently blocked) |
"error" |
Raises PermissionError (signals that approval was needed but unavailable) |
from pydantic_ai_backends.permissions import AskFallback
# Deny operations that need approval (safe for automated pipelines)
backend = LocalBackend(
root_dir="/workspace",
permissions=DEFAULT_RULESET,
ask_fallback="deny",
)
# Raise PermissionError - useful for detecting missing callback setup
backend = LocalBackend(
root_dir="/workspace",
permissions=DEFAULT_RULESET,
ask_fallback="error",
)
Using PermissionChecker Directly¶
For programmatic use outside of backends, use PermissionChecker with explicit callback and fallback:
from pydantic_ai_backends.permissions import PermissionChecker, DEFAULT_RULESET
async def my_callback(op: str, target: str, reason: str) -> bool:
return input(f"Allow {op} on {target}? ").lower() == "y"
checker = PermissionChecker(
ruleset=DEFAULT_RULESET,
ask_callback=my_callback,
ask_fallback="error", # Raise if callback unavailable
)
# Async check - invokes callback for "ask" actions
try:
allowed = await checker.check("write", "/config/settings.yaml", "Save settings")
except PermissionError:
print("No callback available to ask for permission")
except PermissionDeniedError:
print("Permission explicitly denied")
# Sync check - returns action without invoking callback
action = checker.check_sync("write", "/config/settings.yaml")
if action == "ask":
print("This operation would need approval")
Integration with LocalBackend¶
Permissions are checked after allowed_directories:
backend = LocalBackend(
root_dir="/workspace",
allowed_directories=["/workspace", "/data"], # Checked first
permissions=DEFAULT_RULESET, # Checked second
)
# Path must pass BOTH checks:
# 1. Is the path within allowed_directories?
# 2. Does the permission ruleset allow this operation?
Integration with Console Toolset¶
Pass permissions to the toolset:
from pydantic_ai_backends import create_console_toolset
from pydantic_ai_backends.permissions import DEFAULT_RULESET
# Toolset uses permissions to determine approval requirements
toolset = create_console_toolset(permissions=DEFAULT_RULESET)
When permissions is provided, the legacy require_write_approval and require_execute_approval flags are ignored.
PermissionChecker¶
For programmatic permission checking:
from pydantic_ai_backends.permissions import PermissionChecker, DEFAULT_RULESET
checker = PermissionChecker(ruleset=DEFAULT_RULESET)
# Synchronous check (returns action without asking)
action = checker.check_sync("read", "/path/to/file.txt")
# Returns: "allow", "deny", or "ask"
# Convenience methods
if checker.is_allowed("read", "/path/to/file.txt"):
print("Read is allowed")
if checker.is_denied("write", "/etc/passwd"):
print("Write is denied")
if checker.requires_approval("execute", "rm -rf /tmp/*"):
print("Execute requires approval")
Built-in Patterns¶
The presets use these common patterns:
SECRETS_PATTERNS¶
SECRETS_PATTERNS = [
"**/.env",
"**/.env.*",
"**/*.pem",
"**/*.key",
"**/*.crt",
"**/credentials*",
"**/secrets*",
"**/*secret*",
"**/*password*",
"**/.aws/**",
"**/.ssh/**",
"**/.gnupg/**",
]
SYSTEM_PATTERNS¶
SYSTEM_PATTERNS = [
"/etc/**",
"/var/**",
"/usr/**",
"/bin/**",
"/sbin/**",
"/boot/**",
"/sys/**",
"/proc/**",
]
Next Steps¶
- Backends - Backend configuration
- Console Toolset - Tool configuration
- API Reference - Complete API