feat(bash): the sandboxed executor — per-call policy carrier, denial facts, runner-failure classification

dsh-bash grows the per-call policy carrier: BashExecRequest.sandboxMode
(request-optional, spec required-but-nullable — the owner pattern; resolve()
is the one explicit defaulting step) and the BashExecutor.sandboxMode
capability fact (undefined in the base class — composition truth the tool
layer can read). dsh-bash-local carries the field verbatim and confines
nothing.

dsh-bash-sandbox extends LocalBashExecutor and hands ctx.sandbox the exact
argv it is about to spawn. A denial is a RESULT FACT (the command RAN;
result.sandbox.denied is orthogonal to exitCode/signal), classified
conservatively against the wrap own dialect; a RUNNER failure outranks
denial — foreground re-throws the structured SANDBOX_UNAVAILABLE, a settled
background task stamps sandbox.runnerFailed — so a broken sandbox never
reads as a failing command and the command never runs unconfined.
dsh-tool-bash renders the markers and teaches the model not to retry around
a policy denial; escalation and per-session switching are staged follow-ups.
This commit is contained in:
kingwl
2026-07-09 16:05:44 +08:00
parent 7b8c3a9b40
commit 2eed448acf
38 changed files with 1529 additions and 40 deletions

View File

@@ -44,6 +44,7 @@ flowchart LR
pkg_bash["bash"]
svc_bash["ctx.bash<br/>Bash executor seam"]
pkg_bash_local["bash-local"]
pkg_bash_sandbox["bash-sandbox"]
pkg_hooks_claude["hooks-claude"]
pkg_hooks_codex["hooks-codex"]
pkg_sandbox["sandbox"]
@@ -83,6 +84,7 @@ flowchart LR
pkg_approval --> svc_approval
pkg_bash --> svc_bash
pkg_bash_local --> svc_bash
pkg_bash_sandbox --> svc_bash
pkg_code_runtime --> svc_codeRuntime
pkg_code_runtime_worker --> svc_codeRuntime
pkg_compact --> svc_compact
@@ -130,6 +132,7 @@ flowchart LR
svc_fs --> pkg_tool_fs
svc_llm --> pkg_agent_loop
svc_llm --> pkg_compact_basic
svc_sandbox --> pkg_bash_sandbox
svc_sessionPersistence --> pkg_acp
svc_sessionPersistence --> pkg_agent_loop
svc_sessions --> pkg_agent
@@ -169,8 +172,8 @@ flowchart LR
| `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-agent`](../packages/ui/stdio-agent), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. |
| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-agent`](../packages/ui/stdio-agent), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. |
| `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-core`](../packages/core/agent-core) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors can replace bash-local. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | - | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. |
| `ctx.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. |
| `ctx.approval` | `seam` | [`approval`](../packages/approval/approval) | [`acp`](../packages/ui/acp) | [`tools`](../packages/core/tools) | - | One-shot permission decisions dispatched over the `approval/request` waterfall; answerers are listeners (the ACP bridge for its own agents), absence fails closed to `unavailable`. |
| `ctx.codeRuntime` | `seam` | [`code-runtime`](../packages/code-runtime/code-runtime) | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | [`tools`](../packages/core/tools) | - | Runs one model-written program against host-provided async bindings; backends differ by substrate and language (the tool registry consumes it for Code Mode). |
| `ctx.fs` | `seam` | [`fs`](../packages/fs/fs) | [`fs-local`](../packages/fs/fs-local) | [`tool-fs`](../packages/fs/tool-fs) | [`fs-policy`](../packages/fs/fs-policy) | tool-fs executes read/write/edit through ctx.fs; fs-policy contributes observed-state checks through the fs/* event gate. |

View File

@@ -150,6 +150,33 @@ export interface Config {
Source: [`packages/bash/bash-local/src/index.ts:29`](../packages/bash/bash-local/src/index.ts)
## `@deepseek-ai/dsh-bash-sandbox`
Requires: `sandbox`
```ts config-catalog
/**
* Plugin config: the local executor's knobs plus the sandbox policy. All
* optional — `static Config` supplies the defaults (`mode: 'read-only'` is the
* fail-safe default; an example that wants a workspace-writable agent opts in
* explicitly). The runner choice is NOT configured here: which platform
* backend confines the command is the `ctx.sandbox` provider's config.
*/
export interface Config extends LocalConfig {
/** File-sandbox mode commands run under (default: `read-only`). */
mode?: SandboxMode
/**
* Root directory `workspace-write` mode may write under (default: the
* executor's default working directory — `cwd`, else `process.cwd()`).
*/
workspaceRoot?: string
}
```
Depends on: [`LocalConfig`](#deepseek-aidsh-bash-local) · [`SandboxMode`](../packages/sandbox/sandbox/src/index.ts)
Source: [`packages/bash/bash-sandbox/src/index.ts:59`](../packages/bash/bash-sandbox/src/index.ts)
## `@deepseek-ai/dsh-code-runtime-worker`
```ts config-catalog

