Files
deepseek-harness/packages/tool-bash
Tianyi Cui e51dabbb8b feat(acp): align bash terminal card with reference adapters (command title, description block, exit pill)
Match claude-agent-acp / codex-acp: the bash tool_call title IS the command
(an execute card hides rawInput), the model description rides as a content
text block above the card, and the completed card carries an exit-status pill
via _meta.terminal_exit.

Bridge fixes found in review of the prior terminal-card commit:
- tool_call_update.content is OMITTED in terminal mode (an ACP update.content
  REPLACES the call's content collection in Zed, so the fenced ```console block
  would clobber the terminal content block).
- terminal.output preserves RAW newlines (terminal renderers rely on exact
  bytes); only the fenced fallback trims trailing blank lines.
- a relative workdir is resolved against the session cwd for the card header,
  matching where the command actually ran.
- result-side terminal output is gated on the pending call having registered a
  terminal (no orphan _meta.terminal_output for a terminal Zed never made).

The exit pill is recovered by parsing renderResult's status markers (the pure
presentResult seam sees only content blocks); a round-trip test pins the parse
to the marker emission. Neutral ToolTerminal gains exitCode/signal; widened
ToolCallPresentation with a content block. Docs (RFC + 3 READMEs) updated;
with-key e2e verifies the card + exit pill against the real model.
2026-06-18 18:54:32 +08:00
..
2026-06-16 14:55:37 +08:00

@deepseek-ai/dsh-tool-bash

The model-facing bash tools — bash, bash_output, bash_kill — registered over the ctx.bash executor seam (@deepseek-ai/dsh-bash). Pure schema + text shaping; every process concern lives behind the seam, so sandboxed or remote executor implementations swap in without changing what the model sees.

Requires a loaded executor implementation (e.g. @deepseek-ai/dsh-bash-local); the plugin stays pending until ctx.bash exists (inject: ['tools', 'bash']).

Tools

bash

Arg Type Notes
command string (required) Run via bash -c. No state persists between calls — use workdir, not cd.
description string (required) One-line, active-voice summary of the command (5-10 words), for UI/log display only — no effect on execution.
timeoutMs number Default/max from executor config (120s/600s for bash-local).
workdir string Working directory for this call. Defaults to the calling agent's session cwd (session.header.cwd) so each session runs in its own workspace; a relative workdir is resolved against that session cwd.
run_in_background boolean Return a task id immediately; no timeout applies.

command, workdir, and timeoutMs are resolved against the executor's config defaults via ctx.bash.resolve() before execution, so the executor seam (BashExecSpec) receives explicit workdir/timeoutMs values. The workdir default is applied in the tool layer (from the calling agent's session.header.cwd) BEFORE resolve() — the per-session cwd must come from exec.agent, since N sessions share one executor; only when no session cwd is available does the executor fall back to its own config / process.cwd().

Result text: stdout, then a [stderr] section, then status markers — [timed out after Nms] whenever the executor's timer fired (reported independently of how the process ended, so a command that traps SIGTERM and exits 0 still shows it), [killed by signal: …] for a signal death, [exit code: N] for a non-zero exit (reported, not isError: the model decides how to react), and [output truncated; full output: <path>] when the tail was kept. Only infrastructure failures (spawn errors, aborts) surface as isError results.

bash_output

task_id → output produced since the previous bash_output call plus a status line (running / completed, exit code: N / killed). Reads that lost data to buffer bounds say so and point at the full-output spill file.

bash_kill

task_id → SIGTERM→SIGKILL on the task's process group. Killing an already-finished task is a reported no-op; unknown ids are errors.

Task ownership (cross-session isolation)

The owning agent is recorded per task id at spawn and kept for the lifetime of the loaded plugin instance (it is not cleared on completion). bash_output/bash_kill reject a task owned by a different agent with task <id> belongs to another session (a task started with no agent — a non-loop caller — has no owner and is open to anyone; a call with no exec.agent cannot access an owned task). Task ids are global and predictable, so under multi-session ACP this ownership check is the fence that stops one session's agent from reading or killing another session's background task. (TODO(tool-bash-owner-hmr): an independent HMR reload of this plugin starts a fresh map, so a task spawned before the reload becomes un-owned — acceptable as HMR is dev-only and the session boundary is one user's cooperative editor; a durable fix attaches ownership to the executor/task lifetime.)

UI presentation

These tools own how their calls render in a UI (an editor's tool-call card) via the dsh-tools presentCall/presentResult seam — a UI never special-cases tool names. For bash: the title is the exact command ("ls -la src") and kind is execute (terminal/run treatment), matching the reference ACP adapters (claude-agent-acp, codex-acp), which both use the bare command as an execute tool's title. The command is ALSO the rawInput for non-terminal UIs that render it (an execute-kind card hides rawInput — Zed shows it only for non-terminal tools — so the command must BE the title to be seen). The model-written description rides as a content text block shown ABOVE the card (a terminal card has no description slot, so it sits over the command; claude-agent-acp likewise surfaces the description as a separate content block). The completed output is wrapped in a fenced ```console block as the no-terminal-capability fallback — a UI-only affordance, so the model-facing result text stays unfenced. bash also flags itself as a terminal (the neutral terminal field: presentCall sets a cwd from the model workdir when given — absolute as-is, relative for the UI bridge to resolve against the session cwd — else leaves it for the bridge to fill from the session cwd; presentResult carries the raw output plus the parsed exitCode/signal) so a capable client (Zed) renders a terminal card with an exit-status pill instead of the text block — see packages/acp ("Terminal card"). bash_output/bash_kill present a task-scoped title ("Read output from background task bash-3" / "Kill background task bash-3") with the task id as rawInput. These methods are pure/display-only (they also run on session/load replay), and a malformed/older logged arg shape falls back to a generic presentation rather than throwing. See packages/tools ("Tool-owned UI presentation") and packages/acp ("Terminal card" / "Tool-call presentation").

Background completion notices

When a background task finishes, a short notice is injected into the owning agent's session (agent.inject(), source {kind: 'plugin', plugin: 'tool-bash'}). Injection is durable context for the next model request, not a wake-up — an idle agent stays idle until something sends a message. That's why the tool descriptions tell the model to poll with bash_output.

Permissions

TODO(permissions): commands run with the executor's full authority. The permission/sandbox seam is the tools/execute waterfall (veto or ask) plus sandboxing BashExecutor implementations — see docs/architecture.md. @cordisjs/plugin-capability (a named-permission service with a session test()) is a candidate building block for that work.