diff --git a/docs/reference/exec-tool.md b/docs/reference/exec-tool.md new file mode 100644 index 000000000..5a3f758dc --- /dev/null +++ b/docs/reference/exec-tool.md @@ -0,0 +1,394 @@ +# 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): + +```go +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: + +```json +{ + "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`): + - `run` → `runSync()` 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`): + +```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** | `| sh`, `| bash`, `curl ...\| sh`, `wget ...\| bash` | +| **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 + +### Method 1: Disable via Config (Recommended) + +**config.json**: +```json +{ + "tools": { + "exec": { + "enabled": false + } + } +} +``` + +**Environment variable**: +```bash +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 + +```json +{ + "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 + +```json +{ + "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 +```bash +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: +```go +// 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: +```bash +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)**: +```json +{ + "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)**: +```json +{ + "isolation": { + "enabled": true + } +} +``` + +### B. Restrict to Workspace Only + +```json +{ + "agents": { + "defaults": { + "restrict_to_workspace": true, + "allow_read_outside_workspace": false + } + } +} +``` + +### C. Add Custom Deny Patterns + +```json +{ + "tools": { + "exec": { + "custom_deny_patterns": [ + "nano", "vi", "vim", "emacs", + "apt-get", "yum", "dnf", + "docker", "podman" + ] + } + } +} +``` + +### D. Block All Package Managers + +```json +{ + "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 + +```json +{ + "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`: + +```go +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**: +```json +{ + "ForLLM": "total 48\ndrwxr-xr-x 2 user user 4096 ...", + "ForUser": "Command executed successfully", + "IsError": false +} +``` + +**Example error result**: +```json +{ + "ForLLM": "Command blocked: matches deny pattern 'rm -rf'", + "ForUser": "This command is not allowed for security reasons", + "IsError": true +} +``` diff --git a/project-map.md b/project-map.md index 35c3f0f2d..c7b836213 100644 --- a/project-map.md +++ b/project-map.md @@ -9,6 +9,7 @@ web/backend/ — Backend API server (Go, dashboard auth, middleware) workspace/ — Runtime workspace (skills, memory) docs/ — Documentation (architecture, channels, guides, migration, reference) docs/reference/tools-api.md — Complete tools API documentation: available tools, data structures, backend API endpoints, MCP integration +docs/reference/exec-tool.md — Exec tool deep dive: how it works, security measures, how to disable/sandbox/remove completely config/ — Configuration templates and examples build/ — Build scripts and artifacts docker/ — Docker containerization files diff --git a/session-log.md b/session-log.md index 9db18839f..4c1f77964 100644 --- a/session-log.md +++ b/session-log.md @@ -20,4 +20,15 @@ Decisions: Rejected: None Open: None +## 2026-05-05 00:02 [saved] +Goal: Document exec tool internals and how to sandbox/remove it +Decisions: +- Created docs/reference/exec-tool.md with comprehensive exec tool documentation +- Documented exec flow: LLM invocation → action routing → sync/background execution → isolation wrapper +- Documented 40+ built-in deny patterns (rm -rf, dd, shutdown, sudo, docker, etc.) +- Documented 3 ways to disable: config (enabled=false), disable remote, strict timeout +- Documented complete removal steps (delete files, remove registration, config, defaults) +- Documented sandboxing options: bubblewrap (Linux), restricted tokens (Windows), workspace restriction, custom deny patterns +- Listed all key files: shell.go, session.go, spawn.go, instance.go, config.go, defaults.go, isolation/ +Rejected: None Open: None