Files
deepseek-harness/docs/core-data-structures/bash.md
T
Tianyi Cui 58a531ebf5 Merge refreshed docs/i18n-batch-core into docs/i18n-batch-cds-postmortem
# Conflicts:
#	.agents/notes/README.i18n.yaml
#	.agents/notes/README.zh.md
#	docs/core-data-structures/bash.md
#	docs/core-data-structures/code-runtime.md
#	docs/core-data-structures/compaction.md
#	docs/core-data-structures/scope.md
#	docs/core-data-structures/session-query.md
#	docs/core-data-structures/user-interaction.md
#	docs/core-data-structures/web.md
#	docs/rfc/README.md
#	scripts/translation-pairing.manifest.json
#	scripts/type-equiv.manifest.json
2026-07-22 22:26:24 +08:00

12 KiB

Bash Executor

English | 中文

The bash execution seam is split across interface (dsh-bash, ctx.bash), implementations (dsh-bash-local and dsh-bash-sandbox), and consumer (dsh-tool-bash, the bash schema). Generic background-task ids, ownership, and controls live in tasks.md; this seam returns a task-free process handle.

Source: packages/bash/bash/src/types.ts

Managed shell environment namespace

DSH_* variables are Harness-owned child-process facts. The model-facing bash tool collects them through ctx.bashEnv and passes them through BashExecRequest.dshEnv; executors remove inherited DSH_* names before merging the current snapshot.

/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
/** Trusted DeepSeek Harness variables for one bash execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>

Request vs. spec: the resolve() split

The seam separates the model-/plugin-facing request (optional workdir/timeoutMs/stdoutMaxBytes, filled from config or request policy) from the fully-resolved spec the executor acts on (those fields required). The tool layer calls ctx.bash.resolve(request) between them — this is the repo's "explicit > implicit at package seams" rule made concrete: the reader of a BashExecSpec never wonders where the working directory or output budget came from.

/**
 * A caller's execution REQUEST: `workdir` and `timeoutMs` are optional and
 * filled by {@link BashExecutor.resolve} from the implementation's config.
 * This is the model-/plugin-facing shape; pass it to `resolve()` to obtain a
 * fully-resolved {@link BashExecSpec}.
 */
interface BashExecRequest {
  command: string
  /** Working directory override (default: implementation-configured). */
  workdir?: string | undefined
  /** Timeout override in milliseconds (implementations cap it). */
  timeoutMs?: number | undefined
  /**
   * Foreground stdout capture budget in bytes. Absent uses the executor's
   * default output cap. Trusted in-process consumers use this when they must
   * parse complete stdout up to their own bounded limit; the model-facing bash
   * tool does not expose it as a parameter.
   */
  stdoutMaxBytes?: number | undefined
  /** Abort signal — implementations kill the command when it fires. */
  signal?: AbortSignal | undefined
  /**
   * Bytes to write to the command's stdin, then close it. Absent leaves stdin
   * closed/empty (the default for model-driven tool calls). Set by in-process
   * plugins (e.g. the hooks bridges, which write a hook command's JSON payload
   * to its stdin); the model-facing bash tool does not expose it as a parameter
   * (a model that needs stdin uses shell syntax like a heredoc or a pipe).
   */
  stdin?: string | undefined
  /**
   * Ordinary environment entries for the command, merged after the credential
   * scrub. `DSH_*` is reserved for {@link dshEnv} and implementations reject it
   * here. Set by in-process plugins (the hooks bridges set
   * `CLAUDE_PROJECT_DIR`, `CLAUDE_PLUGIN_ROOT`, …); the model-facing bash tool
   * does not expose it as a parameter.
   */
  env?: Record<string, string> | undefined
  /**
   * Harness-owned `DSH_*` variables for this execution. Executors discard
   * ambient `DSH_*` entries before merging this snapshot, so an unavailable
   * current fact cannot inherit a stale value from the harness process, and
   * reject non-`DSH_*` names supplied through this managed channel.
   */
  dshEnv?: DshEnvironment | undefined
  /** Fully resolved per-call sandbox policy; sandboxing executors default it. */
  sandboxPolicy?: SandboxExecutionPolicy | undefined
}
/**
 * A resolved execution spec. {@link BashExecutor.resolve} fills and caps the
 * required fields; {@link BashExecutor.start} ignores `timeoutMs` because
 * background processes have no executor timeout.
 */
interface BashExecSpec {
  command: string
  workdir: string
  timeoutMs: number
  /**
   * Resolved foreground stdout capture budget in bytes. `run()` uses it for
   * stdout; background tasks and stderr keep the executor's own output cap.
   */
  stdoutMaxBytes: number
  /** Abort signal — implementations kill the command when it fires. */
  signal?: AbortSignal | undefined
  /** Bytes to write to stdin before closing it; absent means no stdin. */
  stdin?: string | undefined
  /**
   * Ordinary environment entries carried through from
   * {@link BashExecRequest.env}. `DSH_*` remains reserved for {@link dshEnv}.
   * OPTIONAL on the spec for the same reason as `stdin`: absent means no
   * ordinary extra environment.
   */
  env?: Record<string, string> | undefined
  /** Managed `DSH_*` snapshot; implementations reject ordinary names. */
  dshEnv?: DshEnvironment | undefined
  /** Resolved sandbox policy; ignored by executors that do not confine. */
  sandboxPolicy: SandboxExecutionPolicy | undefined
}

stdin and env are trusted in-process plugin inputs and are not exposed by dsh-tool-bash. The local executor scrubs ambient credentials before merging explicit caller-supplied env. See the bash-stdin-env Agent Note.

stdoutMaxBytes is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background tasks, or the model-facing bash tool's ordinary output cap.

