Permission Plugin
Control which tools the model can execute through policy-based access control with blacklist/whitelist rules and interactive approval.
| Name | permission |
| Type | Client-configured Plugin |
| Tools Provided | None — get_tool_schemas() returns [] |
| User Commands | permissions |
| Auto-approved | permissions |
get_tool_schemas() returns an empty list. The askPermission
executor exists only for internal daemon-runner forwarding. The model calls target tools
directly; the permission middleware (set via
executor.set_permission_plugin()) intercepts each call and prompts for
approval when needed. Exposing askPermission to the model causes the model
to call askPermission instead of the actual tool, creating a redundant
permission flow (source: docstring at plugin.py ~line 594).
Two Distinct Roles
The permission plugin serves two independent roles:
-
Permission Enforcement (Middleware)
Wraps tool execution to check permissions before any tool runs. Enabled viaconfigure_tools(permission_plugin=...) -
Session Management Commands
Exposespermissionsuser command for on-the-fly policy changes. Enabled viaexpose_tool("permission")
from jaato import PluginRegistry
from shared import PermissionPlugin
# Create permission plugin
permission = PermissionPlugin()
permission.initialize({
'channel_type': 'console',
'policy': {
'default': 'ask',
'whitelist_tools': ['readFile', 'web_search'],
'blacklist_tools': ['rm', 'sudo'],
}
})
# Configure on client for enforcement
client.configure_tools(
registry,
permission_plugin=permission,
)
# Optionally expose askPermission tool
registry.expose_tool('permission')
# Enforcement only (no askPermission tool)
client.configure_tools(registry, permission_plugin=permission)
# Enforcement + proactive checks
client.configure_tools(registry, permission_plugin=permission)
registry.expose_tool('permission')
# No enforcement, proactive only (not recommended)
registry.expose_tool('permission')
Configuration Parameters
config_path
Path to permissions.json configuration file.
| Type | str |
| Default | ".jaato/permissions.json" |
channel_type
How to request approval from the user.
| Type | "console" | "webhook" | "file" |
| Default | "console" |
channel_config
Configuration for the approval channel.
| Type | Dict[str, Any] |
| Default | {} |
policy
Inline policy dict (overrides config file).
| Type | Dict[str, Any] |
| Default | From config file |
permission = PermissionPlugin()
permission.initialize({
'channel_type': 'console',
'policy': {
'default': 'ask',
'whitelist_tools': [
'readFile',
'web_search',
'getPlanStatus',
],
'blacklist_tools': [
'rm -rf *',
'sudo *',
],
'whitelist_patterns': ['read*', 'get*'],
'blacklist_patterns': ['*delete*', '*remove*'],
}
})
{
"channel_type": "console",
"channel_timeout": 30,
"default": "ask",
"whitelist_tools": ["readFile", "web_search"],
"blacklist_tools": ["rm", "sudo"],
"whitelist_patterns": ["read*"],
"blacklist_patterns": ["*delete*"]
}
Policy Rules
Permission decisions are made in this order:
- Blacklist check: If tool matches blacklist, deny
- Whitelist check: If tool matches whitelist, allow
- Default policy: Apply default (allow/deny/ask)
Default Policies
| Policy | Behavior |
|---|---|
allow | Auto-approve all unmatched tools |
deny | Auto-deny all unmatched tools |
ask | Prompt channel for each unmatched tool |
Pattern Matching
Whitelist/blacklist support glob patterns:
*matches any characters?matches single character[seq]matches characters in seq
# Permissive: allow all, block dangerous
{
'default': 'allow',
'blacklist_patterns': ['*delete*', '*remove*', 'sudo*'],
}
# Restrictive: deny all, allow specific
{
'default': 'deny',
'whitelist_tools': ['readFile', 'web_search'],
}
# Interactive: ask for unknown
{
'default': 'ask',
'whitelist_tools': ['readFile'], # Auto-approve reads
'blacklist_tools': ['rm'], # Auto-deny deletes
}
User Command
permissions
Manage session permissions on-the-fly. Changes persist for the current session only.
Subcommands
| Command | Description |
|---|---|
permissions show | Display current effective policy |
permissions check <tool> | Test what decision a tool would get |
permissions allow <pattern> | Add to session whitelist |
permissions deny <pattern> | Add to session blacklist |
permissions default <policy> | Set session default (allow/deny/ask) |
permissions clear | Reset all session modifications |
permissions command only affect
the current session. They don't modify the config file.
> permissions show
Current Policy:
Default: ask
Whitelist: readFile, web_search
Blacklist: rm, sudo
> permissions allow cli_based_tool
Added 'cli_based_tool' to session whitelist
> permissions default allow
Session default set to 'allow'
> permissions check updateFile
Tool: updateFile
Decision: ASK (no rule match, default policy)
> permissions clear
Session modifications cleared
Interactive Approval
When a tool requires approval (policy is "ask" or no rule match), the channel prompts the user:
Console Channel
Shows tool details and waits for keyboard input:
[y]es: Allow this execution[n]o: Deny this execution[a]lways: Allow and add to session whitelist[never]: Deny and add to session blacklist
Custom Display
Plugins can provide custom formatting for their tools via
format_permission_request(). For example, file_edit
shows unified diffs for file modifications.
Tool: cli_based_tool
Intent: List files to find config
Arguments:
{
"command": "ls -la /etc"
}
[y]es / [n]o / [a]lways / [never]: _
Tool: updateFile
Summary: Update file: src/main.py (+5/-2 lines)
--- src/main.py (original)
+++ src/main.py (modified)
@@ -1,5 +1,8 @@
import sys
+import os
+from pathlib import Path
def main():
- print("hello")
+ print("Hello, World!")
[y]es / [n]o / [a]lways / [never]: _
askPermission executor (internal)
askPermission is an internal executor registered in
get_executors() for daemon-runner forwarding only.
It is not returned by get_tool_schemas() and
is never exposed to the model.
The correct flow is: model calls target tool → permission middleware
intercepts → prompts user if needed.
How Permission Enforcement Works
When the model calls any tool, the ToolExecutor invokes
check_permission(tool_name, args) before executing the tool body.
The permission plugin evaluates the call against the active policy
(whitelist, blacklist, default, evaluators) and either allows, denies,
or prompts the user via the configured channel.
askPermission executor parameters (internal reference)
| Name | Type | Required |
|---|---|---|
tool_name | string | Yes |
intent | string | Yes |
arguments | object | No |
# The model calls tools directly — permission intercepts automatically
# Model: "I'll run: cli_based_tool(command='ls -la /etc')"
# Framework intercepts before execution:
# 1. check_permission("cli_based_tool", {"command": "ls -la /etc"})
# 2. Policy: default=ask, no rule match → prompts user
# 3. User responds [y]es → tool executes
# 4. User responds [n]o → tool denied, error returned
# Do NOT expose the plugin for askPermission:
registry.expose_tool('permission') # OK: adds 'permissions' user command only
# askPermission is still not in model schemas