FeaturesPermissions
DocsFeaturesPermissions

Permissions

Balance safety and automation with unified permission system

Permission Layers

┌─────────────────────────────────────────────────────────────┐
│ 1. SAFE_TOOLS - Always auto-approved                       │
│    read_file, glob, grep (read-only operations)            │
│    Stored as: source='safe', expires='never'               │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 2. Config Permissions - Project-level auto-approve         │
│    host.yaml: Bash(git status), write(*.md), etc.          │
│    Stored as: source='config', expires='never'             │
│    Pattern: Bash() → 'bash' with when:{command: '...'}     │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 3. Skills - Temporary scoped permissions (one turn)         │
│    /commit → auto-approve git commands for this turn only  │
│    Stored as: source='skill', expires='turn_end'           │
│    Preserves user approvals via snapshot/restore           │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 4. User Approvals - Tool-level session memory              │
│    User approves 'bash' once → ALL bash commands allowed   │
│    Stored as: source='user', expires='session_end'         │
│    TOOL-LEVEL: Approving "bash npm" = approve ALL bash     │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│ 5. Tool Approval - Ask user for dangerous operations       │
│    bash, edit, write → require explicit user approval      │
│    If no permission in unified dict → ask user             │
└─────────────────────────────────────────────────────────────┘

Quick Start

main.py
from connectonion import Agent from connectonion.useful_tools import FileTools from connectonion.useful_plugins import skills, tool_approval file_tools = FileTools() agent = Agent( "assistant", tools=[file_tools], plugins=[skills, tool_approval] ) # Safe tools - auto-approved agent.input("Read the README") # → read_file auto-approved ✓ # Skills - scoped permissions # User types: /commit # → git commands auto-approved for this turn only ✓ # → Turn ends, permissions cleared ✓

Unified Permission Structure

All permissions use the same 4-field structure, stored in session['permissions']

allowed

True/False - Is this tool allowed?

source

"safe" | "skill" | "user"

reason

Human-readable explanation

expires

"never" | "turn_end" | "session_end"

main.py
# Unified permission format - all sources use same structure: session['permissions'] = { "read_file": { "allowed": True, "source": "safe", "reason": "read-only operation", "expires": {"type": "never"} }, "bash": { "allowed": True, "source": "skill", "reason": "commit skill (turn 5)", "when": {"command": "git *"}, # Granular: only git commands "expires": {"type": "turn_end"} }, "write": { "allowed": True, "source": "user", "reason": "approved for session", # NO 'when' field → matches ALL write calls (tool-level) "expires": {"type": "session_end"} } }

Config Files Use Bash() Pattern

User-facing config files (.co/host.yaml) use a friendly Bash() pattern that automatically converts to the unified format at runtime:

User-Facing Config (.co/host.yaml)

code
permissions: "Bash(git status)": # ← Natural pattern allowed: true source: config reason: safe git read expires: type: never

Runtime Format (Internal)

main.py
session['permissions']['bash'] = { "allowed": True, "source": "config", "reason": "safe git read", "when": {"command": "git status"}, "expires": {"type": "never"} }

Automatic conversion: You write Bash(git status) in config, it becomes bash with when:{command: "git status"} at runtime.

Tool-Level vs Granular Permissions

Critical Difference

User approvals are tool-level, not command-specific. This is different from config/skill permissions.

Config/Skill Permissions: Granular

main.py
# Can use 'when' field for parameter matching session['permissions']['bash'] = { "source": "config", "when": {"command": "git status"} # Only "git status" }

User Approvals: Tool-Level

main.py
# When user approves "bash npm install" → Stored as: session['permissions']['bash'] = { "source": "user", "reason": "approved for session" # NO 'when' field → matches ALL bash commands }

Why Tool-Level?

  • Convenience for development workflows
  • Don't re-approve every npm/pytest/git command
  • Clear intent: "I trust bash for this session"

Security

  • Config uses granular 'when' field
  • Skills use granular 'when' field
  • User approvals are simpler

Snapshot/Restore - Preserving User Approvals

Skills use a snapshot → grant → restore pattern to ensure user approvals are never lost:

1

Turn 3: User Approves

User approves write for session (tool-level approval)

2

Turn 5: /commit Skill

📸 Snapshot current permissions (write saved)
Grant skill permissions (bash with when:{command: "git *"} added)
Execute tools with both user + skill permissions
🔄 Restore snapshot when turn ends
3

Turn 6: Continue

write still works (user approval preserved)
bash requires approval (skill cleared)
main.py
# Turn 3: User approved write for session (tool-level) session['permissions'] = { "write": { "allowed": True, "source": "user", "reason": "approved for session", # NO 'when' field → all write calls allowed "expires": {"type": "session_end"} } } # Turn 5: User types /commit # Step 1: Take snapshot snapshot = deepcopy(session['permissions']) # write saved ✓ # Step 2: Grant skill permissions (with 'when' field for granular matching) session['permissions']['bash'] = { "allowed": True, "source": "skill", "reason": "commit skill (turn 5)", "when": {"command": "git *"}, # Only git commands "expires": {"type": "turn_end"} } # During turn 5: # → git status - auto-approved ✓ (skill permission matches "git *") # → write("foo.txt") - auto-approved ✓ (user permission, no 'when' field) # → pytest - BLOCKED ✗ (no permission for pytest) # Turn 5 ends # Step 3: Restore snapshot session['permissions'] = snapshot # User's write preserved ✓

Security Benefits

  • User approvals never overwritten by skills
  • Skills add temporary permissions, don't replace
  • Clean lifecycle - snapshot/restore is predictable
  • No permission escalation across turns

1. SAFE_TOOLS - Always Auto-Approved

Read-only operations that can't harm the system:

main.py
SAFE_TOOLS = [ 'FileTools.read_file', 'FileTools.glob', 'FileTools.grep', 'ls', 'list_directory', 'tree' ]

No approval needed - these tools are always safe to execute.

Example

main.py
agent.input("Find all Python files and read main.py") # → FileTools.glob("**/*.py") - auto-approved ✓ # → FileTools.read_file("main.py") - auto-approved ✓

2. Skills - Temporary Scoped Permissions

Skills provide one-turn auto-approval with automatic cleanup. Perfect for workflows like git commits, deployments, or reviews.

Turn-Based

Permissions tied to specific turn number

Auto-Cleanup

Cleared when turn completes

Secure

No permission escalation across turns

4. Session Memory - Remember User Decisions

When you approve a tool, you can choose to remember it for the session:

main.py
agent.input("Run tests") # → bash approval needed (first time) # → User approves for "session" # → Future bash calls auto-approved ✓

Related

Star us on GitHub

If ConnectOnion saves you time, a ⭐ goes a long way — and earns you a coffee chat with our founder.