From 98337a14b3ef7ab791c7950d6ec1a16a1becf877 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 02:49:55 +0000 Subject: [PATCH] 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}`, ); }