View File

@@ -75,7 +75,7 @@ onTaskDone(listener: BashTaskListener): () => void
Types: [BashExecRequest](../core-data-structures/bash.md) · [BashExecSpec](../core-data-structures/bash.md) · [BashRunResult](../core-data-structures/bash.md) · [BashTask](../core-data-structures/bash.md) · [BashTaskRead](../core-data-structures/bash.md)
Source: [`packages/bash/bash/src/index.ts:59`](../../packages/bash/bash/src/index.ts)
Source: [`packages/bash/bash/src/index.ts:61`](../../packages/bash/bash/src/index.ts)
## `ctx.codeRuntime` — `CodeRuntime` (abstract seam)

View File

@@ -44,6 +44,20 @@ interface BashExecRequest {
* ownerless background start (a non-agent caller).
*/
owner?: OwnerToken | undefined
/**
* Explicit per-call sandbox-policy input, overriding the executor's
* configured default mode for THIS call. Never a silent default: a
* consumer sets it only from an explicit policy source — an
* `'allowed-once'` grant a human just issued through `ctx.approval` (the
* escalation flow in the sandbox RFC § Escalation, which outranks), or the
* session's standing override folded from its own `bash/sandbox-mode`
* events (the sandbox RFC § Per-session mode switching — the user's recorded per-session
* choice). A sandboxing executor confines THIS call under the given mode;
* a non-sandboxing executor carries the field and confines nothing (the
* tool layer stamps neither escalation nor overrides without a sandboxing
* executor — see {@link BashExecutor.sandboxMode}).
*/
sandboxMode?: SandboxMode | undefined
}
```
@@ -79,6 +93,16 @@ interface BashExecSpec {
* task. `start()` stores it; `run()` (foreground) ignores it.
*/
owner: OwnerToken | undefined
/**
* The sandbox mode this call executes under, REQUIRED-but-nullable for the
* same visibility reason as `owner`. A sandboxing executor's `resolve()`
* stamps the effective mode (the request's explicit override, else its
* configured default) so `run()`/`start()` read the spec, never the config;
* a non-sandboxing executor carries the request value through verbatim and
* ignores it (`undefined` under such an executor means what its README says:
* unconfined execution).
*/
sandboxMode: SandboxMode | undefined
}
```
@@ -106,6 +130,12 @@ interface BashRunResult {
timeoutMs: number
stdout: CollectedOutput
stderr: CollectedOutput
/**
* Sandbox facts, present iff a sandboxing executor ran the command — an
* unsandboxed executor (e.g. `dsh-bash-local`) never sets it. See
* {@link BashSandboxInfo} for the `denied` classification semantics.
*/
sandbox?: BashSandboxInfo
}
```
@@ -122,9 +152,56 @@ interface CollectedOutput {
}
```
## File sandbox: `SandboxMode` / `BashSandboxInfo`
A sandbox-consuming executor (`dsh-bash-sandbox`) confines commands under its executor-configured mode — fixed at config time for the executor's lifetime; a runtime/per-session mode surface is the sandbox RFC's config phase, not current behavior; the mode/enforcement vocabulary is owned by the `@deepseek-ai/dsh-sandbox` seam (whose provider wraps the executor's argv), and the mode governs FILE effects only — network and process visibility are deliberately not restricted, because a backend that cannot honestly enforce them must not pretend to:
```ts type-equiv
type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
```
A sandboxed run always reports the facts it executed under on `BashRunResult.sandbox`: `denied` is the executor's conservative classification of a failure as sandbox-caused (a failed exit whose stderr carries a filesystem-permission signature — never a clean exit or a signal kill), read from the collected stderr tail; `enforcement` reports how completely the selected backend governs the mode's file effects (`SandboxEnforcement = 'full' | 'partial'` — `partial` when an older Landlock ABI governs only a subset of the requested accesses; absent under `danger-full-access`, where nothing is confined); `runnerFailed` marks the opposite of a denial — the sandbox RUNNER itself failed and the command never ran (stamped only on settled background tasks; a foreground run surfaces the same condition as the thrown `SANDBOX_UNAVAILABLE` error):
```ts type-equiv
interface BashSandboxInfo {
/** The mode the command actually ran under. */
mode: SandboxMode
/**
* True when the executor classifies this run's failure as the sandbox
* denying a file operation. The classification is CONSERVATIVE (a failed
* exit whose stderr carries a filesystem-permission signature) and reads
* the COLLECTED stderr — the bounded in-memory tail per
* {@link CollectedOutput} semantics, so a signature that survives only in a
* spill file is missed toward `denied: false`. A plain command failure
* keeps `denied: false` even under a sandboxed mode.
*/
denied: boolean
/**
* How completely the runner enforced `mode`'s file effects — see
* {@link SandboxEnforcement}. Absent exactly when `mode` is
* `danger-full-access`: nothing is confined, so there is no enforcement to
* report.
*/
enforcement?: SandboxEnforcement
/**
* True when the executor classifies this failure as the SANDBOX RUNNER
* itself failing (missing binary, refused profile, fail-closed refusal
* before exec) — the command NEVER RAN; this is a sandbox failure, not a
* task failure, and it outranks `denied` (a runner's own error text can
* contain denial words). Only ever stamped on settled BACKGROUND tasks: a
* foreground run surfaces the same condition as the thrown
* `SANDBOX_UNAVAILABLE` error instead (the foreground path has an error
* channel; a settled task's facts are its only channel).
*/
runnerFailed?: boolean
}
```
One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (owned by the sandbox seam) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend: sandboxed modes fail CLOSED instead of silently running unconfined. The model's view of the sandbox is result facts only: the static bash tool description explains the denial marker, and each run's `result.sandbox` carries the mode it executed under (no live-mode getter on the seam and no current-mode prompt statement — both arrive with the runtime-context phase of the RFC below). Denials are deny-only result facts today; the approval/escalated-retry flow on top of them is the [sandbox RFC](../rfc/proposed/feature/2026-07-06-sandbox.md).
## Background tasks: `BashTask`
A long-running command started with `start()` is tracked as a `BashTask`. `BashTaskStatus` is `'running' | 'completed' | 'killed'`; `done` resolves when the underlying process closes and never rejects.
A long-running command started with `start()` is tracked as a `BashTask`. `BashTaskStatus` is `'running' | 'completed' | 'killed'`; `done` resolves when the underlying process closes and never rejects. A sandboxing executor stamps `sandbox` once the task settles — classification runs against the settled task's collected stderr — so the field is absent while running and under an unsandboxed executor.
```ts type-equiv
interface BashTask {
@@ -137,6 +214,16 @@ interface BashTask {
signal: NodeJS.Signals | null
/** Resolves when the underlying process closes (never rejects). */
readonly done: Promise<void>
/**
* Sandbox facts for this task's execution, stamped by a sandboxing executor
* once the task settles and BEFORE completion listeners are notified — an
* `onTaskDone` consumer and a `done` awaiter both see it. Denial
* classification runs against the settled task's collected stderr, so the
* field cannot exist earlier: absent while the task is running and under an
* executor that does not sandbox. See {@link BashSandboxInfo} for the
* `denied` semantics.
*/
sandbox?: BashSandboxInfo
}
```

View File

@@ -27,6 +27,7 @@ flowchart TD
subgraph group_bash["packages/bash"]
pkg_bash["bash"]
pkg_bash_local["bash-local"]
pkg_bash_sandbox["bash-sandbox"]
pkg_tool_bash["tool-bash"]
end
subgraph group_fs["packages/fs"]
@@ -108,15 +109,12 @@ flowchart TD
pkg_workflow_workerthread["workflow-workerthread"]
end
pkg_llm --> pkg_brand
pkg_bash --> pkg_brand
pkg_code_runtime_worker --> pkg_code_runtime
pkg_llm_deepseek --> pkg_llm
pkg_llm_pi_ai --> pkg_llm
pkg_session --> pkg_brand
pkg_session --> pkg_llm
pkg_system_prompt --> pkg_llm
pkg_bash_local --> pkg_bash
pkg_bash_local --> pkg_timeout
pkg_fs --> pkg_brand
pkg_fs --> pkg_llm
pkg_web --> pkg_llm
@@ -125,6 +123,9 @@ flowchart TD
pkg_agent --> pkg_llm
pkg_agent --> pkg_session
pkg_agent --> pkg_system_prompt
pkg_bash --> pkg_brand
pkg_bash --> pkg_sandbox
pkg_bash --> pkg_session
pkg_fs_local --> pkg_fs
pkg_fs_policy --> pkg_fs
pkg_compact --> pkg_llm
@@ -134,17 +135,19 @@ flowchart TD
pkg_web_search_deepseek --> pkg_web
pkg_web_search_exa --> pkg_web
pkg_web_search_perplexity --> pkg_web
pkg_hook_protocol --> pkg_bash
pkg_hook_protocol --> pkg_session
pkg_session_persistence --> pkg_session
pkg_llm_replay --> pkg_llm
pkg_llm_replay --> pkg_session
pkg_sandbox_local --> pkg_llm
pkg_sandbox_local --> pkg_sandbox
pkg_bash_local --> pkg_bash
pkg_bash_local --> pkg_timeout
pkg_compact_basic --> pkg_agent
pkg_compact_basic --> pkg_compact
pkg_compact_basic --> pkg_llm
pkg_compact_basic --> pkg_session
pkg_hook_protocol --> pkg_bash
pkg_hook_protocol --> pkg_session
pkg_session_persistence_jsonl --> pkg_session
pkg_session_persistence_jsonl --> pkg_session_persistence
pkg_session_persistence_sqlite --> pkg_session
@@ -167,6 +170,9 @@ flowchart TD
pkg_tools --> pkg_llm
pkg_tools --> pkg_session
pkg_tools --> pkg_system_prompt
pkg_bash_sandbox --> pkg_bash
pkg_bash_sandbox --> pkg_bash_local
pkg_bash_sandbox --> pkg_sandbox
pkg_agent_loop --> pkg_agent
pkg_agent_loop --> pkg_llm
pkg_agent_loop --> pkg_session
@@ -176,6 +182,7 @@ flowchart TD
pkg_tool_bash --> pkg_agent
pkg_tool_bash --> pkg_bash
pkg_tool_bash --> pkg_llm
pkg_tool_bash --> pkg_sandbox
pkg_tool_bash --> pkg_system_prompt
pkg_tool_bash --> pkg_tools
pkg_tool_fs --> pkg_fs
@@ -286,17 +293,16 @@ flowchart TD
| [`app-boot`](../packages/ui/app-boot) | `ui` | — |
| [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — |
| [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) |
| [`bash`](../packages/bash/bash) | `bash` | [`brand`](../packages/util/brand) |
| [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime) |
| [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`llm`](../packages/llm/llm) |
| [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`llm`](../packages/llm/llm) |
| [`session`](../packages/core/session) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`system-prompt`](../packages/core/system-prompt) | `core` | [`llm`](../packages/llm/llm) |
| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) |
| [`fs`](../packages/fs/fs) | `fs` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`web`](../packages/web/web) | `web` | [`llm`](../packages/llm/llm) |
| [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`llm`](../packages/llm/llm) |
| [`agent`](../packages/core/agent) | `core` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`bash`](../packages/bash/bash) | `bash` | [`brand`](../packages/util/brand), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) |
| [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) |
| [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) |
| [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -304,11 +310,12 @@ flowchart TD
| [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) |
| [`web-search-exa`](../packages/web/web-search-exa) | `web` | [`web`](../packages/web/web) |
| [`web-search-perplexity`](../packages/web/web-search-perplexity) | `web` | [`web`](../packages/web/web) |
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) |
| [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) |
| [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) |
| [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) |
| [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) |
| [`session-persistence-jsonl`](../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
| [`session-persistence-sqlite`](../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence) |
| [`invariants`](../packages/support/invariants) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
@@ -316,8 +323,9 @@ flowchart TD
| [`approval`](../packages/approval/approval) | `approval` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
| [`workflow`](../packages/workflow/workflow) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm) |
| [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`approval`](../packages/approval/approval), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) |
| [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) |
| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) |
| [`tool-web`](../packages/web/tool-web) | `web` | [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`web`](../packages/web/web) |

View File

@@ -124,7 +124,7 @@ Registered by the tool registry itself under `mode: code` / `mode: both` (see th
### `bash`
Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`.
Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way (a background task reports the same marker via bash_output once it has finished). Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a task id immediately; poll it with `bash_output` and stop it with `bash_kill`.
```json
{