Files
deepseek-harness/packages/bash/bash-local
Tianyi Cui 3672cd25b4 feat(subprocess): migrate lsp-local, subagent-acp, and the env scrubs onto the seam
Review direction (tianyicui, PR #660): in a stacked PR, change all other
process-running places to use the new service.

- lsp-local: LspConnection spawns through ctx.subprocess (piped protocol
  streams + a no-spill collected stderr tail); its private process-tree
  helpers (POSIX group signalling, Windows taskkill, liveness polling) are
  deleted in favor of the seam's handle verbs, and its buildChildEnv now
  rides scrubbedParentEnv (LSP children also stop inheriting stale DSH_*).
  The plugin injects 'subprocess'; compositions/tests mount
  dsh-subprocess-local.
- subagent-acp: the ACP child spawns through the seam (piped ndjson streams,
  inherited stderr); spawn failure surfaces through done-rejection into the
  same startup race; disposal is handle.dispose with the plugin's configured
  graces. dsh-subagent-subprocess is DELETED — its dispose ladder and scrub
  are the seam's, and the isolated-config-dir helper had no consumer.
- mcp-client, pty-local, sdk-helper: adopt scrubbedParentEnv as the one
  scrub definition (their spawns stay put by ownership: the MCP SDK and
  node-pty own those calls; the SDK wizard runs outside any composition).
- Coverage: per-file 100% over every touched src file, with each v8 ignore
  carrying a platform or contract reason; new suites cover stdio
  dispositions, the dispose ladder tiers, injected-win32 tree semantics,
  waitForExit, settled-kill/terminate no-ops, and spawn-failure disposal.
- Docs: consumer-migration Agent Note (en; zh follows in this PR), seam note
  updated in place, subprocess.md rewritten for the reshaped vocabulary
  (type-equiv re-registered), READMEs and SERVICE_ROLES updated, taskkill
  added to knip ignoreBinaries.
2026-07-26 15:27:59 +08:00
..

@deepseek-ai/dsh-bash-local

Local implementation of the @deepseek-ai/dsh-bash executor seam over the @deepseek-ai/dsh-subprocess service: LocalBashExecutor spawns bash -c <command> per call as a managed process group through ctx.subprocess, and owns everything bash-shaped — command defaulting and caps, timeout/cancel classification, the model-friendly terminal environment, and the model-facing stdout/stderr merge for background reads. Group mechanics (bounded spill-backed output, credential scrub, kill escalation, disposal) are the subprocess service's.

The package root exports the default and named LocalBashExecutor plugin plus its Config.

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
    maxSpillBytes: 67108864    # per-stream full-output spill cap
    graceMs: 3000              # kill escalation and post-exit pipe-drain grace

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/index.ts records the two proven stateful designs (Claude Code's cwd-only persistence; Codex's PTY exec sessions) for when real workflows demand them.
  • Configured budgets over managed groupsresolve() fills workdir/timeoutMs/stdoutMaxBytes from config, and every spawn hands the service explicit byte caps, spill cap, and graceMs (default 3s — OpenCode's escalation). Process-group kills, the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files are dsh-subprocess-local mechanics. A foreground BashExecRequest.stdoutMaxBytes can raise stdout's capture budget for one trusted caller; stderr and background runs still use maxOutputBytes.
  • Timeout and cancel classificationrun() fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports timedOut, an upstream cancel reports aborted, and a self-signaled command reports neither (timeout-library Agent Note).
  • Model-friendly terminal envNO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat (Codex's hardcoded set) so pagers and ANSI color don't garble results, merged as ordinary env under the service's credential scrub and DSH_* channel rules; an explicit caller entry still wins. See the stdin/env Agent Note and managed environment Agent Note.
  • Background processesstart() returns a live BashProcess handle immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), and the handle's readOutput() merges the service's offset-based stdout/stderr reads into one marked-section delta with a consuming cursor. A still-running process belongs to the subprocess service, so it survives executor reloads and dies (killed and joined) with the service's disposal. Everything task-shaped (ids, ownership, polling, notices) lives in the generic ctx.tasks runtime, which the tool layer registers the handle with — this executor never sees a session or a registry.

Model Experience

Indirectly, through dsh-tool-bash, which renders this executor's bounded stdout/stderr tails, background-process deltas, spill-file paths, and infrastructure failures.

KV Cache effect

No direct invalidation; the named consumer owns any request-prefix changes.

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.
  • No persistent shell or PTY — every call starts a fresh non-login bash -c; cwd-only persistence and interactive terminal sessions remain deferred until a real workflow requires them.
  • POSIX-only — the bash binary is hardcoded, and the underlying service's group semantics are POSIX; Windows is unsupported.
  • A background spawn-failure note is single-delivery — the subprocess service buffers no output for a process that never ran, so the executor injects spawn failed: … into exactly one readOutput() delta; a reader that discards that delta cannot recover it.

Scrub-heuristic and spill-retention caveats live with dsh-subprocess-local, which owns those mechanics.