From 22fc228b9757626fbc0c3c9ab920ec9472b258ea Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 30 Jul 2026 04:23:20 +0800 Subject: [PATCH] refactor(runtime): collapse unused setup layers --- ...rtable-execution-world-consumers.i18n.yaml | 4 ++-- ...7-28-portable-execution-world-consumers.md | 4 ++-- ...8-portable-execution-world-consumers.zh.md | 4 ++-- .../feature/2026-06-15-code-mode.i18n.yaml | 4 ++-- .../feature/2026-06-15-code-mode.md | 6 ++--- .../feature/2026-06-15-code-mode.zh.md | 6 ++--- docs/config-catalog.md | 4 ++-- .../core-data-structures/subprocess.i18n.yaml | 4 ++-- docs/core-data-structures/subprocess.md | 2 +- docs/core-data-structures/subprocess.zh.md | 2 +- packages/README.i18n.yaml | 4 ++-- packages/README.md | 2 +- packages/README.zh.md | 2 +- packages/bash/pwsh-local/tsconfig.json | 10 ++++---- packages/code-runtime/code-runtime/README.md | 4 ++-- packages/pty/pty-local/tests/index.spec.ts | 1 - .../subprocess-local/README.i18n.yaml | 4 ++-- .../subprocess/subprocess-local/README.md | 4 ++-- .../subprocess/subprocess-local/README.zh.md | 24 +++++++++---------- .../subprocess/subprocess-local/src/index.ts | 14 ++++------- .../subprocess-local/tests/local.spec.ts | 19 ++------------- .../subprocess/subprocess/README.i18n.yaml | 4 ++-- packages/subprocess/subprocess/README.md | 4 ++-- packages/subprocess/subprocess/README.zh.md | 4 ++-- packages/subprocess/subprocess/src/index.ts | 7 ++---- .../subprocess/tests/service.spec.ts | 1 - packages/ui/app-boot/tsdown.config.ts | 8 +------ packages/util/atomic-write/README.i18n.yaml | 6 ++--- .../src/runner-source.generated.ts | 2 +- packages/util/atomic-write/src/runner.ts | 17 +++++++++---- 30 files changed, 78 insertions(+), 103 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml index 683a095229..3fce989f78 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.i18n.yaml @@ -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: 03b274688e15661845c326afdd105b342dd6a513 -2026-07-28-portable-execution-world-consumers.zh.md: 1e3d8ae8e46d26f4bea47293104c52dbf33098d4 +2026-07-28-portable-execution-world-consumers.md: 74fa2687782a2cf59be8dddf98fa4a08a953d565 +2026-07-28-portable-execution-world-consumers.zh.md: 35d4f3640a44564894fecfaa22c7255d7452dec9 diff --git a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md index 03b274688e..74fa268778 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md +++ b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md @@ -23,7 +23,7 @@ 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` materializes a dependency-free runner through `ctx.fs` and launches it through `ctx.subprocess`, preserving the Code Runtime binding and output contract across local or remote worlds. The fixed runner is adapter-owned infrastructure below `ctx.subprocess.runtimeRoot`, so its write carries an explicit `danger-full-access` policy instead of inheriting the model-facing filesystem mode. It shares host-side worker mechanics through the non-plugin `dsh-code-runtime-worker/runtime-host` subpath instead of copying them. Preparation carries one lifecycle signal through filesystem resolution, materialization, and executable lookup so disposal can abort a stalled provider. 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-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. @@ -39,7 +39,7 @@ 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 and filesystem materialization; the worker backend has neither requirement and remains the supported single-process path. +**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. diff --git a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md index 1e3d8ae8e4..35d4f3640a 100644 --- a/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.zh.md @@ -23,7 +23,7 @@ Status: implemented - `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` 通过 `ctx.fs` 物化无依赖 runner,并通过 `ctx.subprocess` 启动它,从而在本地或远程执行世界中保留代码运行时的绑定与输出契约。固定 runner 是位于 `ctx.subprocess.runtimeRoot` 下的适配器自有基础设施,因此其写入携带显式 `danger-full-access` 策略,而不继承面向模型的文件系统模式。它通过非插件子路径 `dsh-code-runtime-worker/runtime-host` 共享宿主侧 worker 机制,而不是复制这些机制。准备阶段让同一个生命周期信号贯穿文件系统解析、物化和可执行文件查找,使资源释放能够中止停滞的提供方操作。受堆上限约束的 worker 会在传输前拒绝过大的绑定帧;每个外层转发环节都会在转发前执行相同的上限检查;原始子进程管道承载以换行符分隔的 UTF-8 JSON,无需冗余的 base64 表示;launcher 会在回收 controller 前发布已接纳的终态帧,使继承 controller 管道的后代进程无法阻止完成;宿主仍会等待进程组完全停稳。 +- `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`;它们不需要提供方专用的代码运行时包。 @@ -39,7 +39,7 @@ Status: implemented **在文件系统 seam 中新增稳定的有界读取原语。** 不予采纳,因为只有 LSP 需要完整文档字节上限,而它可以在消费现有文本流时执行该上限。第二项原语会迫使每个提供方实现稳定句柄和不跟随符号链接的机制,远程提供方甚至需要辅助协议,却没有已观察到的并发替换缺陷。 -**删除 worker 线程代码运行时。** 不予采纳,因为可移植性不会消除其当前部署需求。进程管理后端需要 Node 可执行文件和文件系统物化,而 worker 后端两者都不需要,并且仍是受支持的单进程路径。 +**删除 worker 线程代码运行时。** 不予采纳,因为可移植性不会消除其当前部署需求。进程管理后端需要 Node 可执行文件,而 worker 后端不需要,并且仍是受支持的单进程路径。 **在远程环境中运行整个 harness。** 不予采纳,因为这是另一种部署模型。让执行能力可移植,并不意味着移动模型调用、会话状态、插件状态或 agent loop(智能体循环)。 diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml index 112e4eed83..d2655b6d02 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.i18n.yaml @@ -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: 1be25f5b145e50d8fe391d22910b71d85181582e -2026-06-15-code-mode.zh.md: a4e8fbbd339899937c4f39091b4befbbb78ec347 +2026-06-15-code-mode.md: a075ea66c150afb1b48f98c600b72eec57869962 +2026-06-15-code-mode.zh.md: c269c0a8709087e7b1e27051cbda3924f8dd9109 diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.md index 1be25f5b14..a075ea66c1 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.md @@ -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` materializes a runner through `ctx.fs` and launches it through `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. **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. 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. @@ -79,7 +79,7 @@ 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 a filesystem/subprocess execution world. It writes a dependency-free runner below `ctx.subprocess.runtimeRoot`, resolves Node through the 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. +`@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 @@ -95,7 +95,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 filesystem/subprocess composition under plain Node; the latter also has a Loader-driven `cordis.yml` test. +- **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. - **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. diff --git a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md index a4e8fbbd33..c269c0a870 100644 --- a/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md +++ b/.agents/notes/implemented/feature/2026-06-15-code-mode.zh.md @@ -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` 则通过 `ctx.fs` 物化 runner,再通过 `ctx.subprocess` 在另一执行环境中启动。二者都在具有空环境的全新 Node worker 内执行由宿主剥离类型的 TypeScript,并提供桥接绑定、可配置的堆/输出/时间上限和硬终止。其信任姿态在设计上等同于 bash;更强的隔离来自挂载的执行环境。 +3. **两种实现保持同一份全新 worker 契约**:`@deepseek-ai/dsh-code-runtime-worker` 在 harness 进程内运行 worker,`@deepseek-ai/dsh-code-runtime-subprocess` 则将一个内置的 eval runner 传给 `ctx.subprocess`,用于另一执行环境。二者都在具有空环境的全新 Node worker 内执行由宿主剥离类型的 TypeScript,并提供桥接绑定、可配置的堆/输出/时间上限和硬终止。其信任姿态在设计上等同于 bash;更强的隔离来自挂载的执行环境。 本说明负责定义 Code Mode 的呈现、组合、隔离与结算基础。后续的[类型化工具返回值 Agent Note](2026-07-20-code-mode-typed-tool-returns.md)负责定义生成的输出映射、规范绑定值、`ToolCallError` 和无损外层输出边界。 @@ -79,7 +79,7 @@ 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 预算语义。它在 `ctx.subprocess.runtimeRoot` 下写入一个无依赖 runner,通过提供方解析 Node,并在原始管道上使用有界的、以换行符分隔的 UTF-8 JSON 帧承载绑定通信。受堆上限约束的 worker 会在通过 MessagePort 传输前拒绝展开后的完成值 wire;终态结算会请求 launcher 回收其 controller,并让进程组升级终止机制保持待命,直至 `waitForExit()` 确认整棵进程树完全停稳。[可移植执行环境决策](../architecture/2026-07-28-portable-execution-world-consumers.md)说明为何这个通用后端会取代提供方专用的 Code Runtime 包;`dsh-code-runtime-worker` 仍用于单进程和单文件发行版。 +`@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` 仍用于单进程和单文件发行版。 ### 信任姿态 @@ -95,7 +95,7 @@ SDK 指示模型编写一个异步的可擦除 TypeScript 函数体,通过 `aw ## 测试 -- **运行时实现:** 真实 worker 测试套件覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界与逐跳帧边界、compute 和 wall 预算、恶意绑定流量、空环境、后代进程生命周期清理以及 dispose 至完全停稳。构建后包测试会在纯 Node 下分别运行直接 worker 入口与文件系统/子进程组合;后者另有一个由 Loader 驱动的 `cordis.yml` 测试。 +- **运行时实现:** 真实 worker 测试套件覆盖类型化的绑定值与失败、每一种无损 JSON 根类型的完成值、无效和超限输出、精确的组合账本边界与逐跳帧边界、compute 和 wall 预算、恶意绑定流量、空环境、后代进程生命周期清理以及 dispose 至完全停稳。构建后包测试会在纯 Node 下分别运行直接 worker 入口与子进程组合;后者另有一个由 Loader 驱动的 `cordis.yml` 测试。 - **注册表集成:** 测试覆盖代码生成、所有呈现模式、保留名称和限制规则、scoped 可见性、权威组装重写、`toolOrder`、运行时兼容性失败、完整流水线子分发、parent-token 关联、序列化、取消和队列排空、JSON 规范化、错误传播、日志事件、成功与失败程序中的有序上下文延后、外层阻止抑制以及 HMR(热模块替换)清理。 - **带密钥 e2e:** 真实模型在一个程序中组合两次 bash 调用;另一个模型通过 Code Mode fs 分发发现嵌套的工作区指令。测试验证折叠的请求头、关联的分发事件、结果文件、延后上下文和模型行为。 - **快照:** `code-mode-turn`、`both-mode-turn` 和 `code-mode-workspace-context` fixture(测试前置数据)固定 SDK 文本、请求头工具列表、分发事件、延后上下文和结果卡片。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 0514cdd969..a875c190fe 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -286,7 +286,7 @@ Source: [`packages/client/hmr/src/index.ts:29`](../packages/client/hmr/src/index ## `@deepseek-ai/dsh-code-runtime-subprocess` -Requires: `fs` · `subprocess` +Requires: `subprocess` ```ts config-catalog /** Runtime configuration; every execution and bridge bound is deployment-tunable. */ @@ -306,7 +306,7 @@ export interface Config { } ``` -Source: [`packages/code-runtime/code-runtime-subprocess/src/index.ts:28`](../packages/code-runtime/code-runtime-subprocess/src/index.ts) +Source: [`packages/code-runtime/code-runtime-subprocess/src/index.ts:27`](../packages/code-runtime/code-runtime-subprocess/src/index.ts) ## `@deepseek-ai/dsh-code-runtime-worker` diff --git a/docs/core-data-structures/subprocess.i18n.yaml b/docs/core-data-structures/subprocess.i18n.yaml index f3b308b0be..f6c61bd691 100644 --- a/docs/core-data-structures/subprocess.i18n.yaml +++ b/docs/core-data-structures/subprocess.i18n.yaml @@ -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 docs/core-data-structures/subprocess.md -subprocess.md: 95e9454a75112cf98e1ed583f970c50fe90aac37 -subprocess.zh.md: 58127d662fa599d19f632449d42b8aa3259886d6 +subprocess.md: f0fe691219df3afba2b836da8f1594fd5fe5a87b +subprocess.zh.md: f59e787820ba2727fdc0c4b62c4aec7b8d2d6b7a diff --git a/docs/core-data-structures/subprocess.md b/docs/core-data-structures/subprocess.md index 935a7d60ac..89f30095bc 100644 --- a/docs/core-data-structures/subprocess.md +++ b/docs/core-data-structures/subprocess.md @@ -8,7 +8,7 @@ Source: [`packages/subprocess/subprocess/src/types.ts`](../../packages/subproces ## Execution-world coordinates -One provider's `cwd`, `runtimeRoot`, executable paths, ordinary processes, and terminal sessions inhabit the same path and process namespace as the mounted filesystem provider. `resolveExecutable(command, env?, signal?)` verifies absolute executable paths or resolves bare names through the provider's scrubbed `PATH` plus deliberate overrides. Consumers use `runtimeRoot` for private materialized helpers and never assume a host path exists in that world. +One provider's `cwd`, executable paths, ordinary processes, and terminal sessions inhabit the same path and process namespace as the mounted filesystem provider. `resolveExecutable(command, env?, signal?)` verifies absolute executable paths or resolves bare names through the provider's scrubbed `PATH` plus deliberate overrides. ## Managed environment namespace and captured output diff --git a/docs/core-data-structures/subprocess.zh.md b/docs/core-data-structures/subprocess.zh.md index 58127d662f..f59e787820 100644 --- a/docs/core-data-structures/subprocess.zh.md +++ b/docs/core-data-structures/subprocess.zh.md @@ -8,7 +8,7 @@ ## 执行世界坐标 -一个提供方的 `cwd`、`runtimeRoot`、可执行文件路径、普通进程与终端会话,和挂载的文件系统提供方处于同一路径与进程命名空间。`resolveExecutable(command, env?, signal?)` 验证绝对可执行文件路径,或通过提供方清理后的 `PATH` 加有意覆盖来解析裸名称。消费方使用 `runtimeRoot` 存放私有物化辅助程序,绝不假设该执行世界中存在某条宿主路径。 +一个提供方的 `cwd`、可执行文件路径、普通进程与终端会话,和挂载的文件系统提供方处于同一路径与进程命名空间。`resolveExecutable(command, env?, signal?)` 验证绝对可执行文件路径,或通过提供方清理后的 `PATH` 加有意覆盖来解析裸名称。 ## 受管环境命名空间与捕获的输出 diff --git a/packages/README.i18n.yaml b/packages/README.i18n.yaml index c5be7e67d6..eaa48fac05 100644 --- a/packages/README.i18n.yaml +++ b/packages/README.i18n.yaml @@ -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 packages/README.md -README.md: 7fcd98bd19ca3291f0472af57841f2f763bb346f -README.zh.md: 7c4aed9b343ec57001883e094ea3dd0d73e2a920 +README.md: b498c146202f1a69d89805fa3e8966c4d0735d6c +README.zh.md: 2eefc73ed0ea22aae5fde93284c25c5d03fe86c4 diff --git a/packages/README.md b/packages/README.md index a711e4d303..7d591605ef 100644 --- a/packages/README.md +++ b/packages/README.md @@ -19,7 +19,7 @@ Packages live at `packages///`; groups are containers, while names r | [`subprocess/`](subprocess/README.md) | Subprocess capability family: spawn seam + local process-tree implementation | Product — stable surface | | [`bash/`](bash/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable surface | | [`pty/`](pty/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable surface | -| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: runtime seam plus local worker and filesystem/subprocess backends | Product — stable surface | +| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: runtime seam plus local worker and subprocess backends | Product — stable surface | | [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable surface | | [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, bash-backed discovery tools | Product — stable surface | | [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool | Product — stable surface | diff --git a/packages/README.zh.md b/packages/README.zh.md index 41380317f8..a9201d08a9 100644 --- a/packages/README.zh.md +++ b/packages/README.zh.md @@ -19,7 +19,7 @@ | [`subprocess/`](subprocess/README.md) | 进程管理能力系列:spawn seam + 本地进程树实现 | 产品:稳定表面 | | [`bash/`](bash/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定表面 | | [`pty/`](pty/README.md) | 持久 PTY 能力系列:按所有者隔离的会话、本地实现和面向模型的工具 | 产品:稳定表面 | -| [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列:运行时 seam、本地 worker 后端及文件系统/进程管理后端 | 产品:稳定表面 | +| [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列:运行时 seam、本地 worker 后端及进程管理后端 | 产品:稳定表面 | | [`sandbox/`](sandbox/README.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 | 产品:稳定表面 | | [`fs/`](fs/README.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、bash 后端发现工具 | 产品:稳定表面 | | [`lsp/`](lsp/README.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 | 产品:稳定表面 | diff --git a/packages/bash/pwsh-local/tsconfig.json b/packages/bash/pwsh-local/tsconfig.json index 53ccc94926..7381575674 100644 --- a/packages/bash/pwsh-local/tsconfig.json +++ b/packages/bash/pwsh-local/tsconfig.json @@ -18,19 +18,19 @@ "path": "../../../vendor/schemastery" }, { - "path": "../../util/brand" + "path": "../code-runtime" }, { - "path": "../../util/timeout" - }, - { - "path": "../../bash/bash" + "path": "../code-runtime-worker" }, { "path": "../../subprocess/subprocess" }, { "path": "../../support/invariants" + }, + { + "path": "../../util/timeout" } ] } diff --git a/packages/code-runtime/code-runtime/README.md b/packages/code-runtime/code-runtime/README.md index 7014bf517a..d58508cd08 100644 --- a/packages/code-runtime/code-runtime/README.md +++ b/packages/code-runtime/code-runtime/README.md @@ -34,5 +34,5 @@ No direct invalidation; the named consumer owns any request-prefix changes. - **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress surface for a live program's output. - **A persistent REPL-style kernel is recorded future work** — the no-state-between-runs contract stands until a persistent-kernel backend brings its own logging story ([Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)). -- **No runtime claims a hard security boundary** — both shipped implementations use fresh worker threads; the filesystem/subprocess backend can place them inside a stronger execution world, but no runtime reports `'container'` today. -- **Intermediate binding values are implementation-bounded** — the direct worker backend has no per-binding byte cap; the filesystem/subprocess backend bounds each bridge frame, but repeated or concurrent binding traffic remains subject to process memory. +- **No runtime claims a hard security boundary** — both shipped implementations use fresh worker threads; the subprocess backend can place them inside a stronger execution world, but no runtime reports `'container'` today. +- **Intermediate binding values are implementation-bounded** — the direct worker backend has no per-binding byte cap; the subprocess backend bounds each bridge frame, but repeated or concurrent binding traffic remains subject to process memory. diff --git a/packages/pty/pty-local/tests/index.spec.ts b/packages/pty/pty-local/tests/index.spec.ts index f59cd3ab5a..15e73ddc1c 100644 --- a/packages/pty/pty-local/tests/index.spec.ts +++ b/packages/pty/pty-local/tests/index.spec.ts @@ -67,7 +67,6 @@ function terminalHandle(): SubprocessTerminalHandle { class StubSubprocessService extends SubprocessService { readonly cwd = '/tmp' - readonly runtimeRoot = '/tmp/dsh-runtime' async resolveExecutable(command: string): Promise { return command } spawn(_spec: SubprocessSpawnSpec): SubprocessHandle { throw new Error('unused') } async spawnTerminal(_spec: SubprocessTerminalSpawnSpec): Promise { diff --git a/packages/subprocess/subprocess-local/README.i18n.yaml b/packages/subprocess/subprocess-local/README.i18n.yaml index d34e7475f9..8b668b43f6 100644 --- a/packages/subprocess/subprocess-local/README.i18n.yaml +++ b/packages/subprocess/subprocess-local/README.i18n.yaml @@ -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 packages/subprocess/subprocess-local/README.md -README.md: 31c5539750c4af2b1c4169ac1dce4c91da587af7 -README.zh.md: 74aed52084c0db3ed9bfe4fcb991f781976fb7b5 +README.md: 4892ad49e571662b833b1dd6d64dc966f343ae3c +README.zh.md: ca0df5ff18e9feb992cc3f6d0db90dc06cb275d8 diff --git a/packages/subprocess/subprocess-local/README.md b/packages/subprocess/subprocess-local/README.md index 31c5539750..13b51ec6d1 100644 --- a/packages/subprocess/subprocess-local/README.md +++ b/packages/subprocess/subprocess-local/README.md @@ -2,7 +2,7 @@ English | [中文](README.zh.md) -Local implementation of the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessService` owns a private runtime directory, resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling seams ([`dsh-bash-local`](../../bash/bash-local/README.md), [`dsh-lsp-local`](../../lsp/lsp-local/README.md), [`dsh-pty-local`](../../pty/pty-local/README.md), and [`dsh-code-runtime-subprocess`](../../code-runtime/code-runtime-subprocess/README.md)). +Local implementation of the [`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam. `LocalSubprocessService` resolves local executables, spawns ordinary detached process trees with explicit stdio, and implements terminal processes through `node-pty` plus platform process inspection. It has no config: every disposition, limit, terminal dimension, grace, and directory arrives from the calling seams ([`dsh-bash-local`](../../bash/bash-local/README.md), [`dsh-lsp-local`](../../lsp/lsp-local/README.md), [`dsh-pty-local`](../../pty/pty-local/README.md), and [`dsh-code-runtime-subprocess`](../../code-runtime/code-runtime-subprocess/README.md)). ## Behavior (and where it came from) @@ -10,7 +10,7 @@ Local implementation of the [`@deepseek-ai/dsh-subprocess`](../subprocess/README - **Per-stream dispositions** — `'pipe'` hands the raw stream to the caller untouched (protocol framing stays consumer-owned); `'inherit'` passes the parent descriptor through; collect mode keeps the in-memory TAIL beyond its cap (errors and results cluster at the end — pi/OpenCode rationale) while the FULL stream is appended to a private temp file when a spill cap is configured — omitting `spill` keeps only the tail, the diagnostic shape. A stream larger than the spill cap discards its now-incomplete spill and returns only the marked truncated tail; spill fds are sealed at settlement, and a failed final close withholds the path rather than advertising an incomplete file. Spill files are `0600` with random names under a lazily-created `0700` per-process directory. - **Credential scrub + explicit merge** — `process.env` minus credential-shaped vars (`*KEY*`/`*SECRET*`/`*TOKEN*`) and all ambient `DSH_*` names; the spec's explicit `env` merges after that scrub with no namespace validation, so a deliberately supplied credential or current `DSH_*` fact wins while stale nested-harness identity cannot leak in ambiently. Supplied stdin is written and closed; otherwise fd 0 is `/dev/null`. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md). - **Offset-based reads** — collect-mode readers return deltas in whole-stream byte coordinates; the service never holds a cursor, so consumer-owned cursors (the bash background read path) and full-stream re-reads coexist, before and after settlement. -- **Execution-world coordinates** — `cwd` is the host process cwd, `runtimeRoot` is an owner-private temporary directory removed on disposal before any process-cleanup failure is reported, and `resolveExecutable` checks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions. +- **Execution-world coordinates** — `cwd` is the host process cwd, and `resolveExecutable` checks absolute files or searches the scrubbed effective PATH with platform-aware executable extensions. - **Terminal-process ownership** — `spawnTerminal` allocates `node-pty`, bridges UTF-8 terminal text, inspects and signals the current foreground process group, and exposes one awaited termination operation that sweeps descendants before and after terminating the top-level shell. Each foreground inspection retains exact identities from the rooted tree; Linux also enumerates the POSIX session after its leader exits. A previously observed macOS descendant and any same-session Linux member therefore remain fenced after reparenting, while pid/start identity prevents cleanup from following PID reuse. The higher PTY backend owns prompt readiness, buffers, and model-facing operations. - **Terminate-and-join disposal** — the service retains live handles only so its own disposal can escalate every running tree and await its exit; settled and spawn-failed handles leave the live set on settlement. diff --git a/packages/subprocess/subprocess-local/README.zh.md b/packages/subprocess/subprocess-local/README.zh.md index 74aed52084..ca0df5ff18 100644 --- a/packages/subprocess/subprocess-local/README.zh.md +++ b/packages/subprocess/subprocess-local/README.zh.md @@ -2,17 +2,17 @@ [English](README.md) | 中文 -[`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地实现。`LocalSubprocessService` 拥有私有运行时目录,解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 与平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方 seam([`dsh-bash-local`](../../bash/bash-local/README.md)、[`dsh-lsp-local`](../../lsp/lsp-local/README.md)、[`dsh-pty-local`](../../pty/pty-local/README.md)和 [`dsh-code-runtime-subprocess`](../../code-runtime/code-runtime-subprocess/README.md))。 +[`@deepseek-ai/dsh-subprocess`](../subprocess/README.md) seam 的本地实现。`LocalSubprocessService` 解析本地可执行文件,以显式 stdio spawn 普通 detached 进程树,并通过 `node-pty` 加平台进程检查实现终端进程。该实现没有任何配置:每项处置方式、限制、终端尺寸、宽限期与目录都来自调用方 seam([`dsh-bash-local`](../../bash/bash-local/README.md)、[`dsh-lsp-local`](../../lsp/lsp-local/README.md)、[`dsh-pty-local`](../../pty/pty-local/README.md) 和 [`dsh-code-runtime-subprocess`](../../code-runtime/code-runtime-subprocess/README.md))。 ## 行为(以及设计来源) -- **带平台正确信号发送的 detached 进程树**:POSIX 子进程使用 `detached` spawn(拥有独立进程组),信号以负 pgid 发送并以直接子进程作为回退;Windows 通过 `taskkill /PID /T /F` 终止进程树(可为测试注入)。`terminate()`(句柄唯一的终止动词)先发送 SIGTERM,经过 spec 的宽限期后再发送 SIGKILL(沿用 OpenCode 的升级策略;管道与子 shell 会随父进程一起结束),进程树消亡后为空操作;`waitForExit()` 轮询整棵进程树的存活状态,使消费方的拆卸能确认真正的完全停稳。组长进程退出后,仍然打开的管道也只获得同样有界的排空宽限期,因此存活的后代进程无法无限期地拖住结果不结算。系统会容忍 ESRCH;脱离该组重新挂载的 daemon 仍可能存活,这与调研工具的局限相同。 -- **按流划分的处置方式**:`'pipe'` 把原始流原样交给调用方(协议分帧仍归消费方所有);`'inherit'` 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 `spill` 则只保留尾部,即诊断尾部的形状。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 `0600`、名称随机,位于按需延迟创建的 `0700` 每进程目录之下。 -- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。 -- **基于偏移量的读取**:收集模式的读取器以全流字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。 -- **执行世界坐标**:`cwd` 是宿主进程 cwd,`runtimeRoot` 是所有者私有的临时目录,会在资源释放时删除,并且删除发生在报告任何进程清理失败之前;`resolveExecutable` 检查绝对文件,或使用平台感知的可执行扩展名在清理后的有效 PATH 中查找。 -- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,并公开一项须等待的终止操作,该操作会在终止顶层 shell 前后清理后代。每次前台检查都会保留有根进程树中的精确身份;Linux 还会在会话 leader 退出后枚举该 POSIX 会话。因此,先前观察到的 macOS 后代以及任何同会话 Linux 成员在重新设定父进程后仍受身份围栏保护,而 pid/启动身份可防止清理因 PID 复用而跟随到其他进程。上层 PTY 后端负责提示符就绪检测、缓冲和面向模型的操作。 -- **先终止再等待退出的 dispose**:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。 +- **以适合平台的方式发送信号的 detached 进程树**:POSIX 子进程使用 `detached` spawn(拥有独立进程组),信号以负 pgid 发送并以直接子进程作为回退;Windows 通过 `taskkill /PID /T /F` 终止进程树(可为测试注入)。`terminate()`(句柄唯一的终止操作)先发送 SIGTERM,经过 spec 的宽限期后再发送 SIGKILL(沿用 OpenCode 的升级策略;流水线与子 shell 会随父进程一起结束),进程树消亡后为空操作;`waitForExit()` 轮询整棵进程树的存活状态,使消费方的拆卸能确认真正的完全停稳。组长进程退出后,仍然打开的管道也只获得同样有界的排空宽限期,因此存活的后代进程无法无限期地拖住结果不结算。系统会容忍 ESRCH;重新指定父进程并脱离该组的 daemon 仍可能存活,这与所调研工具的局限相同。 +- **按流划分的处置方式**:`'pipe'` 把原始流原样交给调用方(协议分帧仍归消费方所有);`'inherit'` 直通父进程的描述符;收集模式(collect)在输出超过上限后于内存中保留尾部(错误与结果通常聚集在末尾,沿用 pi/OpenCode 的理由),并在配置了 spill 上限时把完整流追加到一个私有临时文件;省略 `spill` 则只保留用于诊断的尾部。某条流大于 spill 上限时,会丢弃已不完整的 spill,仅返回带截断标记的尾部;spill 文件描述符在结算时封存,最终关闭失败时则不公布路径,以免声称存在不完整的文件。spill 文件权限为 `0600`、名称随机,位于按需创建、权限为 `0700` 的每进程目录之下。 +- **凭据清除 + 显式合并**:以 `process.env` 为基础,移除形似凭据的变量(`*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`)和所有环境中已有的 `DSH_*` 名称;spec 的显式 `env` 在该清除之后合并且不做命名空间校验,因此有意提供的凭据或当前 `DSH_*` 事实会胜出,而陈旧的嵌套 harness 身份无法从环境中隐式漏入。提供的 stdin 会被写入后关闭;否则 fd 0 指向 `/dev/null`。参见 [stdin/env Agent Note(agent 决策记录)](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)与[受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。 +- **基于偏移量的读取**:收集模式的读取器按完整流的字节坐标返回增量;服务自身从不持有游标,因此消费方自有的游标(bash 的后台读取路径)与完整流重读可以共存,结算前后皆然。 +- **执行世界坐标**:`cwd` 是宿主进程 cwd,`resolveExecutable` 检查绝对文件,或根据平台可执行文件扩展名在清理后的有效 PATH 中搜索。 +- **终端进程所有权**:`spawnTerminal` 分配 `node-pty`,桥接 UTF-8 终端文本,检查当前前台进程组并向其发送信号,还会公开一项须等待的终止操作,在终止顶层 shell 前后清理后代进程。每次前台检查都会保留根进程树中的精确身份;Linux 还会在 POSIX 会话 leader 退出后枚举该会话。因此,之前观察到的 macOS 后代以及同会话 Linux 成员在重新设定父进程后仍受围栏保护,pid/start 身份则防止清理跟随 PID 复用。上层 PTY 后端负责提示符就绪、缓冲区与面向模型的操作。 +- **先终止再等待退出的 dispose(资源释放)**:服务保留存活句柄,只为让自身的 dispose 能对每个仍在运行的进程树执行升级并等待其退出;已结算与 spawn 失败的句柄在结算时即离开存活集合。 ## 模型体验 @@ -20,14 +20,14 @@ #### KV Cache 影响 -不会直接失效;请求前缀变更由具名消费方负责。 +不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。 ## 已知限制与暂缓事项 - **Windows 进程树支持仅为尽力而为,且未经 CI 测试**:终止经由 `taskkill /PID /T /F` 完成,所有结果都被就地吸收,不向外抛出(进程树已不存在、竞态、二进制缺失),存活探测则回退到直接子进程边界;测试套件只通过注入的运行器覆盖这条路由,且 `packages/subprocess/*` 被排除在 Windows 测试矩阵之外。 -- **终端进程检查仅支持 Linux/macOS**:检查器没有受支持的平台实现时,终端原语会失败;Linux 精确探针覆盖 x64 与 arm64,macOS 使用 `ps` 快照。 -- **守护化的终端后代仍可能逃出可观察边界**:在 macOS 上,子进程若在任何前台检查快照产生前重新设定父进程,便无法再从 `node-pty` 根发现;在 Linux 上,调用 `setsid` 的子进程会同时离开进程树与该提供方拥有的终端会话。本地提供方不会增加持续运行的进程表监视器。 -- **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSWORD*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。 +- **终端进程检查仅支持 Linux/macOS**:检查器没有受支持的平台实现时,终端原语会失败;Linux 精确探针覆盖 x64 与 arm64,macOS 则使用 `ps` 快照。 +- **守护化的终端后代仍可能逃出可观察边界**:在 macOS 上,子进程如果在任何前台检查快照之前重新设定父进程,将无法再从 `node-pty` 根进程发现;在 Linux 上,调用 `setsid` 的子进程会同时离开进程树与自有终端会话。本地提供方不会新增持续进程表监视器。 +- **凭据清除依赖名称启发式规则**:只匹配 `*KEY*`/`*PASSWORD*`/`*SECRET*`/`*TOKEN*`;名称不同的 secret(例如 `*PASSPHRASE*`)会继续传递,对误删变量引入白名单属于已记录的后续工作。 - **不会删除已完成的 spill 文件**:有界的完整输出恢复文件(以及每个进程的私有 spill 目录)会在 OS tmpdir 下累积,直到外部机制进行清理;超大的不完整 spill 会被丢弃并立即尝试删除,但清理失败可能留下一个有界文件。 原始进程处理位于 `src/spawn.ts`;`src/index.ts` 负责服务接线。 diff --git a/packages/subprocess/subprocess-local/src/index.ts b/packages/subprocess/subprocess-local/src/index.ts index 46d179a7b2..262a2567b8 100644 --- a/packages/subprocess/subprocess-local/src/index.ts +++ b/packages/subprocess/subprocess-local/src/index.ts @@ -8,10 +8,8 @@ */ import { constants } from 'node:fs' -import { mkdtempSync } from 'node:fs' -import { access, rm, stat } from 'node:fs/promises' -import { tmpdir } from 'node:os' -import { delimiter, extname, isAbsolute, join, resolve } from 'node:path' +import { access, stat } from 'node:fs/promises' +import { delimiter, extname, isAbsolute, resolve } from 'node:path' import { Context } from 'cordis' import * as nodePty from 'node-pty' import type { IPtyForkOptions } from 'node-pty' @@ -36,7 +34,6 @@ import { LocalTerminalHandle } from './terminal.ts' */ export class LocalSubprocessService extends SubprocessService { readonly cwd = process.cwd() - readonly runtimeRoot = mkdtempSync(join(tmpdir(), 'dsh-subprocess-runtime-')) /** Live handles retained only so disposal can terminate and join them. */ private live = new Set() /** Live terminal sessions retained through whole-session quiescence. */ @@ -63,10 +60,7 @@ export class LocalSubprocessService extends SubprocessService { } this.live.clear() this.terminals.clear() - const outcomes = [ - ...await Promise.allSettled(pending), - ...await Promise.allSettled([rm(this.runtimeRoot, { recursive: true, force: true })]), - ] + const outcomes = await Promise.allSettled(pending) const failures = outcomes.flatMap(outcome => outcome.status === 'rejected' ? [outcome.reason as unknown] : []) @@ -126,7 +120,7 @@ export class LocalSubprocessService extends SubprocessService { } // Local PTY allocation is synchronous, but the provider seam permits remote asynchronous allocation. - // eslint-disable-next-line @typescript-eslint/require-await + // oxlint-disable-next-line typescript/require-await -- Preserve promise rejection semantics at the async provider seam. async spawnTerminal(spec: SubprocessTerminalSpawnSpec): Promise { const file = spec.argv[0] if (file === undefined || file.length === 0) { diff --git a/packages/subprocess/subprocess-local/tests/local.spec.ts b/packages/subprocess/subprocess-local/tests/local.spec.ts index ff83141fab..e74927448d 100644 --- a/packages/subprocess/subprocess-local/tests/local.spec.ts +++ b/packages/subprocess/subprocess-local/tests/local.spec.ts @@ -1,6 +1,5 @@ import { PassThrough } from 'node:stream' import { describe, expect, it, vi } from 'vitest' -import { stat } from 'node:fs/promises' import { basename, dirname, relative, resolve } from 'node:path' import { Context } from 'cordis' import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local' @@ -22,16 +21,6 @@ function spec(command: string, overrides: Partial = {}): Su } describe('LocalSubprocessService', () => { - it('publishes execution-world paths and removes its private runtime directory', async () => { - const ctx = new Context() - const fiber = await ctx.plugin(LocalSubprocessService) - const root = ctx.subprocess.runtimeRoot - expect(ctx.subprocess.cwd).toBe(process.cwd()) - expect((await stat(root)).isDirectory()).toBe(true) - await fiber.dispose() - await expect(stat(root)).rejects.toMatchObject({ code: 'ENOENT' }) - }) - it('resolves absolute and PATH executables and honors lookup cancellation', async () => { const ctx = new Context() const fiber = await ctx.plugin(LocalSubprocessService) @@ -113,11 +102,10 @@ describe('LocalSubprocessService', () => { expect(terminals.size).toBe(0) }) - it('waits for every terminal cleanup and clears single-shot teardown ownership', async () => { + it('waits for every terminal cleanup and aggregates teardown failures', async () => { const ctx = new Context() const fiber = await ctx.plugin(LocalSubprocessService) const service = ctx.subprocess - const runtimeRoot = service.runtimeRoot const firstFailure = new Error('first cleanup failure') const secondFailure = new Error('second cleanup failure') const disposalErrors: unknown[] = [] @@ -155,7 +143,6 @@ describe('LocalSubprocessService', () => { finishCleanup() await disposing expect(terminals.size).toBe(0) - await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) expect(disposalErrors).toHaveLength(1) expect(disposalErrors[0]).toMatchObject({ errors: [firstFailure, secondFailure], @@ -163,14 +150,13 @@ describe('LocalSubprocessService', () => { }) }) - it('reports one cleanup failure without wrapping it after removing runtime state', async () => { + it('reports one cleanup failure without wrapping it', async () => { const ctx = new Context() const failure = new Error('single cleanup failure') const disposalErrors: unknown[] = [] ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error const fiber = await ctx.plugin(LocalSubprocessService) const service = ctx.subprocess - const runtimeRoot = service.runtimeRoot const terminal: SubprocessTerminalHandle = { pid: 1, output: new PassThrough(), @@ -185,7 +171,6 @@ describe('LocalSubprocessService', () => { await fiber.dispose() - await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' }) expect(disposalErrors).toEqual([failure]) }) diff --git a/packages/subprocess/subprocess/README.i18n.yaml b/packages/subprocess/subprocess/README.i18n.yaml index 92887cc247..d12516f11f 100644 --- a/packages/subprocess/subprocess/README.i18n.yaml +++ b/packages/subprocess/subprocess/README.i18n.yaml @@ -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 packages/subprocess/subprocess/README.md -README.md: 12a88a86c3cd7bcde2858bdbd874c99533100e09 -README.zh.md: f4c265a0f55c4e35e4557516593dd0da236164c5 +README.md: 08fb03d6f145c1026180d92de2421b647cb3ebbe +README.zh.md: 592fc83223092b3406e9070e11de2b20a63232a6 diff --git a/packages/subprocess/subprocess/README.md b/packages/subprocess/subprocess/README.md index 12a88a86c3..08fb03d6f1 100644 --- a/packages/subprocess/subprocess/README.md +++ b/packages/subprocess/subprocess/README.md @@ -2,12 +2,12 @@ English | [中文](README.zh.md) -The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessService` exposes its canonical `cwd`, private `runtimeRoot`, executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md). +The subprocess seam (`ctx.subprocess`) is the process half of one execution world. The abstract `SubprocessService` exposes its canonical `cwd`, executable lookup, ordinary managed `spawn`, and one terminal-process primitive; its vocabulary covers raw/collected stdio, process and terminal handles, exit facts, tree/session cleanup, and the managed `DSH_*` environment namespace. The local implementation lives in [`dsh-subprocess-local`](../subprocess-local/README.md). ## Contract - `spawn(spec)` returns immediately with a live handle; `done` resolves at process close with exit facts (`SubprocessOutcome` carries no output and no cause classification) and rejects only for spawn-level failures. -- `cwd` and `runtimeRoot` are absolute paths in the provider's execution world. Consumers materialize private helpers below `runtimeRoot`, never in a host-only temp directory. `resolveExecutable(command, env?, signal?)` verifies absolute commands or resolves bare names against that world's scrubbed PATH plus explicit overrides. +- `cwd` and executable paths belong to the provider's execution world. `resolveExecutable(command, env?, signal?)` verifies absolute commands or resolves bare names against that world's scrubbed PATH plus explicit overrides. - The spec is fully explicit — argv, cwd, per-stream stdio dispositions, grace — because deployment-varying defaults belong to the calling seam's config, not to a hidden subprocess-service default (the `dsh-bash` request/spec split is the owning template). `argv` is never shell-interpreted; a consumer that wants a shell passes `['bash', '-c', command]` itself. - Stdio is Node-shaped per stream: `'pipe'` hands the caller the raw stream for its own protocol framing (LSP JSON-RPC, ACP ndjson), `'inherit'` passes the parent descriptor through for diagnostics, and collect mode (`{ maxBytes, spill? }`) buffers a bounded tail with an optional full-stream spill file. Collect readers take whole-stream byte offsets and never consume, so independent readers cannot steal one another's deltas; a read whose offset slid out of the in-memory tail is `lossy` and points at the spill file when one exists. Collected output stays readable after settlement. - Termination is tree-scoped on every platform (POSIX detached groups with direct-child fallback; Windows `taskkill /T`): `terminate()` — the only termination verb — escalates SIGTERM→grace→SIGKILL (idempotent, driven by the spec's abort signal too, a no-op once the tree is gone), and `waitForExit(signal?)` observes whole-tree liveness so a consumer-owned teardown ladder holds each tier on real quiescence — the manager reacts but never classifies why (callers own deadlines, teardown ladders, and cause classification). diff --git a/packages/subprocess/subprocess/README.zh.md b/packages/subprocess/subprocess/README.zh.md index f4c265a0f5..592fc83223 100644 --- a/packages/subprocess/subprocess/README.zh.md +++ b/packages/subprocess/subprocess/README.zh.md @@ -2,12 +2,12 @@ [English](README.md) | 中文 -子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessService` 公开其规范化 `cwd`、私有 `runtimeRoot`、可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。 +子进程 seam(`ctx.subprocess`)是一个执行世界的进程部分。抽象的 `SubprocessService` 公开其规范化 `cwd`、可执行文件查找、普通受管 `spawn` 和一项终端进程原语;其词汇涵盖原始/收集式 stdio、进程与终端句柄、退出事实、进程树/会话清理,以及受管的 `DSH_*` 环境命名空间。本地实现位于 [`dsh-subprocess-local`](../subprocess-local/README.md)。 ## 契约 - `spawn(spec)` 立即返回一个活动句柄;`done` 在进程关闭时以退出事实 resolve(`SubprocessOutcome` 不携带输出,也不携带原因分类),仅在 spawn 层面失败时 reject。 -- `cwd` 和 `runtimeRoot` 是提供方执行世界中的绝对路径。消费方在 `runtimeRoot` 之下物化私有辅助程序,绝不使用仅宿主可见的临时目录。`resolveExecutable(command, env?, signal?)` 验证绝对命令,或根据该执行世界清理后的 PATH 加显式覆盖来解析裸名称。 +- `cwd` 和可执行文件路径属于提供方的执行世界。`resolveExecutable(command, env?, signal?)` 验证绝对命令,或根据该执行世界清理后的 PATH 加显式覆盖来解析裸名称。 - spec 完全显式(argv、cwd、按流划分的 stdio 处置方式(disposition)、宽限期),因为随部署变化的默认值属于调用方 seam 的配置,而不属于某个隐藏的子进程默认值(`dsh-bash` 的 request/spec 拆分是这条规则的所属模板)。`argv` 绝不经过 shell 解释;需要 shell 的消费方自行传入 `['bash', '-c', command]`。 - stdio 按流采用 Node 风格:`'pipe'` 把原始流交给调用方做自己的协议分帧(LSP 的 JSON-RPC、ACP(Agent Client Protocol)的 ndjson),`'inherit'` 直通父进程描述符以承载诊断输出,收集模式(collect)`{ maxBytes, spill? }` 则缓冲一段有界尾部,外加可选的完整流 spill 文件。收集模式的读取器接受全流字节偏移量且从不消费,因此独立的读取器不会抢走彼此的增量;偏移量滑出内存尾部窗口的读取标记为 `lossy`,并在 spill 文件存在时指向它。收集到的输出在结算后仍可读取。 - 终止在每个平台上都以进程树为范围(POSIX 用 detached 进程组并以直接子进程回退;Windows 用 `taskkill /T`):`terminate()`(唯一的终止动词)执行 SIGTERM→宽限期→SIGKILL 升级(幂等,也由 spec 的 abort 信号驱动,进程树消亡后为空操作);`waitForExit(signal?)` 观察整棵进程树的存活状态,使消费方自有的拆卸阶梯能在真正完全停稳后才进入下一层。管理器只响应中止,但绝不判定原因(deadline、拆卸阶梯与原因分类归调用方所有)。 diff --git a/packages/subprocess/subprocess/src/index.ts b/packages/subprocess/subprocess/src/index.ts index e1749203dd..df3186c9b0 100644 --- a/packages/subprocess/subprocess/src/index.ts +++ b/packages/subprocess/subprocess/src/index.ts @@ -78,8 +78,8 @@ declare module 'cordis' { * duplicate-service behavior). * * Implementations must honor these semantics: - * - {@link cwd}, {@link runtimeRoot}, and executable paths belong to one - * execution world shared with the mounted filesystem provider. + * - {@link cwd} and executable paths belong to one execution world shared + * with the mounted filesystem provider. * - {@link spawn} returns immediately with a live handle; `done` resolves at * process close with exit facts and rejects only for spawn-level failures. * - Collect-mode readers are offset-based and non-consuming, so independent @@ -107,9 +107,6 @@ export abstract class SubprocessService extends Service { /** Canonical default cwd in this provider's execution world. */ abstract readonly cwd: string - /** Private directory for runtime artifacts in this provider's execution world. */ - abstract readonly runtimeRoot: string - /** * Resolve one configured executable in this provider's execution world. * Absolute paths are verified; bare names use the provider's scrubbed PATH diff --git a/packages/subprocess/subprocess/tests/service.spec.ts b/packages/subprocess/subprocess/tests/service.spec.ts index 9f321caf54..2084310da9 100644 --- a/packages/subprocess/subprocess/tests/service.spec.ts +++ b/packages/subprocess/subprocess/tests/service.spec.ts @@ -17,7 +17,6 @@ import type { */ class StubSubprocessService extends SubprocessService { readonly cwd = '/stub' - readonly runtimeRoot = '/stub/.runtime' async resolveExecutable(command: string): Promise { return `/bin/${command}` diff --git a/packages/ui/app-boot/tsdown.config.ts b/packages/ui/app-boot/tsdown.config.ts index 88492d7c26..33db6be1b8 100644 --- a/packages/ui/app-boot/tsdown.config.ts +++ b/packages/ui/app-boot/tsdown.config.ts @@ -1,9 +1,6 @@ import { defineConfig } from 'tsdown' -/** - * Embed Include while keeping Loader external so the built include tree and - * app host bind to one Loader peer. - */ +/** Bundle the host plugin and its dependency-free eval runner. */ export default defineConfig({ entry: ['lib/types/index.js', 'lib/types/invariant.js'], outDir: 'lib', @@ -13,7 +10,4 @@ export default defineConfig({ fixedExtension: false, dts: false, clean: false, - deps: { - alwaysBundle: ['@cordisjs/plugin-include'], - }, }) diff --git a/packages/util/atomic-write/README.i18n.yaml b/packages/util/atomic-write/README.i18n.yaml index 33e1f6014b..efcd06f845 100644 --- a/packages/util/atomic-write/README.i18n.yaml +++ b/packages/util/atomic-write/README.i18n.yaml @@ -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 packages/util/atomic-write/README.md -README.md: 2ff4abb6ac10d8b592ccd2056b4f1f92cc8518b0 -README.zh.md: 4284d06422564268bb9e31d1a1ed06ab5271e562 +# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-subprocess/README.md +README.md: 897112a32fa3e776df756d4caaba97b617c6af9e +README.zh.md: d9ab3713640fe955d2b36a8f0674f77686e4791b diff --git a/packages/util/atomic-write/src/runner-source.generated.ts b/packages/util/atomic-write/src/runner-source.generated.ts index 8592fa2399..95da563dbb 100644 --- a/packages/util/atomic-write/src/runner-source.generated.ts +++ b/packages/util/atomic-write/src/runner-source.generated.ts @@ -3,4 +3,4 @@ * Do not edit by hand; run `pnpm run gen-code-runtime-runner`. */ -export const CODE_RUNNER_SOURCE = "import{Buffer as e}from\"node:buffer\";import{fork as t}from\"node:child_process\";import{createInterface as n}from\"node:readline\";import{inspect as r}from\"node:util\";import{fileURLToPath as i}from\"node:url\";import{Worker as a,isMainThread as o,parentPort as s,workerData as c}from\"node:worker_threads\";const l=Reflect.apply,u=Array.isArray,d=Buffer,f=Reflect.get(Buffer,`byteLength`),ee=Object.create,p=Object.defineProperty,te=Object.keys,ne=String,re=Reflect.get(String.prototype,`charCodeAt`),ie=Reflect.get(String.prototype,`codePointAt`),m=Reflect.get(String.prototype,`slice`);function h(e){let t=ee(null);return t.value=e,t}function ae(e,t,n){let r=h(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,p(e,t,r)}function g(e){return l(f,d,[e,`utf8`])}function _(e,t){ae(e,e.length,t)}function oe(e){if(e.length===0)return;let t=e.length-1,n=e[t];return p(e,`length`,h(t)),n}function se(e,t){return l(m,e,[t,t+(l(ie,e,[t])>65535?2:1)])}function ce(e){if(e.length===2)return 4;if(e===`\"`||e===`\\\\`)return 2;let t=l(re,e,[0]);return t>=55296&&t<=57343?6:t<32?t===8||t===9||t===10||t===12||t===13?2:6:g(e)}function v(e,t){if(t<2)return;let n=2;for(let r=0;rt)return;r+=i.length}return n}function y(e,t){let n=0,r=e=>(n+=e,n<=t),i=[{kind:`value`,value:e}];for(let e=oe(i);e!==void 0;e=oe(i)){if(e.kind===`value`){let a=e.value;if(a===null){if(!r(4))return}else if(typeof a==`string`){let e=v(a,t-n);if(e===void 0)return;n+=e}else if(typeof a==`number`){if(!r(g(ne(a))))return}else if(typeof a==`boolean`){if(!r(a?4:5))return}else if(u(a)){if(!r(2))return;a.length>0&&_(i,{kind:`array`,value:a,index:0})}else{if(!r(2))return;let e=te(a);e.length>0&&_(i,{kind:`object`,value:a,keys:e,index:0})}continue}if(e.index>0&&!r(1))return;if(e.kind===`array`){let t=e.value[e.index];if(t===void 0)return;e.index+1t)break;n+=o,r+=a.length,i+=a.length}return r===e.length?e:l(m,e,[0,r])}const ue=Reflect.get(Function.prototype,`toString`),b=Reflect.get(Reflect,`apply`),x=Error,S=Set,C=Array.isArray,de=Array.prototype,w=Number.isFinite,fe=Number.isSafeInteger,pe=Object.create,T=Object.defineProperty,me=Object.getOwnPropertyDescriptor,E=Object.getPrototypeOf,D=Object.hasOwn,O=Object.is,he=Object.keys,k=Object.prototype,ge=Reflect.get(k,`propertyIsEnumerable`),A=Reflect.ownKeys,_e=Reflect.get(Set.prototype,`add`),ve=Reflect.get(Set.prototype,`delete`),ye=Reflect.get(Set.prototype,`has`);function j(e){let t=pe(null);return t.value=e,t}function M(e,t,n){let r=j(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,T(e,t,r)}function N(e,t){M(e,e.length,t)}function P(e){if(e.length===0)return;let t=e.length-1,n=e[t];return T(e,`length`,j(t)),n}function F(e,t){return b(ye,e,[t])}function I(e,t){b(_e,e,[t])}function be(e,t){b(ve,e,[t])}function L(e,t){let n=me(e,`constructor`)?.value;if(typeof n!=`function`)return!1;try{return n.name===t&&n.prototype===e&&b(ue,n,[])===`function ${t}() { [native code] }`}catch{return!1}}function R(e){return E(e)===null&&L(e,`Object`)}function z(e){let t=E(e);if(t===de)return!0;if(!C(t)||!L(t,`Array`))return!1;let n=E(t);return typeof n==`object`&&!!n&&R(n)}function B(e){let t=E(e);return t===null||t===k||typeof t==`object`&&R(t)}function V(e){let t=A(e);for(let n=0;n{e.kind===`root`?n=t:e.kind===`array`?M(e.target,e.index,t):M(e.target,e.key,t)},i=[{kind:`visit`,value:e,destination:{kind:`root`}}];for(let e=P(i);e!==void 0;e=P(i)){if(e.kind===`leave`){be(t,e.source);continue}if(e.kind===`array-item`){if(!D(e.source,e.index))return;N(i,{kind:`visit`,value:e.source[e.index],destination:{kind:`array`,target:e.target,index:e.index}});continue}if(e.kind===`object-property`){N(i,{kind:`visit`,value:e.source[e.key],destination:{kind:`object`,target:e.target,key:e.key}});continue}let n=e.value;if(n===null){r(e.destination,null);continue}if(typeof n==`boolean`||typeof n==`string`){r(e.destination,n);continue}if(typeof n==`number`){if(!w(n)||O(n,-0))return;r(e.destination,n);continue}if(typeof n!=`object`||F(t,n))return;if(C(n)){if(!z(n))return;let a=n.length;if(A(n).length!==a+1)return;let o=[];r(e.destination,o),I(t,n),N(i,{kind:`leave`,source:n});for(let e=a-1;e>=0;e--)N(i,{kind:`array-item`,source:n,index:e,target:o});continue}if(!B(n))return;let a=V(n);if(a===void 0)return;let o={};r(e.destination,o),I(t,n),N(i,{kind:`leave`,source:n});for(let e=a.length-1;e>=0;e--){let t=a[e];if(t===void 0)return;N(i,{kind:`object-property`,source:n,key:t,target:o})}}return n}function U(e){let t=[],n=[e];for(let e=P(n);e!==void 0;e=P(n)){if(e===null||typeof e==`boolean`||typeof e==`number`||typeof e==`string`){N(t,e);continue}if(C(e)){N(t,{kind:`array`,length:e.length});for(let t=e.length-1;t>=0;t--){let r=e[t];if(r===void 0)throw new x(`cannot encode a sparse JSON array`);N(n,r)}continue}let r=he(e);N(t,{kind:`object`,keys:r});for(let t=r.length-1;t>=0;t--){let i=r[t];if(i===void 0)throw new x(`cannot encode a missing JSON object key`);let a=e[i];if(a===void 0)throw new x(`cannot encode an undefined JSON object property`);N(n,a)}}return t}function xe(e){if(!z(e)||A(e).length!==e.length+1)return!1;for(let t=0;t=0?{kind:`array`,length:e}:void 0}if(n.kind===`object`){if(t.length!==2||!W(t,`kind`)||!W(t,`keys`))return;let e=n.keys;if(!C(e)||!xe(e))return;let r=new S,i=[],a=e;for(let e=0;e{let t=n[n.length-1];if(!t)return i?!1:(r=e,i=!0,!0);if(t.index>=(t.kind===`array`?t.length:t.keys.length))return!1;if(t.kind===`array`)N(t.target,e);else{let n=t.keys[t.index];if(n===void 0)return!1;M(t.target,n,e)}return t.index+=1,!0};for(let e=0;ea)return;let e=[];i=e,n.length>0&&(o={kind:`array`,target:e,length:n.length,index:0})}else{if(n.keys.length>a)return;let e={};i=e,n.keys.length>0&&(o={kind:`object`,target:e,keys:n.keys,index:0})}}if(!a(i))return;for(o&&N(n,o);n.length>0;){let e=n[n.length-1];if(e===void 0||e.index<(e.kind===`array`?e.length:e.keys.length))break;P(n)}}return n.length===0?r:void 0}catch{return}}const K=Error,Ce=Object.create,we=Object.defineProperty;function Te(e,t,n){let r=Ce(null);r.enumerable=!0,r.value=n,we(e,t,r)}var Ee=class{bytes=2;entries=0;truncated=!1;sink;onLimit;maxBytes;constructor(e,t,n=()=>{}){this.maxBytes=e,this.sink=t,this.onLimit=n}push(e){if(this.truncated)return;let t=+(this.entries>0),n=this.maxBytes-this.bytes-t,r=v(e,n);if(r===void 0){this.truncated=!0;let r=le(e,n);if(r.length>0){let e=v(r,n);if(e===void 0)throw new K(`worker output ledger produced an oversized log prefix`);this.bytes+=e+t,this.entries+=1,this.sink(r)}this.onLimit();return}this.bytes+=r+t,this.entries+=1,this.sink(e)}remainingOutputBytes(){return this.maxBytes-this.bytes}};const De=[`log`,`info`,`warn`,`error`,`debug`];function Oe(e){let t=e=>e.map(e=>typeof e==`string`?e:r(e,Ae)).join(` `),n=Object.create(null);for(let r of De)n[r]=(...n)=>{e.push(t(n))};return n}function ke(e,t){let n=t.write;return t.write=(t,...n)=>{e.push(typeof t==`string`?t:String(t));let r=[n[0],n[1]].find(e=>typeof e==`function`);return r&&queueMicrotask(()=>{r(null)}),!0},()=>{t.write=n}}const Ae={depth:4,maxArrayLength:100,maxStringLength:1e4};function je(e,t,n=t){if(e===void 0)return{};let r;try{r=H(e)}catch{r=void 0}return r===void 0?Ne(`invalid-output`,`program completion must be lossless JSON`,t,n):y(r,t)===void 0?Me(n):{value:U(r)}}function Me(e){return{error:{kind:`output-limit`,message:`outer output exceeded ${e} bytes`}}}function Ne(e,t,n,r){return v(t,n)===void 0?Me(r):{error:{kind:e,message:t}}}function Pe(e,t,n=t){let r;try{let t=e instanceof K?e.stack??e.message:e;r=typeof t==`string`?t:String(t)}catch{r=`program threw an unrenderable value`}return Ne(`exception`,r,t,n)}function Fe(e){return class extends K{constructor(t,n){super(n),Te(this,`name`,e.name),Te(this,e.memberNameProperty,t)}}}function q(e,t,n){return e?new e(t,n):new K(n)}function Ie(e){let t=new Map;for(let n of e.namespaces)n.errorClass&&t.set(n.global,Fe(n.errorClass));return t}function Le(e,t){e.on(`message`,e=>{let n=t.get(e.id);if(n)if(t.delete(e.id),e.ok){let t=G(e.value);t===void 0?n.reject(new K(`binding resolution must be lossless JSON`)):n.resolve(t)}else n.reject(new K(e.message))})}function Re(e,t,n,r,i=Ie(e),a){return e.namespaces.map(({global:e,names:o})=>{let s=i.get(e),c=Object.create(null);for(let i of o)Object.defineProperty(c,i,{enumerable:!0,value:o=>{let c;try{c=H(o)}catch{c=void 0}if(c===void 0)return Promise.reject(q(s,i,`binding arguments must be lossless JSON`));let l={type:`call`,id:r.value,global:e,name:i,args:U(c)};return a!==void 0&&y(l,a)===void 0?Promise.reject(q(s,i,`binding arguments exceed maxFrameBytes`)):new Promise((e,a)=>{let o=r.value++;n.set(o,{resolve:e,reject:e=>{a(q(s,i,e.message))}});try{t.postMessage(l)}catch(e){n.delete(o),a(q(s,i,`binding arguments must be structured-cloneable: ${e instanceof K?e.message:String(e)}`))}})}});return c})}async function ze(e,t,n,r){let i=new Ee(t.maxOutputBytes,t=>{e.postMessage({type:`log`,text:t})},()=>{e.postMessage({type:`output-limit`})});ke(i,n.stdout),ke(i,n.stderr);let a=new Map;Le(e,a);let o={value:1},s=Ie(t),c=Re(t,e,a,o,s,r),l=[],u=[];for(let e of t.namespaces){if(!e.errorClass)continue;l.push(e.errorClass.name);let t=s.get(e.global);if(!t)throw new K(`missing binding error class for ${e.global}`);u.push(t)}let d=Oe(i),f;try{let e=(async()=>{}).constructor;f={type:`done`,...je(await new e(...t.namespaces.map(e=>e.global),...l,`console`,`'use strict';\\n${t.code}`)(...c,...u,d),i.remainingOutputBytes(),t.maxOutputBytes)}}catch(e){f={type:`done`,...Pe(e,i.remainingOutputBytes(),t.maxOutputBytes)}}e.postMessage(r!==void 0&&y(f,r)===void 0?{type:`output-limit`}:f)}function J(e){return e.readableEnded||e.destroyed?Promise.resolve():new Promise(t=>{let n=()=>{e.off(`end`,n),e.off(`close`,n),e.off(`error`,n),t()};e.once(`end`,n),e.once(`close`,n),e.once(`error`,n),(e.readableEnded||e.destroyed)&&n()})}const Be=new Set([`exception`,`timeout`,`abort`,`worker-exit`,`invalid-output`,`output-limit`]);let Y=0;function X(e){return typeof e==`object`&&e?e:void 0}function Z(t,n){try{let r=JSON.stringify(t);return typeof r==`string`&&e.byteLength(r)<=n?r:void 0}catch{return}}function Ve(e){process.stdout.write(e),process.stdout.write(`\n`)}function He(e){let t=Z(e,Y);return t===void 0?!1:(Ve(t),!0)}function Ue(e){let t=X(e);return t!==void 0&&typeof t.kind==`string`&&Be.has(t.kind)&&typeof t.message==`string`}function We(e){return Ue(e)&&(e.kind===`exception`||e.kind===`invalid-output`||e.kind===`output-limit`)}function Ge(e,t){return e.error===void 0?{type:`done`,...e.value===void 0?{}:{value:Q(e.value)}}:t(e.error)?{type:`done`,error:e.error}:void 0}function Ke(e){let t=X(e);if(!(t===void 0||t.type!==`boot`||typeof t.code!=`string`||!Array.isArray(t.namespaces)||!Number.isSafeInteger(t.maxOutputBytes)||t.maxOutputBytes<4||!Number.isSafeInteger(t.maxFrameBytes)||t.maxFrameBytes{e.once(`exit`,()=>{t()})})}function $(){return{type:`done`,error:{kind:`worker-exit`,message:`code runtime bridge frame exceeded maxFrameBytes`}}}function Ye(){let e=n({input:process.stdin,crlfDelay:1/0}),r,a=0,o=2,s=0,c=!1,l=t=>{if(c)return;let n=Z(t,Y)??Z($(),Y);c=!0,n!==void 0&&Ve(n);let i=r;r=void 0,(i===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=J(i.stdout),t=J(i.stderr),n=Je(i);i.kill(`SIGKILL`),await Promise.all([n,e,t])})).catch(e=>{process.stderr.write(`dsh-code-runtime-subprocess controller cleanup error: ${String(e)}\\n`)}).then(()=>{e.close(),process.stdin.destroy()})},u=e=>{if(c)return;let t=+(s>0),n=v(e,a-o-t);if(n===void 0){l({type:`output-limit`});return}o+=n+t,s+=1,He({type:`log`,text:e})||l($())},d=e=>{a=e.maxOutputBytes,Y=e.maxFrameBytes,r=t(i(import.meta.url),[],{env:{DSH_CODE_RUNTIME_CONTROLLER:`1`},detached:!1,execArgv:[],stdio:[`ignore`,`pipe`,`pipe`,`ipc`]});let n=r;n.stdout.on(`data`,e=>{u(e.toString(`utf8`))}),n.stderr.on(`data`,e=>{u(e.toString(`utf8`))}),n.on(`message`,e=>{let t=X(e);if(t!==void 0){if(t.type===`log`&&typeof t.text==`string`){u(t.text);return}if(!c){if(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`)He({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||l($());else if(t.type===`output-limit`)l({type:`output-limit`});else if(t.type===`done`){let e=Ge(t,Ue);e!==void 0&&l(e)}}}}),n.on(`error`,e=>{l({type:`done`,error:{kind:`worker-exit`,message:`remote controller error: ${e.message}`}})}),n.on(`exit`,e=>{c||l({type:`done`,error:{kind:`worker-exit`,message:`remote controller exited with code ${e} before completing`}})}),n.send(e,e=>{e!==null&&l({type:`done`,error:{kind:`worker-exit`,message:`remote controller boot failed: ${e.message}`}})})};e.on(`line`,e=>{let t;try{t=JSON.parse(e)}catch(e){process.stderr.write(`dsh-code-runtime-subprocess frame error: ${String(e)}\\n`),l({type:`done`,error:{kind:`worker-exit`,message:`remote runner received a malformed frame`}});return}if(r===void 0){let e=Ke(t);if(e===void 0){l({type:`done`,error:{kind:`worker-exit`,message:`remote runner received an invalid boot frame`}});return}d(e);return}let n=qe(t);n!==void 0&&r.send(n,e=>{e!==null&&l({type:`done`,error:{kind:`worker-exit`,message:`remote controller reply failed: ${e.message}`}})})}),e.on(`close`,()=>{r!==void 0&&!c&&l({type:`done`,error:{kind:`abort`,message:`remote runner input closed`}})})}function Xe(){let e,t=!1,n,i=0,o=e=>process.send===void 0||i>0&&Z(e,i)===void 0?!1:(process.send(e),!0),s=r=>{if(t)return;t=!0,clearInterval(n);let a=i>0&&Z(r,i)===void 0?$():r,s=e;e=void 0,(s===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=J(s.stdout),t=J(s.stderr);await Promise.all([s.terminate(),e,t])})).catch(e=>{o({type:`log`,text:`dsh-code-runtime-subprocess worker cleanup error: ${String(e)}\\n`})}).then(()=>{if(process.send===void 0){process.exitCode=1;return}process.send(a,()=>{process.connected&&process.disconnect()})})};process.on(`message`,c=>{if(e===void 0){let l=Ke(c);if(l===void 0){s({type:`done`,error:{kind:`worker-exit`,message:`remote controller received an invalid boot frame`}});return}i=l.maxFrameBytes,e=new a(new URL(import.meta.url),{workerData:l,env:{},execArgv:[],stdout:!0,stderr:!0,resourceLimits:{maxOldGenerationSizeMb:l.maxOldGenerationSizeMb}});let u=e;u.stdout.on(`data`,e=>{o({type:`log`,text:e.toString(`utf8`)})||s($())}),u.stderr.on(`data`,e=>{o({type:`log`,text:e.toString(`utf8`)})||s($())}),u.on(`message`,e=>{let t=X(e);if(t!==void 0){if(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`)o({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||s($());else if(t.type===`log`&&typeof t.text==`string`)o({type:`log`,text:t.text})||s($());else if(t.type===`output-limit`)s({type:`output-limit`});else if(t.type===`done`){let e=Ge(t,We);e!==void 0&&s(e)}}}),u.on(`error`,e=>{s({type:`done`,error:{kind:`worker-exit`,message:`worker error: ${e.stack||e.message||r(e)}`}})}),u.on(`exit`,e=>{t||s({type:`done`,error:{kind:`worker-exit`,message:`worker exited with code ${e} before completing`}})}),n=setInterval(()=>{e!==void 0&&e.performance.eventLoopUtilization().active>l.computeMs&&s({type:`done`,error:{kind:`timeout`,message:`compute budget exhausted (${l.computeMs}ms busy)`}})},25);return}let l=qe(c);l!==void 0&&e.postMessage(l)}),process.on(`disconnect`,()=>{e!==void 0&&!t&&e.terminate()})}if(o)process.env.DSH_CODE_RUNTIME_CONTROLLER===`1`?Xe():Ye();else{if(s===null)throw Error(`remote worker requires parentPort`);let e=c;ze(s,e,{stdout:process.stdout,stderr:process.stderr},e.maxFrameBytes)}export{};" +export const CODE_RUNNER_SOURCE = "import{Buffer as e}from\"node:buffer\";import{spawn as t}from\"node:child_process\";import{createInterface as n}from\"node:readline\";import{inspect as r}from\"node:util\";import{Worker as i,isMainThread as a,parentPort as o,workerData as s}from\"node:worker_threads\";const c=Reflect.apply,l=Array.isArray,u=Buffer,d=Reflect.get(Buffer,`byteLength`),f=Object.create,p=Object.defineProperty,ee=Object.keys,te=String,ne=Reflect.get(String.prototype,`charCodeAt`),re=Reflect.get(String.prototype,`codePointAt`),ie=Reflect.get(String.prototype,`slice`);function m(e){let t=f(null);return t.value=e,t}function ae(e,t,n){let r=m(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,p(e,t,r)}function h(e){return c(d,u,[e,`utf8`])}function g(e,t){ae(e,e.length,t)}function _(e){if(e.length===0)return;let t=e.length-1,n=e[t];return p(e,`length`,m(t)),n}function v(e,t){return c(ie,e,[t,t+(c(re,e,[t])>65535?2:1)])}function y(e){if(e.length===2)return 4;if(e===`\"`||e===`\\\\`)return 2;let t=c(ne,e,[0]);return t>=55296&&t<=57343?6:t<32?t===8||t===9||t===10||t===12||t===13?2:6:h(e)}function b(e,t){if(t<2)return;let n=2;for(let r=0;rt)return;r+=i.length}return n}function x(e,t){let n=0,r=e=>(n+=e,n<=t),i=[{kind:`value`,value:e}];for(let e=_(i);e!==void 0;e=_(i)){if(e.kind===`value`){let a=e.value;if(a===null){if(!r(4))return}else if(typeof a==`string`){let e=b(a,t-n);if(e===void 0)return;n+=e}else if(typeof a==`number`){if(!r(h(te(a))))return}else if(typeof a==`boolean`){if(!r(a?4:5))return}else if(l(a)){if(!r(2))return;a.length>0&&g(i,{kind:`array`,value:a,index:0})}else{if(!r(2))return;let e=ee(a);e.length>0&&g(i,{kind:`object`,value:a,keys:e,index:0})}continue}if(e.index>0&&!r(1))return;if(e.kind===`array`){let t=e.value[e.index];if(t===void 0)return;e.index+1t)break;n+=o,r+=a.length,i+=a.length}return r===e.length?e:c(ie,e,[0,r])}const se=Reflect.get(Function.prototype,`toString`),S=Reflect.get(Reflect,`apply`),C=Error,ce=Set,w=Array.isArray,le=Array.prototype,T=Number.isFinite,ue=Number.isSafeInteger,de=Object.create,E=Object.defineProperty,fe=Object.getOwnPropertyDescriptor,D=Object.getPrototypeOf,O=Object.hasOwn,k=Object.is,pe=Object.keys,A=Object.prototype,me=Reflect.get(A,`propertyIsEnumerable`),j=Reflect.ownKeys,he=Reflect.get(Set.prototype,`add`),ge=Reflect.get(Set.prototype,`delete`),_e=Reflect.get(Set.prototype,`has`);function M(e){let t=de(null);return t.value=e,t}function N(e,t,n){let r=M(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,E(e,t,r)}function P(e,t){N(e,e.length,t)}function F(e){if(e.length===0)return;let t=e.length-1,n=e[t];return E(e,`length`,M(t)),n}function I(e,t){return S(_e,e,[t])}function L(e,t){S(he,e,[t])}function ve(e,t){S(ge,e,[t])}function R(e,t){let n=fe(e,`constructor`)?.value;if(typeof n!=`function`)return!1;try{return n.name===t&&n.prototype===e&&S(se,n,[])===`function ${t}() { [native code] }`}catch{return!1}}function z(e){return D(e)===null&&R(e,`Object`)}function B(e){let t=D(e);if(t===le)return!0;if(!w(t)||!R(t,`Array`))return!1;let n=D(t);return typeof n==`object`&&!!n&&z(n)}function V(e){let t=D(e);return t===null||t===A||typeof t==`object`&&z(t)}function ye(e){let t=j(e);for(let n=0;n{e.kind===`root`?n=t:e.kind===`array`?N(e.target,e.index,t):N(e.target,e.key,t)},i=[{kind:`visit`,value:e,destination:{kind:`root`}}];for(let e=F(i);e!==void 0;e=F(i)){if(e.kind===`leave`){ve(t,e.source);continue}if(e.kind===`array-item`){if(!O(e.source,e.index))return;P(i,{kind:`visit`,value:e.source[e.index],destination:{kind:`array`,target:e.target,index:e.index}});continue}if(e.kind===`object-property`){P(i,{kind:`visit`,value:e.source[e.key],destination:{kind:`object`,target:e.target,key:e.key}});continue}let n=e.value;if(n===null){r(e.destination,null);continue}if(typeof n==`boolean`||typeof n==`string`){r(e.destination,n);continue}if(typeof n==`number`){if(!T(n)||k(n,-0))return;r(e.destination,n);continue}if(typeof n!=`object`||I(t,n))return;if(w(n)){if(!B(n))return;let a=n.length;if(j(n).length!==a+1)return;let o=[];r(e.destination,o),L(t,n),P(i,{kind:`leave`,source:n});for(let e=a-1;e>=0;e--)P(i,{kind:`array-item`,source:n,index:e,target:o});continue}if(!V(n))return;let a=ye(n);if(a===void 0)return;let o={};r(e.destination,o),L(t,n),P(i,{kind:`leave`,source:n});for(let e=a.length-1;e>=0;e--){let t=a[e];if(t===void 0)return;P(i,{kind:`object-property`,source:n,key:t,target:o})}}return n}function U(e){let t=[],n=[e];for(let e=F(n);e!==void 0;e=F(n)){if(e===null||typeof e==`boolean`||typeof e==`number`||typeof e==`string`){P(t,e);continue}if(w(e)){P(t,{kind:`array`,length:e.length});for(let t=e.length-1;t>=0;t--){let r=e[t];if(r===void 0)throw new C(`cannot encode a sparse JSON array`);P(n,r)}continue}let r=pe(e);P(t,{kind:`object`,keys:r});for(let t=r.length-1;t>=0;t--){let i=r[t];if(i===void 0)throw new C(`cannot encode a missing JSON object key`);let a=e[i];if(a===void 0)throw new C(`cannot encode an undefined JSON object property`);P(n,a)}}return t}function be(e){if(!B(e)||j(e).length!==e.length+1)return!1;for(let t=0;t=0?{kind:`array`,length:e}:void 0}if(n.kind===`object`){if(t.length!==2||!W(t,`kind`)||!W(t,`keys`))return;let e=n.keys;if(!w(e)||!be(e))return;let r=new ce,i=[],a=e;for(let e=0;e{let t=n[n.length-1];if(!t)return i?!1:(r=e,i=!0,!0);if(t.index>=(t.kind===`array`?t.length:t.keys.length))return!1;if(t.kind===`array`)P(t.target,e);else{let n=t.keys[t.index];if(n===void 0)return!1;N(t.target,n,e)}return t.index+=1,!0};for(let e=0;ea)return;let e=[];i=e,n.length>0&&(o={kind:`array`,target:e,length:n.length,index:0})}else{if(n.keys.length>a)return;let e={};i=e,n.keys.length>0&&(o={kind:`object`,target:e,keys:n.keys,index:0})}}if(!a(i))return;for(o&&P(n,o);n.length>0;){let e=n[n.length-1];if(e===void 0||e.index<(e.kind===`array`?e.length:e.keys.length))break;F(n)}}return n.length===0?r:void 0}catch{return}}const G=Error,Ce=Object.create,we=Object.defineProperty;function Te(e,t,n){let r=Ce(null);r.enumerable=!0,r.value=n,we(e,t,r)}var Ee=class{bytes=2;entries=0;truncated=!1;sink;onLimit;maxBytes;constructor(e,t,n=()=>{}){this.maxBytes=e,this.sink=t,this.onLimit=n}push(e){if(this.truncated)return;let t=+(this.entries>0),n=this.maxBytes-this.bytes-t,r=b(e,n);if(r===void 0){this.truncated=!0;let r=oe(e,n);if(r.length>0){let e=b(r,n);if(e===void 0)throw new G(`worker output ledger produced an oversized log prefix`);this.bytes+=e+t,this.entries+=1,this.sink(r)}this.onLimit();return}this.bytes+=r+t,this.entries+=1,this.sink(e)}remainingOutputBytes(){return this.maxBytes-this.bytes}};const De=[`log`,`info`,`warn`,`error`,`debug`];function Oe(e){let t=e=>e.map(e=>typeof e==`string`?e:r(e,Ae)).join(` `),n=Object.create(null);for(let r of De)n[r]=(...n)=>{e.push(t(n))};return n}function ke(e,t){let n=t.write;return t.write=(t,...n)=>{e.push(typeof t==`string`?t:String(t));let r=[n[0],n[1]].find(e=>typeof e==`function`);return r&&queueMicrotask(()=>{r(null)}),!0},()=>{t.write=n}}const Ae={depth:4,maxArrayLength:100,maxStringLength:1e4};function je(e,t,n=t){if(e===void 0)return{};let r;try{r=H(e)}catch{r=void 0}return r===void 0?Ne(`invalid-output`,`program completion must be lossless JSON`,t,n):x(r,t)===void 0?Me(n):{value:U(r)}}function Me(e){return{error:{kind:`output-limit`,message:`outer output exceeded ${e} bytes`}}}function Ne(e,t,n,r){return b(t,n)===void 0?Me(r):{error:{kind:e,message:t}}}function Pe(e,t,n=t){let r;try{let t=e instanceof G?e.stack??e.message:e;r=typeof t==`string`?t:String(t)}catch{r=`program threw an unrenderable value`}return Ne(`exception`,r,t,n)}function Fe(e){return class extends G{constructor(t,n){super(n),Te(this,`name`,e.name),Te(this,e.memberNameProperty,t)}}}function K(e,t,n){return e?new e(t,n):new G(n)}function Ie(e){let t=new Map;for(let n of e.namespaces)n.errorClass&&t.set(n.global,Fe(n.errorClass));return t}function Le(e,t){e.on(`message`,e=>{let n=t.get(e.id);if(n)if(t.delete(e.id),e.ok){let t=Se(e.value);t===void 0?n.reject(new G(`binding resolution must be lossless JSON`)):n.resolve(t)}else n.reject(new G(e.message))})}function Re(e,t,n,r,i=Ie(e),a){return e.namespaces.map(({global:e,names:o})=>{let s=i.get(e),c=Object.create(null);for(let i of o)Object.defineProperty(c,i,{enumerable:!0,value:o=>{let c;try{c=H(o)}catch{c=void 0}if(c===void 0)return Promise.reject(K(s,i,`binding arguments must be lossless JSON`));let l={type:`call`,id:r.value,global:e,name:i,args:U(c)};return a!==void 0&&x(l,a)===void 0?Promise.reject(K(s,i,`binding arguments exceed maxFrameBytes`)):new Promise((e,a)=>{let o=r.value++;n.set(o,{resolve:e,reject:e=>{a(K(s,i,e.message))}});try{t.postMessage(l)}catch(e){n.delete(o),a(K(s,i,`binding arguments must be structured-cloneable: ${e instanceof G?e.message:String(e)}`))}})}});return c})}async function ze(e,t,n,r){let i=new Ee(t.maxOutputBytes,t=>{e.postMessage({type:`log`,text:t})},()=>{e.postMessage({type:`output-limit`})});ke(i,n.stdout),ke(i,n.stderr);let a=new Map;Le(e,a);let o={value:1},s=Ie(t),c=Re(t,e,a,o,s,r),l=[],u=[];for(let e of t.namespaces){if(!e.errorClass)continue;l.push(e.errorClass.name);let t=s.get(e.global);if(!t)throw new G(`missing binding error class for ${e.global}`);u.push(t)}let d=Oe(i),f;try{let e=(async()=>{}).constructor;f={type:`done`,...je(await new e(...t.namespaces.map(e=>e.global),...l,`console`,`'use strict';\\n${t.code}`)(...c,...u,d),i.remainingOutputBytes(),t.maxOutputBytes)}}catch(e){f={type:`done`,...Pe(e,i.remainingOutputBytes(),t.maxOutputBytes)}}e.postMessage(r!==void 0&&x(f,r)===void 0?{type:`output-limit`}:f)}function q(e){return e.readableEnded||e.destroyed?Promise.resolve():new Promise(t=>{let n=()=>{e.off(`end`,n),e.off(`close`,n),e.off(`error`,n),t()};e.once(`end`,n),e.once(`close`,n),e.once(`error`,n),(e.readableEnded||e.destroyed)&&n()})}const Be=new Set([`exception`,`timeout`,`abort`,`worker-exit`,`invalid-output`,`output-limit`]);let J=0;function Ve(){let e=process.execArgv.indexOf(`--eval`);if(e<0)throw Error(`code runtime runner requires its eval source`);let t=process.execArgv[e+1];if(t===void 0)throw Error(`code runtime runner requires its eval source`);return t}function Y(e){return typeof e==`object`&&e?e:void 0}function X(t,n){try{let r=JSON.stringify(t);return typeof r==`string`&&e.byteLength(r)<=n?r:void 0}catch{return}}function He(e){process.stdout.write(e),process.stdout.write(`\n`)}function Ue(e){let t=X(e,J);return t===void 0?!1:(He(t),!0)}function We(e){let t=Y(e);return t!==void 0&&typeof t.kind==`string`&&Be.has(t.kind)&&typeof t.message==`string`}function Ge(e){return We(e)&&(e.kind===`exception`||e.kind===`invalid-output`||e.kind===`output-limit`)}function Ke(e,t){return e.error===void 0?{type:`done`,...e.value===void 0?{}:{value:Q(e.value)}}:t(e.error)?{type:`done`,error:e.error}:void 0}function Z(e){let t=Y(e);if(!(t===void 0||t.type!==`boot`||typeof t.code!=`string`||!Array.isArray(t.namespaces)||!Number.isSafeInteger(t.maxOutputBytes)||t.maxOutputBytes<4||!Number.isSafeInteger(t.maxFrameBytes)||t.maxFrameBytes{e.once(`exit`,()=>{t()})})}function $(){return{type:`done`,error:{kind:`worker-exit`,message:`code runtime bridge frame exceeded maxFrameBytes`}}}function Ye(){let e=n({input:process.stdin,crlfDelay:1/0}),r,i=0,a=2,o=0,s=!1,c=t=>{if(s)return;let n=X(t,J)??X($(),J);s=!0,n!==void 0&&He(n);let i=r;r=void 0,(i===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=q(i.stdout),t=q(i.stderr),n=Je(i);i.kill(`SIGKILL`),await Promise.all([n,e,t])})).catch(e=>{process.stderr.write(`dsh-code-runtime-subprocess controller cleanup error: ${String(e)}\\n`)}).then(()=>{e.close(),process.stdin.destroy()})},l=e=>{if(s)return;let t=+(o>0),n=b(e,i-a-t);if(n===void 0){c({type:`output-limit`});return}a+=n+t,o+=1,Ue({type:`log`,text:e})||c($())},u=e=>{i=e.maxOutputBytes,J=e.maxFrameBytes,r=t(process.execPath,process.execArgv,{env:{DSH_CODE_RUNTIME_CONTROLLER:`1`},detached:!1,stdio:[`ignore`,`pipe`,`pipe`,`ipc`]});let n=r;n.stdout.on(`data`,e=>{l(e.toString(`utf8`))}),n.stderr.on(`data`,e=>{l(e.toString(`utf8`))}),n.on(`message`,e=>{let t=Y(e);if(t!==void 0){if(t.type===`log`&&typeof t.text==`string`){l(t.text);return}if(!s){if(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`)Ue({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||c($());else if(t.type===`output-limit`)c({type:`output-limit`});else if(t.type===`done`){let e=Ke(t,We);e!==void 0&&c(e)}}}}),n.on(`error`,e=>{c({type:`done`,error:{kind:`worker-exit`,message:`remote controller error: ${e.message}`}})}),n.on(`exit`,e=>{s||c({type:`done`,error:{kind:`worker-exit`,message:`remote controller exited with code ${e} before completing`}})}),n.send(e,e=>{e!==null&&c({type:`done`,error:{kind:`worker-exit`,message:`remote controller boot failed: ${e.message}`}})})};e.on(`line`,e=>{let t;try{t=JSON.parse(e)}catch(e){process.stderr.write(`dsh-code-runtime-subprocess frame error: ${String(e)}\\n`),c({type:`done`,error:{kind:`worker-exit`,message:`remote runner received a malformed frame`}});return}if(r===void 0){let e=Z(t);if(e===void 0){c({type:`done`,error:{kind:`worker-exit`,message:`remote runner received an invalid boot frame`}});return}u(e);return}let n=qe(t);n!==void 0&&r.send(n,e=>{e!==null&&c({type:`done`,error:{kind:`worker-exit`,message:`remote controller reply failed: ${e.message}`}})})}),e.on(`close`,()=>{r!==void 0&&!s&&c({type:`done`,error:{kind:`abort`,message:`remote runner input closed`}})})}function Xe(){let t,n=!1,a,o=0,s=e=>process.send===void 0||o>0&&X(e,o)===void 0?!1:(process.send(e),!0),c=e=>{if(n)return;n=!0,clearInterval(a);let r=o>0&&X(e,o)===void 0?$():e,i=t;t=void 0,(i===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=q(i.stdout),t=q(i.stderr);await Promise.all([i.terminate(),e,t])})).catch(e=>{s({type:`log`,text:`dsh-code-runtime-subprocess worker cleanup error: ${String(e)}\\n`})}).then(()=>{if(process.send===void 0){process.exitCode=1;return}process.send(r,()=>{process.connected&&process.disconnect()})})};process.on(`message`,l=>{if(t===void 0){let u=Z(l);if(u===void 0){c({type:`done`,error:{kind:`worker-exit`,message:`remote controller received an invalid boot frame`}});return}o=u.maxFrameBytes,t=new i(new URL(`data:text/javascript;base64,${e.from(Ve()).toString(`base64`)}`),{workerData:u,env:{},execArgv:[],stdout:!0,stderr:!0,resourceLimits:{maxOldGenerationSizeMb:u.maxOldGenerationSizeMb}});let d=t;d.stdout.on(`data`,e=>{s({type:`log`,text:e.toString(`utf8`)})||c($())}),d.stderr.on(`data`,e=>{s({type:`log`,text:e.toString(`utf8`)})||c($())}),d.on(`message`,e=>{let t=Y(e);if(t!==void 0){if(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`)s({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||c($());else if(t.type===`log`&&typeof t.text==`string`)s({type:`log`,text:t.text})||c($());else if(t.type===`output-limit`)c({type:`output-limit`});else if(t.type===`done`){let e=Ke(t,Ge);e!==void 0&&c(e)}}}),d.on(`error`,e=>{c({type:`done`,error:{kind:`worker-exit`,message:`worker error: ${e.stack||e.message||r(e)}`}})}),d.on(`exit`,e=>{n||c({type:`done`,error:{kind:`worker-exit`,message:`worker exited with code ${e} before completing`}})}),a=setInterval(()=>{t!==void 0&&t.performance.eventLoopUtilization().active>u.computeMs&&c({type:`done`,error:{kind:`timeout`,message:`compute budget exhausted (${u.computeMs}ms busy)`}})},25);return}let u=qe(l);u!==void 0&&t.postMessage(u)}),process.on(`disconnect`,()=>{t!==void 0&&!n&&t.terminate()})}if(a)process.env.DSH_CODE_RUNTIME_CONTROLLER===`1`?Xe():Ye();else{if(o===null)throw Error(`remote worker requires parentPort`);let e=s;ze(o,e,{stdout:process.stdout,stderr:process.stderr},e.maxFrameBytes)}export{};" diff --git a/packages/util/atomic-write/src/runner.ts b/packages/util/atomic-write/src/runner.ts index 92fad6db2e..2655d22026 100644 --- a/packages/util/atomic-write/src/runner.ts +++ b/packages/util/atomic-write/src/runner.ts @@ -1,12 +1,11 @@ /** Typed source for the dependency-free execution-world runner bundle. */ import { Buffer } from 'node:buffer' -import { fork } from 'node:child_process' +import { spawn } from 'node:child_process' import type { ChildProcess } from 'node:child_process' import { createInterface } from 'node:readline' import type { Readable } from 'node:stream' import { inspect } from 'node:util' -import { fileURLToPath } from 'node:url' import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads' import { decodeWorkerJson, @@ -56,6 +55,14 @@ const failureKinds = new Set([ let maxFrameBytes = 0 +function runnerSource(): string { + const evalIndex = process.execArgv.indexOf('--eval') + if (evalIndex < 0) throw new Error('code runtime runner requires its eval source') + const source = process.execArgv[evalIndex + 1] + if (source === undefined) throw new Error('code runtime runner requires its eval source') + return source +} + function recordOf(value: unknown): Record | undefined { return typeof value === 'object' && value !== null ? value as Record : undefined } @@ -197,10 +204,9 @@ function runLauncher(): void { const startController = (boot: RuntimeBootData): void => { maxOutputBytes = boot.maxOutputBytes maxFrameBytes = boot.maxFrameBytes - controller = fork(fileURLToPath(import.meta.url), [], { + controller = spawn(process.execPath, process.execArgv, { env: { DSH_CODE_RUNTIME_CONTROLLER: '1' }, detached: false, - execArgv: [], stdio: ['ignore', 'pipe', 'pipe', 'ipc'], }) as Controller const current = controller @@ -325,7 +331,8 @@ function runController(): void { return } controllerMaxFrameBytes = boot.maxFrameBytes - worker = new Worker(new URL(import.meta.url), { + const sourceUrl = new URL(`data:text/javascript;base64,${Buffer.from(runnerSource()).toString('base64')}`) + worker = new Worker(sourceUrl, { workerData: boot, env: {}, execArgv: [],