ConnectOnionConnectOnion
DocsUseful Pluginstool_approval

tool_approval

Web-based approval for dangerous tools via WebSocket

Quick Start

main.py
1from connectonion import Agent, bash 2from connectonion.useful_plugins import tool_approval 3 4agent = Agent("assistant", tools=[bash], plugins=[tool_approval]) 5agent.io = my_websocket_io # Required for web mode 6 7agent.input("Install dependencies") 8# → Client receives: {"type": "approval_needed", "tool": "bash", "arguments": {"command": "npm install"}} 9# → Client responds: {"approved": true, "scope": "session"} 10# ✓ bash approved (session)

Lifecycle

User sends prompt
    ↓
Agent calls LLM
    ↓
LLM returns tool_calls batch: [bash("npm install"), write("config.json"), bash("npm build")]
    ↓
tool_executor iterates sequentially:
    ↓
┌─ Tool #1: bash("npm install")
│   before_each_tool fires → check_approval()
│   → Is it safe? No (bash is DANGEROUS)
│   → Already approved for session? No
│   → Send to client:
│       {
│         "type": "approval_needed",
│         "tool": "bash",
│         "arguments": {"command": "npm install"},
│         "batch_remaining": [
│           {"tool": "write", "arguments": "{...}"},
│           {"tool": "bash", "arguments": "{\"command\": \"npm build\"}"}
│         ]
│       }
│   → BLOCK — wait for client response
│   ↓
│   Client responds: {"approved": true, "scope": "session"}
│   → Execute bash("npm install")
│   → Save "bash" as session-approved
│
├─ Tool #2: write("config.json")
│   before_each_tool fires → check_approval()
│   → Is it safe? No (write is DANGEROUS)
│   → Send approval_needed (batch_remaining: [bash(...)])
│   → Client responds: {"approved": false, "mode": "reject_soft"}
│   → Skip this tool, continue to next
│
├─ Tool #3: bash("npm build")
│   before_each_tool fires → check_approval()
│   → bash is session-approved → skip approval, execute immediately
│
└─ Done. Return results to LLM.

Rejection Modes

When the client rejects a tool, the mode field determines what happens next:

reject_soft (Skip)

Skip this tool, agent loop continues. The LLM receives a hint to ask the user what they prefer.

code
1{"approved": false, "mode": "reject_soft", "feedback": "Don't write that file"}
  • • Current tool is skipped (raises ValueError)
  • • Next tool in the batch proceeds normally
  • • LLM gets: "User rejected tool 'write'. Feedback: Don't write that file\n\n[System reminder: Ask the user...]"

reject_hard (Stop)

Skip this tool AND all remaining tools in the batch. The agent loop stops and waits for new user input.

code
1{"approved": false, "mode": "reject_hard", "feedback": "Wrong approach entirely"}
  • • Current tool is skipped (raises ValueError)
  • stop_signal flag is set in session
  • • All remaining tools in the batch are auto-rejected
  • • Agent loop stops — LLM does NOT get another turn
  • • User must send a new message to continue
Default mode when mode is not provided: reject_hard

Tool Classification

Safe Tools (No Approval)

Read-only operations that never modify state:

read, read_file, glob, grep, search
list_files, get_file_info, task, load_guide
enter_plan_mode, exit_plan_mode, write_plan
task_output, ask_user

Dangerous Tools (Require Approval)

Operations that can modify files or have side effects:

bash, shell, run, run_in_dir
write, edit, multi_edit
run_background, kill_task
send_email, post, delete, remove

Unknown Tools

Tools not in either list are treated as safe (no approval needed).

Config-Based Auto-Approval

Auto-approve safe commands permanently via host.yaml configuration. Config permissions never expire and apply to all sessions.

Configuration Example

code
1# .co/host.yaml 2permissions: 3 # Simple tool name - matches any call 4 "read_file": 5 allowed: true 6 source: config 7 reason: safe read operation 8 expires: 9 type: never 10 11 # Exact bash command 12 "Bash(git status)": 13 allowed: true 14 source: config 15 reason: safe git read 16 expires: 17 type: never 18 19 # Wildcard - matches command prefix 20 "Bash(git diff *)": 21 allowed: true 22 source: config 23 reason: safe git diff 24 expires: 25 type: never 26 27 # Parameter matching - file pattern 28 "write": 29 allowed: true 30 source: config 31 reason: safe doc edits 32 when: 33 file_path: "*.md" 34 expires: 35 type: never

Pattern Types

Simple Tool Name

Matches any call to the tool

"read_file"

Exact Bash Command

Only matches exact command

"Bash(git status)"

Wildcard Bash Command

Matches command prefix

"Bash(git diff *)"

Parameter Matching

Uses 'when' field for granular control

when: {file_path: "*.md"}

Priority Order

  1. 1.
    Safe tools - Always approved (SAFE_TOOLS list)
  2. 2.
    Config permissions - Loaded from host.yaml (source: config)
  3. 3.
    Skill permissions - Temporary, turn-scoped (source: skill)
  4. 4.
    Session approvals - User approved for session (source: user)
  5. 5.
    Runtime approval - Ask user (if none of above match)

Bash Command Chain Permissions

Uses bashlex to parse and validate command chains - ALL commands must be permitted.

✅ All Permitted

code
1# Config: 2permissions: 3 "Bash(pwd)": {allowed: true} 4 "Bash(ls *)": {allowed: true} 5 6# Command: 7pwd && ls -F 8 9# Result: Auto-approved ⚡

❌ Partial Permission

code
1# Config: 2permissions: 3 "Bash(pwd)": {allowed: true} 4 # rm is NOT whitelisted 5 6# Command: 7pwd && rm -rf / 8 9# Result: Requires approval ⚠️

Supported Syntax

SyntaxExampleCommands Extracted
&&pwd && ls["pwd", "ls"]
||test -f file || echo no["test", "echo"]
|cat file | grep test["cat", "grep"]
;echo a; echo b["echo", "echo"]

Security

Whitelist-first: One dangerous command = whole chain rejected.

# ❌ REJECTED even though pwd is safe
pwd && rm -rf /

Client Protocol

Server sends

code
1{ 2 "type": "approval_needed", 3 "tool": "bash", 4 "arguments": {"command": "npm install"}, 5 "batch_remaining": [{"tool": "write", "arguments": "..."}] 6}

Client responds

code
1{"approved": true, "scope": "once"}
code
1{"approved": true, "scope": "session"}
code
1{"approved": false, "mode": "reject_soft", "feedback": "Use yarn instead"}
code
1{"approved": false, "mode": "reject_hard", "feedback": "Wrong approach"}

Approval Scopes

ScopeBehavior
onceApprove this call only
sessionApprove for rest of session (stored in memory)

See Also

Enjoying ConnectOnion?

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