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.
@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)insrc/index.tsrecords 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 groups —
resolve()fillsworkdir/timeoutMs/stdoutMaxBytesfrom config, and every spawn hands the service explicit byte caps, spill cap, andgraceMs(default 3s — OpenCode's escalation). Process-group kills, the post-exit pipe-drain grace, tail-keep truncation, and bounded spill files aredsh-subprocess-localmechanics. A foregroundBashExecRequest.stdoutMaxBytescan raise stdout's capture budget for one trusted caller; stderr and background runs still usemaxOutputBytes. - Timeout and cancel classification —
run()fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reportstimedOut, an upstream cancel reportsaborted, and a self-signaled command reports neither (timeout-library Agent Note). - Model-friendly terminal env —
NO_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 andDSH_*channel rules; an explicit caller entry still wins. See the stdin/env Agent Note and managed environment Agent Note. - Background processes —
start()returns a liveBashProcesshandle immediately, no timeout applies (Claude Code detaches timeouts when backgrounding), and the handle'sreadOutput()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 genericctx.tasksruntime, 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 ontools/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
bashbinary 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 onereadOutput()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.