picoclaw/docs/reference/exec-tool.md

11 KiB

Exec Tool - How It Works & Sandboxing

Overview

The exec tool allows the AI agent to execute shell commands on the host system. It supports both synchronous execution and background sessions with PTY support.

How Exec Works

Registration Flow

The exec tool is registered in pkg/agent/instance.go (lines 104-112):

if cfg.Tools.IsToolEnabled("exec") {
    execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths)
    if err != nil {
        logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec", ...)
    } else {
        toolsRegistry.Register(execTool)
    }
}

Prerequisites: The exec tool only registers when tools.exec.enabled = true in config.

LLM Invocation

The LLM calls the exec tool with JSON arguments:

{
  "action": "run",           // run, list, poll, read, write, kill, send-keys
  "command": "ls -la",      // shell command to execute
  "workdir": "/workspace", // optional, defaults to workspace
  "background": false,       // run in background (returns sessionId)
  "pty": false              // use pseudo-terminal (Unix only)
}

Execution Flow

  1. Action Routing (pkg/tools/shell.go):

    • runrunSync() or runBackground()
    • list → list active sessions
    • poll → check background process status
    • read → read output from background session
    • write → write input to background session (PTY mode)
    • kill → terminate background session
    • send-keys → send key sequence to PTY session
  2. Synchronous Execution (runSync()):

    • Command parsed and validated against deny patterns
    • exec.CommandContext() creates the command
    • Isolation applied via isolation.Start(cmd) (bubblewrap on Linux)
    • stdout/stderr captured (max 1MB output)
    • Waits for completion or timeout (default 60s)
  3. Background Execution (runBackground()):

    • Starts process in background
    • Creates ProcessSession tracked by global SessionManager
    • Returns sessionId for later management
    • Sessions auto-cleanup 30 minutes after process exits
  4. Isolation Wrapper (pkg/isolation/):

    • Linux: bubblewrap sandbox (namespace isolation, readonly filesystem)
    • Windows: Restricted tokens (limited process access)
    • macOS: Currently no isolation (CGO_ENABLED=1 required)

Session Management

Background processes are tracked by SessionManager (singleton in shell.go):

type ProcessSession struct {
    ID        string
    Cmd       *exec.Cmd
    Pty        *pty.Pty
    IsBackground bool
    Output    *bytes.Buffer
    // ...
}
  • Sessions stored in global sessionManager
  • list action returns all active sessions
  • poll returns exit code and output
  • Auto-cleanup after 30 minutes

Security Measures

Built-in Deny Patterns

The exec tool blocks 40+ dangerous command patterns (shell.go lines 50-98):

Category Blocked Patterns
File Deletion rm -rf, rm /f, rmdir /s, del /f
Disk Operations format, mkfs, diskpart, dd if=
Block Devices Writes to /dev/sd*, /dev/hd*, /dev/nvme*
System Control shutdown, reboot, poweroff
Fork Bombs :(){ :|:& };: (function fork bomb)
Command Substitution Backticks, $(...), ${...}
Pipe to Shell `
Privilege Escalation sudo, su -
Permission Changes chmod 777, chmod -R 777, chown
Process Killing kill -9, killall, pkill
Package Managers npm install -g, pip install --user, apt/yum/dnf install/remove
Containers docker run, docker exec, podman
Version Control git push, git reset --hard, git force
Remote Access ssh ...@, scp, rsync -e ssh
Shell Scripts source *.sh, bash *.sh, eval

Configuration-Based Security

Config Key Description Default
tools.exec.enabled Master switch for exec tool true
tools.exec.enable_deny_patterns Enable/disable built-in deny patterns true
tools.exec.custom_deny_patterns Additional regex patterns to block []
tools.exec.custom_allow_patterns Exempt specific commands from deny checks []
tools.exec.allow_remote Allow exec from external channels (Telegram, Discord, etc.) true
tools.exec.timeout_seconds Command timeout in seconds 60

Channel Restrictions

When tools.exec.allow_remote = false:

  • Allowed channels: cli, system, subagent (defined in pkg/constants/channels.go)
  • Blocked channels: telegram, discord, slack, wechat, etc.

Workspace Restriction

When agents.defaults.restrict_to_workspace = true:

  • Commands cannot access paths outside workspace
  • Path traversal (..) blocked in commands
  • Symlinks resolved and checked against workspace bounds

How to Disable Exec

config.json:

{
  "tools": {
    "exec": {
      "enabled": false
    }
  }
}

Environment variable:

export PICOCLAW_TOOLS_EXEC_ENABLED=false

Effect: The exec tool is not registered. The agent cannot execute shell commands. Other tools (read_file, write_file, etc.) remain available.

Method 2: Disable Remote Access Only

{
  "tools": {
    "exec": {
      "enabled": true,
      "allow_remote": false
    }
  }
}

Effect: Exec only works from cli, system, and subagent channels. External chat channels cannot use exec.

Method 3: Strict Timeout

{
  "tools": {
    "exec": {
      "enabled": true,
      "timeout_seconds": 10
    }
  }
}

Effect: All commands timeout after 10 seconds (default 60s).

How to Completely Remove Exec

To completely remove the exec tool from the codebase:

Step 1: Delete Implementation Files

rm pkg/tools/shell.go          # Main ExecTool implementation
rm pkg/tools/shell_test.go     # Tests
rm pkg/tools/shell_process_unix.go
rm pkg/tools/shell_process_windows.go
rm pkg/tools/shell_timeout_unix_test.go
rm pkg/tools/session.go        # ProcessSession, SessionManager
rm pkg/tools/session_test.go
rm pkg/tools/spawn.go           # Spawn tool (depends on exec)
rm pkg/tools/spawn_test.go
rm pkg/tools/spawn_status.go
rm pkg/tools/spawn_status_test.go

Step 2: Remove Registration Code

In pkg/agent/instance.go, remove lines ~104-112:

// REMOVE:
if cfg.Tools.IsToolEnabled("exec") {
    execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths)
    if err != nil {
        logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec", ...)
    } else {
        toolsRegistry.Register(execTool)
    }
}

Step 3: Remove Config Structure

In pkg/config/config.go:

  • Delete ExecConfig struct (around line 763-770)
  • Remove Exec ExecConfig field from ToolsConfig (around line 822)
  • Remove "exec" case from IsToolEnabled() function (around line 1534)

Step 4: Remove Defaults

In pkg/config/defaults.go, delete the Exec: ExecConfig{...} block (around lines 365-372).

Step 5: Remove Isolation (Optional)

If no other tools use isolation:

rm -rf pkg/isolation/

Step 6: Update Documentation

Remove exec references from:

  • docs/reference/tools-api.md
  • docs/reference/tools_configuration.md

How to Further Sandbox Exec

A. Enable Subprocess Isolation

Linux (bubblewrap):

{
  "isolation": {
    "enabled": true,
    "expose_paths": [
      {"source": "/workspace", "target": "/workspace", "mode": "rw"},
      {"source": "/usr", "target": "/usr", "mode": "ro"},
      {"source": "/lib", "target": "/lib", "mode": "ro"},
      {"source": "/bin", "target": "/bin", "mode": "ro"}
    ]
  }
}

Windows (restricted tokens):

{
  "isolation": {
    "enabled": true
  }
}

B. Restrict to Workspace Only

{
  "agents": {
    "defaults": {
      "restrict_to_workspace": true,
      "allow_read_outside_workspace": false
    }
  }
}

C. Add Custom Deny Patterns

{
  "tools": {
    "exec": {
      "custom_deny_patterns": [
        "nano", "vi", "vim", "emacs",
        "apt-get", "yum", "dnf",
        "docker", "podman"
      ]
    }
  }
}

D. Block All Package Managers

{
  "tools": {
    "exec": {
      "custom_deny_patterns": [
        "apt", "apt-get", "yum", "dnf", "pacman", "zypper",
        "npm", "yarn", "pnpm",
        "pip", "pip3", "conda",
        "gem", "bundle",
        "cargo", "rustup",
        "go get", "go install"
      ]
    }
  }
}

E. Combined Strict Mode

{
  "agents": {
    "defaults": {
      "restrict_to_workspace": true
    }
  },
  "tools": {
    "exec": {
      "enabled": true,
      "enable_deny_patterns": true,
      "allow_remote": false,
      "timeout_seconds": 30,
      "custom_deny_patterns": [
        "apt", "yum", "dnf", "npm", "pip", "docker"
      ]
    }
  },
  "isolation": {
    "enabled": true
  }
}

Key Files Reference

File Purpose
pkg/tools/shell.go ExecTool implementation, deny patterns, command guard
pkg/tools/session.go ProcessSession and SessionManager
pkg/tools/spawn.go Spawn tool (uses exec internally)
pkg/tools/registry.go ToolRegistry - how tools are registered
pkg/agent/instance.go Where exec tool is registered (lines 104-112)
pkg/config/config.go ExecConfig struct (line 763), IsToolEnabled (line 1534)
pkg/config/defaults.go Default exec settings (lines 365-372)
pkg/isolation/runtime.go Subprocess isolation implementation
pkg/constants/channels.go Internal channel definitions

Tool Result Structure

The exec tool returns *ToolResult:

type ToolResult struct {
    ForLLM      string        // Command output for LLM processing
    ForUser     string        // Human-readable output for chat
    MediaURLs   []string      // Media attachments (if any)
    IsError     bool          // Whether execution failed
    Async       bool          // True if background process
    Err         error         // Underlying Go error
}

Example success result:

{
  "ForLLM": "total 48\ndrwxr-xr-x 2 user user 4096 ...",
  "ForUser": "Command executed successfully",
  "IsError": false
}

Example error result:

{
  "ForLLM": "Command blocked: matches deny pattern 'rm -rf'",
  "ForUser": "This command is not allowed for security reasons",
  "IsError": true
}

## Permission System (New)

When `tools.exec.ask_permission = true` (default), the exec tool will ask for user permission before accessing paths outside workspace.

### How It Works

1. Exec tool detects command accesses path outside workspace
2. Checks PermissionCache - if no permission, returns early
3. LLM calls `request_permission` tool
4. Tool returns prompt for user: "Allow once" or "Allow for session"
5. User responds, LLM re-calls exec tool
6. Permission cached for "once" (consumed after use) or "session" (persists)

### Request Permission Tool

| Field | Description |
|-------|-------------|
| `path` | Path that needs permission |
| `command` | Original command (for context) |

### Permission Options

- **once**: Permission consumed after first use
- **session**: Permission persists for entire session
- **no**: Access denied