Permissions
Control what tools can do with a flexible permission system. Protect sensitive operations while allowing the model to work freely on safe tasks.
Why Permissions?
AI models are powerful but unpredictable. Without guardrails, a model might:
- Delete important files
- Execute destructive commands
- Access sensitive data
- Make unintended API calls
The permission system lets you allow helpful actions while blocking dangerous ones—with user approval as a middle ground.
Permission Policies
Policies define the rules for each tool or pattern of tools. Each policy specifies one of three actions:
| Policy | Behavior |
|---|---|
allow |
Execute immediately, no questions |
deny |
Block execution, return error to model |
ask |
Prompt user for approval each time |
Policy Matching
Policies can match:
- Exact tool names —
execute_command - Wildcards —
file_*matches all file tools - All tools —
*as default
More specific patterns take precedence over wildcards.
from shared.plugins.permission import (
create_plugin as create_permission,
PermissionPolicy
)
# Create permission plugin
perm = create_permission()
# Define policies
policies = [
# Safe tools: always allow
PermissionPolicy(
pattern="read_file",
action="allow"
),
PermissionPolicy(
pattern="list_directory",
action="allow"
),
# Dangerous: always deny
PermissionPolicy(
pattern="execute_command",
action="deny"
),
# File writes: ask user
PermissionPolicy(
pattern="write_file",
action="ask"
),
PermissionPolicy(
pattern="delete_file",
action="ask"
),
# Default: ask for unknown tools
PermissionPolicy(
pattern="*",
action="ask"
)
]
perm.initialize({"policies": policies})
Configuration
Permission policies can be configured via code or a JSON file.
Config File
For easier management, define policies in a JSON file:
Configuration Options
| Option | Description |
|---|---|
policies |
List of permission policies |
default_action |
Action for unmatched tools (ask) |
ask_timeout |
Seconds to wait for user response |
remember_session |
Remember user choices this session |
{
"default_action": "ask",
"ask_timeout": 60,
"remember_session": true,
"policies": [
{
"pattern": "read_*",
"action": "allow",
"description": "Reading is safe"
},
{
"pattern": "list_*",
"action": "allow"
},
{
"pattern": "search_*",
"action": "allow"
},
{
"pattern": "write_*",
"action": "ask",
"description": "Confirm before writing"
},
{
"pattern": "delete_*",
"action": "ask",
"description": "Confirm before deleting"
},
{
"pattern": "execute_command",
"action": "deny",
"description": "Shell commands blocked"
}
]
}
from shared.plugins.permission import create_plugin
perm = create_plugin()
perm.initialize({
"config_path": "permissions.json"
})
# Use with client
client.configure_tools(
registry,
permission_plugin=perm
)
Using with Client
Pass the permission plugin to configure_tools() to
enable permission checking for all tool calls.
How It Works
- Model decides to call a tool
- Permission plugin checks the policy
- Based on policy:
allow: Tool executesdeny: Error returned to modelask: User prompted
- Result returned to model
User Prompts
When a tool requires approval, the user sees the tool name and arguments, and can approve or deny.
from jaato import JaatoClient, PluginRegistry
from shared.plugins.permission import (
create_plugin as create_permission,
PermissionPolicy
)
# Create client
client = JaatoClient()
client.connect() # Reads JAATO_PROVIDER and MODEL_NAME from env
# Create registry and expose tools
registry = PluginRegistry(model_name=client.model_name)
registry.discover()
registry.expose_tool("cli")
registry.expose_tool("file_edit")
# Create permission plugin
perm = create_permission()
perm.initialize({
"policies": [
PermissionPolicy("read_file", "allow"),
PermissionPolicy("write_file", "ask"),
PermissionPolicy("execute_command", "ask"),
PermissionPolicy("*", "ask"),
],
"remember_session": True
})
# Configure with permissions
client.configure_tools(
registry,
permission_plugin=perm
)
# Now tool calls go through permission check
response = client.send_message(
"Delete the temp files",
on_output=handler
)
# User will be prompted before delete executes
IPC / Daemon Permissions
Everything above describes the in-process plugin
API (PermissionPolicy(pattern, action), the
default_action config field,
configure_tools(..., permission_plugin=...)), where your
own code owns the runtime. When you instead talk to a running
daemon through the
IPCClient
facade, the gate moves server-side: the policy lives in the session
profile, and approvals flow back to your process over
a single callback.
Policy in the profile
Set the policy in the session profile under
plugin_configs.permission.policy.defaultPolicy:
"ask"— every gated tool (cli,file_edit, …) is held for approval before it runs."allow"— gated tools auto-allow, for an unattended loop.
Approving with on_permission
Pass an on_permission callback to
IPCClient.session(...). It receives the permission
event and returns "y" /
"n" (it may be a coroutine). Read the requested tool off
ev.tool_name — for example
on_permission=lambda ev: "y" if approve(ev.tool_name) else "n".
on_permission is set
(or it never answers), the daemon auto-denies the
tool and the in-flight ask() / complete()
raises PermissionUnhandled. This is an intentional
fail-closed boundary, not a bypass — an unattended client can never
silently run a gated tool.
Two surfaces, two names
The names rhyme but scope to different surfaces, so keep them apart:
- In-process plugin —
default_action(the config field documented above). - Daemon / profile —
plugin_configs.permission.policy.defaultPolicy(the facade path in this section).
See IPCClient
for the full session(...) surface and the
PermissionUnhandled error.
import asyncio
from jaato_sdk import IPCClient
def approve(tool_name: str) -> bool:
# In a TUI this would prompt; here, auto-approve.
return True
async def main():
async with IPCClient.session(
profile={
"model": "gpt-4o",
"provider": "openai",
"plugins": ["cli"],
"plugin_configs": {
"permission": {"policy": {"defaultPolicy": "ask"}},
},
},
on_permission=lambda ev: "y" if approve(ev.tool_name) else "n",
) as s:
print(await s.ask("What is the current date? Run `date`."))
asyncio.run(main())
# defaultPolicy "allow": gated tools run without prompting,
# so no on_permission is needed for a headless loop.
async with IPCClient.session(profile={
"model": "gpt-4o",
"provider": "openai",
"plugins": ["cli", "web_search", "todo"],
"plugin_configs": {
"permission": {"policy": {"defaultPolicy": "allow"}},
},
}) as s:
print(await s.ask(
"Get the date with `date` and the listing with `ls`, "
"then write both into a file called report.txt."
))
Auto-Approved Tools
Plugins can declare certain tools as "auto-approved"—these bypass permission checks entirely. Use for tools that are inherently safe.
When to Use
- Read-only operations
- Local calculations
- Status queries
- Tools with no side effects
class MyPlugin:
def get_tool_schemas(self):
return [
ToolSchema(name="get_status", ...),
ToolSchema(name="calculate", ...),
ToolSchema(name="write_file", ...),
]
def get_auto_approved_tools(self):
"""
These tools skip permission checks.
"""
return [
"get_status", # Read-only
"calculate", # No side effects
# NOT write_file - has side effects
]
# See all auto-approved tools
registry.discover()
registry.expose_tool("my_plugin")
plugin = registry.get_plugin("my_plugin")
auto = plugin.get_auto_approved_tools()
print(f"Auto-approved: {auto}")
# These will execute without permission check
# even if policy says "ask" or "deny"
Ask Channels
When a tool needs user approval, the permission plugin uses an "ask channel" to communicate with the user.
Built-in Channels
| Channel | Description |
|---|---|
console |
Prompt in terminal (default) |
queue |
Queue requests for async handling |
auto_deny |
Deny all without asking |
auto_allow |
Allow all without asking |
Queue Channel
The queue channel is useful for GUI apps or async workflows.
Permission requests are queued and can be processed separately from
the main execution flow.
# Console channel prompts in terminal
perm.initialize({
"policies": policies,
"ask_channel": "console"
})
# User sees:
# Allow tool "write_file" with args {"path": "config.json"}?
# [y/n]:
# Queue channel for async/GUI apps
perm.initialize({
"policies": policies,
"ask_channel": "queue"
})
# Permission requests are queued
# Your app processes them asynchronously
# Get pending requests
pending = perm.get_pending_requests()
for req in pending:
print(f"Tool: {req.tool_name}")
print(f"Args: {req.args}")
# Process in your UI, then respond
perm.respond_to_request(
req.request_id,
approved=True # or False
)
# Auto-allow all (useful for testing)
perm.initialize({
"policies": policies,
"ask_channel": "auto_allow"
})
# Auto-deny all (safe default)
perm.initialize({
"policies": policies,
"ask_channel": "auto_deny"
})
Session Memory
With remember_session=True, user choices are remembered
for the current session. This avoids asking repeatedly for the same
tool.
How It Works
- User approves
write_file - Future
write_filecalls auto-approve - Memory clears when session ends
Granularity
Memory can be per-tool or per-tool-with-args:
per_tool: Approve once for all callsper_call: Approve each unique call
perm.initialize({
"policies": policies,
# Remember approvals this session
"remember_session": True,
# Granularity: "per_tool" or "per_call"
"remember_granularity": "per_tool",
})
# With per_tool:
# - User approves write_file once
# - All write_file calls auto-approve
# With per_call:
# - User approves write_file("a.txt")
# - write_file("a.txt") auto-approves
# - write_file("b.txt") still asks
# Clear all remembered approvals
perm.clear_session_memory()
# Check current memory
memory = perm.get_session_memory()
for tool, approved in memory.items():
status = "approved" if approved else "denied"
print(f"{tool}: {status}")
Best Practices
Default to Ask
Use ask as the default action. Only allow
tools you've verified are safe.
Be Specific
Use specific patterns rather than broad wildcards. read_file
is better than *_file.
Document Policies
Add descriptions to policies so users understand why they're being asked.
Review Regularly
As you add tools, review your permission policies to ensure new tools are covered appropriately.
{
"default_action": "ask",
"remember_session": true,
"remember_granularity": "per_tool",
"ask_timeout": 30,
"policies": [
// Explicitly safe - allow
{"pattern": "read_file", "action": "allow"},
{"pattern": "list_directory", "action": "allow"},
{"pattern": "search_files", "action": "allow"},
{"pattern": "get_*", "action": "allow"},
// Destructive - ask with description
{
"pattern": "write_file",
"action": "ask",
"description": "This will modify a file"
},
{
"pattern": "delete_*",
"action": "ask",
"description": "This will delete data"
},
// Very dangerous - deny
{
"pattern": "execute_command",
"action": "deny",
"description": "Shell commands are blocked"
},
{
"pattern": "*_production_*",
"action": "deny",
"description": "Production access blocked"
}
]
}
Next Steps
- Tools — Understand tool execution
- Building Plugins — Create plugins with auto-approved tools
- Permission Plugin Reference — Full API details
# Policy actions
"allow" # Execute immediately
"deny" # Block with error
"ask" # Prompt user
# Pattern matching
"tool_name" # Exact match
"read_*" # Wildcard suffix
"*_file" # Wildcard prefix
"*" # Match all (default)
# Ask channels
"console" # Terminal prompt
"queue" # Async queue for GUI apps
"auto_deny" # Deny all asks
"auto_allow" # Allow all asks
# Memory
"per_tool" # Remember by tool name
"per_call" # Remember by tool + args