From b33bd6aaeb90fe220cbbd19538146814e2afd8fd Mon Sep 17 00:00:00 2001 From: myfunc Date: Sun, 11 Jan 2026 16:35:21 -0800 Subject: [PATCH 1/2] fix(bash): use PowerShell on Windows to capture system utility output Windows system utilities like ipconfig, systeminfo, etc. write directly to the console via WriteConsole API instead of stdout. When Node.js spawns cmd.exe with piped stdio, these utilities produce empty output. Changes: - Switch from cmd.exe to PowerShell on Windows (properly redirects output) - Disable detached mode on Windows (PowerShell doesn't pipe stdout when detached) - Add windowsHide option to prevent console window flashing - Update tests to use PowerShell-compatible syntax (Start-Sleep, semicolons) --- src/agents/bash-tools.test.ts | 11 ++++++----- src/agents/bash-tools.ts | 6 ++++-- src/agents/shell-utils.ts | 11 +++++++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 16c0850fa..56cff37fe 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -9,11 +9,12 @@ import { import { sanitizeBinaryOutput } from "./shell-utils.js"; const isWin = process.platform === "win32"; -const shortDelayCmd = isWin ? "ping -n 2 127.0.0.1 > nul" : "sleep 0.05"; -const yieldDelayCmd = isWin ? "ping -n 3 127.0.0.1 > nul" : "sleep 0.2"; -const longDelayCmd = isWin ? "ping -n 4 127.0.0.1 > nul" : "sleep 2"; -const joinCommands = (commands: string[]) => - commands.join(isWin ? " & " : "; "); +// PowerShell: Start-Sleep for delays, ; for command separation, $null for null device +const shortDelayCmd = isWin ? "Start-Sleep -Milliseconds 50" : "sleep 0.05"; +const yieldDelayCmd = isWin ? "Start-Sleep -Milliseconds 200" : "sleep 0.2"; +const longDelayCmd = isWin ? "Start-Sleep -Seconds 2" : "sleep 2"; +// Both PowerShell and bash use ; for command separation +const joinCommands = (commands: string[]) => commands.join("; "); const echoAfterDelay = (message: string) => joinCommands([shortDelayCmd, `echo ${message}`]); const echoLines = (lines: string[]) => diff --git a/src/agents/bash-tools.ts b/src/agents/bash-tools.ts index 0f37afe40..1d1175373 100644 --- a/src/agents/bash-tools.ts +++ b/src/agents/bash-tools.ts @@ -265,15 +265,17 @@ export function createBashTool( { cwd: workdir, env: process.env, - detached: true, + detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, }, ) : spawn(shell, [...shellArgs, params.command], { cwd: workdir, env, - detached: true, + detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, }); const session = { diff --git a/src/agents/shell-utils.ts b/src/agents/shell-utils.ts index 6302b51d3..27ac5bf21 100644 --- a/src/agents/shell-utils.ts +++ b/src/agents/shell-utils.ts @@ -2,8 +2,15 @@ import { spawn } from "node:child_process"; export function getShellConfig(): { shell: string; args: string[] } { if (process.platform === "win32") { - const shell = process.env.COMSPEC?.trim() || "cmd.exe"; - return { shell, args: ["/d", "/s", "/c"] }; + // Use PowerShell instead of cmd.exe on Windows. + // Problem: Many Windows system utilities (ipconfig, systeminfo, etc.) write + // directly to the console via WriteConsole API, bypassing stdout pipes. + // When Node.js spawns cmd.exe with piped stdio, these utilities produce no output. + // PowerShell properly captures and redirects their output to stdout. + return { + shell: "powershell.exe", + args: ["-NoProfile", "-NonInteractive", "-Command"], + }; } const shell = process.env.SHELL?.trim() || "sh"; From 98337a14b3ef7ab791c7950d6ec1a16a1becf877 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 02:49:55 +0000 Subject: [PATCH 2/2] fix: rename bash tool to exec (#748) (thanks @myfunc) --- CHANGELOG.md | 1 + README.md | 4 +- docs/broadcast-groups.md | 10 ++--- docs/concepts/agent.md | 2 +- docs/concepts/multi-agent.md | 4 +- docs/concepts/session-pruning.md | 2 +- docs/concepts/system-prompt.md | 2 +- docs/docs.json | 8 +++- docs/gateway/background-process.md | 24 ++++++------ docs/gateway/configuration-examples.md | 4 +- docs/gateway/configuration.md | 19 +++++----- docs/gateway/doctor.md | 2 +- docs/gateway/logging.md | 2 +- .../sandbox-vs-tool-policy-vs-elevated.md | 11 +++--- docs/gateway/sandboxing.md | 6 +-- docs/gateway/security.md | 8 ++-- docs/install/docker.md | 6 +-- docs/multi-agent-sandbox-tools.md | 22 +++++------ docs/platforms/mac/menu-bar.md | 6 +-- docs/start/faq.md | 2 +- docs/start/hubs.md | 2 +- docs/testing.md | 10 ++--- docs/tools/elevated.md | 16 ++++---- docs/tools/{bash.md => exec.md} | 14 +++---- docs/tools/index.md | 6 +-- docs/tools/subagents.md | 2 +- scripts/docker/install-sh-e2e/run.sh | 10 ++--- scripts/zai-fallback-repro.ts | 2 +- skills/tmux/SKILL.md | 4 +- src/agents/agent-scope.test.ts | 6 +-- src/agents/bash-tools.test.ts | 26 ++++++------- src/agents/bash-tools.ts | 34 ++++++++--------- src/agents/pi-embedded-helpers.test.ts | 4 +- src/agents/pi-embedded-runner.test.ts | 14 +++---- src/agents/pi-embedded-runner.ts | 30 ++++++++++----- .../pi-extensions/context-pruning.test.ts | 32 ++++++++-------- src/agents/pi-tools-agent-config.test.ts | 38 +++++++++---------- src/agents/pi-tools.test.ts | 12 +++--- src/agents/pi-tools.ts | 33 ++++++++++------ src/agents/pi-tools.workspace-paths.test.ts | 16 ++++---- src/agents/sandbox-agent-config.test.ts | 2 +- src/agents/sandbox.ts | 2 +- src/agents/session-transcript-repair.test.ts | 4 +- src/agents/system-prompt.test.ts | 8 ++-- src/agents/system-prompt.ts | 18 ++++----- src/agents/tool-display.json | 4 +- src/config/config.test.ts | 4 +- src/config/legacy.ts | 10 +++-- src/config/types.ts | 15 ++++++-- src/config/zod-schema.ts | 7 ++++ .../gateway-models.profiles.live.test.ts | 16 ++++---- 51 files changed, 294 insertions(+), 252 deletions(-) rename docs/tools/{bash.md => exec.md} (70%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3baa9cfa..b2f532fd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs. - Tests: add Docker plugin loader + tgz-install smoke test. - Tests: extend Docker plugin E2E to cover installing from local folders (`plugins.load.paths`) and `file:` npm specs. +- Agents/Tools: rename the bash tool to exec (config alias maintained). (#748) — thanks @myfunc. - Config: add `$include` directive for modular config files. (#731) — thanks @pasogott. - Build: set pnpm minimum release age to 2880 minutes (2 days). (#718) — thanks @dan-dr. - macOS: prompt to install the global `clawdbot` CLI when missing in local mode; install via `clawd.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. diff --git a/README.md b/README.md index 18ffa3b11..0d9c7f10f 100644 --- a/README.md +++ b/README.md @@ -200,9 +200,9 @@ Details: [Tailscale guide](https://docs.clawd.bot/tailscale) · [Web surfaces](h It’s perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute device‑local actions when needed. -- **Gateway host** runs the bash tool and provider connections by default. +- **Gateway host** runs the exec tool and provider connections by default. - **Device nodes** run device‑local actions (`system.run`, camera, screen recording, notifications) via `node.invoke`. -In short: bash runs where the Gateway lives; device actions run where the device lives. +In short: exec runs where the Gateway lives; device actions run where the device lives. Details: [Remote access](https://docs.clawd.bot/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/security) diff --git a/docs/broadcast-groups.md b/docs/broadcast-groups.md index d698c3b5e..07068d0d3 100644 --- a/docs/broadcast-groups.md +++ b/docs/broadcast-groups.md @@ -180,7 +180,7 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`: Session: agent:alfred:whatsapp:group:120363403215116621@g.us History: [user message, alfred's previous responses] Workspace: /Users/pascal/clawd-alfred/ -Tools: read, write, bash +Tools: read, write, exec ``` **Bärbel's context:** @@ -230,10 +230,10 @@ Give agents only the tools they need: { "agents": { "reviewer": { - "tools": { "allow": ["read", "bash"] } // Read-only + "tools": { "allow": ["read", "exec"] } // Read-only }, "fixer": { - "tools": { "allow": ["read", "write", "edit", "bash"] } // Read-write + "tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write } } } @@ -330,8 +330,8 @@ tail -f ~/.clawdbot/logs/gateway.log | grep broadcast "agents": { "list": [ { "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } }, - { "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "bash"] } }, - { "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "bash"] } }, + { "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } }, + { "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } }, { "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } } ] } diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index e689d1c14..9a0db312a 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -45,7 +45,7 @@ To disable bootstrap file creation entirely (for pre-seeded workspaces), set: ## Built-in tools -Core tools (read/bash/edit/write and related system tools) are always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used. +Core tools (read/exec/edit/write and related system tools) are always available. `TOOLS.md` does **not** control which tools exist; it’s guidance for how *you* want them used. ## Skills diff --git a/docs/concepts/multi-agent.md b/docs/concepts/multi-agent.md index d94674132..2fed872f8 100644 --- a/docs/concepts/multi-agent.md +++ b/docs/concepts/multi-agent.md @@ -217,7 +217,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio }, tools: { allow: ["read"], // Only read tool - deny: ["bash", "write", "edit"], // Deny others + deny: ["exec", "write", "edit"], // Deny others }, }, ], @@ -231,7 +231,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio - **Flexible policies**: Different permissions per agent Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent. -If you need per-agent boundaries, use `agents.list[].tools` to deny `bash`. +If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`. For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent. See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples. diff --git a/docs/concepts/session-pruning.md b/docs/concepts/session-pruning.md index 6b54a655f..4e76f9bb4 100644 --- a/docs/concepts/session-pruning.md +++ b/docs/concepts/session-pruning.md @@ -93,7 +93,7 @@ Restrict pruning to specific tools: agent: { contextPruning: { mode: "adaptive", - tools: { allow: ["bash", "read"], deny: ["*image*"] } + tools: { allow: ["exec", "read"], deny: ["*image*"] } } } } diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index d1d254766..a81b1583b 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -19,7 +19,7 @@ The prompt is intentionally compact and uses fixed sections: - **Clawdbot Self-Update**: how to run `config.apply` and `update.run`. - **Workspace**: working directory (`agents.defaults.workspace`). - **Workspace Files (injected)**: indicates bootstrap files are included below. -- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated bash is available. +- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available. - **Time**: UTC default + the user’s local time (already converted). - **Reply Tags**: optional reply tag syntax for supported providers. - **Heartbeats**: heartbeat prompt and ack behavior. diff --git a/docs/docs.json b/docs/docs.json index 1ef5e197c..5f963b54e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -103,7 +103,11 @@ }, { "source": "/bash", - "destination": "/tools/bash" + "destination": "/tools/exec" + }, + { + "source": "/tools/bash", + "destination": "/tools/exec" }, { "source": "/bonjour", @@ -696,7 +700,7 @@ "pages": [ "tools", "plugin", - "tools/bash", + "tools/exec", "tools/elevated", "tools/browser", "tools/browser-linux-troubleshooting", diff --git a/docs/gateway/background-process.md b/docs/gateway/background-process.md index f3e819f5a..da8874bc8 100644 --- a/docs/gateway/background-process.md +++ b/docs/gateway/background-process.md @@ -1,15 +1,15 @@ --- -summary: "Background bash execution and process management" +summary: "Background exec execution and process management" read_when: - - Adding or modifying background bash behavior - - Debugging long-running bash tasks + - Adding or modifying background exec behavior + - Debugging long-running exec tasks --- -# Background Bash + Process Tool +# Background Exec + Process Tool -Clawdbot runs shell commands through the `bash` tool and keeps long‑running tasks in memory. The `process` tool manages those background sessions. +Clawdbot runs shell commands through the `exec` tool and keeps long‑running tasks in memory. The `process` tool manages those background sessions. -## bash tool +## exec tool Key parameters: - `command` (required) @@ -24,7 +24,7 @@ Behavior: - Foreground runs return output directly. - When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail. - Output is kept in memory until the session is polled or cleared. -- If the `process` tool is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. +- If the `process` tool is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`. Environment overrides: - `PI_BASH_YIELD_MS`: default yield (ms) @@ -32,9 +32,9 @@ Environment overrides: - `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h) Config (preferred): -- `tools.bash.backgroundMs` (default 10000) -- `tools.bash.timeoutSec` (default 1800) -- `tools.bash.cleanupMs` (default 1800000) +- `tools.exec.backgroundMs` (default 10000) +- `tools.exec.timeoutSec` (default 1800) +- `tools.exec.cleanupMs` (default 1800000) ## process tool @@ -59,7 +59,7 @@ Notes: Run a long task and poll later: ```json -{"tool": "bash", "command": "sleep 5 && echo done", "yieldMs": 1000} +{"tool": "exec", "command": "sleep 5 && echo done", "yieldMs": 1000} ``` ```json {"tool": "process", "action": "poll", "sessionId": ""} @@ -67,7 +67,7 @@ Run a long task and poll later: Start immediately in background: ```json -{"tool": "bash", "command": "npm run build", "background": true} +{"tool": "exec", "command": "npm run build", "background": true} ``` Send stdin: diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md index 976fb20a1..54cdcfac6 100644 --- a/docs/gateway/configuration-examples.md +++ b/docs/gateway/configuration-examples.md @@ -259,9 +259,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number. }, tools: { - allow: ["bash", "process", "read", "write", "edit"], + allow: ["exec", "process", "read", "write", "edit"], deny: ["browser", "canvas"], - bash: { + exec: { backgroundMs: 10000, timeoutSec: 1800, cleanupMs: 1800000 diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 9991fc8a1..a83cc6353 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -638,7 +638,7 @@ Read-only tools + read-only workspace: }, tools: { allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], - deny: ["write", "edit", "bash", "process", "browser"] + deny: ["write", "edit", "exec", "process", "browser"] } } ] @@ -661,7 +661,7 @@ No filesystem access (messaging/session tools enabled): }, tools: { allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"], - deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] + deny: ["read", "write", "edit", "exec", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] } } ] @@ -1274,7 +1274,7 @@ Example: maxConcurrent: 1, archiveAfterMinutes: 60 }, - bash: { + exec: { backgroundMs: 10000, timeoutSec: 1800, cleanupMs: 1800000 @@ -1427,10 +1427,11 @@ Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`. -`tools.bash` configures background bash defaults: +`tools.exec` configures background exec defaults: - `backgroundMs`: time before auto-background (ms, default 10000) - `timeoutSec`: auto-kill after this runtime (seconds, default 1800) - `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000) +Legacy: `tools.bash` is still accepted as an alias. `agents.defaults.subagents` configures sub-agent defaults: - `maxConcurrent`: max concurrent sub-agent runs (default 1) @@ -1447,7 +1448,7 @@ Example (disable browser/canvas everywhere): } ``` -`tools.elevated` controls elevated (host) bash access: +`tools.elevated` controls elevated (host) exec access: - `enabled`: allow elevated mode (default true) - `allowFrom`: per-provider allowlists (empty = disabled) - `whatsapp`: E.164 numbers @@ -1491,8 +1492,8 @@ Per-agent override (further restrict): Notes: - `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow). - `/elevated on|off` stores state per session key; inline directives apply to a single message. -- Elevated `bash` runs on the host and bypasses sandboxing. -- Tool policy still applies; if `bash` is denied, elevated cannot be used. +- Elevated `exec` runs on the host and bypasses sandboxing. +- Tool policy still applies; if `exec` is denied, elevated cannot be used. `agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can execute in parallel across sessions. Each session is still serialized (one run @@ -1513,7 +1514,7 @@ Defaults (if enabled): - `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`) - `"rw"`: mount the agent workspace read/write at `/workspace` - auto-prune: idle > 24h OR age > 7d -- tool policy: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins) +- tool policy: allow only `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins) - configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools` - optional sandboxed browser (Chromium + CDP, noVNC observer) - hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile` @@ -1584,7 +1585,7 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`, tools: { sandbox: { tools: { - allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], + allow: ["exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"] } } diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 6b312f1c0..7ff6826a0 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -111,7 +111,7 @@ Current migrations: - `routing.agentToAgent` → `tools.agentToAgent` - `routing.transcribeAudio` → `tools.audio.transcription` - `identity` → `agents.list[].identity` -- `agent.*` → `agents.defaults` + `tools.*` (tools/elevated/bash/sandbox/subagents) +- `agent.*` → `agents.defaults` + `tools.*` (tools/elevated/exec/sandbox/subagents) - `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks` → `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks` diff --git a/docs/gateway/logging.md b/docs/gateway/logging.md index 0c91e2160..d834afb0d 100644 --- a/docs/gateway/logging.md +++ b/docs/gateway/logging.md @@ -50,7 +50,7 @@ You can tune console verbosity independently via: ## Tool summary redaction -Verbose tool summaries (e.g. `🛠️ bash: ...`) can mask sensitive tokens before they hit the +Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the console stream. This is **tools-only** and does not alter file logs. - `logging.redactSensitive`: `off` | `tools` (default: `tools`) diff --git a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md index 0f0546a60..49c1334dc 100644 --- a/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md +++ b/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @@ -1,6 +1,6 @@ --- title: Sandbox vs Tool Policy vs Elevated -summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated bash gates" +summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated exec gates" read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change." status: active --- @@ -11,7 +11,7 @@ Clawdbot has three related (but different) controls: 1. **Sandbox** (`agents.defaults.sandbox.*` / `agents.list[].sandbox.*`) decides **where tools run** (Docker vs host). 2. **Tool policy** (`tools.*`, `tools.sandbox.tools.*`, `agents.list[].tools.*`) decides **which tools are available/allowed**. -3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is a **bash-only escape hatch** to run on the host when you’re sandboxed. +3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is an **exec-only escape hatch** to run on the host when you’re sandboxed. ## Quick debug @@ -49,10 +49,10 @@ Rules of thumb: - `deny` always wins. - If `allow` is non-empty, everything else is treated as blocked. -## Elevated: bash-only “run on host” +## Elevated: exec-only “run on host” -Elevated does **not** grant extra tools; it only affects `bash`. -- If you’re sandboxed, `/elevated on` (or `bash` with `elevated: true`) runs on the host. +Elevated does **not** grant extra tools; it only affects `exec`. +- If you’re sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host. - If you’re already running direct, elevated is effectively a no-op (still gated). Gates: @@ -74,4 +74,3 @@ Fix-it keys (pick one): ### “I thought this was main, why is it sandboxed?” In `"non-main"` mode, group/channel keys are *not* main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`. - diff --git a/docs/gateway/sandboxing.md b/docs/gateway/sandboxing.md index 3e61f2c99..94f7aaa83 100644 --- a/docs/gateway/sandboxing.md +++ b/docs/gateway/sandboxing.md @@ -17,7 +17,7 @@ This is not a perfect security boundary, but it materially limits filesystem and process access when the model does something dumb. ## What gets sandboxed -- Tool execution (`bash`, `read`, `write`, `edit`, `process`, etc.). +- Tool execution (`exec`, `read`, `write`, `edit`, `process`, etc.). - Optional sandboxed browser (`agents.defaults.sandbox.browser`). - By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it. Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`. @@ -27,7 +27,7 @@ and process access when the model does something dumb. Not sandboxed: - The Gateway process itself. - Any tool explicitly allowed to run on the host (e.g. `tools.elevated`). - - **Elevated bash runs on the host and bypasses sandboxing.** + - **Elevated exec runs on the host and bypasses sandboxing.** - If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated). ## Modes @@ -79,7 +79,7 @@ Docker installs and the containerized gateway live here: Tool allow/deny policies still apply before sandbox rules. If a tool is denied globally or per-agent, sandboxing doesn’t bring it back. -`tools.elevated` is an explicit escape hatch that runs `bash` on the host. +`tools.elevated` is an explicit escape hatch that runs `exec` on the host. Debugging: - Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys. diff --git a/docs/gateway/security.md b/docs/gateway/security.md index 43d3fc80a..0ba634dff 100644 --- a/docs/gateway/security.md +++ b/docs/gateway/security.md @@ -184,7 +184,7 @@ Consider running your AI on a separate phone number from your personal one: You can already build a read-only profile by combining: - `agents.defaults.sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access) -- tool allow/deny lists that block `write`, `edit`, `bash`, `process`, etc. +- tool allow/deny lists that block `write`, `edit`, `exec`, `process`, etc. We may add a single `readOnlyMode` flag later to simplify this configuration. @@ -206,7 +206,7 @@ Also consider agent workspace access inside the sandbox: - `agents.defaults.sandbox.workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`) - `agents.defaults.sandbox.workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace` -Important: `tools.elevated` is the global baseline escape hatch that runs bash on the host. Keep `tools.elevated.allowFrom` tight and don’t enable it for strangers. You can further restrict elevated per agent via `agents.list[].tools.elevated`. See [Elevated Mode](/tools/elevated). +Important: `tools.elevated` is the global baseline escape hatch that runs exec on the host. Keep `tools.elevated.allowFrom` tight and don’t enable it for strangers. You can further restrict elevated per agent via `agents.list[].tools.elevated`. See [Elevated Mode](/tools/elevated). ## Browser control risks @@ -261,7 +261,7 @@ Common use cases: }, tools: { allow: ["read"], - deny: ["write", "edit", "bash", "process", "browser"] + deny: ["write", "edit", "exec", "process", "browser"] } } ] @@ -285,7 +285,7 @@ Common use cases: }, tools: { allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"], - deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] + deny: ["read", "write", "edit", "exec", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] } } ] diff --git a/docs/install/docker.md b/docs/install/docker.md index 112ce5273..73e5dcbfc 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -250,7 +250,7 @@ precedence, and troubleshooting. - `"rw"` mounts the agent workspace read/write at `/workspace` - Auto-prune: idle > 24h OR age > 7d - Network: `none` by default (explicitly opt-in if you need egress) -- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` +- Default allow: `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` - Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway` ### Enable sandboxing @@ -297,7 +297,7 @@ precedence, and troubleshooting. tools: { sandbox: { tools: { - allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], + allow: ["exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"] } } @@ -424,7 +424,7 @@ Example: ### Security notes -- Hard wall only applies to **tools** (bash/read/write/edit). +- Hard wall only applies to **tools** (exec/read/write/edit). - Host-only tools like browser/camera/canvas are blocked by default. - Allowing `browser` in sandbox **breaks isolation** (browser runs on host). diff --git a/docs/multi-agent-sandbox-tools.md b/docs/multi-agent-sandbox-tools.md index 589bbe450..fcf92f75d 100644 --- a/docs/multi-agent-sandbox-tools.md +++ b/docs/multi-agent-sandbox-tools.md @@ -48,7 +48,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate }, "tools": { "allow": ["read"], - "deny": ["bash", "write", "edit", "process", "browser"] + "deny": ["exec", "write", "edit", "process", "browser"] } } ] @@ -95,7 +95,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate "workspaceRoot": "/tmp/work-sandboxes" }, "tools": { - "allow": ["read", "write", "bash"], + "allow": ["read", "write", "exec"], "deny": ["browser", "gateway", "discord"] } } @@ -134,7 +134,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate }, "tools": { "allow": ["read"], - "deny": ["bash", "write", "edit"] + "deny": ["exec", "write", "edit"] } } ] @@ -177,7 +177,7 @@ If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools` `tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow). Mitigation patterns: -- Deny `bash` for untrusted agents (`agents.list[].tools.deny: ["bash"]`) +- Deny `exec` for untrusted agents (`agents.list[].tools.deny: ["exec"]`) - Avoid allowlisting senders that route to restricted agents - Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution - Disable elevated per agent (`agents.list[].tools.elevated.enabled: false`) for sensitive profiles @@ -200,7 +200,7 @@ Mitigation patterns: "tools": { "sandbox": { "tools": { - "allow": ["read", "write", "bash"], + "allow": ["read", "write", "exec"], "deny": [] } } @@ -235,7 +235,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau { "tools": { "allow": ["read"], - "deny": ["bash", "write", "edit", "process"] + "deny": ["exec", "write", "edit", "process"] } } ``` @@ -244,7 +244,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau ```json { "tools": { - "allow": ["read", "bash", "process"], + "allow": ["read", "exec", "process"], "deny": ["write", "edit", "browser", "gateway"] } } @@ -255,7 +255,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau { "tools": { "allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"], - "deny": ["bash", "write", "edit", "read", "browser"] + "deny": ["exec", "write", "edit", "read", "browser"] } } ``` @@ -276,12 +276,12 @@ sandbox, set `agents.list[].sandbox.mode: "off"`. After configuring multi-agent sandbox and tools: 1. **Check agent resolution:** - ```bash + ```exec clawdbot agents list --bindings ``` 2. **Verify sandbox containers:** - ```bash + ```exec docker ps --filter "label=clawdbot.sandbox=1" ``` @@ -290,7 +290,7 @@ After configuring multi-agent sandbox and tools: - Verify the agent cannot use denied tools 4. **Monitor logs:** - ```bash + ```exec tail -f "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/logs/gateway.log" | grep -E "routing|sandbox|tools" ``` diff --git a/docs/platforms/mac/menu-bar.md b/docs/platforms/mac/menu-bar.md index c1979722e..351d2819c 100644 --- a/docs/platforms/mac/menu-bar.md +++ b/docs/platforms/mac/menu-bar.md @@ -25,7 +25,7 @@ read_when: - `overridden(ActivityKind)` (debug override) ### ActivityKind → glyph -- `bash` → 💻 +- `exec` → 💻 - `read` → 📄 - `write` → ✍️ - `edit` → 📝 @@ -40,7 +40,7 @@ read_when: ## Status row text (menu) - While work is active: ` · ` - - Examples: `Main · bash: pnpm test`, `Other · read: apps/macos/Sources/Clawdbot/AppState.swift`. + - Examples: `Main · exec: pnpm test`, `Other · read: apps/macos/Sources/Clawdbot/AppState.swift`. - When idle: falls back to the health summary. ## Event ingestion @@ -49,7 +49,7 @@ read_when: - `stream: "job"` with `data.state` for start/stop. - `stream: "tool"` with `data.phase`, `name`, optional `meta`/`args`. - Labels: - - `bash`: first line of `args.command`. + - `exec`: first line of `args.command`. - `read`/`write`: shortened path. - `edit`: path plus inferred change kind from `meta`/diff counts. - fallback: tool name. diff --git a/docs/start/faq.md b/docs/start/faq.md index acd2b4040..62367f598 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -836,7 +836,7 @@ exit These are abort triggers (not slash commands). -For background processes (from the bash tool), you can ask the agent to run: +For background processes (from the exec tool), you can ask the agent to run: ``` process action:kill sessionId:XXX diff --git a/docs/start/hubs.md b/docs/start/hubs.md index 7802fd9b7..3c996e2ad 100644 --- a/docs/start/hubs.md +++ b/docs/start/hubs.md @@ -95,7 +95,7 @@ Use these hubs to discover every page, including deep dives and reference docs t - [Tools surface](/tools) - [CLI reference](/cli) -- [Bash tool](/tools/bash) +- [Exec tool](/tools/exec) - [Elevated mode](/tools/elevated) - [Cron jobs](/automation/cron-jobs) - [Thinking + verbose](/tools/thinking) diff --git a/docs/testing.md b/docs/testing.md index 5d7c4029f..ecd636cb5 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -120,11 +120,11 @@ Live tests are split into two layers so we can isolate failures: - Iterate models-with-keys and assert: - “meaningful” response (no tools) - a real tool invocation works (read probe) - - optional extra tool probes (bash+read probe) + - optional extra tool probes (exec+read probe) - OpenAI regression paths (tool-call-only → follow-up) keep working - Probe details (so you can explain failures quickly): - `read` probe: the test writes a nonce file in the workspace and asks the agent to `read` it and echo the nonce back. - - `bash+read` probe: the test asks the agent to `bash`-write a nonce into a temp file, then `read` it back. + - `exec+read` probe: the test asks the agent to `exec`-write a nonce into a temp file, then `read` it back. - image probe: the test attaches a generated PNG (cat + randomized code) and expects the model to return `cat `. - Implementation reference: `src/gateway/gateway-models.profiles.live.test.ts` and `src/gateway/live-image-probe.ts`. - How to enable: @@ -136,7 +136,7 @@ Live tests are split into two layers so we can isolate failures: - How to select providers (avoid “OpenRouter everything”): - `CLAWDBOT_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist) - Optional tool-calling stress: - - `CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1` enables an extra “bash writes file → read reads it back → echo nonce” check. + - `CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1` enables an extra “exec writes file → read reads it back → echo nonce” check. - This is specifically meant to catch tool-calling compatibility issues across providers (formatting, history replay, tool_result pairing, etc.). - Optional image send smoke: - `CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1` sends a real image attachment through the gateway agent pipeline (multimodal message) and asserts the model can read back a per-run code from the image. @@ -215,7 +215,7 @@ Narrow, explicit allowlists are fastest and least flaky: - Single model, gateway smoke: - `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` -- Tool calling across several providers (bash + read probe): +- Tool calling across several providers (exec + read probe): - `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - Google focus (Gemini API key + Antigravity): @@ -248,7 +248,7 @@ This is the “common models” run we expect to keep working: Run gateway smoke with tools + image: `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,openai-codex/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-pro,google/gemini-3-flash,google-antigravity/claude-opus-4-5-thinking,google-antigravity/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` -### Baseline: tool calling (Read + optional Bash) +### Baseline: tool calling (Read + optional Exec) Pick at least one per provider family: - OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`) diff --git a/docs/tools/elevated.md b/docs/tools/elevated.md index 96562f681..d42ea76c5 100644 --- a/docs/tools/elevated.md +++ b/docs/tools/elevated.md @@ -1,12 +1,12 @@ --- -summary: "Elevated bash mode and /elevated directives" +summary: "Elevated exec mode and /elevated directives" read_when: - Adjusting elevated mode defaults, allowlists, or slash command behavior --- # Elevated Mode (/elevated directives) ## What it does -- Elevated mode allows the bash tool to run with elevated privileges when the feature is available and the sender is approved. +- Elevated mode allows the exec tool to run with elevated privileges when the feature is available and the sender is approved. - **Optional for sandboxed agents**: elevated only changes behavior when the agent is running in a sandbox. If the agent already runs unsandboxed, elevated is effectively a no-op. - Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`. - Only `on|off` are accepted; anything else returns a hint and does not change state. @@ -16,16 +16,16 @@ read_when: - **Per-session state**: `/elevated on|off` sets the elevated level for the current session key. - **Inline directive**: `/elevated on` inside a message applies to that message only. - **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned. -- **Host execution**: elevated runs `bash` on the host (bypasses sandbox). -- **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `bash` runs. -- **Tool policy still applies**: if `bash` is denied by tool policy, elevated cannot be used. +- **Host execution**: elevated runs `exec` on the host (bypasses sandbox). +- **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `exec` runs. +- **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used. Note: -- Sandbox on: `/elevated on` runs that `bash` command on the host. +- Sandbox on: `/elevated on` runs that `exec` command on the host. - Sandbox off: `/elevated on` does not change execution (already on host). ## When elevated matters -- Only impacts `bash` when the agent is running sandboxed (it drops the sandbox for that command). +- Only impacts `exec` when the agent is running sandboxed (it drops the sandbox for that command). - For unsandboxed agents, elevated does not change execution; it only affects gating, logging, and status. ## Resolution order @@ -48,5 +48,5 @@ Note: - All gates must pass; otherwise elevated is treated as unavailable. ## Logging + status -- Elevated bash calls are logged at info level. +- Elevated exec calls are logged at info level. - Session status includes elevated mode (e.g. `elevated=on`). diff --git a/docs/tools/bash.md b/docs/tools/exec.md similarity index 70% rename from docs/tools/bash.md rename to docs/tools/exec.md index 3c2aef6bc..0b0d8e776 100644 --- a/docs/tools/bash.md +++ b/docs/tools/exec.md @@ -1,14 +1,14 @@ --- -summary: "Bash tool usage, stdin modes, and TTY support" +summary: "Exec tool usage, stdin modes, and TTY support" read_when: - - Using or modifying the bash tool + - Using or modifying the exec tool - Debugging stdin or TTY behavior --- -# Bash tool +# Exec tool Run shell commands in the workspace. Supports foreground + background execution via `process`. -If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. +If `process` is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`. Background sessions are scoped per agent; `process` only sees sessions from the same agent. ## Parameters @@ -19,17 +19,17 @@ Background sessions are scoped per agent; `process` only sees sessions from the - `timeout` (seconds, default 1800): kill on expiry - `elevated` (bool): run on host if elevated mode is enabled/allowed (only changes behavior when the agent is sandboxed) - Need a real TTY? Use the tmux skill. -Note: `elevated` is ignored when sandboxing is off (bash already runs on the host). +Note: `elevated` is ignored when sandboxing is off (exec already runs on the host). ## Examples Foreground: ```json -{"tool":"bash","command":"ls -la"} +{"tool":"exec","command":"ls -la"} ``` Background + poll: ```json -{"tool":"bash","command":"npm run build","yieldMs":1000} +{"tool":"exec","command":"npm run build","yieldMs":1000} {"tool":"process","action":"poll","sessionId":""} ``` diff --git a/docs/tools/index.md b/docs/tools/index.md index 80fb6587e..8427c98b1 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -31,7 +31,7 @@ alongside tools (for example, the voice-call plugin). ## Tool inventory -### `bash` +### `exec` Run shell commands in the workspace. Core parameters: @@ -45,12 +45,12 @@ Core parameters: Notes: - Returns `status: "running"` with a `sessionId` when backgrounded. - Use `process` to poll/log/write/kill/clear background sessions. -- If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. +- If `process` is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`. - `elevated` is gated by `tools.elevated` plus any `agents.list[].tools.elevated` override (both must allow) and runs on the host. - `elevated` only changes behavior when the agent is sandboxed (otherwise it’s a no-op). ### `process` -Manage background bash sessions. +Manage background exec sessions. Core actions: - `list`, `poll`, `log`, `write`, `kill`, `clear`, `remove` diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index d7970d293..e39d20ab7 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -80,7 +80,7 @@ Override via config: // deny wins deny: ["gateway", "cron"], // if allow is set, it becomes allow-only (deny still wins) - // allow: ["read", "bash", "process"] + // allow: ["read", "exec", "process"] } } } diff --git a/scripts/docker/install-sh-e2e/run.sh b/scripts/docker/install-sh-e2e/run.sh index 9d615b868..6d9f02612 100755 --- a/scripts/docker/install-sh-e2e/run.sh +++ b/scripts/docker/install-sh-e2e/run.sh @@ -265,7 +265,7 @@ function walk(node, parent) { if (name) seen.add(name); } if (typeof obj.name === "string" && typeof obj.input === "object" && obj.input) { - // Many tool-use blocks look like { type: "...", name: "bash", input: {...} } + // Many tool-use blocks look like { type: "...", name: "exec", input: {...} } // but some transcripts omit/rename type. seen.add(obj.name); } @@ -405,7 +405,7 @@ run_profile() { TURN4_JSON="/tmp/agent-${profile}-4.json" run_agent_turn "$profile" "$SESSION_ID" \ - "Use the read tool (not bash) to read proof.txt. Reply with the exact contents only (no extra whitespace)." \ + "Use the read tool (not exec) to read proof.txt. Reply with the exact contents only (no extra whitespace)." \ "$TURN1_JSON" assert_agent_json_has_text "$TURN1_JSON" assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider" @@ -417,7 +417,7 @@ run_profile() { fi local prompt2 - prompt2=$'Use the write tool (not bash) to write exactly this string into copy.txt:\n'"${reply1}"$'\nThen use the read tool (not bash) to read copy.txt and reply with the exact contents only (no extra whitespace).' + prompt2=$'Use the write tool (not exec) to write exactly this string into copy.txt:\n'"${reply1}"$'\nThen use the read tool (not exec) to read copy.txt and reply with the exact contents only (no extra whitespace).' run_agent_turn "$profile" "$SESSION_ID" "$prompt2" "$TURN2_JSON" assert_agent_json_has_text "$TURN2_JSON" assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider" @@ -435,7 +435,7 @@ run_profile() { fi local prompt3 - prompt3=$'Use the bash tool to run: cat /etc/hostname\nThen use the write tool to write the exact stdout (trim trailing newline) into hostname.txt. Reply with the hostname only.' + prompt3=$'Use the exec tool to run: cat /etc/hostname\nThen use the write tool to write the exact stdout (trim trailing newline) into hostname.txt. Reply with the hostname only.' run_agent_turn "$profile" "$SESSION_ID" "$prompt3" "$TURN3_JSON" assert_agent_json_has_text "$TURN3_JSON" assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider" @@ -468,7 +468,7 @@ run_profile() { ls -la "/root/.clawdbot-${profile}/agents/main/sessions" >&2 || true exit 1 fi - assert_session_used_tools "$SESSION_JSONL" read write bash image + assert_session_used_tools "$SESSION_JSONL" read write exec image cleanup_profile trap - EXIT diff --git a/scripts/zai-fallback-repro.ts b/scripts/zai-fallback-repro.ts index 6c8aa1211..14167f5d5 100644 --- a/scripts/zai-fallback-repro.ts +++ b/scripts/zai-fallback-repro.ts @@ -122,7 +122,7 @@ async function main() { console.log("== Run 1: create tool history (primary only)"); const toolPrompt = - "Use the bash tool to create a file named zai-fallback-tool.txt with the content tool-ok. " + + "Use the exec tool to create a file named zai-fallback-tool.txt with the content tool-ok. " + "Then use the read tool to display the file contents. Reply with just the file contents."; const run1 = await runCommand( "run1", diff --git a/skills/tmux/SKILL.md b/skills/tmux/SKILL.md index 66be7ff49..42f5825cb 100644 --- a/skills/tmux/SKILL.md +++ b/skills/tmux/SKILL.md @@ -6,9 +6,9 @@ metadata: {"clawdbot":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins" # tmux Skill (Clawdbot) -Use tmux only when you need an interactive TTY. Prefer bash background mode for long-running, non-interactive tasks. +Use tmux only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks. -## Quickstart (isolated socket, bash tool) +## Quickstart (isolated socket, exec tool) ```bash SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}" diff --git a/src/agents/agent-scope.test.ts b/src/agents/agent-scope.test.ts index e510c287c..069b1c67a 100644 --- a/src/agents/agent-scope.test.ts +++ b/src/agents/agent-scope.test.ts @@ -84,7 +84,7 @@ describe("resolveAgentConfig", () => { workspace: "~/clawd-restricted", tools: { allow: ["read"], - deny: ["bash", "write", "edit"], + deny: ["exec", "write", "edit"], elevated: { enabled: false, allowFrom: { whatsapp: ["+15555550123"] }, @@ -97,7 +97,7 @@ describe("resolveAgentConfig", () => { const result = resolveAgentConfig(cfg, "restricted"); expect(result?.tools).toEqual({ allow: ["read"], - deny: ["bash", "write", "edit"], + deny: ["exec", "write", "edit"], elevated: { enabled: false, allowFrom: { whatsapp: ["+15555550123"] }, @@ -118,7 +118,7 @@ describe("resolveAgentConfig", () => { }, tools: { allow: ["read"], - deny: ["bash"], + deny: ["exec"], }, }, ], diff --git a/src/agents/bash-tools.test.ts b/src/agents/bash-tools.test.ts index 56cff37fe..3e587186b 100644 --- a/src/agents/bash-tools.test.ts +++ b/src/agents/bash-tools.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { resetProcessRegistryForTests } from "./bash-process-registry.js"; import { - bashTool, - createBashTool, + createExecTool, createProcessTool, + execTool, processTool, } from "./bash-tools.js"; import { sanitizeBinaryOutput } from "./shell-utils.js"; @@ -50,7 +50,7 @@ beforeEach(() => { resetProcessRegistryForTests(); }); -describe("bash tool backgrounding", () => { +describe("exec tool backgrounding", () => { const originalShell = process.env.SHELL; beforeEach(() => { @@ -64,7 +64,7 @@ describe("bash tool backgrounding", () => { it( "backgrounds after yield and can be polled", async () => { - const result = await bashTool.execute("call1", { + const result = await execTool.execute("call1", { command: joinCommands([yieldDelayCmd, "echo done"]), yieldMs: 10, }); @@ -97,7 +97,7 @@ describe("bash tool backgrounding", () => { ); it("supports explicit background", async () => { - const result = await bashTool.execute("call1", { + const result = await execTool.execute("call1", { command: echoAfterDelay("later"), background: true, }); @@ -113,7 +113,7 @@ describe("bash tool backgrounding", () => { }); it("derives a session name from the command", async () => { - const result = await bashTool.execute("call1", { + const result = await execTool.execute("call1", { command: "echo hello", background: true, }); @@ -129,7 +129,7 @@ describe("bash tool backgrounding", () => { }); it("uses default timeout when timeout is omitted", async () => { - const customBash = createBashTool({ timeoutSec: 1, backgroundMs: 10 }); + const customBash = createExecTool({ timeoutSec: 1, backgroundMs: 10 }); const customProcess = createProcessTool(); const result = await customBash.execute("call1", { @@ -156,7 +156,7 @@ describe("bash tool backgrounding", () => { }); it("rejects elevated requests when not allowed", async () => { - const customBash = createBashTool({ + const customBash = createExecTool({ elevated: { enabled: true, allowed: false, defaultLevel: "off" }, }); @@ -169,7 +169,7 @@ describe("bash tool backgrounding", () => { }); it("does not default to elevated when not allowed", async () => { - const customBash = createBashTool({ + const customBash = createExecTool({ elevated: { enabled: true, allowed: false, defaultLevel: "on" }, backgroundMs: 1000, timeoutSec: 5, @@ -183,7 +183,7 @@ describe("bash tool backgrounding", () => { }); it("logs line-based slices and defaults to last lines", async () => { - const result = await bashTool.execute("call1", { + const result = await execTool.execute("call1", { command: echoLines(["one", "two", "three"]), background: true, }); @@ -203,7 +203,7 @@ describe("bash tool backgrounding", () => { }); it("supports line offsets for log slices", async () => { - const result = await bashTool.execute("call1", { + const result = await execTool.execute("call1", { command: echoLines(["alpha", "beta", "gamma"]), background: true, }); @@ -221,9 +221,9 @@ describe("bash tool backgrounding", () => { }); it("scopes process sessions by scopeKey", async () => { - const bashA = createBashTool({ backgroundMs: 10, scopeKey: "agent:alpha" }); + const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" }); const processA = createProcessTool({ scopeKey: "agent:alpha" }); - const bashB = createBashTool({ backgroundMs: 10, scopeKey: "agent:beta" }); + const bashB = createExecTool({ backgroundMs: 10, scopeKey: "agent:beta" }); const processB = createProcessTool({ scopeKey: "agent:beta" }); const resultA = await bashA.execute("call1", { diff --git a/src/agents/bash-tools.ts b/src/agents/bash-tools.ts index 1d1175373..97fa45cc2 100644 --- a/src/agents/bash-tools.ts +++ b/src/agents/bash-tools.ts @@ -54,11 +54,11 @@ const _stringEnum = ( ...options, }); -export type BashToolDefaults = { +export type ExecToolDefaults = { backgroundMs?: number; timeoutSec?: number; sandbox?: BashSandboxConfig; - elevated?: BashElevatedDefaults; + elevated?: ExecElevatedDefaults; allowBackground?: boolean; scopeKey?: string; cwd?: string; @@ -76,14 +76,14 @@ export type BashSandboxConfig = { env?: Record; }; -export type BashElevatedDefaults = { +export type ExecElevatedDefaults = { enabled: boolean; allowed: boolean; defaultLevel: "on" | "off"; }; -const bashSchema = Type.Object({ - command: Type.String({ description: "Bash command to execute" }), +const execSchema = Type.Object({ + command: Type.String({ description: "Shell command to execute" }), workdir: Type.Optional( Type.String({ description: "Working directory (defaults to cwd)" }), ), @@ -108,7 +108,7 @@ const bashSchema = Type.Object({ ), }); -export type BashToolDetails = +export type ExecToolDetails = | { status: "running"; sessionId: string; @@ -125,10 +125,10 @@ export type BashToolDetails = cwd?: string; }; -export function createBashTool( - defaults?: BashToolDefaults, +export function createExecTool( + defaults?: ExecToolDefaults, // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. -): AgentTool { +): AgentTool { const defaultBackgroundMs = clampNumber( defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), 10_000, @@ -142,11 +142,11 @@ export function createBashTool( : 1800; return { - name: "bash", - label: "bash", + name: "exec", + label: "exec", description: - "Execute bash with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.", - parameters: bashSchema, + "Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.", + parameters: execSchema, execute: async (_toolCallId, args, signal, onUpdate) => { const params = args as { command: string; @@ -218,7 +218,7 @@ export function createBashTool( ); } logInfo( - `bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle( + `exec: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle( params.command, 120, )}`, @@ -363,7 +363,7 @@ export function createBashTool( } }); - return new Promise>( + return new Promise>( (resolve, reject) => { const resolveRunning = () => { settle(() => @@ -482,7 +482,7 @@ export function createBashTool( }; } -export const bashTool = createBashTool(); +export const execTool = createExecTool(); const processSchema = Type.Object({ action: Type.String({ description: "Process action" }), @@ -509,7 +509,7 @@ export function createProcessTool( return { name: "process", label: "process", - description: "Manage running bash sessions: list, poll, log, write, kill.", + description: "Manage running exec sessions: list, poll, log, write, kill.", parameters: processSchema, execute: async (_toolCallId, args) => { const params = args as { diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts index c7d06d416..5bc3f1dcb 100644 --- a/src/agents/pi-embedded-helpers.test.ts +++ b/src/agents/pi-embedded-helpers.test.ts @@ -356,7 +356,7 @@ describe("sanitizeGoogleTurnOrdering", () => { { role: "assistant", content: [ - { type: "toolCall", id: "call_1", name: "bash", arguments: {} }, + { type: "toolCall", id: "call_1", name: "exec", arguments: {} }, ], }, ] satisfies AgentMessage[]; @@ -403,7 +403,7 @@ describe("sanitizeSessionMessagesImages", () => { { type: "toolCall", id: "call_abc|item:456", - name: "bash", + name: "exec", arguments: {}, }, ], diff --git a/src/agents/pi-embedded-runner.test.ts b/src/agents/pi-embedded-runner.test.ts index c1a7ffae7..75421bc5e 100644 --- a/src/agents/pi-embedded-runner.test.ts +++ b/src/agents/pi-embedded-runner.test.ts @@ -44,7 +44,7 @@ describe("buildEmbeddedSandboxInfo", () => { env: { LANG: "C.UTF-8" }, }, tools: { - allow: ["bash"], + allow: ["exec"], deny: ["browser"], }, browserAllowHostControl: true, @@ -87,7 +87,7 @@ describe("buildEmbeddedSandboxInfo", () => { env: { LANG: "C.UTF-8" }, }, tools: { - allow: ["bash"], + allow: ["exec"], deny: ["browser"], }, browserAllowHostControl: false, @@ -171,7 +171,7 @@ function createStubTool(name: string): AgentTool { describe("splitSdkTools", () => { const tools = [ createStubTool("read"), - createStubTool("bash"), + createStubTool("exec"), createStubTool("edit"), createStubTool("write"), createStubTool("browser"), @@ -185,7 +185,7 @@ describe("splitSdkTools", () => { expect(builtInTools).toEqual([]); expect(customTools.map((tool) => tool.name)).toEqual([ "read", - "bash", + "exec", "edit", "write", "browser", @@ -200,7 +200,7 @@ describe("splitSdkTools", () => { expect(builtInTools).toEqual([]); expect(customTools.map((tool) => tool.name)).toEqual([ "read", - "bash", + "exec", "edit", "write", "browser", @@ -226,7 +226,7 @@ describe("applyGoogleTurnOrderingFix", () => { { role: "assistant", content: [ - { type: "toolCall", id: "call_1", name: "bash", arguments: {} }, + { type: "toolCall", id: "call_1", name: "exec", arguments: {} }, ], }, ] satisfies AgentMessage[]; @@ -360,7 +360,7 @@ describe("limitHistoryTurns", () => { { role: "user", content: [{ type: "text", text: "first" }] }, { role: "assistant", - content: [{ type: "toolCall", id: "1", name: "bash", arguments: {} }], + content: [{ type: "toolCall", id: "1", name: "exec", arguments: {} }], }, { role: "user", content: [{ type: "text", text: "second" }] }, { role: "assistant", content: [{ type: "text", text: "response" }] }, diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index a5aa190ce..f8acdd185 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -51,7 +51,7 @@ import { markAuthProfileGood, markAuthProfileUsed, } from "./auth-profiles.js"; -import type { BashElevatedDefaults } from "./bash-tools.js"; +import type { ExecElevatedDefaults, ExecToolDefaults } from "./bash-tools.js"; import { CONTEXT_WINDOW_HARD_MIN_TOKENS, CONTEXT_WINDOW_WARN_BELOW_TOKENS, @@ -768,11 +768,11 @@ function describeUnknownError(error: unknown): string { export function buildEmbeddedSandboxInfo( sandbox?: Awaited>, - bashElevated?: BashElevatedDefaults, + execElevated?: ExecElevatedDefaults, ): EmbeddedSandboxInfo | undefined { if (!sandbox?.enabled) return undefined; const elevatedAllowed = Boolean( - bashElevated?.enabled && bashElevated.allowed, + execElevated?.enabled && execElevated.allowed, ); return { enabled: true, @@ -790,7 +790,7 @@ export function buildEmbeddedSandboxInfo( ? { elevated: { allowed: true, - defaultLevel: bashElevated?.defaultLevel ?? "off", + defaultLevel: execElevated?.defaultLevel ?? "off", }, } : {}), @@ -949,6 +949,16 @@ function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { return level; } +function resolveExecToolDefaults( + config?: ClawdbotConfig, +): ExecToolDefaults | undefined { + const tools = config?.tools; + if (!tools) return undefined; + if (!tools.exec) return tools.bash; + if (!tools.bash) return tools.exec; + return { ...tools.bash, ...tools.exec }; +} + function resolveModel( provider: string, modelId: string, @@ -987,7 +997,7 @@ export async function compactEmbeddedPiSession(params: { model?: string; thinkLevel?: ThinkLevel; reasoningLevel?: ReasoningLevel; - bashElevated?: BashElevatedDefaults; + bashElevated?: ExecElevatedDefaults; customInstructions?: string; lane?: string; enqueue?: typeof enqueueCommand; @@ -1087,8 +1097,8 @@ export async function compactEmbeddedPiSession(params: { const contextFiles = buildBootstrapContextFiles(bootstrapFiles); const runAbortController = new AbortController(); const tools = createClawdbotCodingTools({ - bash: { - ...params.config?.tools?.bash, + exec: { + ...resolveExecToolDefaults(params.config), elevated: params.bashElevated, }, sandbox, @@ -1289,7 +1299,7 @@ export async function runEmbeddedPiAgent(params: { thinkLevel?: ThinkLevel; verboseLevel?: VerboseLevel; reasoningLevel?: ReasoningLevel; - bashElevated?: BashElevatedDefaults; + bashElevated?: ExecElevatedDefaults; timeoutMs: number; runId: string; abortSignal?: AbortSignal; @@ -1499,8 +1509,8 @@ export async function runEmbeddedPiAgent(params: { // Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`). // `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged. const tools = createClawdbotCodingTools({ - bash: { - ...params.config?.tools?.bash, + exec: { + ...resolveExecToolDefaults(params.config), elevated: params.bashElevated, }, sandbox, diff --git a/src/agents/pi-extensions/context-pruning.test.ts b/src/agents/pi-extensions/context-pruning.test.ts index 43c06346b..4df741951 100644 --- a/src/agents/pi-extensions/context-pruning.test.ts +++ b/src/agents/pi-extensions/context-pruning.test.ts @@ -94,28 +94,28 @@ describe("context-pruning", () => { makeAssistant("a1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "x".repeat(20_000), }), makeUser("u2"), makeAssistant("a2"), makeToolResult({ toolCallId: "t2", - toolName: "bash", + toolName: "exec", text: "y".repeat(20_000), }), makeUser("u3"), makeAssistant("a3"), makeToolResult({ toolCallId: "t3", - toolName: "bash", + toolName: "exec", text: "z".repeat(20_000), }), makeUser("u4"), makeAssistant("a4"), makeToolResult({ toolCallId: "t4", - toolName: "bash", + toolName: "exec", text: "w".repeat(20_000), }), ]; @@ -161,7 +161,7 @@ describe("context-pruning", () => { makeUser("u1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "y".repeat(20_000), }), ]; @@ -184,19 +184,19 @@ describe("context-pruning", () => { makeAssistant("a1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "x".repeat(20_000), }), makeToolResult({ toolCallId: "t2", - toolName: "bash", + toolName: "exec", text: "y".repeat(20_000), }), makeUser("u2"), makeAssistant("a2"), makeToolResult({ toolCallId: "t3", - toolName: "bash", + toolName: "exec", text: "z".repeat(20_000), }), ]; @@ -225,7 +225,7 @@ describe("context-pruning", () => { makeAssistant("a1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "x".repeat(20_000), }), makeAssistant("a2"), @@ -273,7 +273,7 @@ describe("context-pruning", () => { makeAssistant("a1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "x".repeat(20_000), }), makeAssistant("a2"), @@ -313,7 +313,7 @@ describe("context-pruning", () => { makeUser("u1"), makeToolResult({ toolCallId: "t1", - toolName: "Bash", + toolName: "Exec", text: "x".repeat(20_000), }), makeToolResult({ @@ -329,7 +329,7 @@ describe("context-pruning", () => { softTrimRatio: 0.0, hardClearRatio: 0.0, minPrunableToolChars: 0, - tools: { allow: ["ba*"], deny: ["bash"] }, + tools: { allow: ["ex*"], deny: ["exec"] }, hardClear: { enabled: true, placeholder: "[cleared]" }, softTrim: { maxChars: 10, headChars: 3, tailChars: 3 }, }; @@ -339,7 +339,7 @@ describe("context-pruning", () => { } as unknown as ExtensionContext; const next = pruneContextMessages({ messages, settings, ctx }); - // Deny wins => bash is not pruned, even though allow matches. + // Deny wins => exec is not pruned, even though allow matches. expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000)); // allow is non-empty and browser is not allowed => never pruned. expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000)); @@ -350,7 +350,7 @@ describe("context-pruning", () => { makeUser("u1"), makeImageToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "x".repeat(20_000), }), ]; @@ -384,7 +384,7 @@ describe("context-pruning", () => { { role: "toolResult", toolCallId: "t1", - toolName: "bash", + toolName: "exec", content: [ { type: "text", text: "AAAAA" }, { type: "text", text: "BBBBB" }, @@ -418,7 +418,7 @@ describe("context-pruning", () => { makeUser("u1"), makeToolResult({ toolCallId: "t1", - toolName: "bash", + toolName: "exec", text: "abcdefghij".repeat(1000), }), ]; diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts index 485cc0be1..0b6553447 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/pi-tools-agent-config.test.ts @@ -30,7 +30,7 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); expect(toolNames).toContain("read"); expect(toolNames).toContain("write"); - expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("exec"); }); it("should keep global tool policy when agent only sets tools.elevated", () => { @@ -62,7 +62,7 @@ describe("Agent-specific tool filtering", () => { }); const toolNames = tools.map((t) => t.name); - expect(toolNames).toContain("bash"); + expect(toolNames).toContain("exec"); expect(toolNames).toContain("read"); expect(toolNames).not.toContain("write"); }); @@ -70,7 +70,7 @@ describe("Agent-specific tool filtering", () => { it("should apply agent-specific tool policy", () => { const cfg: ClawdbotConfig = { tools: { - allow: ["read", "write", "bash"], + allow: ["read", "write", "exec"], deny: [], }, agents: { @@ -80,7 +80,7 @@ describe("Agent-specific tool filtering", () => { workspace: "~/clawd-restricted", tools: { allow: ["read"], // Agent override: only read - deny: ["bash", "write", "edit"], + deny: ["exec", "write", "edit"], }, }, ], @@ -96,7 +96,7 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); expect(toolNames).toContain("read"); - expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("exec"); expect(toolNames).not.toContain("write"); expect(toolNames).not.toContain("edit"); }); @@ -115,7 +115,7 @@ describe("Agent-specific tool filtering", () => { workspace: "~/clawd-family", tools: { allow: ["read"], - deny: ["bash", "write", "edit", "process"], + deny: ["exec", "write", "edit", "process"], }, }, ], @@ -130,7 +130,7 @@ describe("Agent-specific tool filtering", () => { agentDir: "/tmp/agent-main", }); const mainToolNames = mainTools.map((t) => t.name); - expect(mainToolNames).toContain("bash"); + expect(mainToolNames).toContain("exec"); expect(mainToolNames).toContain("write"); expect(mainToolNames).toContain("edit"); @@ -143,7 +143,7 @@ describe("Agent-specific tool filtering", () => { }); const familyToolNames = familyTools.map((t) => t.name); expect(familyToolNames).toContain("read"); - expect(familyToolNames).not.toContain("bash"); + expect(familyToolNames).not.toContain("exec"); expect(familyToolNames).not.toContain("write"); expect(familyToolNames).not.toContain("edit"); }); @@ -159,7 +159,7 @@ describe("Agent-specific tool filtering", () => { id: "work", workspace: "~/clawd-work", tools: { - deny: ["bash", "process"], // Agent deny (override) + deny: ["exec", "process"], // Agent deny (override) }, }, ], @@ -176,7 +176,7 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); // Agent policy overrides global: browser is allowed again expect(toolNames).toContain("browser"); - expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("exec"); expect(toolNames).not.toContain("process"); }); @@ -199,7 +199,7 @@ describe("Agent-specific tool filtering", () => { }, tools: { allow: ["read"], // Agent further restricts to only read - deny: ["bash", "write"], + deny: ["exec", "write"], }, }, ], @@ -207,7 +207,7 @@ describe("Agent-specific tool filtering", () => { tools: { sandbox: { tools: { - allow: ["read", "write", "bash"], // Sandbox allows these + allow: ["read", "write", "exec"], // Sandbox allows these deny: [], }, }, @@ -237,7 +237,7 @@ describe("Agent-specific tool filtering", () => { capDrop: [], } satisfies SandboxDockerConfig, tools: { - allow: ["read", "write", "bash"], + allow: ["read", "write", "exec"], deny: [], }, browserAllowHostControl: false, @@ -246,14 +246,14 @@ describe("Agent-specific tool filtering", () => { const toolNames = tools.map((t) => t.name); // Agent policy should be applied first, then sandbox - // Agent allows only "read", sandbox allows ["read", "write", "bash"] + // Agent allows only "read", sandbox allows ["read", "write", "exec"] // Result: only "read" (most restrictive wins) expect(toolNames).toContain("read"); - expect(toolNames).not.toContain("bash"); + expect(toolNames).not.toContain("exec"); expect(toolNames).not.toContain("write"); }); - it("should run bash synchronously when process is denied", async () => { + it("should run exec synchronously when process is denied", async () => { const cfg: ClawdbotConfig = { tools: { deny: ["process"], @@ -266,10 +266,10 @@ describe("Agent-specific tool filtering", () => { workspaceDir: "/tmp/test-main", agentDir: "/tmp/agent-main", }); - const bash = tools.find((tool) => tool.name === "bash"); - expect(bash).toBeDefined(); + const execTool = tools.find((tool) => tool.name === "exec"); + expect(execTool).toBeDefined(); - const result = await bash?.execute("call1", { + const result = await execTool?.execute("call1", { command: "echo done", yieldMs: 10, }); diff --git a/src/agents/pi-tools.test.ts b/src/agents/pi-tools.test.ts index 0f2470b50..600fbddac 100644 --- a/src/agents/pi-tools.test.ts +++ b/src/agents/pi-tools.test.ts @@ -153,9 +153,9 @@ describe("createClawdbotCodingTools", () => { } }); - it("includes bash and process tools", () => { + it("includes exec and process tools", () => { const tools = createClawdbotCodingTools(); - expect(tools.some((tool) => tool.name === "bash")).toBe(true); + expect(tools.some((tool) => tool.name === "exec")).toBe(true); expect(tools.some((tool) => tool.name === "process")).toBe(true); }); @@ -165,7 +165,7 @@ describe("createClawdbotCodingTools", () => { modelAuthMode: "oauth", }); const names = new Set(tools.map((tool) => tool.name)); - expect(names.has("bash")).toBe(true); + expect(names.has("exec")).toBe(true); expect(names.has("read")).toBe(true); expect(names.has("write")).toBe(true); expect(names.has("edit")).toBe(true); @@ -210,7 +210,7 @@ describe("createClawdbotCodingTools", () => { expect(names.has("sessions_spawn")).toBe(false); expect(names.has("read")).toBe(true); - expect(names.has("bash")).toBe(true); + expect(names.has("exec")).toBe(true); expect(names.has("process")).toBe(true); }); @@ -330,7 +330,7 @@ describe("createClawdbotCodingTools", () => { browserAllowHostControl: false, }; const tools = createClawdbotCodingTools({ sandbox }); - expect(tools.some((tool) => tool.name === "bash")).toBe(true); + expect(tools.some((tool) => tool.name === "exec")).toBe(true); expect(tools.some((tool) => tool.name === "read")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false); }); @@ -371,7 +371,7 @@ describe("createClawdbotCodingTools", () => { const tools = createClawdbotCodingTools({ config: { tools: { deny: ["browser"] } }, }); - expect(tools.some((tool) => tool.name === "bash")).toBe(true); + expect(tools.some((tool) => tool.name === "exec")).toBe(true); expect(tools.some((tool) => tool.name === "browser")).toBe(false); }); diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts index 7f4a23758..854801996 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/pi-tools.ts @@ -15,9 +15,9 @@ import { resolveAgentIdFromSessionKey, } from "./agent-scope.js"; import { - type BashToolDefaults, - createBashTool, + createExecTool, createProcessTool, + type ExecToolDefaults, type ProcessToolDefaults, } from "./bash-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js"; @@ -290,9 +290,18 @@ function cleanToolSchemaForGemini(schema: Record): unknown { return cleanSchemaForGemini(schema); } +const TOOL_NAME_ALIASES: Record = { + bash: "exec", +}; + +function normalizeToolName(name: string) { + const normalized = name.trim().toLowerCase(); + return TOOL_NAME_ALIASES[normalized] ?? normalized; +} + function normalizeToolNames(list?: string[]) { if (!list) return []; - return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean); + return list.map(normalizeToolName).filter(Boolean); } const DEFAULT_SUBAGENT_TOOL_DENY = [ @@ -354,7 +363,7 @@ function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) { const deny = new Set(normalizeToolNames(policy.deny)); const allowRaw = normalizeToolNames(policy.allow); const allow = allowRaw.length > 0 ? new Set(allowRaw) : null; - const normalized = name.trim().toLowerCase(); + const normalized = normalizeToolName(name); if (deny.has(normalized)) return false; if (allow) return allow.has(normalized); return true; @@ -467,7 +476,7 @@ function wrapToolWithAbortSignal( } export function createClawdbotCodingTools(options?: { - bash?: BashToolDefaults & ProcessToolDefaults; + exec?: ExecToolDefaults & ProcessToolDefaults; messageProvider?: string; agentAccountId?: string; sandbox?: SandboxContext | null; @@ -495,14 +504,14 @@ export function createClawdbotCodingTools(options?: { /** Mutable ref to track if a reply was sent (for "first" mode). */ hasRepliedRef?: { value: boolean }; }): AnyAgentTool[] { - const bashToolName = "bash"; + const execToolName = "exec"; const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined; const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({ config: options?.config, sessionKey: options?.sessionKey, }); const scopeKey = - options?.bash?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); + options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); const subagentPolicy = isSubagentSessionKey(options?.sessionKey) && options?.sessionKey ? resolveSubagentToolPolicy(options.config) @@ -524,7 +533,7 @@ export function createClawdbotCodingTools(options?: { const freshReadTool = createReadTool(workspaceRoot); return [createClawdbotReadTool(freshReadTool)]; } - if (tool.name === bashToolName) return []; + if (tool.name === "bash" || tool.name === execToolName) return []; if (tool.name === "write") { if (sandboxRoot) return []; return [createWriteTool(workspaceRoot)]; @@ -535,8 +544,8 @@ export function createClawdbotCodingTools(options?: { } return [tool as AnyAgentTool]; }); - const bashTool = createBashTool({ - ...options?.bash, + const execTool = createExecTool({ + ...options?.exec, cwd: options?.workspaceDir, allowBackground, scopeKey, @@ -550,7 +559,7 @@ export function createClawdbotCodingTools(options?: { : undefined, }); const processTool = createProcessTool({ - cleanupMs: options?.bash?.cleanupMs, + cleanupMs: options?.exec?.cleanupMs, scopeKey, }); const tools: AnyAgentTool[] = [ @@ -563,7 +572,7 @@ export function createClawdbotCodingTools(options?: { ] : [] : []), - bashTool as unknown as AnyAgentTool, + execTool as unknown as AnyAgentTool, processTool as unknown as AnyAgentTool, // Provider docking: include provider-defined agent tools (login, etc.). ...listProviderAgentTools({ cfg: options?.config }), diff --git a/src/agents/pi-tools.workspace-paths.test.ts b/src/agents/pi-tools.workspace-paths.test.ts index c27b3df53..1d1590089 100644 --- a/src/agents/pi-tools.workspace-paths.test.ts +++ b/src/agents/pi-tools.workspace-paths.test.ts @@ -110,13 +110,13 @@ describe("workspace path resolution", () => { }); }); - it("defaults bash cwd to workspaceDir when workdir is omitted", async () => { + it("defaults exec cwd to workspaceDir when workdir is omitted", async () => { await withTempDir("clawdbot-ws-", async (workspaceDir) => { const tools = createClawdbotCodingTools({ workspaceDir }); - const bashTool = tools.find((tool) => tool.name === "bash"); - expect(bashTool).toBeDefined(); + const execTool = tools.find((tool) => tool.name === "exec"); + expect(execTool).toBeDefined(); - const result = await bashTool?.execute("ws-bash", { + const result = await execTool?.execute("ws-exec", { command: "echo ok", }); const cwd = @@ -134,14 +134,14 @@ describe("workspace path resolution", () => { }); }); - it("lets bash workdir override the workspace default", async () => { + it("lets exec workdir override the workspace default", async () => { await withTempDir("clawdbot-ws-", async (workspaceDir) => { await withTempDir("clawdbot-override-", async (overrideDir) => { const tools = createClawdbotCodingTools({ workspaceDir }); - const bashTool = tools.find((tool) => tool.name === "bash"); - expect(bashTool).toBeDefined(); + const execTool = tools.find((tool) => tool.name === "exec"); + expect(execTool).toBeDefined(); - const result = await bashTool?.execute("ws-bash-override", { + const result = await execTool?.execute("ws-exec-override", { command: "echo ok", workdir: overrideDir, }); diff --git a/src/agents/sandbox-agent-config.test.ts b/src/agents/sandbox-agent-config.test.ts index 8cd22da88..0923c0b5e 100644 --- a/src/agents/sandbox-agent-config.test.ts +++ b/src/agents/sandbox-agent-config.test.ts @@ -450,7 +450,7 @@ describe("Agent-specific sandbox config", () => { sandbox: { tools: { allow: ["read"], - deny: ["bash"], + deny: ["exec"], }, }, }, diff --git a/src/agents/sandbox.ts b/src/agents/sandbox.ts index 0c2204f3b..b0339afdb 100644 --- a/src/agents/sandbox.ts +++ b/src/agents/sandbox.ts @@ -163,7 +163,7 @@ const DEFAULT_SANDBOX_WORKDIR = "/workspace"; const DEFAULT_SANDBOX_IDLE_HOURS = 24; const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7; const DEFAULT_TOOL_ALLOW = [ - "bash", + "exec", "process", "read", "write", diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index 7c6819ac4..7d736f014 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -9,14 +9,14 @@ describe("sanitizeToolUseResultPairing", () => { role: "assistant", content: [ { type: "toolCall", id: "call_1", name: "read", arguments: {} }, - { type: "toolCall", id: "call_2", name: "bash", arguments: {} }, + { type: "toolCall", id: "call_2", name: "exec", arguments: {} }, ], }, { role: "user", content: "user message that should come after tool use" }, { role: "toolResult", toolCallId: "call_2", - toolName: "bash", + toolName: "exec", content: [{ type: "text", text: "ok" }], isError: false, }, diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index 8626f7bcb..85da9c896 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -37,7 +37,7 @@ describe("buildAgentSystemPrompt", () => { it("lists available tools when provided", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", - toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"], + toolNames: ["exec", "sessions_list", "sessions_history", "sessions_send"], }); expect(prompt).toContain("Tool availability (filtered by policy):"); @@ -49,13 +49,13 @@ describe("buildAgentSystemPrompt", () => { it("preserves tool casing in the prompt", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", - toolNames: ["Read", "Bash", "process"], + toolNames: ["Read", "Exec", "process"], skillsPrompt: "\n \n demo\n \n", }); expect(prompt).toContain("- Read: Read file contents"); - expect(prompt).toContain("- Bash: Run shell commands"); + expect(prompt).toContain("- Exec: Run shell commands"); expect(prompt).toContain( "Use `Read` to load the SKILL.md at the location listed for that skill.", ); @@ -90,7 +90,7 @@ describe("buildAgentSystemPrompt", () => { it("adds ClaudeBot self-update guidance when gateway tool is available", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/clawd", - toolNames: ["gateway", "bash"], + toolNames: ["gateway", "exec"], }); expect(prompt).toContain("## Clawdbot Self-Update"); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index 9264a5599..8d2756e97 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -53,8 +53,8 @@ export function buildAgentSystemPrompt(params: { grep: "Search file contents for patterns", find: "Find files by glob pattern", ls: "List directory contents", - bash: "Run shell commands", - process: "Manage background bash sessions", + exec: "Run shell commands", + process: "Manage background exec sessions", // Provider docking: add provider login tools here when a provider needs interactive linking. browser: "Control web browser", canvas: "Present/eval/snapshot the Canvas", @@ -80,7 +80,7 @@ export function buildAgentSystemPrompt(params: { "grep", "find", "ls", - "bash", + "exec", "process", "browser", "canvas", @@ -133,7 +133,7 @@ export function buildAgentSystemPrompt(params: { const hasGateway = availableTools.has("gateway"); const readToolName = resolveToolName("read"); - const bashToolName = resolveToolName("bash"); + const execToolName = resolveToolName("exec"); const processToolName = resolveToolName("process"); const extraSystemPrompt = params.extraSystemPrompt?.trim(); const ownerNumbers = (params.ownerNumbers ?? []) @@ -195,8 +195,8 @@ export function buildAgentSystemPrompt(params: { "- grep: search file contents for patterns", "- find: find files by glob pattern", "- ls: list directory contents", - `- ${bashToolName}: run shell commands (supports background via yieldMs/background)`, - `- ${processToolName}: manage background bash sessions`, + `- ${execToolName}: run shell commands (supports background via yieldMs/background)`, + `- ${processToolName}: manage background exec sessions`, "- browser: control clawd's dedicated browser", "- canvas: present/eval/snapshot the Canvas", "- nodes: list/describe/notify/camera/screen on paired nodes", @@ -277,7 +277,7 @@ export function buildAgentSystemPrompt(params: { )}` : "", params.sandboxInfo.elevated?.allowed - ? "Elevated bash is available for this session." + ? "Elevated exec is available for this session." : "", params.sandboxInfo.elevated?.allowed ? "User can toggle with /elevated on|off." @@ -288,7 +288,7 @@ export function buildAgentSystemPrompt(params: { params.sandboxInfo.elevated?.allowed ? `Current elevated level: ${ params.sandboxInfo.elevated.defaultLevel - } (on runs bash on host; off runs in sandbox).` + } (on runs exec on host; off runs in sandbox).` : "", ] .filter(Boolean) @@ -315,7 +315,7 @@ export function buildAgentSystemPrompt(params: { "## Messaging", "- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)", "- Cross-session messaging → use sessions_send(sessionKey, message)", - "- Never use bash/curl for provider messaging; Clawdbot handles all routing internally.", + "- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.", availableTools.has("message") ? [ "", diff --git a/src/agents/tool-display.json b/src/agents/tool-display.json index 5ab8a8483..c3174c31d 100644 --- a/src/agents/tool-display.json +++ b/src/agents/tool-display.json @@ -25,9 +25,9 @@ ] }, "tools": { - "bash": { + "exec": { "emoji": "🛠️", - "title": "Bash", + "title": "Exec", "detailKeys": ["command"] }, "process": { diff --git a/src/config/config.test.ts b/src/config/config.test.ts index ef23b1911..a2d91d47a 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -1100,7 +1100,7 @@ describe("legacy config detection", () => { expect(res.changes).toContain("Moved agent.tools.allow → tools.allow."); expect(res.changes).toContain("Moved agent.tools.deny → tools.deny."); expect(res.changes).toContain("Moved agent.elevated → tools.elevated."); - expect(res.changes).toContain("Moved agent.bash → tools.bash."); + expect(res.changes).toContain("Moved agent.bash → tools.exec."); expect(res.changes).toContain( "Moved agent.sandbox.tools → tools.sandbox.tools.", ); @@ -1118,7 +1118,7 @@ describe("legacy config detection", () => { enabled: true, allowFrom: { discord: ["user:1"] }, }); - expect(res.config?.tools?.bash).toEqual({ timeoutSec: 12 }); + expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 }); expect(res.config?.tools?.sandbox?.tools).toEqual({ allow: ["browser.open"], }); diff --git a/src/config/legacy.ts b/src/config/legacy.ts index 3f895079b..aca672848 100644 --- a/src/config/legacy.ts +++ b/src/config/legacy.ts @@ -179,7 +179,7 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [ { path: ["agent"], message: - "agent.* was moved; use agents.defaults (and tools.* for tool/elevated/bash settings) instead (run `clawdbot doctor` to migrate).", + "agent.* was moved; use agents.defaults (and tools.* for tool/elevated/exec settings) instead (run `clawdbot doctor` to migrate).", }, { path: ["agent", "model"], @@ -819,9 +819,11 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [ const bash = getRecord(agent.bash); if (bash) { - if (tools.bash === undefined) { - tools.bash = bash; - changes.push("Moved agent.bash → tools.bash."); + if (tools.exec === undefined && tools.bash === undefined) { + tools.exec = bash; + changes.push("Moved agent.bash → tools.exec."); + } else if (tools.exec !== undefined) { + changes.push("Removed agent.bash (tools.exec already set)."); } else { changes.push("Removed agent.bash (tools.bash already set)."); } diff --git a/src/config/types.ts b/src/config/types.ts index 9bf8b6f06..37fdfeb04 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -972,7 +972,7 @@ export type QueueConfig = { export type AgentToolsConfig = { allow?: string[]; deny?: string[]; - /** Per-agent elevated bash gate (can only further restrict global tools.elevated). */ + /** Per-agent elevated exec gate (can only further restrict global tools.elevated). */ elevated?: { /** Enable or disable elevated mode for this agent (default: true). */ enabled?: boolean; @@ -1003,14 +1003,23 @@ export type ToolsConfig = { /** Allowlist of agent ids or patterns (implementation-defined). */ allow?: string[]; }; - /** Elevated bash permissions for the host machine. */ + /** Elevated exec permissions for the host machine. */ elevated?: { /** Enable or disable elevated mode (default: true). */ enabled?: boolean; /** Approved senders for /elevated (per-provider allowlists). */ allowFrom?: AgentElevatedAllowFromConfig; }; - /** Bash tool defaults. */ + /** Exec tool defaults. */ + exec?: { + /** Default time (ms) before an exec command auto-backgrounds. */ + backgroundMs?: number; + /** Default timeout (seconds) before auto-killing exec commands. */ + timeoutSec?: number; + /** How long to keep finished sessions in memory (ms). */ + cleanupMs?: number; + }; + /** @deprecated Use tools.exec. */ bash?: { /** Default time (ms) before a bash command auto-backgrounds. */ backgroundMs?: number; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index c30dba4bd..66e842130 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -905,6 +905,13 @@ const ToolsSchema = z allowFrom: ElevatedAllowFromSchema, }) .optional(), + exec: z + .object({ + backgroundMs: z.number().int().positive().optional(), + timeoutSec: z.number().int().positive().optional(), + cleanupMs: z.number().int().positive().optional(), + }) + .optional(), bash: z .object({ backgroundMs: z.number().int().positive().optional(), diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 5dee21e9f..358804303 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -419,14 +419,14 @@ describeLive("gateway live (dev agent, profile keys)", () => { `write-${runIdTool}.txt`, ); - const bashReadProbe = await client.request( + const execReadProbe = await client.request( "agent", { sessionKey, - idempotencyKey: `idem-${runIdTool}-bash-read`, + idempotencyKey: `idem-${runIdTool}-exec-read`, message: "Clawdbot live tool probe (local, safe): " + - "use the tool named `bash` (or `Bash`) to run this command: " + + "use the tool named `exec` (or `Exec`) to run this command: " + `mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` + `Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` + "Finally reply including the nonce text you read back.", @@ -434,15 +434,15 @@ describeLive("gateway live (dev agent, profile keys)", () => { }, { expectFinal: true }, ); - if (bashReadProbe?.status !== "ok") { + if (execReadProbe?.status !== "ok") { throw new Error( - `bash+read probe failed: status=${String(bashReadProbe?.status)}`, + `exec+read probe failed: status=${String(execReadProbe?.status)}`, ); } - const bashReadText = extractPayloadText(bashReadProbe?.result); - if (!bashReadText.includes(nonceC)) { + const execReadText = extractPayloadText(execReadProbe?.result); + if (!execReadText.includes(nonceC)) { throw new Error( - `bash+read probe missing nonce: ${bashReadText}`, + `exec+read probe missing nonce: ${execReadText}`, ); }