docs(docs): document exec tool architecture and sandboxing
Add detailed technical reference for the `exec` tool, covering: - Execution flow (registration, LLM invocation, and action routing) - Synchronous vs. background session management - Platform-specific isolation mechanisms (bubblewrap, restricted tokens) - Security implementation including built-in deny patterns - Instructions for disabling or removing the tool Update project map and session log to reflect these documentation changes.
This commit is contained in:
parent
ab23be43a9
commit
8d720c5e93
3 changed files with 406 additions and 0 deletions
394
docs/reference/exec-tool.md
Normal file
394
docs/reference/exec-tool.md
Normal file
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue