refactor(code-runtime): remove subprocess backend

This commit is contained in:
Tianyi Cui
2026-07-30 14:24:47 +08:00
parent c1d08edd83
commit 4d5345794d
44 changed files with 192 additions and 1614 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 .agents/notes/implemented/architecture/2026-07-26-subprocess-seam.md
2026-07-26-subprocess-seam.md: 214a46959c48862176e3c790d541def3444e31ad
2026-07-26-subprocess-seam.zh.md: 13ba397458a041ec358828bab53e75516fd3e76d
2026-07-26-subprocess-seam.md: 8797102c65ebab663bcf72fced5791364fe2c6ae
2026-07-26-subprocess-seam.zh.md: 8a7ff14079374d6d74a1ec729dd02c8961fa23e6

View File

@@ -12,7 +12,7 @@ English | [中文](2026-07-26-subprocess-seam.zh.md)
A new `subprocess/` capability family owns "run and manage a process"; the bash family keeps "run a bash command" and consumes it:
- **`@deepseek-ai/dsh-subprocess` (interface)** — the abstract `SubprocessService` owning `ctx.subprocess`: execution-world cwd, executable lookup, fully explicit ordinary spawns, and the terminal primitive added by the [portable execution-world decision](2026-07-28-portable-execution-world-consumers.md). Each stdio stream independently selects `'pipe'`, `'inherit'`, or bounded collection `{ maxBytes, spill? }`; stdin selects `'ignore'`, `'pipe'`, or `{ data }`. `SubprocessOutcome` carries exit facts with deliberately no timeout/cancel classification, while collected output remains on the handle after settlement. The seam also owns process and terminal handles, the shared scrub, and `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput`; `argv` is never shell-interpreted.
- **`@deepseek-ai/dsh-subprocess` (interface)** — the abstract `SubprocessService` owning `ctx.subprocess`: executable lookup, fully explicit ordinary spawns, and the terminal primitive added by the [portable execution-world decision](2026-07-28-portable-execution-world-consumers.md). Each stdio stream independently selects `'pipe'`, `'inherit'`, or bounded collection `{ maxBytes, spill? }`; stdin selects `'ignore'`, `'pipe'`, or `{ data }`. `SubprocessOutcome` carries exit facts with deliberately no timeout/cancel classification, while collected output remains on the handle after settlement. The seam also owns process and terminal handles, the shared scrub, and `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput`; `argv` is never shell-interpreted.
- **`@deepseek-ai/dsh-subprocess-local` (implementation)** — `LocalSubprocessService` over the former `run.ts` plumbing (`spawn.ts`) plus `node-pty`: detached groups, bounded collection and private spill files, executable lookup, foreground/session inspection, and disposal that terminates and joins every managed process. `terminate()` owns TERM→grace→KILL for the tree, `waitForExit()` observes tree liveness, and injected `taskkill /T` covers Windows. Ordinary and terminal spawns apply the seam's case-insensitive `KEY`/`PASSWORD`/`SECRET`/`TOKEN` scrub before explicit env. The implementation has no config; every limit arrives on the spec, while Bash and PTY presentation environment overrides stay in their consumers.
- **`dsh-bash-local` (consumer)** — `inject: ['subprocess']`; maps each resolved `BashExecSpec` onto a `SubprocessSpawnSpec` (`['bash', '-c', command]`), keeps its config, `resolve()` defaulting, fused-deadline `timedOut`/`aborted` classification, the `[stderr]`-marked background read merge with its consuming cursor, and the `onProcessDone` subclass hook. `dsh-bash-sandbox` is unchanged apart from redeclaring the inherited inject; it still wraps at the command-string level and re-enters the inherited spawn path.
- **`dsh-bash` (seam)** — re-exports the moved vocabulary from `dsh-subprocess`, so no bash consumer changes an import; `BashExecRequest`/`BashExecSpec`/`BashProcess` and the sandbox facts remain bash-owned.
@@ -21,13 +21,13 @@ Every composition that loads a bash executor now also loads `@deepseek-ai/dsh-su
Background-process lifetime moved from the executor to the subprocess service: the executor no longer retains a live-process set, so an executor reload leaves background work running and readable, and composition teardown (the service's disposal) remains the kill-and-join boundary. One behavioral seam shifted with it: a background spawn failure can no longer be buffered as fake stderr inside the plumbing (the service rejects `done` and buffers nothing for a process that never ran), so the executor injects the `spawn failed: …` note into exactly one `readOutput()` delta.
Observed stream and lifecycle needs then moved the eligible process consumers onto the seam: LSP uses piped protocol streams plus a collected stderr tail; the ACP backend uses piped ndjson, inherited stderr, and a consumer-owned stdin-EOF disposal ladder; PTY uses `spawnTerminal()` while keeping readiness and terminal policy; Code Runtime uses ordinary raw pipes. `dsh-subagent-subprocess` and the private LSP tree helpers were deleted. MCP transport spawning, the SDK package-manager runner, synchronous TUI Git probing, and dependency-light test-support launchers remain outside by ownership or execution shape; their production callers share the scrub where applicable.
Observed stream and lifecycle needs then moved the eligible process consumers onto the seam: LSP uses piped protocol streams plus a collected stderr tail; the ACP backend uses piped ndjson, inherited stderr, and a consumer-owned stdin-EOF disposal ladder; PTY uses `spawnTerminal()` while keeping readiness and terminal policy. `dsh-subagent-subprocess` and the private LSP tree helpers were deleted. MCP transport spawning, the SDK package-manager runner, synchronous TUI Git probing, and dependency-light test-support launchers remain outside by ownership or execution shape; their production callers share the scrub where applicable.
## Alternatives considered
**Leave the process plumbing inside `dsh-bash-local` (status quo).** Rejected for the same reason the [task registry split](2026-07-26-task-registry-seam.md) landed: the boundary is stable and already documented in-code (`run.ts`'s module doc said "this layer reacts to an abort signal; the executor owns deadlines and classifies causes"), and keeping it private makes every future non-shell runner either fork the mechanics or depend on a bash-named package for non-bash work. The user-visible driver for this stack was exactly this split.
**Keep the original batch-only interface and leave stream consumers bespoke.** Rejected after the observed LSP, ACP, PTY, and Code Runtime shapes showed that private process-tree signalling and environment scrubs would otherwise remain duplicated. The Node-shaped dispositions cover those consumers without buffering piped streams.
**Keep the original batch-only interface and leave stream consumers bespoke.** Rejected after the observed LSP, ACP, and PTY shapes showed that private process-tree signalling and environment scrubs would otherwise remain duplicated. The Node-shaped dispositions cover those consumers without buffering piped streams.
**Use one `stdio: 'pipe' | 'inherit' | 'collect'` mode for all streams.** Rejected because real consumers mix modes per stream: LSP uses pipe/pipe/collect, ACP uses pipe/pipe/inherit, and Bash uses data/collect/collect.
@@ -39,6 +39,6 @@ Observed stream and lifecycle needs then moved the eligible process consumers on
## Consequences
Bought: "run and manage a process" is a swappable capability used by Bash, LSP, PTY, Code Runtime, and ACP consumers; a containerized or remote process backend slots in without changing their domain semantics; tree signalling, escalation, bounded collection, terminal mechanics, and credential scrubbing each have one implementation; and background processes survive executor reloads, matching the task registry's lifetime model. Process and terminal plumbing is tested through `dsh-subprocess-local`; consumer suites pin only their owned behavior against the real service.
Bought: "run and manage a process" is a swappable capability used by Bash, LSP, PTY, and ACP consumers; a containerized or remote process backend slots in without changing their domain semantics; tree signalling, escalation, bounded collection, terminal mechanics, and credential scrubbing each have one implementation; and background processes survive executor reloads, matching the task registry's lifetime model. Process and terminal plumbing is tested through `dsh-subprocess-local`; consumer suites pin only their owned behavior against the real service.
Cost: one more package pair and one more composition row wherever a consumer loads; a missing subprocess provider leaves the consumer pending by standard service-injection behavior. Every backend implements execution-world coordinates, executable lookup, three stdio modes, tree lifecycle, and one terminal primitive. The moved-vocabulary re-exports keep `dsh-bash` imports working but mean two packages name the same types; the subprocess seam is the owner. The spawn-failure note became single-delivery through Bash's consuming read cursor instead of repeatable stderr-buffer content.
Cost: one more package pair and one more composition row wherever a consumer loads; a missing subprocess provider leaves the consumer pending by standard service-injection behavior. Every backend implements executable lookup, three stdio modes, tree lifecycle, and one terminal primitive. The moved-vocabulary re-exports keep `dsh-bash` imports working but mean two packages name the same types; the subprocess seam is the owner. The spawn-failure note became single-delivery through Bash's consuming read cursor instead of repeatable stderr-buffer content.

View File

@@ -12,7 +12,7 @@ Status: implemented
新的 `subprocess/` 能力家族拥有「运行并管理一个进程」bash 家族保留「运行一条 bash 命令」,并成为前者的消费方:
- **`@deepseek-ai/dsh-subprocess`(接口)**——拥有 `ctx.subprocess` 的抽象 `SubprocessService`执行环境 cwd、可执行文件查找、完全显式的普通 spawn以及[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)新增的终端原语。每条 stdio 流独立选择 `'pipe'``'inherit'` 或有界收集 `{ maxBytes, spill? }`stdin 选择 `'ignore'``'pipe'``{ data }``SubprocessOutcome` 只承载刻意不含超时/取消分类的退出事实,收集输出在结算后仍留在句柄上。该 seam 还拥有进程与终端句柄、共享凭据清除,以及 `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput``argv` 绝不经过 shell 解释。
- **`@deepseek-ai/dsh-subprocess`(接口)**——拥有 `ctx.subprocess` 的抽象 `SubprocessService`:可执行文件查找、完全显式的普通 spawn以及[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)新增的终端原语。每条 stdio 流独立选择 `'pipe'``'inherit'` 或有界收集 `{ maxBytes, spill? }`stdin 选择 `'ignore'``'pipe'``{ data }``SubprocessOutcome` 只承载刻意不含超时/取消分类的退出事实,收集输出在结算后仍留在句柄上。该 seam 还拥有进程与终端句柄、共享凭据清除,以及 `DSH_ENV_PREFIX`/`DshEnvironment`/`CollectedOutput``argv` 绝不经过 shell 解释。
- **`@deepseek-ai/dsh-subprocess-local`(实现)**——`LocalSubprocessService` 构建在原 `run.ts` 管道(现为 `spawn.ts`)与 `node-pty` 之上detached 进程组、有界收集与私有 spill 文件、可执行文件查找、前台/会话检查,以及终止每个受管进程并等待其退出的 dispose。`terminate()` 拥有面向进程树的 TERM→宽限→KILL`waitForExit()` 观察进程树存活性,可注入的 `taskkill /T` 覆盖 Windows。普通与终端 spawn 都先应用 seam 对 `KEY`/`PASSWORD`/`SECRET`/`TOKEN` 不区分大小写的清除,再合并显式 env。该实现没有配置每项限制都随 spec 到达Bash 与 PTY 的呈现环境覆盖仍归各自消费方所有。
- **`dsh-bash-local`(消费方)**——`inject: ['subprocess']`;把每个解析后的 `BashExecSpec` 映射为一个 `SubprocessSpawnSpec``['bash', '-c', command]`),并保留自身配置、`resolve()` 默认值补全、基于融合 deadline 的 `timedOut`/`aborted` 分类、带 `[stderr]` 标记的后台读取合并及其消费游标,以及 `onProcessDone` 子类钩子。`dsh-bash-sandbox` 除了重新声明继承来的 inject 之外没有变化;它仍在命令字符串层面做包装,并重新进入继承的 spawn 路径。
- **`dsh-bash`seam**——把迁走的词汇从 `dsh-subprocess` 重导出,因此没有任何 bash 消费方需要改动导入;`BashExecRequest`/`BashExecSpec`/`BashProcess` 与沙箱事实仍归 bash 所有。
@@ -21,13 +21,13 @@ Status: implemented
后台进程的存续期从执行器移到了管理器:执行器不再保有存活进程集合,于是重载执行器后,后台工作会继续运行且仍可读取,而组合拆除(管理器的 dispose仍是先终止再等待退出的边界。一条行为 seam 随之挪动:后台 spawn 失败不再能在管道内部被缓冲成伪造的 stderr对一个从未真正运行的进程管理器会 reject `done`,且不缓冲任何内容),因此执行器把 `spawn failed: …` 提示注入恰好一个 `readOutput()` 增量。
基于已观察到的流与生命周期需求,具备条件的进程消费方随后迁到该 seamLSP 使用管道化协议流加收集式 stderr 尾部ACPAgent Client Protocol后端使用管道化 ndjson、继承式 stderr 和消费方拥有的 stdin-EOF dispose 阶梯PTY 使用 `spawnTerminal()`,同时保留就绪与终端策略Code Runtime 使用普通原始管道`dsh-subagent-subprocess` 与 LSP 私有进程树辅助函数均被删除。MCP 传输 spawn、SDK 包管理器运行器、同步 TUI Git 探测和刻意保持轻依赖的 test-support 启动器因所有权或执行形状仍留在外部;适用的生产调用方共享凭据清除。
基于已观察到的流与生命周期需求,具备条件的进程消费方随后迁到该 seamLSP 使用管道化协议流加收集式 stderr 尾部ACPAgent Client Protocol后端使用管道化 ndjson、继承式 stderr 和消费方拥有的 stdin-EOF dispose 阶梯PTY 使用 `spawnTerminal()`,同时保留就绪与终端策略。`dsh-subagent-subprocess` 与 LSP 私有进程树辅助函数均被删除。MCP 传输 spawn、SDK 包管理器运行器、同步 TUI Git 探测和刻意保持轻依赖的 test-support 启动器因所有权或执行形状仍留在外部;适用的生产调用方共享凭据清除。
## 曾考虑的替代方案
**把进程管道留在 `dsh-bash-local` 里(维持现状)。**否决的理由与[任务注册表拆分](2026-07-26-task-registry-seam.md)得以落地的理由相同:这条边界既稳定,也早已记录在代码里(`run.ts` 的模块文档曾写明「this layer reacts to an abort signal; the executor owns deadlines and classifies causes」而若继续将它保持私有未来每个非 shell 运行器就只能要么 fork 这套机制,要么为非 bash 工作去依赖一个以 bash 命名的包。这组堆叠变更对用户可见的动因正是这一拆分。
**保留最初只支持批量的接口,让流式消费方继续各自实现。**否决:已观察到的 LSP、ACP、PTY 与 Code Runtime 形状表明这会继续保留重复的私有进程树信号与环境清除。Node 形状的处置方式覆盖这些消费方,又不缓冲管道化流。
**保留最初只支持批量的接口,让流式消费方继续各自实现。**否决:已观察到的 LSP、ACP 与 PTY 形状表明这会继续保留重复的私有进程树信号与环境清除。Node 形状的处置方式覆盖这些消费方,又不缓冲管道化流。
**用单个 `stdio: 'pipe' | 'inherit' | 'collect'` 模式统一全部流。**否决真实消费方按流混用模式——LSP 使用 pipe/pipe/collectACP 使用 pipe/pipe/inheritBash 使用 data/collect/collect。
@@ -39,6 +39,6 @@ Status: implemented
## 后果
换来的是:「运行并管理一个进程」成为 Bash、LSP、PTY、Code Runtime 与 ACP 消费方共用的可替换能力;容器化或远程进程后端可以直接接入,而无需改变各领域语义;进程树信号、升级终止、有界收集、终端机制与凭据清除各自只剩一份实现;后台进程也能在执行器重载后存活,与任务注册表的存续期模型一致。进程与终端管道通过 `dsh-subprocess-local` 测试;消费方测试套件只需针对真实服务固定各自拥有的行为。
换来的是:「运行并管理一个进程」成为 Bash、LSP、PTY 与 ACP 消费方共用的可替换能力;容器化或远程进程后端可以直接接入,而无需改变各领域语义;进程树信号、升级终止、有界收集、终端机制与凭据清除各自只剩一份实现;后台进程也能在执行器重载后存活,与任务注册表的存续期模型一致。进程与终端管道通过 `dsh-subprocess-local` 测试;消费方测试套件只需针对真实服务固定各自拥有的行为。
代价是:多出一对包,而且凡加载消费方之处都多一行组合配置;缺少 subprocess 提供方时,消费方会按标准服务注入行为保持挂起。每个后端都要实现执行环境坐标、可执行文件查找、三种 stdio 模式、进程树生命周期和一个终端原语。迁移词汇的重导出让 `dsh-bash` 的导入继续可用,但也意味着两个包命名同一批类型;进程 seam 是所有者。spawn 失败提示经由 Bash 的消费式读取游标变为单次交付,不再是可重复读取的 stderr 缓冲内容。
代价是:多出一对包,而且凡加载消费方之处都多一行组合配置;缺少 subprocess 提供方时,消费方会按标准服务注入行为保持挂起。每个后端都要实现可执行文件查找、三种 stdio 模式、进程树生命周期和一个终端原语。迁移词汇的重导出让 `dsh-bash` 的导入继续可用,但也意味着两个包命名同一批类型;进程 seam 是所有者。spawn 失败提示经由 Bash 的消费式读取游标变为单次交付,不再是可重复读取的 stderr 缓冲内容。

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 .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md
2026-07-28-portable-execution-world-consumers.md: 97c8bcfc7bf84db4b6a2862fcc9d991ee7f4a7f6
2026-07-28-portable-execution-world-consumers.zh.md: 025393f9e5ae99feba41fa8badeef8276151ba6f
2026-07-28-portable-execution-world-consumers.md: 402aa580255a5bd6aa0d046e3bcc16f712da520d
2026-07-28-portable-execution-world-consumers.zh.md: 26d493a79f8adb9e729ff69b0673f2f2775868b0

View File

@@ -6,7 +6,7 @@ English | [中文](2026-07-28-portable-execution-world-consumers.zh.md)
## Problem
The filesystem and subprocess seams made file and ordinary process access replaceable, but several higher capabilities still reached host Node APIs directly. A remote execution provider therefore appeared to need separate PTY, LSP, and Code Runtime packages even though their domain behavior did not change. Those packages would be shallow adapters: each would duplicate an existing consumer merely to replace its file and process operations.
The filesystem and subprocess seams made file and ordinary process access replaceable, but PTY and LSP still reached host Node APIs directly. A remote execution provider therefore appeared to need separate PTY and LSP packages even though their domain behavior did not change. Those packages would be shallow adapters: each would duplicate an existing consumer merely to replace its file and process operations.
Ordinary pipes do not cover one requirement. A persistent terminal needs PTY allocation, foreground-process-group inspection and signalling, and cleanup of the complete terminal session. Pretending those operations can be rebuilt in `dsh-pty-local` from an ordinary `spawn()` handle would either leak provider internals or weaken its lifecycle contract.
@@ -16,20 +16,17 @@ Ordinary pipes do not cover one requirement. A persistent terminal needs PTY all
The filesystem interface owns the path facts that another capability needs without exposing its opaque target identity: a canonical process path, canonical `file:` URI, and containment. Existing whole and streaming text operations remain filesystem-owned; protocol consumers enforce their own retention limits while consuming the stream.
The subprocess interface owns the process coordinates and primitives: canonical cwd, executable lookup, ordinary raw or collected process spawning, and `spawnTerminal()`. The terminal operation is one deep primitive whose handle owns text I/O, foreground groups, signalling, and one awaited TERM-to-KILL operation that settles in-flight handle calls and reaches quiescence for every session member the provider can still observe. Its signal cancels allocation only; the published handle owns its lifetime. Prompt detection, idle inference, scrollback, sandbox policy, and owner lifecycle remain in the PTY consumer.
The subprocess interface owns executable lookup and process primitives: ordinary raw or collected process spawning and `spawnTerminal()`. The terminal operation is one deep primitive whose handle owns text I/O, foreground groups, signalling, and one awaited TERM-to-KILL operation that settles in-flight handle calls and reaches quiescence for every session member the provider can still observe. Its signal cancels allocation only; the published handle owns its lifetime. Prompt detection, idle inference, scrollback, sandbox policy, and owner lifecycle remain in the PTY consumer.
Generic consumers use that execution world:
- `dsh-bash-local` continues to map Bash semantics onto ordinary `ctx.subprocess.spawn()`.
- `dsh-lsp-local` reads and contains source through `ctx.fs`, resolves and launches language servers through `ctx.subprocess`, and carries provider-owned file URIs through initialization and result rendering. One provider-lifetime signal aborts filesystem and protocol work during disposal, including workspace lookup before queue ownership; its JSON-RPC, pooling, synchronization, and normalization stay unchanged.
- `dsh-pty-local` maps persistent-shell semantics onto `ctx.subprocess.spawnTerminal()`. The local `node-pty` and process-inspection implementation moves into `dsh-subprocess-local`; another subprocess provider supplies the same primitive. `danger-full-access` needs no `ctx.sandbox`; a confined mode requires a same-world sandbox provider and fails before spawn when none is mounted. Prompt and silence evidence collected during asynchronous pre-write inspection is discarded when the provider write begins. Cancellation retains the send reservation while an in-flight write settles and then signals the foreground group, so late bytes or the signal cannot target a successor; an in-flight readiness poll cannot release that reservation, and a rejected write sends no signal. The absolute deadline remains armed throughout cancellation. A signal failure becomes terminal transport failure. Completion of a stale inspection resumes polling for the current send. Startup cancellation begins terminal rollback without waiting for a stalled readiness or signalling call. Close rejects new public signals and delegates provider-observable session quiescence to the handle's awaited termination operation.
- `dsh-code-runtime-subprocess` passes a bundled dependency-free eval runner directly to `ctx.subprocess`, preserving the Code Runtime binding and output contract across local or remote process worlds without a filesystem dependency or provider-specific package. It shares host-side worker mechanics through the non-plugin `dsh-code-runtime-worker/runtime-host` subpath instead of copying them. The heap-bounded worker rejects oversized binding frames before transfer, each outer hop enforces the same bound before forwarding, and raw subprocess pipes carry newline-delimited UTF-8 JSON without a redundant base64 representation. The launcher publishes an accepted terminal frame before reaping its controller so a descendant that inherits controller pipes cannot suppress completion; the host still awaits process-group quiescence.
`dsh-code-runtime-worker` remains a separate implementation. It is the smaller in-process backend and works in single-file distributions that cannot assume an installed Node executable. Remote filesystem/process compositions select `dsh-code-runtime-subprocess`; they do not need a provider-specific Code Runtime package.
## Alternatives considered
**Keep one PTY, LSP, and Code Runtime package per remote provider.** Rejected because provider mechanics would be repeated above the existing seams. The deletion test exposes the problem: deleting those adapters should not scatter domain behavior into the remote provider; the generic consumers already own it.
**Keep one PTY and LSP package per remote provider.** Rejected because provider mechanics would be repeated above the existing seams. The deletion test exposes the problem: deleting those adapters should not scatter domain behavior into the remote provider; the generic consumers already own it.
**Model a terminal as an ordinary piped subprocess.** Rejected because pipes cannot allocate a controlling terminal, resolve the current foreground process group, or prove complete terminal-session cleanup. One terminal primitive is smaller and more honest than exposing substrate-specific escape hatches.
@@ -39,13 +36,11 @@ Generic consumers use that execution world:
**Add a stable bounded-read primitive to the filesystem seam.** Rejected because only LSP needs a complete-document byte ceiling, which it can enforce while consuming the existing text stream. A second primitive forces every provider to implement stable-handle and no-follow mechanics, including a remote helper protocol, without an observed concurrent-replacement defect.
**Delete the worker-thread Code Runtime.** Rejected because portability does not erase its current deployment need. The subprocess backend requires a Node executable; the worker backend does not and remains the supported single-process path.
**Run the whole harness inside the remote environment.** Rejected as a different deployment model. Making execution capabilities portable does not move model calls, session state, plugin state, or the agent loop.
## Consequences
A remote execution provider implements only its shared sandbox owner plus filesystem and subprocess adapters. Bash, PTY, LSP, and subprocess Code Runtime compose above them, so fixes to those capabilities remain provider-neutral.
A remote execution provider implements only its shared sandbox owner plus filesystem and subprocess adapters. Bash, PTY, and LSP compose above them, so fixes to those capabilities remain provider-neutral.
The fundamental interfaces are wider, and a filesystem/subprocess pair must agree on one execution world. The added operations are limited to facts and lifecycle mechanics that current generic consumers require; model schemas, protocol framing, readiness policy, and presentation do not leak into the providers.

View File

@@ -6,7 +6,7 @@ Status: implemented
## 问题
文件系统与进程管理 seam 使文件访问和普通进程访问具备可替换性,但若干上层能力仍直接调用宿主 Node API。因此即使领域行为没有变化远程执行提供方看起来仍需要独立的 PTYLSP 与代码运行时package。这些包只会成为浅层适配器每个包都仅为替换文件与进程操作而复制一个现有消费方。
文件系统与进程管理 seam 使文件访问和普通进程访问具备可替换性,但 PTY 和 LSP 仍直接调用宿主 Node API。因此即使领域行为没有变化远程执行提供方看起来仍需要独立的 PTYLSP 包package。这些包只会成为浅层适配器每个包都仅为替换文件与进程操作而复制一个现有消费方。
普通管道无法满足其中一项要求。持久终端需要分配 PTY、检查前台进程组并发送信号以及清理完整的终端会话。如果假设可以在 `dsh-pty-local` 中基于普通 `spawn()` 句柄重建这些操作,最终不是泄漏提供方内部细节,就是削弱其生命周期契约。
@@ -16,20 +16,16 @@ Status: implemented
文件系统接口负责其他能力需要的路径事实,同时不公开其不透明目标身份:规范化进程路径、规范化 `file:` URI 和包含关系。现有完整文本与流式文本操作仍归文件系统负责;协议消费方在消费流时执行各自的保留上限。
进程管理接口负责进程运行坐标与原语:规范化 cwd、可执行文件查找以原始或收集模式 spawn 普通进程,以及 `spawnTerminal()`。终端操作是一项深层原语,其句柄负责文本 I/O、前台进程组、信号发送以及一项须等待的 TERM→KILL 操作该操作会结算所有在途句柄调用并使提供方仍可观察到的每个会话成员完全停稳。其信号只取消分配句柄一经发布便负责自身生命周期。提示符检测、空闲推断、scrollback、沙箱策略和所有者生命周期仍由 PTY 消费方负责。
进程管理接口负责可执行文件查找与进程原语:以原始或收集模式 spawn 普通进程,以及 `spawnTerminal()`。终端操作是一项深层原语,其句柄负责文本 I/O、前台进程组、信号发送以及一项须等待的 TERM→KILL 操作该操作会结算所有在途句柄调用并使提供方仍可观察到的每个会话成员完全停稳。其信号只取消分配句柄一经发布便负责自身生命周期。提示符检测、空闲推断、scrollback、沙箱策略和所有者生命周期仍由 PTY 消费方负责。
通用消费方使用该执行世界:
- `dsh-bash-local` 继续把 Bash 语义映射到普通的 `ctx.subprocess.spawn()`
- `dsh-lsp-local` 通过 `ctx.fs` 读取源文件并验证包含关系,通过 `ctx.subprocess` 解析和启动语言服务器,并让由提供方负责的文件 URI 贯穿初始化与结果渲染。一个提供方生命周期信号会在资源释放期间中止文件系统与协议操作,包括取得队列所有权之前的工作区查找;其 JSON-RPC、池化、同步和规范化保持不变。
- `dsh-pty-local` 把持久 shell 语义映射到 `ctx.subprocess.spawnTerminal()`。本地 `node-pty` 与进程检查实现移入 `dsh-subprocess-local`;其他进程管理提供方则提供相同原语。`danger-full-access` 不需要 `ctx.sandbox`;受限模式要求同一执行世界中存在沙箱提供方,未挂载时会在 spawn 前失败。提供方开始写入时,系统会丢弃异步写入前检查期间收集的提示符与静默证据。取消会在在途写入结算期间保留发送预留,随后向前台进程组发送信号,因此延迟字节和该信号都无法落到后续发送;在途就绪检查无法释放该预留,写入被拒绝时也不会发送信号。绝对截止时间会在整个取消期间保持启用。信号发送失败会成为终结性传输失败。陈旧检查完成后,会针对当前发送恢复轮询。启动取消会立即开始终端回滚,而不等待停滞的就绪检查或信号发送调用。关闭操作会拒绝新的公开信号,并把提供方可观察会话成员的完全停稳委托给句柄上须等待的终止操作。
- `dsh-code-runtime-subprocess` 将一个内置的无依赖 eval runner 直接传给 `ctx.subprocess`,从而在本地或远程进程执行环境中保留代码运行时的绑定与输出契约,且不依赖文件系统或提供方专用包。它通过非插件子路径 `dsh-code-runtime-worker/runtime-host` 共享宿主侧 worker 机制,而不是复制这些机制。受堆上限约束的 worker 会在传输前拒绝过大的绑定帧;每个外层转发环节都会在转发前执行相同的上限检查;原始子进程管道承载以换行符分隔的 UTF-8 JSON无需冗余的 base64 表示launcher 会在回收 controller 前发布已接纳的终态帧,使继承 controller 管道的后代进程无法阻止完成;宿主仍会等待进程组完全停稳。
`dsh-code-runtime-worker` 仍是独立实现。它是较小的进程内后端,可用于无法假定已安装 Node 可执行文件的单文件分发。远程文件系统/进程组合选择 `dsh-code-runtime-subprocess`;它们不需要提供方专用的代码运行时包。
## 考虑过的替代方案
**为每个远程提供方分别保留 PTYLSP 和代码运行时包。** 不予采纳,因为这会在现有 seam 之上重复实现提供方机制。删除检验揭示了这一问题:删除这些适配器不应使领域行为散落到远程提供方中;通用消费方本已负责这些行为。
**为每个远程提供方分别保留 PTYLSP 包。** 不予采纳,因为这会在现有 seam 之上重复实现提供方机制。删除检验揭示了这一问题:删除这些适配器不应使领域行为散落到远程提供方中;通用消费方本已负责这些行为。
**把终端建模为普通的管道子进程。** 不予采纳,因为管道无法分配控制终端、确定当前前台进程组或证明完整终端会话已清理。一项终端原语比公开特定于执行基底的逃生口更小,也更能如实表达契约。
@@ -39,13 +35,11 @@ Status: implemented
**在文件系统 seam 中新增稳定的有界读取原语。** 不予采纳,因为只有 LSP 需要完整文档字节上限,而它可以在消费现有文本流时执行该上限。第二项原语会迫使每个提供方实现稳定句柄和不跟随符号链接的机制,远程提供方甚至需要辅助协议,却没有已观察到的并发替换缺陷。
**删除 worker 线程代码运行时。** 不予采纳,因为可移植性不会消除其当前部署需求。进程管理后端需要 Node 可执行文件,而 worker 后端不需要,并且仍是受支持的单进程路径。
**在远程环境中运行整个 harness。** 不予采纳,因为这是另一种部署模型。让执行能力可移植,并不意味着移动模型调用、会话状态、插件状态或 agent loop智能体循环
## 后果
远程执行提供方只需实现共享沙箱所有者以及文件系统与进程管理适配器。Bash、PTYLSP 和基于进程管理的代码运行时组合在这些适配器之上,因此对这些能力的修复仍与提供方无关。
远程执行提供方只需实现共享沙箱所有者以及文件系统与进程管理适配器。Bash、PTYLSP 组合在这些适配器之上,因此对这些能力的修复仍与提供方无关。
基础接口更宽,一对文件系统/进程管理提供方必须在同一个执行世界上保持一致。新增操作仅限当前通用消费方所需的事实与生命周期机制;模型 schema、协议分帧、就绪策略和呈现不会渗入提供方。

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 .agents/notes/implemented/feature/2026-06-15-code-mode.md
2026-06-15-code-mode.md: a075ea66c150afb1b48f98c600b72eec57869962
2026-06-15-code-mode.zh.md: c269c0a8709087e7b1e27051cbda3924f8dd9109
2026-06-15-code-mode.md: b6a24ecd9700e32912b8112b59cbd8b6ab131eb5
2026-06-15-code-mode.zh.md: 4d0a4cf8fa31cf9d9954e5bd95f823dfc0668444

View File

@@ -20,7 +20,7 @@ Three decisions, each elaborated in its own section below:
1. **Code Mode is a first-class presentation mode of `ToolRegistry`** (`dsh-tools`), selected by a validated `mode` config: `'native'` (the default, contributing the visible capability schemas), `'code'` (the registry contributes only its reserved `run_code` transport plus a generated SDK `.d.ts` in the system prompt), or `'both'` (native schemas and the transport + SDK). The registry shapes its canonical contribution at the source; the cooperative prompt-assembly result remains authoritative, and the logged request header records exactly that returned presentation.
2. **Code execution is a capability seam**`packages/code-runtime/` contains the interface package `@deepseek-ai/dsh-code-runtime`, which owns `ctx.codeRuntime` ([capability seams](../architecture/2026-06-13-capability-seams.md); consumer = `dsh-tools`, with core-consumes-a-seam precedent in `agent-loop``dsh-llm`). The runtime knows nothing about tools: it is handed a program and named async bindings, runs the program, and reports `{ value, logs, error? }`. Language and substrate are backend properties, so a future Python or container backend is another implementation package, not a redesign.
3. **Two implementations preserve one fresh-worker contract**: `@deepseek-ai/dsh-code-runtime-worker` runs the worker in the harness process, while `@deepseek-ai/dsh-code-runtime-subprocess` passes a bundled eval runner to `ctx.subprocess` for another execution world. Both execute host-stripped TypeScript in a fresh Node worker with an empty environment, bridged bindings, configurable heap/output/time caps, and hard termination. Their trust posture is bash-equivalent by design; stronger isolation comes from the mounted execution world.
3. **The shipped implementation is `@deepseek-ai/dsh-code-runtime-worker`**: one fresh Node worker thread per run, executing the model's TypeScript after type-strip, with bindings bridged over the message port, an empty environment, configurable heap/output/time caps, and hard termination. Its trust posture is bash-equivalent by design — no unsafe-acknowledgement flags — because the harness already ships `dsh-bash-local`, which executes arbitrary model-written shell commands with strictly *more* ambient authority.
This note owns Code Mode's presentation, composition, isolation, and settlement foundation. The later [typed tool-return Agent Note](2026-07-20-code-mode-typed-tool-returns.md) owns the generated output map, canonical binding values, `ToolCallError`, and the lossless outer-output boundary.
@@ -64,7 +64,7 @@ Each sub-dispatch appends a log-only `tool/code-dispatch-start` event at pool en
- `CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } }` — the runtime exposes each namespace as a global object of async functions inside the program; the optional descriptor asks the runtime to inject a real program-visible rejection class without teaching the seam consumer-specific names. `CodeJsonValue` is this dependency-light seam's structural lossless-JSON type, so binding arguments and resolutions cross the implementation's serialization boundary whole.
- `CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure }` — program execution outcomes resolve as the `error` field. `run()` may reject only for caller/seam misuse (for example a duplicate binding namespace); consumers still contain a non-conforming backend rejection at their own error boundary.
- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string }` — orthogonal outcomes reported independently per [defensive patterns](../../../../docs/defensive-patterns.md); a timed-out run is not an exception, an abort is not a timeout, a lossy completion is not an overflow, and a substrate exit is none of them.
- Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for both shipped backends; a Python backend would pair with its own SDK generator) and `isolation` (`'worker-thread'` for both shipped backends; the subprocess provider may add a container boundary). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all).
- Two readonly backend descriptors, informational not gating: `language` (what the program must be written in — `'typescript'` for the shipped backend; a Python backend would say so, and pair with its own SDK generator on the presentation side) and `isolation` (`'worker-thread'` for the shipped backend; `'process'`, `'container'`, … for future ones). `dsh-tools` requires `language === 'typescript'` in the MVP — its codegen emits TS — and fails the assembly loudly otherwise, the same misconfiguration idiom as `toolOrder` violations (as when `mode` is non-native with no `ctx.codeRuntime` loaded at all).
Requests contain every runtime input; implementations own validated timeout and cap defaults. The registry looks up the optional runtime only when Code Mode is assembled, so native mode does not depend on one. Missing or language-incompatible runtimes fail loudly. Alternate substrates or languages can replace the implementation behind the same seam, paired with the appropriate SDK generator.
@@ -79,11 +79,9 @@ Requests contain every runtime input; implementations own validated timeout and
5. **Enforce independent budgets.** `computeMs` meters worker busy time, allowing slow awaited tools without excusing a hot loop. `maxWallMs` bounds total elapsed time, including unresolved waits. `maxOutputBytes` bounds only the combined serialized outer logs, completion, or diagnostic; intermediate binding values have no byte cap. Expiry, cancellation, and completion terminate the worker, and heap exits or outer overflow are explicit failures.
6. **Dispose to quiescence**: the service's own disposal terminates in-flight workers and *awaits* their exits before resolving, per [defensive patterns](../../../../docs/defensive-patterns.md).
`@deepseek-ai/dsh-code-runtime-subprocess` preserves those program, binding, output, and worker-budget semantics across another process world. It passes a dependency-free eval runner directly through the subprocess provider and carries binding traffic over bounded newline-delimited UTF-8 JSON frames on raw pipes. The heap-bounded worker rejects expanded completion wires before MessagePort transfer; terminal settlement asks the launcher to reap its controller and keeps process-group escalation armed until `waitForExit()` confirms whole-tree quiescence. The [portable execution-world decision](../architecture/2026-07-28-portable-execution-world-consumers.md) owns why this generic backend replaces provider-specific Code Runtime packages; `dsh-code-runtime-worker` remains the single-process and single-file-distribution path.
### Trust posture
The runtimes provide containment, not an independent security boundary: model code can reach Node APIs and has the authority of its mounted execution world. Code Mode uses the same `tools/pre-execute` policy gate as bash and adds an empty worker environment, heap limits, a separate isolate, and hard termination of the program itself. Deployments that need a hard multi-tenant boundary mount container-class filesystem/subprocess providers for both code and bash.
The worker runtime provides containment, not a security boundary: model code can reach Node APIs and has authority comparable to the bash tool. `worker.terminate()` stops the thread but not OS processes it spawned. Code Mode uses the same `tools/pre-execute` policy gate as bash and adds an empty environment, heap limits, a separate isolate, and hard termination of the program itself. Deployments that need a hard multi-tenant boundary need a container-class backend for both code and bash; the runtime's isolation descriptor lets them distinguish that backend.
### What the model sees
@@ -95,7 +93,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
## Testing
- **Runtime implementations:** Real-worker suites cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger and per-hop frame boundaries, compute and wall budgets, hostile binding traffic, empty environment, descendant lifetime cleanup, and disposal to quiescence. Built-package tests run both the direct worker entry and the subprocess composition under plain Node; the latter also has a Loader-driven `cordis.yml` test.
- **Worker runtime:** Real-worker tests cover typed binding values and failures, every lossless JSON completion root, invalid and over-limit output, exact combined ledger boundaries, compute and wall budgets, hostile binding traffic, empty environment, and disposal to quiescence. A built-package test runs the worker entry under plain Node.
- **Registry integration:** Tests cover code generation, all presentation modes, reserved-name and restriction rules, scoped visibility, authoritative assembly rewrites, `toolOrder`, runtime compatibility failures, full-pipeline sub-dispatch, parent-token correlation, serialization, cancellation and queue drain, JSON normalization, error propagation, log events, ordered context deferral across successful and failed programs, outer-block suppression, and HMR cleanup.
- **With-key e2e:** A real model composes two bash calls in one program; another discovers nested workspace instructions through a Code Mode fs dispatch. The tests verify collapsed request headers, correlated dispatch events, resulting files, deferred context, and model behavior.
- **Snapshot:** The `code-mode-turn`, `both-mode-turn`, and `code-mode-workspace-context` fixtures pin SDK text, header tool lists, dispatch events, deferred context, and result cards.
@@ -120,7 +118,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
## Risks
**A worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool and gating uses the same seams. The subprocess implementation can run inside a container-class execution world, but its worker descriptor does not itself claim that boundary.
**The worker is not a hard security boundary.** Deliberate and documented (§Trust posture): posture equals the existing bash tool, containment exceeds it, gating uses the same seams. Deployments needing more need a future `isolation: 'container'` backend — tracked as the seam's designed extension, not a TODO on this design.
**`stripTypeScriptTypes` is marked experimental.** It is the same engine (amaro/swc) behind Node's own native `.ts` execution, exposed as an API across this repo's whole engines range. Mitigations: the runtime's unit suite pins the behaviors relied on (position preservation, erasable-only rejection message shape loosely), the call sits behind one private function, and `amaro`/`sucrase` are drop-in replacements if the API shifts. The erasable-only subset is a model-facing contract line, and the error path is a working feedback loop, not a dead end.
@@ -128,7 +126,7 @@ Deployments switching to `'code'` must update any native-only `toolOrder`. Assem
**Registry scope growth.** `dsh-tools` absorbs codegen, a tool, a bridge, and an event. Contained by module boundaries inside the package (`ts-types.ts`, `code-mode.ts` beside `schema.ts`/`json-schema.ts`/`presentation.ts`) and by the seam: everything substrate-shaped lives behind `ctx.codeRuntime`.
**Large lossless JSON values can exhaust memory.** Tool bindings snapshot lossless JSON before dispatch and return canonical JSON resolutions whole. The direct worker runtime applies no per-binding byte cap; the subprocess runtime bounds each transport frame with `maxFrameBytes`, but repeated or concurrent calls can still consume process or worker memory. The combined outer-output ledger for logs, the completion value, and a failure diagnostic is separately bounded by `maxOutputBytes`.
**Large lossless JSON values can exhaust memory.** Tool bindings snapshot lossless JSON before dispatch and return canonical JSON resolutions whole. The runtime validates both sides of the worker port and applies no per-binding byte cap; structured-clone cost and process or worker memory are the practical bounds. The combined outer-output ledger for logs, the completion value, and a failure diagnostic is the only byte-capped boundary.
**Serialized-only sub-dispatch.** `Promise.all` gains no wall-clock parallelism yet, only fewer round-trips; models may over-expect. The instructions state it; lifting it is tied to the same concurrency-safety metadata the native parallel-dispatch TODO needs.

