Merge remote-tracking branch 'origin/master' into feature/subagent-policy-inheritance

# Conflicts:
#	docs/module-graph.md
This commit is contained in:
kingwl
2026-07-27 19:46:19 +08:00
204 changed files with 4380 additions and 2747 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
bash.md: 35cf2061588907dde41123efb01e453eb9cc929d
bash.zh.md: 0cfeb9e1a858f7057e720215c41a757588751122
bash.md: 3747244662301a256e12037ea67c21017b5ac2c5
bash.zh.md: 9927aa8d51ee410d70bed7a2d00e40061b499e15

View File

@@ -2,23 +2,13 @@
English | [中文](bash.zh.md)
The bash execution seam is split across interface ([dsh-bash](../../packages/bash/bash), `ctx.bash`), implementations ([dsh-bash-local](../../packages/bash/bash-local) and [dsh-bash-sandbox](../../packages/bash/bash-sandbox)), and consumer ([dsh-tool-bash](../../packages/bash/tool-bash), the `bash` schema). Generic background-task ids, ownership, and controls live in [tasks.md](tasks.md); this seam returns a task-free process handle.
The bash execution seam is split across interface ([dsh-bash](../../packages/bash/bash), `ctx.bash`), implementations ([dsh-bash-local](../../packages/bash/bash-local) and [dsh-bash-sandbox](../../packages/bash/bash-sandbox)), and consumer ([dsh-tool-bash](../../packages/bash/tool-bash), the `bash` schema). Generic background-task ids, ownership, and controls live in [tasks.md](tasks.md); this seam returns a task-free process handle. Raw process-group mechanics live behind the [subprocess seam](subprocess.md).
Source: [`packages/bash/bash/src/types.ts`](../../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.
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
```
```ts type-equiv
/** Trusted DeepSeek Harness variables for one bash execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
```
`DSH_*` variables are Harness-owned child-process facts. The model-facing bash tool collects them through `ctx.bashEnv` and passes them through `BashExecRequest.dshEnv`; the subprocess service removes inherited `DSH_*` names before merging the current snapshot. The `DshEnvironmentKey`/`DshEnvironment` vocabulary is owned by the [subprocess seam](subprocess.md) and re-exported by `dsh-bash`.
## Request vs. spec: the `resolve()` split
@@ -56,17 +46,18 @@ interface BashExecRequest {
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.
* scrub. Managed facts belong in {@link dshEnv}, which merges after this
* map, so an entry here can never displace one. 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.
* Harness-owned `DSH_*` variables for this execution (typed to managed
* keys). Executors discard ambient `DSH_*` entries before merging this
* snapshot last, so an unavailable current fact cannot inherit a stale
* value from the harness process and a caller {@link env} entry cannot
* displace a managed one.
*/
dshEnv?: DshEnvironment | undefined
/** Fully resolved per-call sandbox policy; sandboxing executors default it. */
@@ -95,12 +86,12 @@ interface BashExecSpec {
stdin?: string | undefined
/**
* Ordinary environment entries carried through from
* {@link BashExecRequest.env}. `DSH_*` remains reserved for {@link dshEnv}.
* {@link BashExecRequest.env}; {@link dshEnv} still merges after them.
* 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. */
/** Managed `DSH_*` snapshot (typed to managed keys); merges after {@link env}. */
dshEnv?: DshEnvironment | undefined
/** Resolved sandbox policy; ignored by executors that do not confine. */
sandboxPolicy: SandboxExecutionPolicy | undefined
@@ -145,19 +136,7 @@ interface BashRunResult {
}
```
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:
```ts type-equiv
/** 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
}
```
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. The shape is owned by the [subprocess seam](subprocess.md) and re-exported by `dsh-bash`.
## File sandbox: `BashSandboxInfo`
@@ -192,8 +171,9 @@ One more piece completes the vocabulary: the `SANDBOX_UNAVAILABLE` error code (o
```ts type-equiv
/**
* 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}.
* only access path; buffered output remains readable after exit. Composition
* teardown (the subprocess service's disposal) kills running processes and
* awaits {@link done}; an executor-only reload leaves them running.
*/
interface BashProcess {
/** Process lifecycle state (settled exactly once). */
@@ -238,4 +218,4 @@ interface BashProcessRead {
## 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](tasks.md).
`BashExecutor` owns `resolve`, foreground `run`, background-process `start`, and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service](subprocess.md)'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic task runtime](tasks.md).

View File

@@ -2,23 +2,13 @@
[English](bash.md) | 中文
bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash)`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local) 与 [dsh-bash-sandbox](../../packages/bash/bash-sandbox))和消费方([dsh-tool-bash](../../packages/bash/tool-bash),即 `bash` schema。通用后台任务的 id、所有权与控制位于 [tasks.md](tasks.md);本 seam 返回一个不含任务概念的进程句柄。
bash 执行 seam 分为接口([dsh-bash](../../packages/bash/bash)`ctx.bash`)、实现([dsh-bash-local](../../packages/bash/bash-local) 与 [dsh-bash-sandbox](../../packages/bash/bash-sandbox))和消费方([dsh-tool-bash](../../packages/bash/tool-bash),即 `bash` schema。通用后台任务的 id、所有权与控制位于 [tasks.md](tasks.md);本 seam 返回一个不含任务概念的进程句柄。原始进程组机制位于[进程管理器 seam](subprocess.md)之后。
源码:[`packages/bash/bash/src/types.ts`](../../packages/bash/bash/src/types.ts)
## 受管 shell 环境命名空间
`DSH_*` 变量是归 Harness 所有的子进程事实。面向模型的 bash 工具通过 `ctx.bashEnv` 收集它们,再经由 `BashExecRequest.dshEnv` 传递;执行器在合并当前快照之前会移除继承而来的 `DSH_*` 名称。
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
```
```ts type-equiv
/** Trusted DeepSeek Harness variables for one bash execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
```
`DSH_*` 变量是归 Harness 所有的子进程事实。面向模型的 bash 工具通过 `ctx.bashEnv` 收集它们,再经由 `BashExecRequest.dshEnv` 传递;进程管理器在合并当前快照之前会移除继承而来的 `DSH_*` 名称。`DshEnvironmentKey``DshEnvironment` 词汇归[进程管理器 seam](subprocess.md)所有,由 `dsh-bash` 重导出。
## 请求与规格:`resolve()` 拆分
@@ -56,17 +46,18 @@ interface BashExecRequest {
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.
* scrub. Managed facts belong in {@link dshEnv}, which merges after this
* map, so an entry here can never displace one. 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.
* Harness-owned `DSH_*` variables for this execution (typed to managed
* keys). Executors discard ambient `DSH_*` entries before merging this
* snapshot last, so an unavailable current fact cannot inherit a stale
* value from the harness process and a caller {@link env} entry cannot
* displace a managed one.
*/
dshEnv?: DshEnvironment | undefined
/** Fully resolved per-call sandbox policy; sandboxing executors default it. */
@@ -95,12 +86,12 @@ interface BashExecSpec {
stdin?: string | undefined
/**
* Ordinary environment entries carried through from
* {@link BashExecRequest.env}. `DSH_*` remains reserved for {@link dshEnv}.
* {@link BashExecRequest.env}; {@link dshEnv} still merges after them.
* 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. */
/** Managed `DSH_*` snapshot (typed to managed keys); merges after {@link env}. */
dshEnv?: DshEnvironment | undefined
/** Resolved sandbox policy; ignored by executors that do not confine. */
sandboxPolicy: SandboxExecutionPolicy | undefined
@@ -145,19 +136,7 @@ interface BashRunResult {
}
```
每个流是一个 `CollectedOutput`:(可能被截断的)文本加恢复信息截断时,`text` 是**尾部**,完整流溢出到一个私有文件
```ts type-equiv
/** 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
}
```
每个流是一个 `CollectedOutput`:(可能被截断的)文本加恢复信息截断时,`text` 是**尾部**,完整流溢出到一个私有文件。该形状归[进程管理器 seam](subprocess.md)所有,由 `dsh-bash` 重导出。
## 文件沙箱:`BashSandboxInfo`
@@ -192,8 +171,9 @@ interface BashSandboxInfo {
```ts type-equiv
/**
* 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}.
* only access path; buffered output remains readable after exit. Composition
* teardown (the subprocess service's disposal) kills running processes and
* awaits {@link done}; an executor-only reload leaves them running.
*/
interface BashProcess {
/** Process lifecycle state (settled exactly once). */
@@ -238,4 +218,4 @@ interface BashProcessRead {
## 服务
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有进程组、超时/中止处理、有界收集器、spill 文件、凭据清除以及 dispose资源释放后完全停稳。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。
`BashExecutor` 拥有 `resolve`、前台 `run`、后台进程 `start` 以及 `sandboxMode` 能力事实。`dsh-bash-local` 拥有命令默认值补全、超时/中止分类、终端环境以及后台读取合并;进程组、有界收集器、spill 文件、凭据清除 dispose资源释放后完全停稳归[进程管理器](subprocess.md)所有。`dsh-tool-bash` 拥有面向模型的渲染,并将后台句柄适配到[通用任务运行时](tasks.md)。

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
core.md: 86aea325f5401d728a9b4aa147d78db9163a32c1
core.zh.md: d09fb48419c14959eefb5a1df1c593b36c188cf5
# pnpm run verify-translation-pairing --write docs/core-data-structures/core.md
core.md: 1fb3288a6d01860220191f0ee1f914804dd2b33e
core.zh.md: 74ddc5f935c8138a7fdda601650f08702dae34d3

View File

@@ -31,6 +31,7 @@ Everything else is documented on a **sub-page**, not here. The rule that draws t
| [user-interaction.md](user-interaction.md) | the UI-backed human question/answer seam: `AskUserQuestionRequest`, answer/options vocabulary, provider API, error taxonomy |
| [approval.md](approval.md) | the one-shot user-approval seam: `ApprovalRequest`, `ApprovalOutcome`, per-session policy, audit and answerer contracts |
| [bash.md](bash.md) | the bash executor seam: `BashExecRequest`/`Spec`, `BashRunResult`, background `BashProcess` handles |
| [subprocess.md](subprocess.md) | the subprocess seam: fully-explicit `SubprocessSpawnSpec`, offset-based output readers, unclassified `SubprocessOutcome`, and the managed `DSH_*` environment vocabulary |
| [pty.md](pty.md) | persistent terminal ids, backend/session contracts, send readiness, bounded reads, and owner-visible snapshots |
| [sandbox.md](sandbox.md) | per-session policy resolution and the process-confinement seam: file-effect modes, execution/provider policies, `ConfinedArgv`, enforcement and fail-closed errors |
| [code-runtime.md](code-runtime.md) | the code-execution seam: `CodeRunRequest`/`Result`, binding namespaces, captured logs, the `CodeRunFailure` taxonomy |

View File

@@ -31,6 +31,7 @@ harness 是一个微内核:一个极小的核心加上众多插件。大多数
| [user-interaction.md](user-interaction.md) | UI 支持的人工问答 seam`AskUserQuestionRequest`、answer/options 词汇、提供方 API、错误分类体系 |
| [approval.md](approval.md) | 一次性用户审批 seam`ApprovalRequest``ApprovalOutcome`、逐会话策略、审计与 answerer 契约 |
| [bash.md](bash.md) | bash 执行器 seam`BashExecRequest`/`Spec``BashRunResult`、后台 `BashProcess` 句柄 |
| [subprocess.md](subprocess.md) | 子进程 seam完全显式的 `SubprocessSpawnSpec`、基于偏移的输出读取器、不含分类的 `SubprocessOutcome`,以及受管 `DSH_*` 环境词汇 |
| [pty.md](pty.md) | 持久化终端 ID、后端/会话契约、发送就绪状态、有界读取与 owner 可见快照 |
| [sandbox.md](sandbox.md) | 每会话策略解析与进程约束 seam文件效果模式、执行/提供方策略、`ConfinedArgv`、强制执行与故障关闭错误 |
| [code-runtime.md](code-runtime.md) | 代码执行 seam`CodeRunRequest`/`Result`、绑定命名空间、捕获日志、`CodeRunFailure` 分类体系 |

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
subprocess.md: 922e7ad0ee8b5c0dbcd0a6a4553c9d2a580f3ee2
subprocess.zh.md: 5befdcdfc9b0e1d2a9adc825b177c90e53269def

View File

@@ -0,0 +1,239 @@
# Subprocess
English | [中文](subprocess.zh.md)
The subprocess seam is split across interface ([dsh-subprocess](../../packages/subprocess/subprocess), `ctx.subprocess`) and implementation ([dsh-subprocess-local](../../packages/subprocess/subprocess-local)); its consumers are other capability seams and out-of-process backends — the [bash executor family](bash.md) (collect-mode batch output), the LSP host (piped protocol streams + a collected stderr tail), and the ACP subagent backend (piped protocol streams + inherited stderr). This seam owns the managed `DSH_*` environment namespace, the shared credential scrub (`scrubbedParentEnv`), and the `CollectedOutput` shape; [dsh-bash](../../packages/bash/bash) re-exports the vocabulary so bash consumers keep one import root.
Source: [`packages/subprocess/subprocess/src/types.ts`](../../packages/subprocess/subprocess/src/types.ts)
## Managed environment namespace and captured output
`DSH_*` variables are Harness-owned child-process facts; implementations discard ambient `DSH_*` names before the caller's explicit `env` merges, so a current fact arrives only as a deliberate entry, and each collected stream reports its truncation and spill-recovery state through `CollectedOutput`.
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
```
```ts type-equiv
/** Trusted DeepSeek Harness variables for one child-process execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
```
```ts type-equiv
/** 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
}
```
## Node-shaped stdio dispositions
Each stream's disposition is explicit, chosen per consumer: raw pipes for protocol framing (LSP JSON-RPC, ACP ndjson), inherit for pass-through diagnostics, and collect mode for bounded batch output — with the spill file optional, so a diagnostic tail (a language server's stderr) buffers without leaving files behind.
```ts type-equiv
/**
* stdin disposition. `'ignore'` leaves fd 0 on `/dev/null`; `'pipe'` exposes
* {@link SubprocessHandle.stdin} for the caller's ongoing protocol writes;
* `{ data }` writes the bytes and closes (the batch shape).
*/
type SubprocessStdinMode = 'ignore' | 'pipe' | { readonly data: string }
```
```ts type-equiv
/**
* Bounded in-memory collection for one output stream, with an optional
* full-stream spill file. Omitting `spill` keeps only the in-memory tail —
* the diagnostic-tail shape (a language server's stderr); including it makes
* the complete stream recoverable up to its cap (the bash tool shape).
*/
interface SubprocessCollect {
/** In-memory cap in bytes; overflow keeps the TAIL. */
maxBytes: number
/** Full-stream spill file; absent disables spilling entirely. */
spill?: {
/** Whole-stream byte cap; a larger stream discards its now-incomplete spill. */
maxBytes: number
}
}
```
```ts type-equiv
/**
* stdout/stderr disposition. `'pipe'` exposes the raw `Readable` for the
* caller's protocol decoding; `'inherit'` passes the parent's descriptor
* through (child diagnostics land on the harness's own stream); a
* {@link SubprocessCollect} object buffers boundedly with offset-based reads.
*/
type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect
```
```ts type-equiv
/** Per-stream stdio dispositions, all explicit — this seam applies no defaults. */
interface SubprocessStdio {
stdin: SubprocessStdinMode
stdout: SubprocessOutputMode
stderr: SubprocessOutputMode
}
```
## The fully-explicit spawn spec
The seam applies no defaults: every disposition, limit, and directory is explicit on the spec, so the caller's own config — not a hidden subprocess-service default — decides them. `argv` is never shell-interpreted.
```ts type-equiv
/**
* A fully-specified spawn request. This seam applies no defaults: every
* disposition, limit, and directory is explicit, so the caller's own config —
* not a hidden subprocess-service default — decides them (the `dsh-bash`
* request/spec split is the owning template).
*/
interface SubprocessSpawnSpec {
/** Executable and arguments; `argv[0]` is the program. Never shell-interpreted here. */
argv: readonly string[]
/** Working directory for the child. */
cwd: string
/** Per-stream stdio dispositions. */
stdio: SubprocessStdio
/**
* Grace period in milliseconds for the {@link SubprocessHandle.terminate}
* escalation and for draining still-open collected pipes after the process
* exits (an inherited descriptor held by a surviving descendant cannot hold
* the outcome open indefinitely).
*/
graceMs: number
/**
* Abort signal — starts the terminate escalation on the process tree when
* it fires. The caller owns deadlines and cause classification; this seam
* only reacts to the abort.
*/
signal?: AbortSignal | undefined
/**
* Explicit environment entries merged onto the implementation's scrubbed
* parent base (see `scrubbedParentEnv`), with no namespace validation:
* every entry is a deliberate caller opt-in, so a forwarded
* credential-shaped entry or a current `DSH_*` fact survives precisely
* because this layer merges after the scrub that drops its ambient
* namesake.
*/
env?: Record<string, string> | undefined
}
```
## Handles: streams, readers, and tree-scoped termination
A spawn returns a live handle immediately. Collect-mode readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; piped streams belong to the caller. Termination is tree-scoped on every platform: `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL, and `waitForExit()` observes the whole tree — enough for a consumer to build its own teardown ladder (the ACP backend's stdin-EOF-first `disposeAcpChild` is the template).
```ts type-equiv
/**
* A live child process rooted in its own process tree. Collected output
* remains readable after exit; piped streams belong to the caller.
*
* Termination is tree-scoped everywhere: POSIX signals the detached process
* group (falling back to the direct child when the group is gone), Windows
* terminates the tree via `taskkill /T`, so helper processes cannot outlive
* the handle unnoticed.
*/
interface SubprocessHandle {
/** Process id (tree root); -1 when the spawn itself failed. */
readonly pid: number
/** The child's stdin, present iff spawned with `stdin: 'pipe'`. */
readonly stdin: Writable | undefined
/** The child's raw stdout, present iff spawned with `stdout: 'pipe'`. */
readonly stdout: Readable | undefined
/** The child's raw stderr, present iff spawned with `stderr: 'pipe'`. */
readonly stderr: Readable | undefined
/** Offset-based readers for collect-mode streams (also readable after exit). */
readonly collected: SubprocessCollectedOutputs
/** Resolves at process close with exit facts; rejects only for spawn-level failures. */
readonly done: Promise<SubprocessOutcome>
/**
* Begin the SIGTERM → `graceMs` → SIGKILL escalation on the process tree
* (Windows force-terminates immediately) — the seam's only termination
* verb. Idempotent, a no-op once the tree is gone (the pid may be reused),
* and also triggered by the spec's abort signal.
*/
terminate(): void
/**
* Wait until the process tree has exited — the tree, not just the direct
* child, so a still-running helper is observable before teardown returns.
* @param signal - optional bound for the wait.
* @returns `true` when the tree exited, `false` when the signal aborted first.
*/
waitForExit(signal?: AbortSignal): Promise<boolean>
}
```
```ts type-equiv
/**
* Cursor-free incremental access to one collected output stream. Offsets are
* whole-stream byte coordinates owned by the caller, so independent readers
* cannot consume one another's output; `readFrom(0)` after settlement is the
* batch result (`lossy` then means the in-memory tail lost its head — the
* {@link CollectedOutput.truncated} fact).
*/
interface SubprocessOutputReader {
/**
* Read everything captured since `fromByte`. When that offset has slid out
* of the in-memory tail window the read is `lossy` — it returns the whole
* retained tail and the gap is only recoverable from the spill file.
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
* @returns the delta text, the next offset, the `lossy` flag, and the spill path when one exists.
*/
readFrom(fromByte: number): SubprocessOutputRead
}
```
```ts type-equiv
/** One incremental {@link SubprocessOutputReader.readFrom} read. */
interface SubprocessOutputRead {
/** Stream text from the requested offset (the whole retained tail when lossy). */
text: string
/** Whole-stream offset to resume from on the next read. */
nextOffset: number
/** True when the requested offset slid out of the in-memory tail window. */
lossy: boolean
/** Path to the full-stream spill file, when one was created and remains intact. */
spillPath?: string
}
```
```ts type-equiv
/** Offset-based readers for the streams spawned in collect mode. */
interface SubprocessCollectedOutputs {
/** Present iff stdout is a {@link SubprocessCollect}. */
readonly stdout?: SubprocessOutputReader
/** Present iff stderr is a {@link SubprocessCollect}. */
readonly stderr?: SubprocessOutputReader
}
```
## Outcomes carry exit facts only
`done` reports Node's close-event vocabulary and no cause classification — the service kills on abort but never decides why (the caller reads the deadline signal it owns, e.g. the bash executor's `timedOut`/`aborted` split). Collected output stays readable through `handle.collected` after settlement, so batch and streaming callers share one access path.
```ts type-equiv
/**
* Exit facts of one closed process — Node's `close`-event vocabulary.
* Deliberately carries NO timeout or cancellation classification (the caller
* reads the signal it owns to classify causes) and NO output: collected
* streams stay readable through {@link SubprocessHandle.collected} after
* settlement, so batch and streaming callers share one access path.
*/
interface SubprocessOutcome {
/** 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
}
```
## Service behavior
The abstract [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam defines `spawn` only; [`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) is the local implementation (detached trees, per-disposition wiring, credential scrub, terminate-and-join disposal). See [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md) for the seam contract and [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md) for the mechanics.

View File

@@ -0,0 +1,239 @@
# 进程管理器
[English](subprocess.md) | 中文
进程管理器 seam 分为接口([dsh-subprocess](../../packages/subprocess/subprocess)`ctx.subprocess`)与实现([dsh-subprocess-local](../../packages/subprocess/subprocess-local));它的消费方是其他能力 seam 与进程外后端:[bash 执行器家族](bash.md)使用收集模式collect的批量输出LSP 主机使用管道化的协议流 + 收集的 stderr 尾部ACPAgent Client Protocolsubagent 后端则使用管道化的协议流 + inherit 的 stderr。该 seam 拥有受管的 `DSH_*` 环境命名空间、共享的凭据清除(`scrubbedParentEnv`)与 `CollectedOutput` 形状;[dsh-bash](../../packages/bash/bash) 重导出这套词汇,使 bash 消费方保持单一导入入口。
源码:[`packages/subprocess/subprocess/src/types.ts`](../../packages/subprocess/subprocess/src/types.ts)
## 受管环境命名空间与捕获的输出
`DSH_*` 变量是归 Harness 所有的子进程事实;实现会在合并调用方显式 `env` 之前丢弃环境中已有的 `DSH_*` 名称,因此当前事实只会以有意提供的条目形式到达,每条被收集的流都通过 `CollectedOutput` 报告自身的截断与 spill 恢复状态。
```ts type-equiv
/** One environment key inside the managed {@link DSH_ENV_PREFIX} namespace. */
type DshEnvironmentKey = `${typeof DSH_ENV_PREFIX}${string}`
```
```ts type-equiv
/** Trusted DeepSeek Harness variables for one child-process execution. */
type DshEnvironment = Readonly<Record<DshEnvironmentKey, string>>
```
```ts type-equiv
/** 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
}
```
## Node 形状的 stdio 处置方式disposition
每条流的处置方式都显式给出由各消费方自行选择原始管道用于协议分帧LSP JSON-RPC、ACP ndjsoninherit 用于直通的诊断输出,收集模式用于有界的批量输出;其中 spill 文件是可选的,因此诊断尾部(语言服务器的 stderr可以只在内存中缓冲不留下任何文件。
```ts type-equiv
/**
* stdin disposition. `'ignore'` leaves fd 0 on `/dev/null`; `'pipe'` exposes
* {@link SubprocessHandle.stdin} for the caller's ongoing protocol writes;
* `{ data }` writes the bytes and closes (the batch shape).
*/
type SubprocessStdinMode = 'ignore' | 'pipe' | { readonly data: string }
```
```ts type-equiv
/**
* Bounded in-memory collection for one output stream, with an optional
* full-stream spill file. Omitting `spill` keeps only the in-memory tail —
* the diagnostic-tail shape (a language server's stderr); including it makes
* the complete stream recoverable up to its cap (the bash tool shape).
*/
interface SubprocessCollect {
/** In-memory cap in bytes; overflow keeps the TAIL. */
maxBytes: number
/** Full-stream spill file; absent disables spilling entirely. */
spill?: {
/** Whole-stream byte cap; a larger stream discards its now-incomplete spill. */
maxBytes: number
}
}
```
```ts type-equiv
/**
* stdout/stderr disposition. `'pipe'` exposes the raw `Readable` for the
* caller's protocol decoding; `'inherit'` passes the parent's descriptor
* through (child diagnostics land on the harness's own stream); a
* {@link SubprocessCollect} object buffers boundedly with offset-based reads.
*/
type SubprocessOutputMode = 'pipe' | 'inherit' | SubprocessCollect
```
```ts type-equiv
/** Per-stream stdio dispositions, all explicit — this seam applies no defaults. */
interface SubprocessStdio {
stdin: SubprocessStdinMode
stdout: SubprocessOutputMode
stderr: SubprocessOutputMode
}
```
## 完全显式的 spawn spec
该 seam 不应用任何默认值:每项处置方式、限制与目录都在 spec 上显式给出,因此由调用方自己的配置决定它们,而不是由某个隐藏的进程管理器默认值决定。`argv` 绝不经过 shell 解释。
```ts type-equiv
/**
* A fully-specified spawn request. This seam applies no defaults: every
* disposition, limit, and directory is explicit, so the caller's own config —
* not a hidden subprocess-service default — decides them (the `dsh-bash`
* request/spec split is the owning template).
*/
interface SubprocessSpawnSpec {
/** Executable and arguments; `argv[0]` is the program. Never shell-interpreted here. */
argv: readonly string[]
/** Working directory for the child. */
cwd: string
/** Per-stream stdio dispositions. */
stdio: SubprocessStdio
/**
* Grace period in milliseconds for the {@link SubprocessHandle.terminate}
* escalation and for draining still-open collected pipes after the process
* exits (an inherited descriptor held by a surviving descendant cannot hold
* the outcome open indefinitely).
*/
graceMs: number
/**
* Abort signal — starts the terminate escalation on the process tree when
* it fires. The caller owns deadlines and cause classification; this seam
* only reacts to the abort.
*/
signal?: AbortSignal | undefined
/**
* Explicit environment entries merged onto the implementation's scrubbed
* parent base (see `scrubbedParentEnv`), with no namespace validation:
* every entry is a deliberate caller opt-in, so a forwarded
* credential-shaped entry or a current `DSH_*` fact survives precisely
* because this layer merges after the scrub that drops its ambient
* namesake.
*/
env?: Record<string, string> | undefined
}
```
## 句柄:流、读取器与以进程树为范围的终止
spawn 会立即返回一个实时句柄。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;管道化的流归调用方所有。终止在每个平台上都以进程树为范围:`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级,`waitForExit()` 观察整棵进程树——这足以让消费方构建自己的拆卸阶梯ACP 后端以 stdin EOF 打头的 `disposeAcpChild` 即是模板)。
```ts type-equiv
/**
* A live child process rooted in its own process tree. Collected output
* remains readable after exit; piped streams belong to the caller.
*
* Termination is tree-scoped everywhere: POSIX signals the detached process
* group (falling back to the direct child when the group is gone), Windows
* terminates the tree via `taskkill /T`, so helper processes cannot outlive
* the handle unnoticed.
*/
interface SubprocessHandle {
/** Process id (tree root); -1 when the spawn itself failed. */
readonly pid: number
/** The child's stdin, present iff spawned with `stdin: 'pipe'`. */
readonly stdin: Writable | undefined
/** The child's raw stdout, present iff spawned with `stdout: 'pipe'`. */
readonly stdout: Readable | undefined
/** The child's raw stderr, present iff spawned with `stderr: 'pipe'`. */
readonly stderr: Readable | undefined
/** Offset-based readers for collect-mode streams (also readable after exit). */
readonly collected: SubprocessCollectedOutputs
/** Resolves at process close with exit facts; rejects only for spawn-level failures. */
readonly done: Promise<SubprocessOutcome>
/**
* Begin the SIGTERM → `graceMs` → SIGKILL escalation on the process tree
* (Windows force-terminates immediately) — the seam's only termination
* verb. Idempotent, a no-op once the tree is gone (the pid may be reused),
* and also triggered by the spec's abort signal.
*/
terminate(): void
/**
* Wait until the process tree has exited — the tree, not just the direct
* child, so a still-running helper is observable before teardown returns.
* @param signal - optional bound for the wait.
* @returns `true` when the tree exited, `false` when the signal aborted first.
*/
waitForExit(signal?: AbortSignal): Promise<boolean>
}
```
```ts type-equiv
/**
* Cursor-free incremental access to one collected output stream. Offsets are
* whole-stream byte coordinates owned by the caller, so independent readers
* cannot consume one another's output; `readFrom(0)` after settlement is the
* batch result (`lossy` then means the in-memory tail lost its head — the
* {@link CollectedOutput.truncated} fact).
*/
interface SubprocessOutputReader {
/**
* Read everything captured since `fromByte`. When that offset has slid out
* of the in-memory tail window the read is `lossy` — it returns the whole
* retained tail and the gap is only recoverable from the spill file.
* @param fromByte - whole-stream offset to resume from (a prior read's `nextOffset`; 0 for the first read).
* @returns the delta text, the next offset, the `lossy` flag, and the spill path when one exists.
*/
readFrom(fromByte: number): SubprocessOutputRead
}
```
```ts type-equiv
/** One incremental {@link SubprocessOutputReader.readFrom} read. */
interface SubprocessOutputRead {
/** Stream text from the requested offset (the whole retained tail when lossy). */
text: string
/** Whole-stream offset to resume from on the next read. */
nextOffset: number
/** True when the requested offset slid out of the in-memory tail window. */
lossy: boolean
/** Path to the full-stream spill file, when one was created and remains intact. */
spillPath?: string
}
```
```ts type-equiv
/** Offset-based readers for the streams spawned in collect mode. */
interface SubprocessCollectedOutputs {
/** Present iff stdout is a {@link SubprocessCollect}. */
readonly stdout?: SubprocessOutputReader
/** Present iff stderr is a {@link SubprocessCollect}. */
readonly stderr?: SubprocessOutputReader
}
```
## 结果只承载退出事实
`done` 报告 Node close 事件的词汇,不携带原因分类:服务会在中止时终止进程,但绝不判定原因(调用方读取归自己所有的 deadline 信号,例如 bash 执行器的 `timedOut`/`aborted` 拆分)。收集到的输出在结算后仍可经 `handle.collected` 读取,因此批量与流式调用方共用一条访问路径。
```ts type-equiv
/**
* Exit facts of one closed process — Node's `close`-event vocabulary.
* Deliberately carries NO timeout or cancellation classification (the caller
* reads the signal it owns to classify causes) and NO output: collected
* streams stay readable through {@link SubprocessHandle.collected} after
* settlement, so batch and streaming callers share one access path.
*/
interface SubprocessOutcome {
/** 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
}
```
## 服务行为
抽象的 [`SubprocessService`](../../packages/subprocess/subprocess/src/index.ts) seam 只定义 `spawn`[`LocalSubprocessService`](../../packages/subprocess/subprocess-local/src/index.ts) 是本地实现detached 进程树、按处置方式接线的流、凭据清除、先终止再等待退出的 dispose资源释放。seam 契约见 [`dsh-subprocess`](../../packages/subprocess/subprocess/README.md),具体机制见 [`dsh-subprocess-local`](../../packages/subprocess/subprocess-local/README.md)。