ConnectOnionConnectOnion
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
1from connectonion import Agent 2from connectonion.useful_tools import FileTools 3from connectonion.useful_plugins import skills, tool_approval 4 5file_tools = FileTools() 6agent = Agent( 7 "assistant", 8 tools=[file_tools], 9 plugins=[skills, tool_approval] 10) 11 12# Safe tools - auto-approved 13agent.input("Read the README") 14# → read_file auto-approved ✓ 15 16# Skills - scoped permissions 17# User types: /commit 18# → git commands auto-approved for this turn only ✓ 19# → 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
1# Unified permission format - all sources use same structure: 2session['permissions'] = { 3 "read_file": { 4 "allowed": True, 5 "source": "safe", 6 "reason": "read-only operation", 7 "expires": {"type": "never"} 8 }, 9 "bash": { 10 "allowed": True, 11 "source": "skill", 12 "reason": "commit skill (turn 5)", 13 "when": {"command": "git *"}, # Granular: only git commands 14 "expires": {"type": "turn_end"} 15 }, 16 "write": { 17 "allowed": True, 18 "source": "user", 19 "reason": "approved for session", 20 # NO 'when' field → matches ALL write calls (tool-level) 21 "expires": {"type": "session_end"} 22 } 23}

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
1permissions: 2 "Bash(git status)": # ← Natural pattern 3 allowed: true 4 source: config 5 reason: safe git read 6 expires: 7 type: never

Runtime Format (Internal)

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

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
1# Can use 'when' field for parameter matching 2session['permissions']['bash'] = { 3 "source": "config", 4 "when": {"command": "git status"} # Only "git status" 5}

User Approvals: Tool-Level

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

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
1# Turn 3: User approved write for session (tool-level) 2session['permissions'] = { 3 "write": { 4 "allowed": True, 5 "source": "user", 6 "reason": "approved for session", 7 # NO 'when' field → all write calls allowed 8 "expires": {"type": "session_end"} 9 } 10} 11 12# Turn 5: User types /commit 13# Step 1: Take snapshot 14snapshot = deepcopy(session['permissions']) # write saved ✓ 15 16# Step 2: Grant skill permissions (with 'when' field for granular matching) 17session['permissions']['bash'] = { 18 "allowed": True, 19 "source": "skill", 20 "reason": "commit skill (turn 5)", 21 "when": {"command": "git *"}, # Only git commands 22 "expires": {"type": "turn_end"} 23} 24 25# During turn 5: 26# → git status - auto-approved ✓ (skill permission matches "git *") 27# → write("foo.txt") - auto-approved ✓ (user permission, no 'when' field) 28# → pytest - BLOCKED ✗ (no permission for pytest) 29 30# Turn 5 ends 31# Step 3: Restore snapshot 32session['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
1SAFE_TOOLS = [ 2 'FileTools.read_file', 3 'FileTools.glob', 4 'FileTools.grep', 5 'ls', 6 'list_directory', 7 'tree' 8]

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

Example

main.py
1agent.input("Find all Python files and read main.py") 2# → FileTools.glob("**/*.py") - auto-approved ✓ 3# → 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
1agent.input("Run tests") 2# → bash approval needed (first time) 3# → User approves for "session" 4# → Future bash calls auto-approved ✓

Related

Enjoying ConnectOnion?

⭐ Star us on GitHub = ☕ Coffee chat with our founder. We love meeting builders.