Foreground runs: BashRunResult

The outcome of one completed (or killed) foreground run. Orthogonal outcomes are reported independently — a process can both time out AND exit 0 because it trapped the signal — so timedOut, aborted, signal, and exitCode are each their own field; a caller never reads a cut-short run as a clean success.

/** The outcome of one completed (or killed) foreground run. */
interface BashRunResult {
  /** Exit code; null when the process died from a signal. */
  exitCode: number | null
  /** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */
  signal: NodeJS.Signals | null
  /**
   * True when the executor's own timeout was the FIRST cause to cut the command
   * short. Mutually exclusive with {@link aborted}: one fused deadline drives
   * both the timeout and the caller's cancellation, so a timeout and an abort
   * racing before process close report the single first-abort cause, not both
   * (see the [timeout-library Agent Note](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
   */
  timedOut: boolean
  /**
   * True when the caller's `AbortSignal` was the FIRST cause to kill the command
   * (and it was not the executor's own timeout). Mutually exclusive with
   * {@link timedOut} — see there for the first-cause classification.
   */
  aborted: boolean
  /** The effective timeout applied to this run (after defaulting/capping). */
  timeoutMs: number
  stdout: CollectedOutput
  stderr: CollectedOutput
  /** Sandbox execution facts, absent for an unsandboxed executor. */
  sandbox?: BashSandboxInfo
}

Each stream is a CollectedOutput — the (possibly truncated) text plus recovery info. When truncated, text is the tail and the complete stream spills to a private file:

/** One captured stream: the (possibly truncated) text plus recovery info. */
interface CollectedOutput {
  /** Collected text — the TAIL of the stream when truncated. */
  text: string
  /** True when bytes were dropped from `text`. */
  truncated: boolean
  /** Path to a file holding the COMPLETE stream, when truncated and available. */
  spillPath?: string
}

File sandbox: BashSandboxInfo

A sandbox-consuming executor exposes its configured mode fallback through BashExecutor.sandboxMode. The tool layer asks @deepseek-ai/dsh-sandbox-policy to resolve each calling session's durable sandbox/mode override and immutable cwd into BashExecRequest.sandboxPolicy; a user-approved strictly wider call replaces only the mode. The mode/root/enforcement vocabulary is owned by the @deepseek-ai/dsh-sandbox seam; modes govern file effects only.

A sandboxed run reports its mode, conservative denial classification, and enforcement completeness. runnerFailed marks a sandbox runner failure before the command ran; foreground execution throws SANDBOX_UNAVAILABLE, while a settled background process has only its facts channel.

/**
 * Sandbox facts for one run, present iff a sandboxing executor handled it.
 * Facts are reported independently of process exit status so callers can
 * distinguish command failures from policy denials and runner failures.
 */
interface BashSandboxInfo {
  /** The mode the command actually ran under. */
  mode: SandboxMode
  /** Whether the sandbox denied a file operation. */
  denied: boolean
  /** How completely the selected runner enforced the requested mode. */
  enforcement?: SandboxEnforcement
  /** Whether the sandbox runner failed before the command could run. */
  runnerFailed?: boolean
}

One more piece completes the vocabulary: the SANDBOX_UNAVAILABLE error code (owned by the sandbox seam) is what the ctx.sandbox provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background task records runnerFailed. The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through sandbox_permissions plus justification; ctx.approval must grant that exact call before anything executes. The complete policy and switching design is the sandbox Agent Note.

Background processes: BashProcess

start() returns a handle with no id or owner. dsh-tool-bash adapts it into ctx.tasks.start() hooks; the generic runtime then owns task identity and lifecycle. done resolves when the process closes and never rejects, reads remain valid after settlement, and sandbox facts are stamped before done resolves.

/**
 * A background process handle returned by {@link BashExecutor.start}. It is the
 * only access path; buffered output remains readable after exit. Executor
 * disposal kills running processes and awaits {@link done}.
 */
interface BashProcess {
  /** Process lifecycle state (settled exactly once). */
  status: BashProcessStatus
  /** Exit code once finished (null = killed by signal / still running). */
  exitCode: number | null
  /** Terminating signal name, when signal-killed. */
  signal: NodeJS.Signals | null
  /** Resolves when the underlying process closes (never rejects — a spawn failure settles as `killed` with the error on stderr). */
  readonly done: Promise<void>
  /** Sandbox facts, stamped once a confined process settles. */
  sandbox?: BashSandboxInfo
  /**
   * Read output produced since the previous read (consuming — consecutive
   * reads never re-deliver). Reads that lost data flag `lossy` and point at
   * full-stream spill files when available.
   */
  readOutput(): BashProcessRead
  /**
   * Kill the process group. Returns false when it had already finished
   * (no-op); idempotent.
   */
  kill(): boolean
}

readOutput() returns the incremental delta and spill recovery facts:

/** One incremental {@link BashProcess.readOutput} read. */
interface BashProcessRead {
  /** Output produced since the previous read (stderr in a marked section). */
  delta: string
  /** True when truncation dropped unread bytes the delta cannot include. */
  lossy: boolean
  /** Full stdout spill file, when stdout truncation occurred and a safe path is available. */
  stdoutSpillPath?: string
  /** Full stderr spill file, when stderr truncation occurred and a safe path is available. */
  stderrSpillPath?: string
}

The service

BashExecutor owns resolve, foreground run, background-process start, and the sandboxMode capability fact. dsh-bash-local owns process groups, timeout/abort handling, bounded collectors, spill files, credential scrubbing, and disposal quiescence. dsh-tool-bash owns model-facing rendering and adapts background handles into the generic task runtime.