Files
deepseek-harness/packages/bash/bash/README.md
T
Tianyi Cui bb9ae2ba99 docs(bash): reframe stdin/env — the scrub is the security control, not a trust boundary
Address review: the "trusted-plugin surface" framing overstated the security
story. A model driving the `bash` tool already has equivalent power to set env
vars and feed stdin through ordinary shell syntax (`FOO=bar cmd`, heredocs), so
the `env`/`stdin` seam fields grant it no new capability — and they cannot
exfiltrate the harness's ambient credentials, because the credential SCRUB in
dsh-bash-local (which strips *KEY*/*SECRET*/*TOKEN* from process.env before the
child sees it) is the actual control, and it works regardless of these fields
(tool-call args are static JSON, never shell-evaluated).

So drop the "dangerous / trusted-plugin boundary" language across the RFC, the
three bash-package READMEs, the bash/src/types.ts JSDoc, and docs/bash.md (both
the type-equiv blocks — kept 1:1 with source — and the prose). The reality that
remains: the `bash` tool doesn't EXPOSE env/stdin as parameters because they'd
be redundant with shell syntax; the fields exist for in-process plugins (the
hooks bridges) to pass a JSON payload + CLAUDE_* vars cleanly. The guard test is
kept but reframed: it catches a future `...args` spread that would silently
forward model input into the post-scrub env merge, NOT a trust wall. No code or
behavior change.
2026-07-02 04:08:24 +08:00

4.2 KiB

@deepseek-ai/dsh-bash

The bash executor seam: an abstract BashExecutor service (ctx.bash) defining WHAT a bash backend does — run commands, manage background tasks — without saying HOW.

This package is one third of the bash capability, split so each concern can evolve (and be swapped) independently:

Package Role
@deepseek-ai/dsh-bash (this) the interface: abstract service + vocabulary types
@deepseek-ai/dsh-bash-local an implementation: local subprocesses
@deepseek-ai/dsh-tool-bash the model-facing tool schemas over ctx.bash

The split mirrors the LLM seam (LlmService/LlmAdapter) and the agent-tool survey: pi hides execution behind a BashOperations interface (local shell / SSH / VM backends), Codex behind an exec-server protocol. A future sandboxed, containerized, or remote executor implements this interface and the tool schemas don't change.

Service API (ctx.bash)

Member Semantics
run(spec) Foreground execution. Resolves when the command finishes. Rejects only for infrastructure failures (unusable workdir, missing shell, pre-aborted signal); nonzero exits, timeout kills, and abort kills resolve with a descriptive BashRunResult.
start(spec) Background execution. Returns a BashTask handle immediately; no timeout applies (stop tasks via kill).
get(id) / list() Task lookup.
ownerOf(id) The opaque OWNER token recorded for a background task at start (from the spec's owner), or undefined for an unknown id OR a known-but-ownerless task. The executor stores/returns it verbatim and NEVER interprets it — the access POLICY lives in the consumer (dsh-tool-bash), which compares ownerOf(id) to the caller's token. Storing ownership here (disposed with the executor's fiber) is what makes it survive a consumer HMR reload.
readOutput(id) Incremental output read — consecutive reads never re-deliver. Reads that lost data to buffer bounds flag lossy and point at full-stream spill files. Throws for unknown ids.
kill(id) Kill a running task. Returns false when it already finished; throws for unknown ids.
onTaskDone(listener) Completion listener (effect-based, disposer returned). Fires exactly once per task; never after the service is disposed.

Implementations subclass BashExecutor, implement the abstract methods, and call notifyTaskDone(task) on background completion. Disposal must kill every running task (no orphan processes) — see the HMR-safety tests.

Vocabulary

BashExecRequest (command, workdir?, timeoutMs?, signal?, stdin?, env?, owner?) resolves to BashExecSpec (command, workdir, timeoutMs, signal?, stdin?, env?, owner) before execution; owner is optional on the request and required-but-nullable (OwnerToken | undefined) on the resolved spec, so a forgotten owner is a visible undefined rather than a silently-absent property. The task id (BashTaskId) and the owner token (OwnerToken) are brandedOwnerToken is a DISTINCT brand from SessionId (the seam never imports dsh-session; the dsh-tool-bash consumer is the single boundary that casts its SessionId into one). run() returns BashRunResult (exitCode, signal, timedOut, aborted, timeoutMs, stdout/stderr as CollectedOutput) and start()/readOutput() use BashTask/BashTaskRead for the background side. See src/types.ts for the full contracts.

stdin and env are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload on stdin and its CLAUDE_PROJECT_DIR/CLAUDE_PLUGIN_ROOT env. The model-facing dsh-tool-bash tool does not expose them as parameters — a model already has equivalent power through shell syntax (FOO=bar cmd, a heredoc), so they would be redundant tool params. This is not a security boundary: the implementation's credential scrub (not these fields) is what keeps the harness's ambient secrets out of a spawned command. They are plain optionals on the resolved spec (unlike owner's required-but-nullable): a missing one means "none", the safe default. See the bash-stdin-env RFC.