fix: rename bash tool to exec (#748) (thanks @myfunc)
This commit is contained in:
parent
b33bd6aaeb
commit
98337a14b3
51 changed files with 294 additions and 252 deletions
|
|
@ -10,6 +10,7 @@
|
||||||
- Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs.
|
- Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs.
|
||||||
- Tests: add Docker plugin loader + tgz-install smoke test.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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`.
|
- **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)
|
Details: [Remote access](https://docs.clawd.bot/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/security)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
|
||||||
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
|
Session: agent:alfred:whatsapp:group:120363403215116621@g.us
|
||||||
History: [user message, alfred's previous responses]
|
History: [user message, alfred's previous responses]
|
||||||
Workspace: /Users/pascal/clawd-alfred/
|
Workspace: /Users/pascal/clawd-alfred/
|
||||||
Tools: read, write, bash
|
Tools: read, write, exec
|
||||||
```
|
```
|
||||||
|
|
||||||
**Bärbel's context:**
|
**Bärbel's context:**
|
||||||
|
|
@ -230,10 +230,10 @@ Give agents only the tools they need:
|
||||||
{
|
{
|
||||||
"agents": {
|
"agents": {
|
||||||
"reviewer": {
|
"reviewer": {
|
||||||
"tools": { "allow": ["read", "bash"] } // Read-only
|
"tools": { "allow": ["read", "exec"] } // Read-only
|
||||||
},
|
},
|
||||||
"fixer": {
|
"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": {
|
"agents": {
|
||||||
"list": [
|
"list": [
|
||||||
{ "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } },
|
{ "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } },
|
||||||
{ "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "bash"] } },
|
{ "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } },
|
||||||
{ "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "bash"] } },
|
{ "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } },
|
||||||
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
|
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
|
||||||
|
|
||||||
## Built-in tools
|
## 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
|
## Skills
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"], // Only read tool
|
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
|
- **Flexible policies**: Different permissions per agent
|
||||||
|
|
||||||
Note: `tools.elevated` is **global** and sender-based; it is not configurable 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.
|
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.
|
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples.
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ Restrict pruning to specific tools:
|
||||||
agent: {
|
agent: {
|
||||||
contextPruning: {
|
contextPruning: {
|
||||||
mode: "adaptive",
|
mode: "adaptive",
|
||||||
tools: { allow: ["bash", "read"], deny: ["*image*"] }
|
tools: { allow: ["exec", "read"], deny: ["*image*"] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ The prompt is intentionally compact and uses fixed sections:
|
||||||
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
|
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
|
||||||
- **Workspace**: working directory (`agents.defaults.workspace`).
|
- **Workspace**: working directory (`agents.defaults.workspace`).
|
||||||
- **Workspace Files (injected)**: indicates bootstrap files are included below.
|
- **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).
|
- **Time**: UTC default + the user’s local time (already converted).
|
||||||
- **Reply Tags**: optional reply tag syntax for supported providers.
|
- **Reply Tags**: optional reply tag syntax for supported providers.
|
||||||
- **Heartbeats**: heartbeat prompt and ack behavior.
|
- **Heartbeats**: heartbeat prompt and ack behavior.
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/bash",
|
"source": "/bash",
|
||||||
"destination": "/tools/bash"
|
"destination": "/tools/exec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "/tools/bash",
|
||||||
|
"destination": "/tools/exec"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "/bonjour",
|
"source": "/bonjour",
|
||||||
|
|
@ -696,7 +700,7 @@
|
||||||
"pages": [
|
"pages": [
|
||||||
"tools",
|
"tools",
|
||||||
"plugin",
|
"plugin",
|
||||||
"tools/bash",
|
"tools/exec",
|
||||||
"tools/elevated",
|
"tools/elevated",
|
||||||
"tools/browser",
|
"tools/browser",
|
||||||
"tools/browser-linux-troubleshooting",
|
"tools/browser-linux-troubleshooting",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
---
|
---
|
||||||
summary: "Background bash execution and process management"
|
summary: "Background exec execution and process management"
|
||||||
read_when:
|
read_when:
|
||||||
- Adding or modifying background bash behavior
|
- Adding or modifying background exec behavior
|
||||||
- Debugging long-running bash tasks
|
- 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:
|
Key parameters:
|
||||||
- `command` (required)
|
- `command` (required)
|
||||||
|
|
@ -24,7 +24,7 @@ Behavior:
|
||||||
- Foreground runs return output directly.
|
- Foreground runs return output directly.
|
||||||
- When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail.
|
- 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.
|
- 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:
|
Environment overrides:
|
||||||
- `PI_BASH_YIELD_MS`: default yield (ms)
|
- `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)
|
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h)
|
||||||
|
|
||||||
Config (preferred):
|
Config (preferred):
|
||||||
- `tools.bash.backgroundMs` (default 10000)
|
- `tools.exec.backgroundMs` (default 10000)
|
||||||
- `tools.bash.timeoutSec` (default 1800)
|
- `tools.exec.timeoutSec` (default 1800)
|
||||||
- `tools.bash.cleanupMs` (default 1800000)
|
- `tools.exec.cleanupMs` (default 1800000)
|
||||||
|
|
||||||
## process tool
|
## process tool
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ Notes:
|
||||||
|
|
||||||
Run a long task and poll later:
|
Run a long task and poll later:
|
||||||
```json
|
```json
|
||||||
{"tool": "bash", "command": "sleep 5 && echo done", "yieldMs": 1000}
|
{"tool": "exec", "command": "sleep 5 && echo done", "yieldMs": 1000}
|
||||||
```
|
```
|
||||||
```json
|
```json
|
||||||
{"tool": "process", "action": "poll", "sessionId": "<id>"}
|
{"tool": "process", "action": "poll", "sessionId": "<id>"}
|
||||||
|
|
@ -67,7 +67,7 @@ Run a long task and poll later:
|
||||||
|
|
||||||
Start immediately in background:
|
Start immediately in background:
|
||||||
```json
|
```json
|
||||||
{"tool": "bash", "command": "npm run build", "background": true}
|
{"tool": "exec", "command": "npm run build", "background": true}
|
||||||
```
|
```
|
||||||
|
|
||||||
Send stdin:
|
Send stdin:
|
||||||
|
|
|
||||||
|
|
@ -259,9 +259,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
|
||||||
},
|
},
|
||||||
|
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["bash", "process", "read", "write", "edit"],
|
allow: ["exec", "process", "read", "write", "edit"],
|
||||||
deny: ["browser", "canvas"],
|
deny: ["browser", "canvas"],
|
||||||
bash: {
|
exec: {
|
||||||
backgroundMs: 10000,
|
backgroundMs: 10000,
|
||||||
timeoutSec: 1800,
|
timeoutSec: 1800,
|
||||||
cleanupMs: 1800000
|
cleanupMs: 1800000
|
||||||
|
|
|
||||||
|
|
@ -638,7 +638,7 @@ Read-only tools + read-only workspace:
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
|
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: {
|
tools: {
|
||||||
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"],
|
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,
|
maxConcurrent: 1,
|
||||||
archiveAfterMinutes: 60
|
archiveAfterMinutes: 60
|
||||||
},
|
},
|
||||||
bash: {
|
exec: {
|
||||||
backgroundMs: 10000,
|
backgroundMs: 10000,
|
||||||
timeoutSec: 1800,
|
timeoutSec: 1800,
|
||||||
cleanupMs: 1800000
|
cleanupMs: 1800000
|
||||||
|
|
@ -1427,10 +1427,11 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
|
||||||
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
|
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
|
||||||
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
|
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)
|
- `backgroundMs`: time before auto-background (ms, default 10000)
|
||||||
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
|
||||||
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
|
- `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:
|
`agents.defaults.subagents` configures sub-agent defaults:
|
||||||
- `maxConcurrent`: max concurrent sub-agent runs (default 1)
|
- `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)
|
- `enabled`: allow elevated mode (default true)
|
||||||
- `allowFrom`: per-provider allowlists (empty = disabled)
|
- `allowFrom`: per-provider allowlists (empty = disabled)
|
||||||
- `whatsapp`: E.164 numbers
|
- `whatsapp`: E.164 numbers
|
||||||
|
|
@ -1491,8 +1492,8 @@ Per-agent override (further restrict):
|
||||||
Notes:
|
Notes:
|
||||||
- `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow).
|
- `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 on|off` stores state per session key; inline directives apply to a single message.
|
||||||
- Elevated `bash` runs on the host and bypasses sandboxing.
|
- Elevated `exec` runs on the host and bypasses sandboxing.
|
||||||
- Tool policy still applies; if `bash` is denied, elevated cannot be used.
|
- 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
|
`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
|
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`)
|
- `"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`
|
- `"rw"`: mount the agent workspace read/write at `/workspace`
|
||||||
- auto-prune: idle > 24h OR age > 7d
|
- 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`
|
- configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
|
||||||
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
- optional sandboxed browser (Chromium + CDP, noVNC observer)
|
||||||
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
|
||||||
|
|
@ -1584,7 +1585,7 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
|
||||||
tools: {
|
tools: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
tools: {
|
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"]
|
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ Current migrations:
|
||||||
- `routing.agentToAgent` → `tools.agentToAgent`
|
- `routing.agentToAgent` → `tools.agentToAgent`
|
||||||
- `routing.transcribeAudio` → `tools.audio.transcription`
|
- `routing.transcribeAudio` → `tools.audio.transcription`
|
||||||
- `identity` → `agents.list[].identity`
|
- `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`
|
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
|
||||||
→ `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
|
→ `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ You can tune console verbosity independently via:
|
||||||
|
|
||||||
## Tool summary redaction
|
## 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.
|
console stream. This is **tools-only** and does not alter file logs.
|
||||||
|
|
||||||
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
- `logging.redactSensitive`: `off` | `tools` (default: `tools`)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
title: Sandbox vs Tool Policy vs Elevated
|
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."
|
read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change."
|
||||||
status: active
|
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).
|
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**.
|
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
|
## Quick debug
|
||||||
|
|
||||||
|
|
@ -49,10 +49,10 @@ Rules of thumb:
|
||||||
- `deny` always wins.
|
- `deny` always wins.
|
||||||
- If `allow` is non-empty, everything else is treated as blocked.
|
- 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`.
|
Elevated does **not** grant extra tools; it only affects `exec`.
|
||||||
- If you’re sandboxed, `/elevated on` (or `bash` with `elevated: true`) runs on the host.
|
- 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).
|
- If you’re already running direct, elevated is effectively a no-op (still gated).
|
||||||
|
|
||||||
Gates:
|
Gates:
|
||||||
|
|
@ -74,4 +74,3 @@ Fix-it keys (pick one):
|
||||||
### “I thought this was main, why is it sandboxed?”
|
### “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"`.
|
In `"non-main"` mode, group/channel keys are *not* main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
and process access when the model does something dumb.
|
||||||
|
|
||||||
## What gets sandboxed
|
## 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`).
|
- Optional sandboxed browser (`agents.defaults.sandbox.browser`).
|
||||||
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it.
|
- 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`.
|
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:
|
Not sandboxed:
|
||||||
- The Gateway process itself.
|
- The Gateway process itself.
|
||||||
- Any tool explicitly allowed to run on the host (e.g. `tools.elevated`).
|
- 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).
|
- If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated).
|
||||||
|
|
||||||
## Modes
|
## 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
|
Tool allow/deny policies still apply before sandbox rules. If a tool is denied
|
||||||
globally or per-agent, sandboxing doesn’t bring it back.
|
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:
|
Debugging:
|
||||||
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.
|
||||||
|
|
|
||||||
|
|
@ -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:
|
You can already build a read-only profile by combining:
|
||||||
- `agents.defaults.sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access)
|
- `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.
|
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: "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`
|
- `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
|
## Browser control risks
|
||||||
|
|
||||||
|
|
@ -261,7 +261,7 @@ Common use cases:
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["write", "edit", "bash", "process", "browser"]
|
deny: ["write", "edit", "exec", "process", "browser"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -285,7 +285,7 @@ Common use cases:
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"],
|
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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ precedence, and troubleshooting.
|
||||||
- `"rw"` mounts the agent workspace read/write at `/workspace`
|
- `"rw"` mounts the agent workspace read/write at `/workspace`
|
||||||
- Auto-prune: idle > 24h OR age > 7d
|
- Auto-prune: idle > 24h OR age > 7d
|
||||||
- Network: `none` by default (explicitly opt-in if you need egress)
|
- 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`
|
- Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`
|
||||||
|
|
||||||
### Enable sandboxing
|
### Enable sandboxing
|
||||||
|
|
@ -297,7 +297,7 @@ precedence, and troubleshooting.
|
||||||
tools: {
|
tools: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
tools: {
|
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"]
|
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -424,7 +424,7 @@ Example:
|
||||||
|
|
||||||
### Security notes
|
### 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.
|
- Host-only tools like browser/camera/canvas are blocked by default.
|
||||||
- Allowing `browser` in sandbox **breaks isolation** (browser runs on host).
|
- Allowing `browser` in sandbox **breaks isolation** (browser runs on host).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read"],
|
"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"
|
"workspaceRoot": "/tmp/work-sandboxes"
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read", "write", "bash"],
|
"allow": ["read", "write", "exec"],
|
||||||
"deny": ["browser", "gateway", "discord"]
|
"deny": ["browser", "gateway", "discord"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +134,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read"],
|
"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).
|
`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:
|
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
|
- Avoid allowlisting senders that route to restricted agents
|
||||||
- Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution
|
- 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
|
- Disable elevated per agent (`agents.list[].tools.elevated.enabled: false`) for sensitive profiles
|
||||||
|
|
@ -200,7 +200,7 @@ Mitigation patterns:
|
||||||
"tools": {
|
"tools": {
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read", "write", "bash"],
|
"allow": ["read", "write", "exec"],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -235,7 +235,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau
|
||||||
{
|
{
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read"],
|
"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
|
```json
|
||||||
{
|
{
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["read", "bash", "process"],
|
"allow": ["read", "exec", "process"],
|
||||||
"deny": ["write", "edit", "browser", "gateway"]
|
"deny": ["write", "edit", "browser", "gateway"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -255,7 +255,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau
|
||||||
{
|
{
|
||||||
"tools": {
|
"tools": {
|
||||||
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
|
"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:
|
After configuring multi-agent sandbox and tools:
|
||||||
|
|
||||||
1. **Check agent resolution:**
|
1. **Check agent resolution:**
|
||||||
```bash
|
```exec
|
||||||
clawdbot agents list --bindings
|
clawdbot agents list --bindings
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Verify sandbox containers:**
|
2. **Verify sandbox containers:**
|
||||||
```bash
|
```exec
|
||||||
docker ps --filter "label=clawdbot.sandbox=1"
|
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
|
- Verify the agent cannot use denied tools
|
||||||
|
|
||||||
4. **Monitor logs:**
|
4. **Monitor logs:**
|
||||||
```bash
|
```exec
|
||||||
tail -f "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/logs/gateway.log" | grep -E "routing|sandbox|tools"
|
tail -f "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/logs/gateway.log" | grep -E "routing|sandbox|tools"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ read_when:
|
||||||
- `overridden(ActivityKind)` (debug override)
|
- `overridden(ActivityKind)` (debug override)
|
||||||
|
|
||||||
### ActivityKind → glyph
|
### ActivityKind → glyph
|
||||||
- `bash` → 💻
|
- `exec` → 💻
|
||||||
- `read` → 📄
|
- `read` → 📄
|
||||||
- `write` → ✍️
|
- `write` → ✍️
|
||||||
- `edit` → 📝
|
- `edit` → 📝
|
||||||
|
|
@ -40,7 +40,7 @@ read_when:
|
||||||
|
|
||||||
## Status row text (menu)
|
## Status row text (menu)
|
||||||
- While work is active: `<Session role> · <activity label>`
|
- While work is active: `<Session role> · <activity label>`
|
||||||
- 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.
|
- When idle: falls back to the health summary.
|
||||||
|
|
||||||
## Event ingestion
|
## Event ingestion
|
||||||
|
|
@ -49,7 +49,7 @@ read_when:
|
||||||
- `stream: "job"` with `data.state` for start/stop.
|
- `stream: "job"` with `data.state` for start/stop.
|
||||||
- `stream: "tool"` with `data.phase`, `name`, optional `meta`/`args`.
|
- `stream: "tool"` with `data.phase`, `name`, optional `meta`/`args`.
|
||||||
- Labels:
|
- Labels:
|
||||||
- `bash`: first line of `args.command`.
|
- `exec`: first line of `args.command`.
|
||||||
- `read`/`write`: shortened path.
|
- `read`/`write`: shortened path.
|
||||||
- `edit`: path plus inferred change kind from `meta`/diff counts.
|
- `edit`: path plus inferred change kind from `meta`/diff counts.
|
||||||
- fallback: tool name.
|
- fallback: tool name.
|
||||||
|
|
|
||||||
|
|
@ -836,7 +836,7 @@ exit
|
||||||
|
|
||||||
These are abort triggers (not slash commands).
|
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
|
process action:kill sessionId:XXX
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||||
|
|
||||||
- [Tools surface](/tools)
|
- [Tools surface](/tools)
|
||||||
- [CLI reference](/cli)
|
- [CLI reference](/cli)
|
||||||
- [Bash tool](/tools/bash)
|
- [Exec tool](/tools/exec)
|
||||||
- [Elevated mode](/tools/elevated)
|
- [Elevated mode](/tools/elevated)
|
||||||
- [Cron jobs](/automation/cron-jobs)
|
- [Cron jobs](/automation/cron-jobs)
|
||||||
- [Thinking + verbose](/tools/thinking)
|
- [Thinking + verbose](/tools/thinking)
|
||||||
|
|
|
||||||
|
|
@ -120,11 +120,11 @@ Live tests are split into two layers so we can isolate failures:
|
||||||
- Iterate models-with-keys and assert:
|
- Iterate models-with-keys and assert:
|
||||||
- “meaningful” response (no tools)
|
- “meaningful” response (no tools)
|
||||||
- a real tool invocation works (read probe)
|
- 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
|
- OpenAI regression paths (tool-call-only → follow-up) keep working
|
||||||
- Probe details (so you can explain failures quickly):
|
- 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.
|
- `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 <CODE>`.
|
- image probe: the test attaches a generated PNG (cat + randomized code) and expects the model to return `cat <CODE>`.
|
||||||
- Implementation reference: `src/gateway/gateway-models.profiles.live.test.ts` and `src/gateway/live-image-probe.ts`.
|
- Implementation reference: `src/gateway/gateway-models.profiles.live.test.ts` and `src/gateway/live-image-probe.ts`.
|
||||||
- How to enable:
|
- 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”):
|
- How to select providers (avoid “OpenRouter everything”):
|
||||||
- `CLAWDBOT_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist)
|
- `CLAWDBOT_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist)
|
||||||
- Optional tool-calling stress:
|
- 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.).
|
- This is specifically meant to catch tool-calling compatibility issues across providers (formatting, history replay, tool_result pairing, etc.).
|
||||||
- Optional image send smoke:
|
- 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.
|
- `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:
|
- 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`
|
- `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`
|
- `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):
|
- 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:
|
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`
|
`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:
|
Pick at least one per provider family:
|
||||||
- OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`)
|
- OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
---
|
---
|
||||||
summary: "Elevated bash mode and /elevated directives"
|
summary: "Elevated exec mode and /elevated directives"
|
||||||
read_when:
|
read_when:
|
||||||
- Adjusting elevated mode defaults, allowlists, or slash command behavior
|
- Adjusting elevated mode defaults, allowlists, or slash command behavior
|
||||||
---
|
---
|
||||||
# Elevated Mode (/elevated directives)
|
# Elevated Mode (/elevated directives)
|
||||||
|
|
||||||
## What it does
|
## 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.
|
- **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`.
|
- 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.
|
- 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.
|
- **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.
|
- **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.
|
- **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).
|
- **Host execution**: elevated runs `exec` on the host (bypasses sandbox).
|
||||||
- **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `bash` runs.
|
- **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `exec` runs.
|
||||||
- **Tool policy still applies**: if `bash` is denied by tool policy, elevated cannot be used.
|
- **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used.
|
||||||
|
|
||||||
Note:
|
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).
|
- Sandbox off: `/elevated on` does not change execution (already on host).
|
||||||
|
|
||||||
## When elevated matters
|
## 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.
|
- For unsandboxed agents, elevated does not change execution; it only affects gating, logging, and status.
|
||||||
|
|
||||||
## Resolution order
|
## Resolution order
|
||||||
|
|
@ -48,5 +48,5 @@ Note:
|
||||||
- All gates must pass; otherwise elevated is treated as unavailable.
|
- All gates must pass; otherwise elevated is treated as unavailable.
|
||||||
|
|
||||||
## Logging + status
|
## 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`).
|
- Session status includes elevated mode (e.g. `elevated=on`).
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
---
|
---
|
||||||
summary: "Bash tool usage, stdin modes, and TTY support"
|
summary: "Exec tool usage, stdin modes, and TTY support"
|
||||||
read_when:
|
read_when:
|
||||||
- Using or modifying the bash tool
|
- Using or modifying the exec tool
|
||||||
- Debugging stdin or TTY behavior
|
- Debugging stdin or TTY behavior
|
||||||
---
|
---
|
||||||
|
|
||||||
# Bash tool
|
# Exec tool
|
||||||
|
|
||||||
Run shell commands in the workspace. Supports foreground + background execution via `process`.
|
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.
|
Background sessions are scoped per agent; `process` only sees sessions from the same agent.
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
@ -19,17 +19,17 @@ Background sessions are scoped per agent; `process` only sees sessions from the
|
||||||
- `timeout` (seconds, default 1800): kill on expiry
|
- `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)
|
- `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.
|
- 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
|
## Examples
|
||||||
|
|
||||||
Foreground:
|
Foreground:
|
||||||
```json
|
```json
|
||||||
{"tool":"bash","command":"ls -la"}
|
{"tool":"exec","command":"ls -la"}
|
||||||
```
|
```
|
||||||
|
|
||||||
Background + poll:
|
Background + poll:
|
||||||
```json
|
```json
|
||||||
{"tool":"bash","command":"npm run build","yieldMs":1000}
|
{"tool":"exec","command":"npm run build","yieldMs":1000}
|
||||||
{"tool":"process","action":"poll","sessionId":"<id>"}
|
{"tool":"process","action":"poll","sessionId":"<id>"}
|
||||||
```
|
```
|
||||||
|
|
@ -31,7 +31,7 @@ alongside tools (for example, the voice-call plugin).
|
||||||
|
|
||||||
## Tool inventory
|
## Tool inventory
|
||||||
|
|
||||||
### `bash`
|
### `exec`
|
||||||
Run shell commands in the workspace.
|
Run shell commands in the workspace.
|
||||||
|
|
||||||
Core parameters:
|
Core parameters:
|
||||||
|
|
@ -45,12 +45,12 @@ Core parameters:
|
||||||
Notes:
|
Notes:
|
||||||
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
- Returns `status: "running"` with a `sessionId` when backgrounded.
|
||||||
- Use `process` to poll/log/write/kill/clear background sessions.
|
- 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` 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).
|
- `elevated` only changes behavior when the agent is sandboxed (otherwise it’s a no-op).
|
||||||
|
|
||||||
### `process`
|
### `process`
|
||||||
Manage background bash sessions.
|
Manage background exec sessions.
|
||||||
|
|
||||||
Core actions:
|
Core actions:
|
||||||
- `list`, `poll`, `log`, `write`, `kill`, `clear`, `remove`
|
- `list`, `poll`, `log`, `write`, `kill`, `clear`, `remove`
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ Override via config:
|
||||||
// deny wins
|
// deny wins
|
||||||
deny: ["gateway", "cron"],
|
deny: ["gateway", "cron"],
|
||||||
// if allow is set, it becomes allow-only (deny still wins)
|
// if allow is set, it becomes allow-only (deny still wins)
|
||||||
// allow: ["read", "bash", "process"]
|
// allow: ["read", "exec", "process"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ function walk(node, parent) {
|
||||||
if (name) seen.add(name);
|
if (name) seen.add(name);
|
||||||
}
|
}
|
||||||
if (typeof obj.name === "string" && typeof obj.input === "object" && obj.input) {
|
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.
|
// but some transcripts omit/rename type.
|
||||||
seen.add(obj.name);
|
seen.add(obj.name);
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +405,7 @@ run_profile() {
|
||||||
TURN4_JSON="/tmp/agent-${profile}-4.json"
|
TURN4_JSON="/tmp/agent-${profile}-4.json"
|
||||||
|
|
||||||
run_agent_turn "$profile" "$SESSION_ID" \
|
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"
|
"$TURN1_JSON"
|
||||||
assert_agent_json_has_text "$TURN1_JSON"
|
assert_agent_json_has_text "$TURN1_JSON"
|
||||||
assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider"
|
assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider"
|
||||||
|
|
@ -417,7 +417,7 @@ run_profile() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local prompt2
|
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"
|
run_agent_turn "$profile" "$SESSION_ID" "$prompt2" "$TURN2_JSON"
|
||||||
assert_agent_json_has_text "$TURN2_JSON"
|
assert_agent_json_has_text "$TURN2_JSON"
|
||||||
assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider"
|
assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider"
|
||||||
|
|
@ -435,7 +435,7 @@ run_profile() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local prompt3
|
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"
|
run_agent_turn "$profile" "$SESSION_ID" "$prompt3" "$TURN3_JSON"
|
||||||
assert_agent_json_has_text "$TURN3_JSON"
|
assert_agent_json_has_text "$TURN3_JSON"
|
||||||
assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider"
|
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
|
ls -la "/root/.clawdbot-${profile}/agents/main/sessions" >&2 || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
assert_session_used_tools "$SESSION_JSONL" read write bash image
|
assert_session_used_tools "$SESSION_JSONL" read write exec image
|
||||||
|
|
||||||
cleanup_profile
|
cleanup_profile
|
||||||
trap - EXIT
|
trap - EXIT
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ async function main() {
|
||||||
|
|
||||||
console.log("== Run 1: create tool history (primary only)");
|
console.log("== Run 1: create tool history (primary only)");
|
||||||
const toolPrompt =
|
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.";
|
"Then use the read tool to display the file contents. Reply with just the file contents.";
|
||||||
const run1 = await runCommand(
|
const run1 = await runCommand(
|
||||||
"run1",
|
"run1",
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ metadata: {"clawdbot":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins"
|
||||||
|
|
||||||
# tmux Skill (Clawdbot)
|
# 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
|
```bash
|
||||||
SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}"
|
SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}"
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ describe("resolveAgentConfig", () => {
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash", "write", "edit"],
|
deny: ["exec", "write", "edit"],
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allowFrom: { whatsapp: ["+15555550123"] },
|
allowFrom: { whatsapp: ["+15555550123"] },
|
||||||
|
|
@ -97,7 +97,7 @@ describe("resolveAgentConfig", () => {
|
||||||
const result = resolveAgentConfig(cfg, "restricted");
|
const result = resolveAgentConfig(cfg, "restricted");
|
||||||
expect(result?.tools).toEqual({
|
expect(result?.tools).toEqual({
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash", "write", "edit"],
|
deny: ["exec", "write", "edit"],
|
||||||
elevated: {
|
elevated: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
allowFrom: { whatsapp: ["+15555550123"] },
|
allowFrom: { whatsapp: ["+15555550123"] },
|
||||||
|
|
@ -118,7 +118,7 @@ describe("resolveAgentConfig", () => {
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash"],
|
deny: ["exec"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
import { resetProcessRegistryForTests } from "./bash-process-registry.js";
|
||||||
import {
|
import {
|
||||||
bashTool,
|
createExecTool,
|
||||||
createBashTool,
|
|
||||||
createProcessTool,
|
createProcessTool,
|
||||||
|
execTool,
|
||||||
processTool,
|
processTool,
|
||||||
} from "./bash-tools.js";
|
} from "./bash-tools.js";
|
||||||
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
import { sanitizeBinaryOutput } from "./shell-utils.js";
|
||||||
|
|
@ -50,7 +50,7 @@ beforeEach(() => {
|
||||||
resetProcessRegistryForTests();
|
resetProcessRegistryForTests();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("bash tool backgrounding", () => {
|
describe("exec tool backgrounding", () => {
|
||||||
const originalShell = process.env.SHELL;
|
const originalShell = process.env.SHELL;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
@ -64,7 +64,7 @@ describe("bash tool backgrounding", () => {
|
||||||
it(
|
it(
|
||||||
"backgrounds after yield and can be polled",
|
"backgrounds after yield and can be polled",
|
||||||
async () => {
|
async () => {
|
||||||
const result = await bashTool.execute("call1", {
|
const result = await execTool.execute("call1", {
|
||||||
command: joinCommands([yieldDelayCmd, "echo done"]),
|
command: joinCommands([yieldDelayCmd, "echo done"]),
|
||||||
yieldMs: 10,
|
yieldMs: 10,
|
||||||
});
|
});
|
||||||
|
|
@ -97,7 +97,7 @@ describe("bash tool backgrounding", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
it("supports explicit background", async () => {
|
it("supports explicit background", async () => {
|
||||||
const result = await bashTool.execute("call1", {
|
const result = await execTool.execute("call1", {
|
||||||
command: echoAfterDelay("later"),
|
command: echoAfterDelay("later"),
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
@ -113,7 +113,7 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("derives a session name from the command", async () => {
|
it("derives a session name from the command", async () => {
|
||||||
const result = await bashTool.execute("call1", {
|
const result = await execTool.execute("call1", {
|
||||||
command: "echo hello",
|
command: "echo hello",
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
@ -129,7 +129,7 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses default timeout when timeout is omitted", async () => {
|
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 customProcess = createProcessTool();
|
||||||
|
|
||||||
const result = await customBash.execute("call1", {
|
const result = await customBash.execute("call1", {
|
||||||
|
|
@ -156,7 +156,7 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects elevated requests when not allowed", async () => {
|
it("rejects elevated requests when not allowed", async () => {
|
||||||
const customBash = createBashTool({
|
const customBash = createExecTool({
|
||||||
elevated: { enabled: true, allowed: false, defaultLevel: "off" },
|
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 () => {
|
it("does not default to elevated when not allowed", async () => {
|
||||||
const customBash = createBashTool({
|
const customBash = createExecTool({
|
||||||
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
elevated: { enabled: true, allowed: false, defaultLevel: "on" },
|
||||||
backgroundMs: 1000,
|
backgroundMs: 1000,
|
||||||
timeoutSec: 5,
|
timeoutSec: 5,
|
||||||
|
|
@ -183,7 +183,7 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs line-based slices and defaults to last lines", async () => {
|
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"]),
|
command: echoLines(["one", "two", "three"]),
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
@ -203,7 +203,7 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports line offsets for log slices", async () => {
|
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"]),
|
command: echoLines(["alpha", "beta", "gamma"]),
|
||||||
background: true,
|
background: true,
|
||||||
});
|
});
|
||||||
|
|
@ -221,9 +221,9 @@ describe("bash tool backgrounding", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("scopes process sessions by scopeKey", async () => {
|
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 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 processB = createProcessTool({ scopeKey: "agent:beta" });
|
||||||
|
|
||||||
const resultA = await bashA.execute("call1", {
|
const resultA = await bashA.execute("call1", {
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,11 @@ const _stringEnum = <T extends readonly string[]>(
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BashToolDefaults = {
|
export type ExecToolDefaults = {
|
||||||
backgroundMs?: number;
|
backgroundMs?: number;
|
||||||
timeoutSec?: number;
|
timeoutSec?: number;
|
||||||
sandbox?: BashSandboxConfig;
|
sandbox?: BashSandboxConfig;
|
||||||
elevated?: BashElevatedDefaults;
|
elevated?: ExecElevatedDefaults;
|
||||||
allowBackground?: boolean;
|
allowBackground?: boolean;
|
||||||
scopeKey?: string;
|
scopeKey?: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
|
|
@ -76,14 +76,14 @@ export type BashSandboxConfig = {
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BashElevatedDefaults = {
|
export type ExecElevatedDefaults = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
allowed: boolean;
|
allowed: boolean;
|
||||||
defaultLevel: "on" | "off";
|
defaultLevel: "on" | "off";
|
||||||
};
|
};
|
||||||
|
|
||||||
const bashSchema = Type.Object({
|
const execSchema = Type.Object({
|
||||||
command: Type.String({ description: "Bash command to execute" }),
|
command: Type.String({ description: "Shell command to execute" }),
|
||||||
workdir: Type.Optional(
|
workdir: Type.Optional(
|
||||||
Type.String({ description: "Working directory (defaults to cwd)" }),
|
Type.String({ description: "Working directory (defaults to cwd)" }),
|
||||||
),
|
),
|
||||||
|
|
@ -108,7 +108,7 @@ const bashSchema = Type.Object({
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type BashToolDetails =
|
export type ExecToolDetails =
|
||||||
| {
|
| {
|
||||||
status: "running";
|
status: "running";
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
|
|
@ -125,10 +125,10 @@ export type BashToolDetails =
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createBashTool(
|
export function createExecTool(
|
||||||
defaults?: BashToolDefaults,
|
defaults?: ExecToolDefaults,
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
|
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
|
||||||
): AgentTool<any, BashToolDetails> {
|
): AgentTool<any, ExecToolDetails> {
|
||||||
const defaultBackgroundMs = clampNumber(
|
const defaultBackgroundMs = clampNumber(
|
||||||
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
|
||||||
10_000,
|
10_000,
|
||||||
|
|
@ -142,11 +142,11 @@ export function createBashTool(
|
||||||
: 1800;
|
: 1800;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "bash",
|
name: "exec",
|
||||||
label: "bash",
|
label: "exec",
|
||||||
description:
|
description:
|
||||||
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
|
"Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
|
||||||
parameters: bashSchema,
|
parameters: execSchema,
|
||||||
execute: async (_toolCallId, args, signal, onUpdate) => {
|
execute: async (_toolCallId, args, signal, onUpdate) => {
|
||||||
const params = args as {
|
const params = args as {
|
||||||
command: string;
|
command: string;
|
||||||
|
|
@ -218,7 +218,7 @@ export function createBashTool(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
logInfo(
|
logInfo(
|
||||||
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
`exec: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
|
||||||
params.command,
|
params.command,
|
||||||
120,
|
120,
|
||||||
)}`,
|
)}`,
|
||||||
|
|
@ -363,7 +363,7 @@ export function createBashTool(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise<AgentToolResult<BashToolDetails>>(
|
return new Promise<AgentToolResult<ExecToolDetails>>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
const resolveRunning = () => {
|
const resolveRunning = () => {
|
||||||
settle(() =>
|
settle(() =>
|
||||||
|
|
@ -482,7 +482,7 @@ export function createBashTool(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bashTool = createBashTool();
|
export const execTool = createExecTool();
|
||||||
|
|
||||||
const processSchema = Type.Object({
|
const processSchema = Type.Object({
|
||||||
action: Type.String({ description: "Process action" }),
|
action: Type.String({ description: "Process action" }),
|
||||||
|
|
@ -509,7 +509,7 @@ export function createProcessTool(
|
||||||
return {
|
return {
|
||||||
name: "process",
|
name: "process",
|
||||||
label: "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,
|
parameters: processSchema,
|
||||||
execute: async (_toolCallId, args) => {
|
execute: async (_toolCallId, args) => {
|
||||||
const params = args as {
|
const params = args as {
|
||||||
|
|
|
||||||
|
|
@ -356,7 +356,7 @@ describe("sanitizeGoogleTurnOrdering", () => {
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} },
|
{ type: "toolCall", id: "call_1", name: "exec", arguments: {} },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies AgentMessage[];
|
] satisfies AgentMessage[];
|
||||||
|
|
@ -403,7 +403,7 @@ describe("sanitizeSessionMessagesImages", () => {
|
||||||
{
|
{
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
id: "call_abc|item:456",
|
id: "call_abc|item:456",
|
||||||
name: "bash",
|
name: "exec",
|
||||||
arguments: {},
|
arguments: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||||
env: { LANG: "C.UTF-8" },
|
env: { LANG: "C.UTF-8" },
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["bash"],
|
allow: ["exec"],
|
||||||
deny: ["browser"],
|
deny: ["browser"],
|
||||||
},
|
},
|
||||||
browserAllowHostControl: true,
|
browserAllowHostControl: true,
|
||||||
|
|
@ -87,7 +87,7 @@ describe("buildEmbeddedSandboxInfo", () => {
|
||||||
env: { LANG: "C.UTF-8" },
|
env: { LANG: "C.UTF-8" },
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["bash"],
|
allow: ["exec"],
|
||||||
deny: ["browser"],
|
deny: ["browser"],
|
||||||
},
|
},
|
||||||
browserAllowHostControl: false,
|
browserAllowHostControl: false,
|
||||||
|
|
@ -171,7 +171,7 @@ function createStubTool(name: string): AgentTool {
|
||||||
describe("splitSdkTools", () => {
|
describe("splitSdkTools", () => {
|
||||||
const tools = [
|
const tools = [
|
||||||
createStubTool("read"),
|
createStubTool("read"),
|
||||||
createStubTool("bash"),
|
createStubTool("exec"),
|
||||||
createStubTool("edit"),
|
createStubTool("edit"),
|
||||||
createStubTool("write"),
|
createStubTool("write"),
|
||||||
createStubTool("browser"),
|
createStubTool("browser"),
|
||||||
|
|
@ -185,7 +185,7 @@ describe("splitSdkTools", () => {
|
||||||
expect(builtInTools).toEqual([]);
|
expect(builtInTools).toEqual([]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
"read",
|
"read",
|
||||||
"bash",
|
"exec",
|
||||||
"edit",
|
"edit",
|
||||||
"write",
|
"write",
|
||||||
"browser",
|
"browser",
|
||||||
|
|
@ -200,7 +200,7 @@ describe("splitSdkTools", () => {
|
||||||
expect(builtInTools).toEqual([]);
|
expect(builtInTools).toEqual([]);
|
||||||
expect(customTools.map((tool) => tool.name)).toEqual([
|
expect(customTools.map((tool) => tool.name)).toEqual([
|
||||||
"read",
|
"read",
|
||||||
"bash",
|
"exec",
|
||||||
"edit",
|
"edit",
|
||||||
"write",
|
"write",
|
||||||
"browser",
|
"browser",
|
||||||
|
|
@ -226,7 +226,7 @@ describe("applyGoogleTurnOrderingFix", () => {
|
||||||
{
|
{
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} },
|
{ type: "toolCall", id: "call_1", name: "exec", arguments: {} },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies AgentMessage[];
|
] satisfies AgentMessage[];
|
||||||
|
|
@ -360,7 +360,7 @@ describe("limitHistoryTurns", () => {
|
||||||
{ role: "user", content: [{ type: "text", text: "first" }] },
|
{ role: "user", content: [{ type: "text", text: "first" }] },
|
||||||
{
|
{
|
||||||
role: "assistant",
|
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: "user", content: [{ type: "text", text: "second" }] },
|
||||||
{ role: "assistant", content: [{ type: "text", text: "response" }] },
|
{ role: "assistant", content: [{ type: "text", text: "response" }] },
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ import {
|
||||||
markAuthProfileGood,
|
markAuthProfileGood,
|
||||||
markAuthProfileUsed,
|
markAuthProfileUsed,
|
||||||
} from "./auth-profiles.js";
|
} from "./auth-profiles.js";
|
||||||
import type { BashElevatedDefaults } from "./bash-tools.js";
|
import type { ExecElevatedDefaults, ExecToolDefaults } from "./bash-tools.js";
|
||||||
import {
|
import {
|
||||||
CONTEXT_WINDOW_HARD_MIN_TOKENS,
|
CONTEXT_WINDOW_HARD_MIN_TOKENS,
|
||||||
CONTEXT_WINDOW_WARN_BELOW_TOKENS,
|
CONTEXT_WINDOW_WARN_BELOW_TOKENS,
|
||||||
|
|
@ -768,11 +768,11 @@ function describeUnknownError(error: unknown): string {
|
||||||
|
|
||||||
export function buildEmbeddedSandboxInfo(
|
export function buildEmbeddedSandboxInfo(
|
||||||
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
|
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
|
||||||
bashElevated?: BashElevatedDefaults,
|
execElevated?: ExecElevatedDefaults,
|
||||||
): EmbeddedSandboxInfo | undefined {
|
): EmbeddedSandboxInfo | undefined {
|
||||||
if (!sandbox?.enabled) return undefined;
|
if (!sandbox?.enabled) return undefined;
|
||||||
const elevatedAllowed = Boolean(
|
const elevatedAllowed = Boolean(
|
||||||
bashElevated?.enabled && bashElevated.allowed,
|
execElevated?.enabled && execElevated.allowed,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -790,7 +790,7 @@ export function buildEmbeddedSandboxInfo(
|
||||||
? {
|
? {
|
||||||
elevated: {
|
elevated: {
|
||||||
allowed: true,
|
allowed: true,
|
||||||
defaultLevel: bashElevated?.defaultLevel ?? "off",
|
defaultLevel: execElevated?.defaultLevel ?? "off",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
|
@ -949,6 +949,16 @@ function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
|
||||||
return level;
|
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(
|
function resolveModel(
|
||||||
provider: string,
|
provider: string,
|
||||||
modelId: string,
|
modelId: string,
|
||||||
|
|
@ -987,7 +997,7 @@ export async function compactEmbeddedPiSession(params: {
|
||||||
model?: string;
|
model?: string;
|
||||||
thinkLevel?: ThinkLevel;
|
thinkLevel?: ThinkLevel;
|
||||||
reasoningLevel?: ReasoningLevel;
|
reasoningLevel?: ReasoningLevel;
|
||||||
bashElevated?: BashElevatedDefaults;
|
bashElevated?: ExecElevatedDefaults;
|
||||||
customInstructions?: string;
|
customInstructions?: string;
|
||||||
lane?: string;
|
lane?: string;
|
||||||
enqueue?: typeof enqueueCommand;
|
enqueue?: typeof enqueueCommand;
|
||||||
|
|
@ -1087,8 +1097,8 @@ export async function compactEmbeddedPiSession(params: {
|
||||||
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
|
||||||
const runAbortController = new AbortController();
|
const runAbortController = new AbortController();
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
exec: {
|
||||||
...params.config?.tools?.bash,
|
...resolveExecToolDefaults(params.config),
|
||||||
elevated: params.bashElevated,
|
elevated: params.bashElevated,
|
||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
|
|
@ -1289,7 +1299,7 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
thinkLevel?: ThinkLevel;
|
thinkLevel?: ThinkLevel;
|
||||||
verboseLevel?: VerboseLevel;
|
verboseLevel?: VerboseLevel;
|
||||||
reasoningLevel?: ReasoningLevel;
|
reasoningLevel?: ReasoningLevel;
|
||||||
bashElevated?: BashElevatedDefaults;
|
bashElevated?: ExecElevatedDefaults;
|
||||||
timeoutMs: number;
|
timeoutMs: number;
|
||||||
runId: string;
|
runId: string;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
|
|
@ -1499,8 +1509,8 @@ export async function runEmbeddedPiAgent(params: {
|
||||||
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
|
||||||
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
bash: {
|
exec: {
|
||||||
...params.config?.tools?.bash,
|
...resolveExecToolDefaults(params.config),
|
||||||
elevated: params.bashElevated,
|
elevated: params.bashElevated,
|
||||||
},
|
},
|
||||||
sandbox,
|
sandbox,
|
||||||
|
|
|
||||||
|
|
@ -94,28 +94,28 @@ describe("context-pruning", () => {
|
||||||
makeAssistant("a1"),
|
makeAssistant("a1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeUser("u2"),
|
makeUser("u2"),
|
||||||
makeAssistant("a2"),
|
makeAssistant("a2"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t2",
|
toolCallId: "t2",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "y".repeat(20_000),
|
text: "y".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeUser("u3"),
|
makeUser("u3"),
|
||||||
makeAssistant("a3"),
|
makeAssistant("a3"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t3",
|
toolCallId: "t3",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "z".repeat(20_000),
|
text: "z".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeUser("u4"),
|
makeUser("u4"),
|
||||||
makeAssistant("a4"),
|
makeAssistant("a4"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t4",
|
toolCallId: "t4",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "w".repeat(20_000),
|
text: "w".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
@ -161,7 +161,7 @@ describe("context-pruning", () => {
|
||||||
makeUser("u1"),
|
makeUser("u1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "y".repeat(20_000),
|
text: "y".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
@ -184,19 +184,19 @@ describe("context-pruning", () => {
|
||||||
makeAssistant("a1"),
|
makeAssistant("a1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t2",
|
toolCallId: "t2",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "y".repeat(20_000),
|
text: "y".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeUser("u2"),
|
makeUser("u2"),
|
||||||
makeAssistant("a2"),
|
makeAssistant("a2"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t3",
|
toolCallId: "t3",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "z".repeat(20_000),
|
text: "z".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
@ -225,7 +225,7 @@ describe("context-pruning", () => {
|
||||||
makeAssistant("a1"),
|
makeAssistant("a1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeAssistant("a2"),
|
makeAssistant("a2"),
|
||||||
|
|
@ -273,7 +273,7 @@ describe("context-pruning", () => {
|
||||||
makeAssistant("a1"),
|
makeAssistant("a1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeAssistant("a2"),
|
makeAssistant("a2"),
|
||||||
|
|
@ -313,7 +313,7 @@ describe("context-pruning", () => {
|
||||||
makeUser("u1"),
|
makeUser("u1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "Bash",
|
toolName: "Exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
|
|
@ -329,7 +329,7 @@ describe("context-pruning", () => {
|
||||||
softTrimRatio: 0.0,
|
softTrimRatio: 0.0,
|
||||||
hardClearRatio: 0.0,
|
hardClearRatio: 0.0,
|
||||||
minPrunableToolChars: 0,
|
minPrunableToolChars: 0,
|
||||||
tools: { allow: ["ba*"], deny: ["bash"] },
|
tools: { allow: ["ex*"], deny: ["exec"] },
|
||||||
hardClear: { enabled: true, placeholder: "[cleared]" },
|
hardClear: { enabled: true, placeholder: "[cleared]" },
|
||||||
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
|
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
|
||||||
};
|
};
|
||||||
|
|
@ -339,7 +339,7 @@ describe("context-pruning", () => {
|
||||||
} as unknown as ExtensionContext;
|
} as unknown as ExtensionContext;
|
||||||
const next = pruneContextMessages({ messages, settings, ctx });
|
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));
|
expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000));
|
||||||
// allow is non-empty and browser is not allowed => never pruned.
|
// allow is non-empty and browser is not allowed => never pruned.
|
||||||
expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000));
|
expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000));
|
||||||
|
|
@ -350,7 +350,7 @@ describe("context-pruning", () => {
|
||||||
makeUser("u1"),
|
makeUser("u1"),
|
||||||
makeImageToolResult({
|
makeImageToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "x".repeat(20_000),
|
text: "x".repeat(20_000),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
@ -384,7 +384,7 @@ describe("context-pruning", () => {
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
content: [
|
content: [
|
||||||
{ type: "text", text: "AAAAA" },
|
{ type: "text", text: "AAAAA" },
|
||||||
{ type: "text", text: "BBBBB" },
|
{ type: "text", text: "BBBBB" },
|
||||||
|
|
@ -418,7 +418,7 @@ describe("context-pruning", () => {
|
||||||
makeUser("u1"),
|
makeUser("u1"),
|
||||||
makeToolResult({
|
makeToolResult({
|
||||||
toolCallId: "t1",
|
toolCallId: "t1",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
text: "abcdefghij".repeat(1000),
|
text: "abcdefghij".repeat(1000),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
const toolNames = tools.map((t) => t.name);
|
const toolNames = tools.map((t) => t.name);
|
||||||
expect(toolNames).toContain("read");
|
expect(toolNames).toContain("read");
|
||||||
expect(toolNames).toContain("write");
|
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", () => {
|
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);
|
const toolNames = tools.map((t) => t.name);
|
||||||
expect(toolNames).toContain("bash");
|
expect(toolNames).toContain("exec");
|
||||||
expect(toolNames).toContain("read");
|
expect(toolNames).toContain("read");
|
||||||
expect(toolNames).not.toContain("write");
|
expect(toolNames).not.toContain("write");
|
||||||
});
|
});
|
||||||
|
|
@ -70,7 +70,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
it("should apply agent-specific tool policy", () => {
|
it("should apply agent-specific tool policy", () => {
|
||||||
const cfg: ClawdbotConfig = {
|
const cfg: ClawdbotConfig = {
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read", "write", "bash"],
|
allow: ["read", "write", "exec"],
|
||||||
deny: [],
|
deny: [],
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
|
|
@ -80,7 +80,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
workspace: "~/clawd-restricted",
|
workspace: "~/clawd-restricted",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"], // Agent override: only read
|
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);
|
const toolNames = tools.map((t) => t.name);
|
||||||
expect(toolNames).toContain("read");
|
expect(toolNames).toContain("read");
|
||||||
expect(toolNames).not.toContain("bash");
|
expect(toolNames).not.toContain("exec");
|
||||||
expect(toolNames).not.toContain("write");
|
expect(toolNames).not.toContain("write");
|
||||||
expect(toolNames).not.toContain("edit");
|
expect(toolNames).not.toContain("edit");
|
||||||
});
|
});
|
||||||
|
|
@ -115,7 +115,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
workspace: "~/clawd-family",
|
workspace: "~/clawd-family",
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
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",
|
agentDir: "/tmp/agent-main",
|
||||||
});
|
});
|
||||||
const mainToolNames = mainTools.map((t) => t.name);
|
const mainToolNames = mainTools.map((t) => t.name);
|
||||||
expect(mainToolNames).toContain("bash");
|
expect(mainToolNames).toContain("exec");
|
||||||
expect(mainToolNames).toContain("write");
|
expect(mainToolNames).toContain("write");
|
||||||
expect(mainToolNames).toContain("edit");
|
expect(mainToolNames).toContain("edit");
|
||||||
|
|
||||||
|
|
@ -143,7 +143,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
});
|
});
|
||||||
const familyToolNames = familyTools.map((t) => t.name);
|
const familyToolNames = familyTools.map((t) => t.name);
|
||||||
expect(familyToolNames).toContain("read");
|
expect(familyToolNames).toContain("read");
|
||||||
expect(familyToolNames).not.toContain("bash");
|
expect(familyToolNames).not.toContain("exec");
|
||||||
expect(familyToolNames).not.toContain("write");
|
expect(familyToolNames).not.toContain("write");
|
||||||
expect(familyToolNames).not.toContain("edit");
|
expect(familyToolNames).not.toContain("edit");
|
||||||
});
|
});
|
||||||
|
|
@ -159,7 +159,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
id: "work",
|
id: "work",
|
||||||
workspace: "~/clawd-work",
|
workspace: "~/clawd-work",
|
||||||
tools: {
|
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);
|
const toolNames = tools.map((t) => t.name);
|
||||||
// Agent policy overrides global: browser is allowed again
|
// Agent policy overrides global: browser is allowed again
|
||||||
expect(toolNames).toContain("browser");
|
expect(toolNames).toContain("browser");
|
||||||
expect(toolNames).not.toContain("bash");
|
expect(toolNames).not.toContain("exec");
|
||||||
expect(toolNames).not.toContain("process");
|
expect(toolNames).not.toContain("process");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -199,7 +199,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
},
|
},
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"], // Agent further restricts to only read
|
allow: ["read"], // Agent further restricts to only read
|
||||||
deny: ["bash", "write"],
|
deny: ["exec", "write"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -207,7 +207,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
tools: {
|
tools: {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read", "write", "bash"], // Sandbox allows these
|
allow: ["read", "write", "exec"], // Sandbox allows these
|
||||||
deny: [],
|
deny: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -237,7 +237,7 @@ describe("Agent-specific tool filtering", () => {
|
||||||
capDrop: [],
|
capDrop: [],
|
||||||
} satisfies SandboxDockerConfig,
|
} satisfies SandboxDockerConfig,
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read", "write", "bash"],
|
allow: ["read", "write", "exec"],
|
||||||
deny: [],
|
deny: [],
|
||||||
},
|
},
|
||||||
browserAllowHostControl: false,
|
browserAllowHostControl: false,
|
||||||
|
|
@ -246,14 +246,14 @@ describe("Agent-specific tool filtering", () => {
|
||||||
|
|
||||||
const toolNames = tools.map((t) => t.name);
|
const toolNames = tools.map((t) => t.name);
|
||||||
// Agent policy should be applied first, then sandbox
|
// 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)
|
// Result: only "read" (most restrictive wins)
|
||||||
expect(toolNames).toContain("read");
|
expect(toolNames).toContain("read");
|
||||||
expect(toolNames).not.toContain("bash");
|
expect(toolNames).not.toContain("exec");
|
||||||
expect(toolNames).not.toContain("write");
|
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 = {
|
const cfg: ClawdbotConfig = {
|
||||||
tools: {
|
tools: {
|
||||||
deny: ["process"],
|
deny: ["process"],
|
||||||
|
|
@ -266,10 +266,10 @@ describe("Agent-specific tool filtering", () => {
|
||||||
workspaceDir: "/tmp/test-main",
|
workspaceDir: "/tmp/test-main",
|
||||||
agentDir: "/tmp/agent-main",
|
agentDir: "/tmp/agent-main",
|
||||||
});
|
});
|
||||||
const bash = tools.find((tool) => tool.name === "bash");
|
const execTool = tools.find((tool) => tool.name === "exec");
|
||||||
expect(bash).toBeDefined();
|
expect(execTool).toBeDefined();
|
||||||
|
|
||||||
const result = await bash?.execute("call1", {
|
const result = await execTool?.execute("call1", {
|
||||||
command: "echo done",
|
command: "echo done",
|
||||||
yieldMs: 10,
|
yieldMs: 10,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,9 @@ describe("createClawdbotCodingTools", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes bash and process tools", () => {
|
it("includes exec and process tools", () => {
|
||||||
const tools = createClawdbotCodingTools();
|
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);
|
expect(tools.some((tool) => tool.name === "process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -165,7 +165,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
modelAuthMode: "oauth",
|
modelAuthMode: "oauth",
|
||||||
});
|
});
|
||||||
const names = new Set(tools.map((tool) => tool.name));
|
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("read")).toBe(true);
|
||||||
expect(names.has("write")).toBe(true);
|
expect(names.has("write")).toBe(true);
|
||||||
expect(names.has("edit")).toBe(true);
|
expect(names.has("edit")).toBe(true);
|
||||||
|
|
@ -210,7 +210,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
expect(names.has("sessions_spawn")).toBe(false);
|
expect(names.has("sessions_spawn")).toBe(false);
|
||||||
|
|
||||||
expect(names.has("read")).toBe(true);
|
expect(names.has("read")).toBe(true);
|
||||||
expect(names.has("bash")).toBe(true);
|
expect(names.has("exec")).toBe(true);
|
||||||
expect(names.has("process")).toBe(true);
|
expect(names.has("process")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -330,7 +330,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
browserAllowHostControl: false,
|
browserAllowHostControl: false,
|
||||||
};
|
};
|
||||||
const tools = createClawdbotCodingTools({ sandbox });
|
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 === "read")).toBe(false);
|
||||||
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
@ -371,7 +371,7 @@ describe("createClawdbotCodingTools", () => {
|
||||||
const tools = createClawdbotCodingTools({
|
const tools = createClawdbotCodingTools({
|
||||||
config: { tools: { deny: ["browser"] } },
|
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);
|
expect(tools.some((tool) => tool.name === "browser")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,9 @@ import {
|
||||||
resolveAgentIdFromSessionKey,
|
resolveAgentIdFromSessionKey,
|
||||||
} from "./agent-scope.js";
|
} from "./agent-scope.js";
|
||||||
import {
|
import {
|
||||||
type BashToolDefaults,
|
createExecTool,
|
||||||
createBashTool,
|
|
||||||
createProcessTool,
|
createProcessTool,
|
||||||
|
type ExecToolDefaults,
|
||||||
type ProcessToolDefaults,
|
type ProcessToolDefaults,
|
||||||
} from "./bash-tools.js";
|
} from "./bash-tools.js";
|
||||||
import { createClawdbotTools } from "./clawdbot-tools.js";
|
import { createClawdbotTools } from "./clawdbot-tools.js";
|
||||||
|
|
@ -290,9 +290,18 @@ function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
|
||||||
return cleanSchemaForGemini(schema);
|
return cleanSchemaForGemini(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TOOL_NAME_ALIASES: Record<string, string> = {
|
||||||
|
bash: "exec",
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeToolName(name: string) {
|
||||||
|
const normalized = name.trim().toLowerCase();
|
||||||
|
return TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeToolNames(list?: string[]) {
|
function normalizeToolNames(list?: string[]) {
|
||||||
if (!list) return [];
|
if (!list) return [];
|
||||||
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
|
return list.map(normalizeToolName).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
const DEFAULT_SUBAGENT_TOOL_DENY = [
|
||||||
|
|
@ -354,7 +363,7 @@ function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) {
|
||||||
const deny = new Set(normalizeToolNames(policy.deny));
|
const deny = new Set(normalizeToolNames(policy.deny));
|
||||||
const allowRaw = normalizeToolNames(policy.allow);
|
const allowRaw = normalizeToolNames(policy.allow);
|
||||||
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
|
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 (deny.has(normalized)) return false;
|
||||||
if (allow) return allow.has(normalized);
|
if (allow) return allow.has(normalized);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -467,7 +476,7 @@ function wrapToolWithAbortSignal(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createClawdbotCodingTools(options?: {
|
export function createClawdbotCodingTools(options?: {
|
||||||
bash?: BashToolDefaults & ProcessToolDefaults;
|
exec?: ExecToolDefaults & ProcessToolDefaults;
|
||||||
messageProvider?: string;
|
messageProvider?: string;
|
||||||
agentAccountId?: string;
|
agentAccountId?: string;
|
||||||
sandbox?: SandboxContext | null;
|
sandbox?: SandboxContext | null;
|
||||||
|
|
@ -495,14 +504,14 @@ export function createClawdbotCodingTools(options?: {
|
||||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||||
hasRepliedRef?: { value: boolean };
|
hasRepliedRef?: { value: boolean };
|
||||||
}): AnyAgentTool[] {
|
}): AnyAgentTool[] {
|
||||||
const bashToolName = "bash";
|
const execToolName = "exec";
|
||||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||||
const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({
|
const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({
|
||||||
config: options?.config,
|
config: options?.config,
|
||||||
sessionKey: options?.sessionKey,
|
sessionKey: options?.sessionKey,
|
||||||
});
|
});
|
||||||
const scopeKey =
|
const scopeKey =
|
||||||
options?.bash?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
|
||||||
const subagentPolicy =
|
const subagentPolicy =
|
||||||
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
|
||||||
? resolveSubagentToolPolicy(options.config)
|
? resolveSubagentToolPolicy(options.config)
|
||||||
|
|
@ -524,7 +533,7 @@ export function createClawdbotCodingTools(options?: {
|
||||||
const freshReadTool = createReadTool(workspaceRoot);
|
const freshReadTool = createReadTool(workspaceRoot);
|
||||||
return [createClawdbotReadTool(freshReadTool)];
|
return [createClawdbotReadTool(freshReadTool)];
|
||||||
}
|
}
|
||||||
if (tool.name === bashToolName) return [];
|
if (tool.name === "bash" || tool.name === execToolName) return [];
|
||||||
if (tool.name === "write") {
|
if (tool.name === "write") {
|
||||||
if (sandboxRoot) return [];
|
if (sandboxRoot) return [];
|
||||||
return [createWriteTool(workspaceRoot)];
|
return [createWriteTool(workspaceRoot)];
|
||||||
|
|
@ -535,8 +544,8 @@ export function createClawdbotCodingTools(options?: {
|
||||||
}
|
}
|
||||||
return [tool as AnyAgentTool];
|
return [tool as AnyAgentTool];
|
||||||
});
|
});
|
||||||
const bashTool = createBashTool({
|
const execTool = createExecTool({
|
||||||
...options?.bash,
|
...options?.exec,
|
||||||
cwd: options?.workspaceDir,
|
cwd: options?.workspaceDir,
|
||||||
allowBackground,
|
allowBackground,
|
||||||
scopeKey,
|
scopeKey,
|
||||||
|
|
@ -550,7 +559,7 @@ export function createClawdbotCodingTools(options?: {
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
const processTool = createProcessTool({
|
const processTool = createProcessTool({
|
||||||
cleanupMs: options?.bash?.cleanupMs,
|
cleanupMs: options?.exec?.cleanupMs,
|
||||||
scopeKey,
|
scopeKey,
|
||||||
});
|
});
|
||||||
const tools: AnyAgentTool[] = [
|
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,
|
processTool as unknown as AnyAgentTool,
|
||||||
// Provider docking: include provider-defined agent tools (login, etc.).
|
// Provider docking: include provider-defined agent tools (login, etc.).
|
||||||
...listProviderAgentTools({ cfg: options?.config }),
|
...listProviderAgentTools({ cfg: options?.config }),
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
await withTempDir("clawdbot-ws-", async (workspaceDir) => {
|
||||||
const tools = createClawdbotCodingTools({ workspaceDir });
|
const tools = createClawdbotCodingTools({ workspaceDir });
|
||||||
const bashTool = tools.find((tool) => tool.name === "bash");
|
const execTool = tools.find((tool) => tool.name === "exec");
|
||||||
expect(bashTool).toBeDefined();
|
expect(execTool).toBeDefined();
|
||||||
|
|
||||||
const result = await bashTool?.execute("ws-bash", {
|
const result = await execTool?.execute("ws-exec", {
|
||||||
command: "echo ok",
|
command: "echo ok",
|
||||||
});
|
});
|
||||||
const cwd =
|
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-ws-", async (workspaceDir) => {
|
||||||
await withTempDir("clawdbot-override-", async (overrideDir) => {
|
await withTempDir("clawdbot-override-", async (overrideDir) => {
|
||||||
const tools = createClawdbotCodingTools({ workspaceDir });
|
const tools = createClawdbotCodingTools({ workspaceDir });
|
||||||
const bashTool = tools.find((tool) => tool.name === "bash");
|
const execTool = tools.find((tool) => tool.name === "exec");
|
||||||
expect(bashTool).toBeDefined();
|
expect(execTool).toBeDefined();
|
||||||
|
|
||||||
const result = await bashTool?.execute("ws-bash-override", {
|
const result = await execTool?.execute("ws-exec-override", {
|
||||||
command: "echo ok",
|
command: "echo ok",
|
||||||
workdir: overrideDir,
|
workdir: overrideDir,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -450,7 +450,7 @@ describe("Agent-specific sandbox config", () => {
|
||||||
sandbox: {
|
sandbox: {
|
||||||
tools: {
|
tools: {
|
||||||
allow: ["read"],
|
allow: ["read"],
|
||||||
deny: ["bash"],
|
deny: ["exec"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ const DEFAULT_SANDBOX_WORKDIR = "/workspace";
|
||||||
const DEFAULT_SANDBOX_IDLE_HOURS = 24;
|
const DEFAULT_SANDBOX_IDLE_HOURS = 24;
|
||||||
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
|
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
|
||||||
const DEFAULT_TOOL_ALLOW = [
|
const DEFAULT_TOOL_ALLOW = [
|
||||||
"bash",
|
"exec",
|
||||||
"process",
|
"process",
|
||||||
"read",
|
"read",
|
||||||
"write",
|
"write",
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,14 @@ describe("sanitizeToolUseResultPairing", () => {
|
||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: [
|
content: [
|
||||||
{ type: "toolCall", id: "call_1", name: "read", arguments: {} },
|
{ 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: "user", content: "user message that should come after tool use" },
|
||||||
{
|
{
|
||||||
role: "toolResult",
|
role: "toolResult",
|
||||||
toolCallId: "call_2",
|
toolCallId: "call_2",
|
||||||
toolName: "bash",
|
toolName: "exec",
|
||||||
content: [{ type: "text", text: "ok" }],
|
content: [{ type: "text", text: "ok" }],
|
||||||
isError: false,
|
isError: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ describe("buildAgentSystemPrompt", () => {
|
||||||
it("lists available tools when provided", () => {
|
it("lists available tools when provided", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
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):");
|
expect(prompt).toContain("Tool availability (filtered by policy):");
|
||||||
|
|
@ -49,13 +49,13 @@ describe("buildAgentSystemPrompt", () => {
|
||||||
it("preserves tool casing in the prompt", () => {
|
it("preserves tool casing in the prompt", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
toolNames: ["Read", "Bash", "process"],
|
toolNames: ["Read", "Exec", "process"],
|
||||||
skillsPrompt:
|
skillsPrompt:
|
||||||
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
|
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("- Read: Read file contents");
|
expect(prompt).toContain("- Read: Read file contents");
|
||||||
expect(prompt).toContain("- Bash: Run shell commands");
|
expect(prompt).toContain("- Exec: Run shell commands");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
"Use `Read` to load the SKILL.md at the location listed for that skill.",
|
"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", () => {
|
it("adds ClaudeBot self-update guidance when gateway tool is available", () => {
|
||||||
const prompt = buildAgentSystemPrompt({
|
const prompt = buildAgentSystemPrompt({
|
||||||
workspaceDir: "/tmp/clawd",
|
workspaceDir: "/tmp/clawd",
|
||||||
toolNames: ["gateway", "bash"],
|
toolNames: ["gateway", "exec"],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(prompt).toContain("## Clawdbot Self-Update");
|
expect(prompt).toContain("## Clawdbot Self-Update");
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@ export function buildAgentSystemPrompt(params: {
|
||||||
grep: "Search file contents for patterns",
|
grep: "Search file contents for patterns",
|
||||||
find: "Find files by glob pattern",
|
find: "Find files by glob pattern",
|
||||||
ls: "List directory contents",
|
ls: "List directory contents",
|
||||||
bash: "Run shell commands",
|
exec: "Run shell commands",
|
||||||
process: "Manage background bash sessions",
|
process: "Manage background exec sessions",
|
||||||
// Provider docking: add provider login tools here when a provider needs interactive linking.
|
// Provider docking: add provider login tools here when a provider needs interactive linking.
|
||||||
browser: "Control web browser",
|
browser: "Control web browser",
|
||||||
canvas: "Present/eval/snapshot the Canvas",
|
canvas: "Present/eval/snapshot the Canvas",
|
||||||
|
|
@ -80,7 +80,7 @@ export function buildAgentSystemPrompt(params: {
|
||||||
"grep",
|
"grep",
|
||||||
"find",
|
"find",
|
||||||
"ls",
|
"ls",
|
||||||
"bash",
|
"exec",
|
||||||
"process",
|
"process",
|
||||||
"browser",
|
"browser",
|
||||||
"canvas",
|
"canvas",
|
||||||
|
|
@ -133,7 +133,7 @@ export function buildAgentSystemPrompt(params: {
|
||||||
|
|
||||||
const hasGateway = availableTools.has("gateway");
|
const hasGateway = availableTools.has("gateway");
|
||||||
const readToolName = resolveToolName("read");
|
const readToolName = resolveToolName("read");
|
||||||
const bashToolName = resolveToolName("bash");
|
const execToolName = resolveToolName("exec");
|
||||||
const processToolName = resolveToolName("process");
|
const processToolName = resolveToolName("process");
|
||||||
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
const extraSystemPrompt = params.extraSystemPrompt?.trim();
|
||||||
const ownerNumbers = (params.ownerNumbers ?? [])
|
const ownerNumbers = (params.ownerNumbers ?? [])
|
||||||
|
|
@ -195,8 +195,8 @@ export function buildAgentSystemPrompt(params: {
|
||||||
"- grep: search file contents for patterns",
|
"- grep: search file contents for patterns",
|
||||||
"- find: find files by glob pattern",
|
"- find: find files by glob pattern",
|
||||||
"- ls: list directory contents",
|
"- ls: list directory contents",
|
||||||
`- ${bashToolName}: run shell commands (supports background via yieldMs/background)`,
|
`- ${execToolName}: run shell commands (supports background via yieldMs/background)`,
|
||||||
`- ${processToolName}: manage background bash sessions`,
|
`- ${processToolName}: manage background exec sessions`,
|
||||||
"- browser: control clawd's dedicated browser",
|
"- browser: control clawd's dedicated browser",
|
||||||
"- canvas: present/eval/snapshot the Canvas",
|
"- canvas: present/eval/snapshot the Canvas",
|
||||||
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
"- nodes: list/describe/notify/camera/screen on paired nodes",
|
||||||
|
|
@ -277,7 +277,7 @@ export function buildAgentSystemPrompt(params: {
|
||||||
)}`
|
)}`
|
||||||
: "",
|
: "",
|
||||||
params.sandboxInfo.elevated?.allowed
|
params.sandboxInfo.elevated?.allowed
|
||||||
? "Elevated bash is available for this session."
|
? "Elevated exec is available for this session."
|
||||||
: "",
|
: "",
|
||||||
params.sandboxInfo.elevated?.allowed
|
params.sandboxInfo.elevated?.allowed
|
||||||
? "User can toggle with /elevated on|off."
|
? "User can toggle with /elevated on|off."
|
||||||
|
|
@ -288,7 +288,7 @@ export function buildAgentSystemPrompt(params: {
|
||||||
params.sandboxInfo.elevated?.allowed
|
params.sandboxInfo.elevated?.allowed
|
||||||
? `Current elevated level: ${
|
? `Current elevated level: ${
|
||||||
params.sandboxInfo.elevated.defaultLevel
|
params.sandboxInfo.elevated.defaultLevel
|
||||||
} (on runs bash on host; off runs in sandbox).`
|
} (on runs exec on host; off runs in sandbox).`
|
||||||
: "",
|
: "",
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
@ -315,7 +315,7 @@ export function buildAgentSystemPrompt(params: {
|
||||||
"## Messaging",
|
"## Messaging",
|
||||||
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
|
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
|
||||||
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
"- 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")
|
availableTools.has("message")
|
||||||
? [
|
? [
|
||||||
"",
|
"",
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"bash": {
|
"exec": {
|
||||||
"emoji": "🛠️",
|
"emoji": "🛠️",
|
||||||
"title": "Bash",
|
"title": "Exec",
|
||||||
"detailKeys": ["command"]
|
"detailKeys": ["command"]
|
||||||
},
|
},
|
||||||
"process": {
|
"process": {
|
||||||
|
|
|
||||||
|
|
@ -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.allow → tools.allow.");
|
||||||
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
|
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.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(
|
expect(res.changes).toContain(
|
||||||
"Moved agent.sandbox.tools → tools.sandbox.tools.",
|
"Moved agent.sandbox.tools → tools.sandbox.tools.",
|
||||||
);
|
);
|
||||||
|
|
@ -1118,7 +1118,7 @@ describe("legacy config detection", () => {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowFrom: { discord: ["user:1"] },
|
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({
|
expect(res.config?.tools?.sandbox?.tools).toEqual({
|
||||||
allow: ["browser.open"],
|
allow: ["browser.open"],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -179,7 +179,7 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||||
{
|
{
|
||||||
path: ["agent"],
|
path: ["agent"],
|
||||||
message:
|
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"],
|
path: ["agent", "model"],
|
||||||
|
|
@ -819,9 +819,11 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
||||||
|
|
||||||
const bash = getRecord(agent.bash);
|
const bash = getRecord(agent.bash);
|
||||||
if (bash) {
|
if (bash) {
|
||||||
if (tools.bash === undefined) {
|
if (tools.exec === undefined && tools.bash === undefined) {
|
||||||
tools.bash = bash;
|
tools.exec = bash;
|
||||||
changes.push("Moved agent.bash → tools.bash.");
|
changes.push("Moved agent.bash → tools.exec.");
|
||||||
|
} else if (tools.exec !== undefined) {
|
||||||
|
changes.push("Removed agent.bash (tools.exec already set).");
|
||||||
} else {
|
} else {
|
||||||
changes.push("Removed agent.bash (tools.bash already set).");
|
changes.push("Removed agent.bash (tools.bash already set).");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -972,7 +972,7 @@ export type QueueConfig = {
|
||||||
export type AgentToolsConfig = {
|
export type AgentToolsConfig = {
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
deny?: 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?: {
|
elevated?: {
|
||||||
/** Enable or disable elevated mode for this agent (default: true). */
|
/** Enable or disable elevated mode for this agent (default: true). */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
|
@ -1003,14 +1003,23 @@ export type ToolsConfig = {
|
||||||
/** Allowlist of agent ids or patterns (implementation-defined). */
|
/** Allowlist of agent ids or patterns (implementation-defined). */
|
||||||
allow?: string[];
|
allow?: string[];
|
||||||
};
|
};
|
||||||
/** Elevated bash permissions for the host machine. */
|
/** Elevated exec permissions for the host machine. */
|
||||||
elevated?: {
|
elevated?: {
|
||||||
/** Enable or disable elevated mode (default: true). */
|
/** Enable or disable elevated mode (default: true). */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/** Approved senders for /elevated (per-provider allowlists). */
|
/** Approved senders for /elevated (per-provider allowlists). */
|
||||||
allowFrom?: AgentElevatedAllowFromConfig;
|
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?: {
|
bash?: {
|
||||||
/** Default time (ms) before a bash command auto-backgrounds. */
|
/** Default time (ms) before a bash command auto-backgrounds. */
|
||||||
backgroundMs?: number;
|
backgroundMs?: number;
|
||||||
|
|
|
||||||
|
|
@ -905,6 +905,13 @@ const ToolsSchema = z
|
||||||
allowFrom: ElevatedAllowFromSchema,
|
allowFrom: ElevatedAllowFromSchema,
|
||||||
})
|
})
|
||||||
.optional(),
|
.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
|
bash: z
|
||||||
.object({
|
.object({
|
||||||
backgroundMs: z.number().int().positive().optional(),
|
backgroundMs: z.number().int().positive().optional(),
|
||||||
|
|
|
||||||
|
|
@ -419,14 +419,14 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||||
`write-${runIdTool}.txt`,
|
`write-${runIdTool}.txt`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const bashReadProbe = await client.request<AgentFinalPayload>(
|
const execReadProbe = await client.request<AgentFinalPayload>(
|
||||||
"agent",
|
"agent",
|
||||||
{
|
{
|
||||||
sessionKey,
|
sessionKey,
|
||||||
idempotencyKey: `idem-${runIdTool}-bash-read`,
|
idempotencyKey: `idem-${runIdTool}-exec-read`,
|
||||||
message:
|
message:
|
||||||
"Clawdbot live tool probe (local, safe): " +
|
"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}". ` +
|
`mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` +
|
||||||
`Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` +
|
`Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` +
|
||||||
"Finally reply including the nonce text you read back.",
|
"Finally reply including the nonce text you read back.",
|
||||||
|
|
@ -434,15 +434,15 @@ describeLive("gateway live (dev agent, profile keys)", () => {
|
||||||
},
|
},
|
||||||
{ expectFinal: true },
|
{ expectFinal: true },
|
||||||
);
|
);
|
||||||
if (bashReadProbe?.status !== "ok") {
|
if (execReadProbe?.status !== "ok") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`bash+read probe failed: status=${String(bashReadProbe?.status)}`,
|
`exec+read probe failed: status=${String(execReadProbe?.status)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const bashReadText = extractPayloadText(bashReadProbe?.result);
|
const execReadText = extractPayloadText(execReadProbe?.result);
|
||||||
if (!bashReadText.includes(nonceC)) {
|
if (!execReadText.includes(nonceC)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`bash+read probe missing nonce: ${bashReadText}`,
|
`exec+read probe missing nonce: ${execReadText}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue