420 lines
11 KiB
Markdown
420 lines
11 KiB
Markdown
# 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
|
|
}
|
|
|
|
## 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
|
|
```
|