Files
deepseek-harness/packages/bash/bash-local/README.md
Tianyi Cui ecb8aa5b8e Add a gated Known Limitations and Deferred Work section to every package README
Every packages/*/* README now carries a canonical '## Known Limitations and
Deferred Work' section: condensed, evidence-backed bullets for consumer-visible
gaps (unimplemented features, platform caveats, MVP cuts) and consciously
postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten
pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred',
'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized
into the canonical heading.

A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape:
exactly one limitations-like heading per package README, byte-equal to the
canonical h2, with at least one bullet; near-miss headings fail so variants
cannot creep back. Packages with genuinely nothing to declare (dsh-brand,
dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and
must NOT carry the section; whitelist entries are validated against the scanned
package set so a rename fails loud.

Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf
set; the standing rule lands in packages/AGENTS.md and the adding-a-package
cookbook; decision record in
docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
(RFC index regenerated).

Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is
unimplemented (the dsh-compact seam README's package table and the seam's
module doc comment).
2026-07-12 01:46:34 +08:00

4.4 KiB

@deepseek-ai/dsh-bash-local

Local-subprocess implementation of the @deepseek-ai/dsh-bash executor seam: LocalBashExecutor spawns bash -c <command> per call in its own process group, collects bounded output with full-stream spill files, and escalates kills SIGTERM→SIGKILL across the whole group.

Config

- id: bash
  name: '@deepseek-ai/dsh-bash-local'
  config:
    cwd: /path/to/workspace   # default: process.cwd()
    timeoutMs: 120000          # default foreground timeout
    maxTimeoutMs: 600000       # cap for per-call overrides
    maxOutputBytes: 64000      # per-stream in-memory cap; overflow spills to disk
    graceMs: 3000              # SIGTERM→SIGKILL escalation grace on kills

Behavior (and where it came from)

Design surveyed against the bash tools of Claude Code, OpenCode, Codex, and pi; the notable choices:

  • Spawn per call, no shell state — every call is a fresh non-login bash -c (deterministic; no rc files). All four surveyed tools spawn per call. XXX(stateful-shell) in src/run.ts records the two proven stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec sessions) for when real workflows demand them.
  • Process-group kills with escalation — children are spawned detached (own process group); kills send SIGTERM to the group, then SIGKILL after the graceMs grace (default 3s — OpenCode's escalation; pipelines and subshells die with the parent). ESRCH is tolerated; daemons that re-parent away from the group can still survive — same caveat as the surveyed tools.
  • Tail-keep truncation + spill files — output beyond maxOutputBytes keeps the in-memory TAIL (errors/results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a temp file whose path is reported when available. If the final spill close reports a delayed writeback failure, the executor still returns the tail but withholds the path rather than advertising a possibly incomplete file.
  • Model-friendly env + credential scrubprocess.env minus credential-shaped vars (*KEY*/*SECRET*/*TOKEN*), then NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat (Codex's hardcoded set) so pagers and ANSI color don't garble results. This scrub is the security control that keeps the harness's ambient credentials out of a spawned command. A spec's env is merged LAST (after the scrub), so a caller's explicit entry — a value it already holds — wins even on a credential-shaped name. The spec's stdin, when supplied, is written to the child and closed; with none supplied, fd 0 is /dev/null — the exact pre-seam default, so a command that probes stdin's file type is unaffected. Both env/stdin are set by in-process plugins (the hooks bridges); the model-facing tool doesn't expose them. See the bash-stdin-env RFC.
  • Background tasksstart() returns immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), readOutput() is incremental with whole-stream byte offsets, and disposal kills everything. The spec's opaque owner token is stored on the tracked task and returned by ownerOf(id) — the executor never interprets it (the consumer's access policy does), and because it lives with the task here it survives a tool-bash HMR reload.

Known Limitations and Deferred Work

  • Unconfined by itself — this executor always runs commands with the harness process's authority; deployments needing confinement compose dsh-bash-sandbox, while per-call allow/deny/ask policy belongs on tools/pre-execute.
  • POSIX-only — the bash binary, detached process groups, group kills, and SIGTERM→SIGKILL escalation are hardcoded; Windows is unsupported.
  • The credential scrub is a name heuristic*KEY*/*SECRET*/*TOKEN* only; differently-named secrets (e.g. *PASSWORD*) pass through, and a whitelist for over-scrubbed vars is noted future work.
  • Spill files are never deleted — full-output recovery files (and the private per-process spill dir) accumulate under the OS tmpdir until something external cleans them.
  • Finished background tasks are never evicted — they stay in the task map, retaining their in-memory output tails, until executor disposal.

The raw process handling lives in src/run.ts; src/index.ts is the service wiring.