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
askPermission is intentionally NOT exposed to the model
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:

  1. Permission Enforcement (Middleware)
    Wraps tool execution to check permissions before any tool runs. Enabled via configure_tools(permission_plugin=...)
  2. Session Management Commands
    Exposes permissions user command for on-the-fly policy changes. Enabled via expose_tool("permission")
Basic usage
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')
Usage patterns
# 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.

Typestr
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.

TypeDict[str, Any]
Default{}

policy

Inline policy dict (overrides config file).

TypeDict[str, Any]
DefaultFrom config file
Configuration example
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*'],
    }
})
permissions.json
{
  "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:

  1. Blacklist check: If tool matches blacklist, deny
  2. Whitelist check: If tool matches whitelist, allow
  3. Default policy: Apply default (allow/deny/ask)

Default Policies

PolicyBehavior
allowAuto-approve all unmatched tools
denyAuto-deny all unmatched tools
askPrompt channel for each unmatched tool

Pattern Matching

Whitelist/blacklist support glob patterns:

  • * matches any characters
  • ? matches single character
  • [seq] matches characters in seq
Policy examples
# 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

CommandDescription
permissions showDisplay 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 clearReset all session modifications
Session-Only Changes
Changes made via the permissions command only affect the current session. They don't modify the config file.
User command examples
> 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.

Approval prompt example
Tool: cli_based_tool
Intent: List files to find config

Arguments:
{
  "command": "ls -la /etc"
}

[y]es / [n]o / [a]lways / [never]: _
With custom display (file_edit)
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)

Not a model-visible tool
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)

NameTypeRequired
tool_namestringYes
intentstringYes
argumentsobjectNo
Correct enforcement flow (no explicit askPermission call)
# 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