Files
deepseek-harness/docs/core-data-structures/bash.zh.md
2026-07-23 01:05:50 +08:00

12 KiB
Raw Blame History

Bash 执行器

English | 中文

bash 执行 seam 分为接口(dsh-bashctx.bash)、实现(dsh-bash-localdsh-bash-sandbox)和消费方(dsh-tool-bash,即 bash schema。通用后台任务的 id、所有权与控制位于 tasks.md;本 seam 返回一个不含任务概念的进程句柄。

源码:packages/bash/bash/src/types.ts

受管 shell 环境命名空间

DSH_* 变量是归 Harness 所有的子进程事实。面向模型的 bash 工具通过 ctx.bashEnv 收集它们,再经由 BashExecRequest.dshEnv 传递;执行器在合并当前快照之前会移除继承而来的 DSH_* 名称。

/** 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>>

请求与规格:resolve() 拆分

该 seam 将面向模型/插件的请求workdir/timeoutMs/stdoutMaxBytes 可选,由配置或请求策略补全)与执行器实际使用的完全解析后的 spec(这些字段均为必填)分开。工具层在二者之间调用 ctx.bash.resolve(request)——这具体落实了仓库的「包 seam 上显式优于隐式」规则:BashExecSpec 的读者不必猜测工作目录或输出预算来自何处。

/**
 * 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
}

stdinenv 是受信任的进程内插件输入,不由 dsh-tool-bash 暴露。本地执行器会先清除环境中的凭据,再合并调用方显式提供的 env。见 bash-stdin-env Agent Noteagent 决策记录)

stdoutMaxBytes 同样仅供受信任插件使用。它让前台消费方能在有界解析预算内请求完整 stdout而不会改变 stderr、后台任务或面向模型的 bash 工具的常规输出上限。

前台运行:BashRunResult

一次已完成(或被终止)的前台运行的结果。正交的结果独立报告:一个进程可以同时超时并以退出码 0 退出(因为它捕获了信号),因此 timedOutabortedsignalexitCode 各自独立为一个字段;调用方永远不会把一次被截断的运行误读为干净的成功。

/** 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
}

每个流是一个 CollectedOutput:(可能被截断的)文本加恢复信息。截断时,text尾部,完整流溢出到一个私有文件:

/** 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
}

文件沙箱:BashSandboxInfo

使用沙箱的执行器通过 BashExecutor.sandboxMode 暴露其已配置的模式回退值。工具层请求 @deepseek-ai/dsh-sandbox-policy,把每个调用会话的持久 sandbox/mode 覆盖值与不可变 cwd 解析为 BashExecRequest.sandboxPolicy;经用户批准、严格更宽松的调用只替换模式。模式/root/enforcement 词汇归 @deepseek-ai/dsh-sandbox 沙箱 seam 所有;模式仅管辖文件效果。

沙箱化运行会报告其模式、保守的拒绝分类与强制执行完整度。runnerFailed 标记命令运行前沙箱 runner 已失败;前台执行会抛出 SANDBOX_UNAVAILABLE,而已结束的后台进程只能通过其事实通道报告。

/**
 * 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
}

最后一项补全了这套词汇:当受限模式没有可用后端时,ctx.sandbox 提供方会抛出、执行器会传播由沙箱 seam所有的 SANDBOX_UNAVAILABLE 错误码。选定的 runner 拒绝其 profile 时会触达同一个故障关闭的前台错误;已结束的后台任务则记录 runnerFailed。模型会在结果中收到拒绝/runner 事实,仅当拒绝标记指出生效模式时才得知该模式,并可通过 sandbox_permissionsjustification 请求一次性、严格更宽松的重试;执行任何操作前,ctx.approval 必须批准该次确切调用。完整的策略与切换设计见沙箱 Agent Note

后台进程:BashProcess

start() 返回不含 id 或所有者的句柄。dsh-tool-bash 将它适配为 ctx.tasks.start() 钩子;随后由通用运行时拥有任务标识与生命周期。done 在进程关闭时 resolve 且绝不 reject进程结束后仍可读取并且沙箱事实会在 done resolve 前写入。

/**
 * 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() 返回增量 delta 与 spill 恢复事实:

/** 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
}

服务

BashExecutor 拥有 resolve、前台 run、后台进程 start 以及 sandboxMode 能力事实。dsh-bash-local 拥有进程组、超时/中止处理、有界收集器、spill 文件、凭据清除以及 dispose 后完全停稳。dsh-tool-bash 拥有面向模型的渲染,并将后台句柄适配到通用任务运行时