View File

@@ -20,7 +20,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
1. **Code Mode 是 `ToolRegistry``dsh-tools`)的一等呈现模式**,通过经校验的 `mode` 配置选择:`'native'`(默认,贡献可见能力 schema`'code'`(注册表仅贡献其保留的 `run_code` 传输通道加一份生成的 SDK `.d.ts` 到系统提示词中)或 `'both'`(原生 schema 加传输通道 + SDK。注册表在源头塑造其权威贡献协作式提示词组装的结果仍具权威性记录在日志中的请求头精确反映该返回的呈现。
2. **代码执行是一个能力 seam**——`packages/code-runtime/` 包含接口包 `@deepseek-ai/dsh-code-runtime`,拥有 `ctx.codeRuntime`[能力 seam](../architecture/2026-06-13-capability-seams.md);消费方 = `dsh-tools`core 消费 seam 的先例见 `agent-loop``dsh-llm`)。运行时对工具一无所知:它接收一段程序和命名的异步绑定,执行程序,报告 `{ value, logs, error? }`。语言和基底是后端属性,因此未来的 Python 或容器后端只是另一个实现包,而非重新设计。
3. **两种实现保持同一份全新 worker 契约**`@deepseek-ai/dsh-code-runtime-worker` 在 harness 进程内运行 worker`@deepseek-ai/dsh-code-runtime-subprocess` 则将一个内置的 eval runner 传给 `ctx.subprocess`,用于另一执行环境。二者都在具有空环境的全新 Node worker 内执行由宿主剥离类型的 TypeScript,并提供桥接绑定、可配置的堆/输出时间上限硬终止。其信任姿态在设计上等同于 bash;更强的隔离来自挂载的执行环境
3. **交付的实现是 `@deepseek-ai/dsh-code-runtime-worker`**:每次运行 spawn 一个全新 Node worker 线程,对模型的 TypeScript 进行 type-strip 后执行,绑定通过消息端口桥接,环境为空,堆/输出/时间上限可配置,并支持硬终止。其信任姿态在设计上等同于 bash——无需 unsafe-acknowledgement flag——因为 harness 已经交付了 `dsh-bash-local`,后者以严格*更高*的环境权限执行模型编写的任意 shell 命令
本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-code-mode-typed-tool-returns.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。
@@ -64,7 +64,7 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
- `CodeBindingNamespace = { global: string; functions: Record<string, (args: unknown) => Promise<CodeJsonValue>>; errorClass?: { name: string; memberNameProperty: string } }`——运行时将每个命名空间作为程序内部的全局异步函数对象暴露;可选描述符要求运行时注入真正的、程序可见的 reject 类,而无需让 seam 获知消费方专用名称。`CodeJsonValue` 是这个低依赖 seam 的结构化无损 JSON 类型,因此绑定参数与解析值可以完整跨越实现的序列化边界。
- `CodeRunResult = { value?: CodeJsonValue; logs: string[]; error?: CodeRunFailure }`——程序执行失败时,执行 promise 仍会 fulfill并通过 `error` 字段返回失败结果。只有调用方/seam 误用(例如重复的绑定命名空间)时,`run()` 才会 reject消费方仍在自己的错误边界处理不合规后端的拒绝。
- `CodeRunFailure = { kind: 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit'; message: string }`——按[防御性模式](../../../../docs/defensive-patterns.md)独立报告的正交结果;超时的 run 不是异常abort 不是超时,有损完成值不是溢出,基底退出也与上述情况相互独立。
- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——两种已交付后端`'typescript'`Python 后端会配对自己的 SDK 生成器)和 `isolation`两种已交付后端`'worker-thread'`子进程提供方可以增加容器边界)。`dsh-tools` 在 MVP 中要求 `language === 'typescript'`——其代码生成输出 TS——否则组装会大声失败`toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。
- 两个只读的后端描述符,仅供信息参考而非门禁判定:`language`(程序必须使用的语言——交付后端为 `'typescript'`Python 后端会声明自己,并在呈现侧配对自己的 SDK 生成器)和 `isolation`(交付后端为 `'worker-thread'`未来可为 `'process'``'container'`)。`dsh-tools` 在 MVP 中要求 `language === 'typescript'`——其代码生成输出 TS——否则组装会大声失败`toolOrder` 违规时的配置错误惯用法相同(如 `mode` 为非 native 但根本没有加载 `ctx.codeRuntime`)。
请求包含所有运行时输入;实现方拥有经校验的超时和上限默认值。注册表仅在组装 Code Mode 时查找可选的运行时,因此 native 模式不依赖它。缺失或语言不兼容的运行时会大声失败。替代基底或语言可以在同一 seam 背后替换实现,配对相应的 SDK 生成器。
@@ -79,11 +79,9 @@ Cloudflare 的 [Code Mode](https://blog.cloudflare.com/code-mode/) 提出了一
5. **强制独立预算。** `computeMs` 计量 worker 忙碌时间,允许慢速的 awaited 工具而不放过热循环。`maxWallMs` 约束总经过时间,包括未解析的等待。`maxOutputBytes` 只约束序列化后的外层日志、完成值或诊断的组合;中间绑定值没有字节数上限。到期、取消和完成都终止 worker堆退出或外层溢出会作为显式失败报告。
6. **dispose 至完全停稳**:服务自身的 dispose资源释放终止进行中的 worker 并*等待*其退出后再 resolve遵循[防御性模式](../../../../docs/defensive-patterns.md)。
`@deepseek-ai/dsh-code-runtime-subprocess` 在另一进程执行环境中保持相同的程序、绑定、输出与 worker 预算语义。它通过子进程提供方直接传递一个无依赖的 eval runner并在原始管道上使用有界的、以换行符分隔的 UTF-8 JSON 帧承载绑定通信。受堆上限约束的 worker 会在通过 MessagePort 传输前拒绝展开后的完成值 wire终态结算会请求 launcher 回收其 controller并让进程组升级终止机制保持待命直至 `waitForExit()` 确认整棵进程树完全停稳。[可移植执行环境决策](../architecture/2026-07-28-portable-execution-world-consumers.md)说明为何这个通用后端会取代提供方专用的 Code Runtime 包;`dsh-code-runtime-worker` 仍用于单进程和单文件发行版。
### 信任姿态
这些运行时提供的是隔离,而非独立安全边界:模型代码可以访问 Node API并拥有挂载的执行环境所授予的权限。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空 worker 环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署应当为代码和 bash 都挂载容器级文件系统/子进程提供方
worker 运行时提供的是隔离,而非安全边界:模型代码可以访问 Node API权限与 bash 工具相当。`worker.terminate()` 停止线程但不停止它 spawn 的 OS 进程。Code Mode 使用与 bash 相同的 `tools/pre-execute` 策略门禁,并额外提供空环境、堆限制、独立 isolate 和对程序本身的硬终止。需要硬多租户边界的部署需要为代码和 bash 都使用容器级后端;运行时的 isolation 描述符让它们能区分该后端
### 模型看到的内容
@@ -95,7 +93,7 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw
## 测试
- **运行时实现** 真实 worker 测试套件覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界与逐跳帧边界、compute 和 wall 预算、恶意绑定流量、空环境、后代进程生命周期清理以及 dispose 至完全停稳。构建后包测试在纯 Node 下分别运行直接 worker 入口与子进程组合;后者另有一个由 Loader 驱动的 `cordis.yml` 测试
- **Worker 运行时:** 真实 worker 测试覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界、compute 和 wall 预算、恶意绑定流量、空环境以及 dispose 至完全停稳。一个构建后包测试在纯 Node 下运行 worker 入口。
- **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR热模块替换清理。
- **带密钥 e2e** 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 Code Mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。
- **快照:** `code-mode-turn``both-mode-turn``code-mode-workspace-context` fixture测试前置数据固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。
@@ -120,7 +118,7 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw
## 风险
**worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,门禁使用相同的 seam。子进程实现可以在容器级执行环境中运行,但其 worker 描述符本身不声称具备该边界
**Worker 不是硬安全边界。** 有意为之且已文档化(§信任姿态):姿态等同于既有的 bash 工具,隔离程度超过它,门禁使用相同的 seam。需要更强隔离的部署需要未来的 `isolation: 'container'` 后端——作为 seam 设计的扩展点跟踪,而非本设计的 TODO
**`stripTypeScriptTypes` 标记为 experimental。** 它与 Node 自身原生 `.ts` 执行背后的引擎amaro/swc相同在本仓库的整个引擎范围内作为 API 暴露。缓解措施:运行时的单元测试套件固定了所依赖的行为(位置保持、可擦除限制的拒绝消息形状宽松匹配),调用位于一个私有函数之后,且 `amaro`/`sucrase` 是 API 变化时的直接替代品。仅可擦除子集是面向模型的契约线,错误路径是一个可工作的反馈循环,而非死胡同。
@@ -128,7 +126,7 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw
**注册表 scope 增长。** `dsh-tools` 吸收了代码生成、一个工具、一个桥和一个事件。通过包内的模块边界(`ts-types.ts``code-mode.ts``schema.ts`/`json-schema.ts`/`presentation.ts` 并列)和 seam 约束:所有基底相关的内容都在 `ctx.codeRuntime` 之后。
**大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 解析值。直接 worker 运行时不对单次绑定设置字节数上限;子进程运行时使用 `maxFrameBytes` 限制每个传输帧,但重复或并发调用仍可能消耗进程或 worker 内存包含日志、完成值和失败诊断的组合外层输出账本另由 `maxOutputBytes` 限制
**大型无损 JSON 值可能耗尽内存。** 工具绑定会在分发前对无损 JSON 创建快照,并完整返回规范 JSON 解析值。运行时会校验 worker 端口两侧,但不对单次绑定设置字节数上限;结构化克隆成本以及进程或 worker 内存构成实际边界。只有包含日志、完成值和失败诊断的组合外层输出账本受字节数上限约束
**仅序列化的子分发。** `Promise.all` 尚未获得挂钟并行性,仅减少往返次数;模型可能过度期望。说明中已声明;解除此限制与原生并行分发 TODO 所需的并发安全元数据绑定。