DSH plugin shell and subprocess seams: ShellExecRequest
DSH plugin execution has two layers: the shell seam for "run one command," and the subprocess seam for "start a process and own its streams." The first gives you ctx.shell and resolve(), which turns optional request fields into a required spec; the second gives you fully explicit stdio dispositions and the managed DSH_* namespace. They share one vocabulary — DSH_* naming, credential scrubbing and the CollectedOutput shape belong to the subprocess seam and are re-exported by shell. That split explains why stdin, env and stdoutMaxBytes are open to in-process plugins but not exposed by the model-facing bash tool.
DSH plugin ShellExecRequest: requests, specs and results
Bottom line: requests are optional, specs are required, and resolve() is the only conversion point and the only source of caps.
- Request shape: only
commandis required;workdir,timeoutMs,onExpiryandstdoutMaxBytesare optional. - Spec shape: all of those fields are required and filled by
resolve()from implementation configuration. - Expiry policy:
onExpirydefaults tokill; withnoneno timer is armed andtimeoutMsis echoed only. - Orthogonal results:
exitCode,signal,timedOutandabortedare independent;timedOutandabortedare mutually exclusive by first cause. - Output shape:
stdoutandstderrareCollectedOutput, keeping the tail when truncated and optionally spilling the full stream. - Handles are not jobs: the seam returns process handles; background job ids, ownership and control belong to the jobs subsystem.
DSH plugin DSH_* environment: why inheritance is forbidden
Bottom line: DSH_* are Harness facts, not caller input; implementations clear inherited values before merging the snapshot.
- Collection: the model-facing bash tool collects them through
ctx.shellEnvand passesShellExecRequest.dshEnv. - Clear order: the subprocess service removes inherited
DSH_*names before merging this snapshot. - Why: an unavailable current fact must not inherit a stale value, and caller
envmust not displace managed values. - Ordinary environment:
envmerges after credential scrubbing, and an explicitundefinedtombstone deletes an existing value. - Verify: print a
DSH_*variable inside the command; even if the parent set an old value, it must reflect this snapshot.
DSH plugin sandbox facts: denied versus runnerFailed
Bottom line: denied is a policy refusal, runnerFailed means the executor never started — handle them differently.
| Field | Meaning | Handling |
|---|---|---|
mode | The mode the command actually ran under | Check whether it is looser than expected |
denied | The sandbox refused a file operation | A policy refusal |
enforcement | How completely the runner enforced the mode | Detect degradation |
runnerFailed | The runner failed before the command ran | Foreground throws SANDBOX_UNAVAILABLE |
- Foreground versus background: foreground throws on
runnerFailed; a finished background process reports through facts only. - Mode visibility: the model learns the effective mode only when a denial marks it.
- One-off relaxation needs approval:
sandbox_permissionswith ajustificationrequests a strictly looser retry, andctx.approvalmust approve that exact call first. - Modes govern file effects only: process constraints belong to the sandbox seam.
- Common misread: a non-zero exit is not automatically a sandbox denial; check
denied.
DSH plugin subprocess seam: explicit stdio and no shell parsing
Bottom line: there are no defaults — every disposition, cap and directory is on the spec, and argv never passes through a shell.
interface SubprocessStdio {
stdin: 'ignore' | 'pipe' | { data: string }
stdout: 'pipe' | 'inherit' | SubprocessCollect
stderr: 'pipe' | 'inherit' | SubprocessCollect
control?: 'pipe'
}
- stdin three ways:
ignoreuses/dev/null,pipeexposes the stream for ongoing writes,{ data }writes once and closes. - Output three ways:
pipegives the rawReadablefor protocol decoding,inheritpasses the parent descriptor through,SubprocessCollectbuffers boundedly. - Collect and spill:
maxBytescaps memory and keeps the tail;spill.maxBytesmakes the complete stream recoverable, discarding an incomplete spill on overflow. - Control channel: declare
control: 'pipe'for a byte-mode duplex channel; without it none exists. - Executable resolution:
resolveExecutable(command, env?, signal?)validates absolute paths or resolves bare names through the provider's cleanedPATH. - Results carry exit facts only: classification and semantics belong to consumers.
DSH plugin consumers: one seam, four stream dispositions
Bottom line: collect for batch output, pipe for protocol framing, inherit for diagnostics, terminal primitives for PTY.
- Bash family: collect mode with bounded batch output.
- LSP: raw protocol pipes with JSON-RPC framing.
- PTY backend: terminal process primitives.
- ACP subagents: ndjson over a pipe with stderr inherited.
- Selection rule:
pipefor structured parsing,inheritfor human diagnostics, collect for display. - Diagnostic tails may skip spill: omit
spillto keep only the in-memory tail.
DSH plugin execution troubleshooting
Bottom line: separate missing spec fields, wrong environment, sandbox refusal and the wrong stream disposition.
- Missing-field errors — you passed a request where a spec was required; call
resolve(). - Stale
DSH_*values — do not inherit; pass thedshEnvsnapshot. - Command refused — check
deniedandmodebefore doubting the command. - Foreground throws
SANDBOX_UNAVAILABLE—runnerFailed; the runner never started. - Output missing its head — truncation keeps the tail; enable spill for the complete stream.
- Repair status, stated honestly: request/spec split,
DSH_*rules, sandbox facts and stdio dispositions follow the official Shell executor, Subprocess and Sandbox pages; confirm signatures against their Cordis APIs and your version.
Sources: Shell executor, Subprocess, Sandbox.
FAQ
The DSH plugin shell seam separates the model-facing request from the executor's spec: workdir and timeoutMs are optional in the request and resolve fills them from configuration with caps. Tools must resolve first, because the required spec is what actually executes.
DSH_* are DeepSeek Harness-owned child-process facts, and implementations discard inherited DSH_* names before merging this execution's snapshot. Otherwise an unavailable fact would inherit a stale value and caller env entries could displace managed ones.
In DSH plugin shell results, denied means the sandbox refused a file operation, while runnerFailed means the runner failed before the command could run. Foreground execution throws SANDBOX_UNAVAILABLE on runnerFailed; finished background processes report through their facts channel.
DSH plugin CollectedOutput keeps the tail of the stream in text when truncated, and the complete stream goes to a private file exposed through spillPath. Keeping the spill file is the consumer's choice; diagnostic tails can stay in memory only.
No: the DSH plugin subprocess seam applies no defaults, so stdin, stdout and stderr dispositions, caps and directories are all explicit. argv is never interpreted by a shell either, avoiding implicit parsing and injection surface.
Related Terms
- ShellExecRequest
- ShellExecRequest is the model- or plugin-facing execution request where command is required and workdir, timeoutMs, onExpiry and stdoutMaxBytes are optional. It must be resolved into a ShellExecSpec before execution.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/shell
- CollectedOutput
- CollectedOutput is one captured output stream: text keeps the tail when truncated, truncated marks dropped bytes, and spillPath points at a file holding the complete stream.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
- DshEnvironment
- DshEnvironment is the set of DeepSeek Harness-owned DSH_* variables for one child-process execution, keyed inside the managed namespace. Implementations clear inherited DSH_* names before merging this snapshot.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
- SubprocessCollect
- SubprocessCollect is a subprocess output disposition that buffers boundedly in memory by maxBytes, keeps the tail on overflow, and optionally spills the complete stream to a capped file.— https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/subprocess
Sources
- DeepSeek Harness docs — Shell executor (requests/specs, run results, sandbox facts, background processes)· deepseek-harness.github.io
- DeepSeek Harness docs — Subprocess (DSH_* namespace, stdio dispositions, spawn spec)· deepseek-harness.github.io
- DeepSeek Harness docs — Sandbox (modes, enforcement and fail-closed behavior)· deepseek-harness.github.io