description: "The subprocess service (ctx.subprocess) for composition authors and capability consumers starting, observing, and terminating managed child processes and terminal sessions."
English | 中文
Any composition that runs child processes can start a fully specified child process or a real terminal session through ctx.subprocess, receive a live handle with streams and direct exit facts, then terminate and wait for the provider-managed range. The service provides executable lookup, the shared environment scrub, and bounded output capture, while every default — argv, deadlines, shell semantics — stays explicit on the request, so the consuming capability seams decide what a process means. A composition mounts one provider implementation (such as dsh-subprocess-local) that registers the service; the seam package itself is an abstract contract, not a loadable plugin. Nothing here reaches a model directly: process output and lifecycle are rendered by the consuming tools.
Mount a subprocess provider in any composition that must run child processes, and call ctx.subprocess from the capability that owns the command. The common path is explicit: resolve the executable, spawn with a fully specified request, read the output you asked for, and terminate the managed range when the work is done.
One provider registers ctx.subprocess per composition; load it beside the consumers that spawn through it — the bash executors, the LSP host, the PTY shell backend, or an out-of-process subagent backend. Loading a second provider fails loudly (one service per context, cordis standard).
- name: '@deepseek-ai/dsh-subprocess-local'
- name: '@deepseek-ai/dsh-bash-local'
The request is fully explicit: the program and arguments, the working directory, one stdio disposition per stream, a termination grace, an optional abort signal, and optional environment overrides. Target and managed-range identities remain provider-private. done resolves with the direct command's exit facts (exitCode and signal) and rejects for spawn or provider failures; collected output stays readable after exit.
const executable = await ctx.subprocess.resolveExecutable('bash')
const handle = ctx.subprocess.spawn({
argv: [executable, '-c', 'echo hello'],
cwd: '/workspace',
stdio: { stdin: 'ignore', stdout: { maxBytes: 64 * 1024 }, stderr: 'inherit' },
graceMs: 5000,
})
const { exitCode, signal } = await handle.done
const output = handle.collected.stdout?.readFrom(0)
'pipe' hands you the raw stream for your own protocol framing — JSON-RPC for the LSP host, ndjson for the ACP backend.'inherit' lets the child write to the parent's own stream, for pass-through diagnostics.spill cap and the complete stream is also recoverable from a spill file.Reads are offset-based and non-consuming: a background reader and a final batch read can share one stream without stealing each other's bytes.
Termination and waiting use one provider-managed range. terminate() starts the provider's documented procedure, is idempotent, and becomes a no-op after that range is empty; the request's abort signal starts the same procedure. waitForExit() observes the same range and resolves only after the provider proves it quiescent, so direct command completion does not hide a surviving descendant. It rejects when the selected owner can no longer prove quiescence. Providers document their native owners and weaker fallbacks; callers own deadlines, teardown ladders, and cause classification.
For interactive programs, spawnTerminal allocates a real PTY: write text, read UTF-8 output, inspect and signal the current foreground process group, and await one terminate() that settles every session member the provider can still observe. Readiness, scrollback, and prompt policy stay with the PTY consumer.
Children never inherit the harness's ambient secrets: credential-shaped names and ambient DSH_* facts are scrubbed, and the caller's explicit env merges after that scrub. A deliberately forwarded credential or a current DSH_* deployment fact still reaches the child; an explicit undefined tombstone removes an ordinary ambient entry.
An executable that cannot be resolved fails loud with a stable error. A spawn that never starts rejects done; there is no buffered output for a process that never ran. waitForExit() also rejects when the provider cannot prove its selected range is empty, and a provider fallback may not own descendants that escape its process group or observed session. When a transport owns its own spawn (the SDK client, MCP), route around the service and import scrubbedParentEnv directly so environment policy stays single-sourced.
Read these pages when the package-level contract is not enough. They move from the exhaustive type reference to the providers and the decision evidence behind the seam.
DSH_* environment in full.Indirectly, through consumer seams such as the bash executor family, which own all model-facing rendering of process output and lifecycle.
No direct invalidation; the named consumers own any request-prefix changes.
These limits define when the seam is a poor fit or leaves work to its consumers. They are current package constraints, not a comparison or a backlog.
scrubbedParentEnv so environment policy stays single-sourced.