fix(rebase): migrate the replayed E2B branch onto the rebased parent

The linear replay carried old-lineage content over parent-owned files;
this checkpoint restores them and adapts the branch to the parent's
post-rebase seam:

- restore all pty/lsp/subprocess/code-runtime surfaces to the parent's
  exact content (this branch claims none of them) and drop the net-zero
  code-runtime-e2b/pty-e2b/lsp-e2b residue and its registrations
- widen serializeRemoteEnvironment to the seam's NodeJS.ProcessEnv
  tombstone contract: an explicit undefined removes an ambient entry
- migrate the two E2B fixture Agent stubs to the Inbox-model interface
  and Session.create
- re-apply the branch's gen-doc-graphs roles, THIRD_PARTY_NOTICES e2b
  row, and packages/README group row (trimmed to the doc budget);
  regenerate catalogs and re-record bilingual pairings
This commit is contained in:
Tianyi Cui
2026-08-08 22:19:13 +08:00
parent 6e1ae76c9b
commit d488330ba4
40 changed files with 1043 additions and 446 deletions
@@ -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/code-runtime/code-runtime-worker/README.md
README.md: 35196a0b4fba5cd0388246a70354308ece08b39f
README.zh.md: 49871c38c540addd06f5d24793ae00b45e2f8bb0
README.md: 590b79dcd1bc322350060b55767b09c6305edacc
README.zh.md: 12c25f892bd20892cb47e593f16b6aadd9ffa84c
@@ -32,7 +32,7 @@ Every field is validated and defaulted; `maxOutputBytes` is a safe integer of at
## The worker entry, unbuilt and built
Source mode loads erasable-only `src/worker.ts` through Node's native type stripping. Its transitive runtime closure contains only Node built-ins and relative source modules, so a fresh checkout never requires a sibling workspace package's unbuilt `lib/` export. The worker-local JSON snapshotter is parity-tested against the session-owned canonical boundary; both sides flatten and rebuild validated values around the message port so application nesting never reaches structured clone. Built mode passes the sibling `lib/worker.cjs` as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node. `tests/built-lib.e2e.ts` pins the real load path required by [docs/testing.md](../../../docs/testing.md).
Source mode loads erasable-only `src/worker.ts` through Node's native type stripping. Its transitive runtime closure contains only Node built-ins and relative source modules, so a fresh checkout never requires a sibling workspace package's unbuilt `lib/` export. The worker-local and session-owned JSON boundaries both flatten and rebuild validated values around the message port so application nesting never reaches structured clone. Built mode passes the sibling `lib/worker.cjs` as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node. The repository-wide requirement to exercise this published entry path belongs to the [testing policy](../../../docs/testing.md).
The SDK surface is the default/named `WorkerCodeRuntime` class plus `Config`. The operational `./worker` subpath exists only as the packaged spawn entry; the wire protocol and bootstrap helpers are source-private implementation details.
@@ -46,8 +46,8 @@ No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **OS processes a program spawns survive termination** — `worker.terminate()` ends the thread only; deployments requiring process-tree cleanup select `dsh-code-runtime-subprocess`, whose mounted subprocess provider owns that cleanup.
- **Type-strip rides Node's experimental `stripTypeScriptTypes` API** — the relied-on behavior is pinned by unit tests, with amaro/sucrase as named drop-in replacements if it shifts.
- **OS processes a program spawns survive termination** — `worker.terminate()` ends the thread only, weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists.
- **Type-strip rides Node's experimental `stripTypeScriptTypes` API** — amaro or sucrase are the named drop-in replacements if the relied-on behavior shifts.
- **`computeMs` expiry can overshoot by up to one poll interval** — busy time is sampled every 25 ms (an internal constant, deliberately not config).
- **Programs get a five-method `console` shim** (`log`/`info`/`warn`/`error`/`debug`) — deliberately not Node's full console surface.
- **Intermediate binding values have no byte cap** — a program can exhaust process or worker memory with a value that never becomes outer output.
@@ -23,18 +23,18 @@
- **每次运行使用一个全新 worker,不设池化**:程序所在的世界会随 worker 一同终止,不会留下需要记录的跨运行状态,也无法发生状态泄漏;仅凭会话日志即可重建运行。
- **在执行上下文中,由宿主侧剥离类型**:程序会包裹在异步函数外壳中,通过 `node:module``stripTypeScriptTypes` 剥离类型(只支持可擦除语法;`enum`namespace 会作为程序 `exception` 被拒绝,且不会启动 worker),再按字节位置切回原内容。之后程序作为 `AsyncFunction` 的函数体执行,因此顶层 `await``return` 可用。
- **端口把对端视为不可信**:模型代码能够访问 `parentPort` 并伪造通信,因此任何代码读取入站消息前,系统都会验证其形状并重新构建(`null`、原始值、无效类型和格式错误的载荷会被静默丢弃;伪造的额外字段绝不会被带入);宿主对每个调用 id 最多响应一次,只将绑定名称解析为自有属性(伪造的 `constructor` 无法沿原型链访问),丢弃结算后的回复,并验证每个绑定 resolve 值与完成值是否为无损 JSON。伪造的 `log``done` 消息无法绕过外层上限:宿主会再次验证,并统计每条获准日志以及完成值或诊断。worker 侧命名空间使用 null-prototype 和 `defineProperty`,因此形似 `__proto__` 的绑定名称只是普通键。
- **绑定 reject 类属于请求数据**:可选命名空间描述符会指定构造器全局变量,以及用于接收失败成员名称的自有属性。worker 会创建并注入该真实类,使 `instanceof` 生效,同时无需硬编码 `tools``ToolCallError`;全局变量无效或冲突的声明会在启动 worker 前失败。失败路径使用模块捕获的错误与属性定义 intrinsic,以及 null-prototype 描述符,因此模型之后的修改无法把被拒绝的绑定变成 worker 崩溃。
- **两个独立预算,因为对端不可信**:`computeMs` 统计 worker 实际测得的忙碌时间(轮询 `worker.performance.eventLoopUtilization()`);热循环无法借助待完成的诱饵 dispatch 隐藏,程序等待慢工具时则不累计。`maxWallMs` 为忙碌时间无法观测的情况兜底(例如等待永远不会 resolve 的 promise)。二者最终都会调用 `worker.terminate()`,连同步热循环也能终止;堆溢出会表现为 worker 的 OOM 退出(`kind: 'worker-exit'`)。`maxWallMs` 在加载时会对照 `MAX_TIMER_DELAY_MS` 做范围校验:`setTimeout` 会把更长的延迟夹到 1 ms,仅有正数校验会放行一个在第一个 tick 就到期的上限。`computeMs` 不需要这道上界,因为它对照的是实测占用率,而不是喂给定时器。
- **中间绑定值是完整 JSON**:绑定参数与 resolve 值会接受迭代式无损 JSON 验证。程序执行前,worker 会捕获自己 realm 中的普通容器原型身份,以及只用于外部 realm 的原生函数源码检查,因此构造器槽修改和用户编写的仿冒对象都无法改变容器分类。它还会捕获该 JSON 边界使用的每一个结构与计量 intrinsic,以无原型对象创建属性描述符,并绕过可变集合原型管理私有遍历状态;因此,模型对全局对象、原型方法或 `Object.prototype` 上形似描述符字段的修改,都无法改变验证、wire 传输或字节计量。值会展平为有深度上限的前序 wire 值,供 structured clone 使用,并在另一侧迭代式重建。它们没有字节、JavaScript 调用栈或嵌套 structured-clone 深度上限,绝不会进入外层输出账本或模型上下文;上限仍来自提供方/执行器获取限制与进程/worker 内存。
- **日志主动流入一个外层账本**consolestdoutstderr 文本按发送顺序穿过端口,因此超时或被终止的程序仍会显示已经打印的内容。worker 会 JSON 字符串精确计费,并在发送完成值和异常诊断前,根据组合预算的剩余量预检;因此,抛出的百万字节 stack 会在 worker 边界变成固定的 `output-limit` 诊断。绕过补丁 stream 槽的原生写入会到达独立于完成端口的 pipe,因此宿主会针对这些字节和不可信伪造通信再次执行账本统计;在物化结果前,结算过程会持续进行有界 pipe 捕获,直到 worker 完成终止。`maxOutputBytes` 统计外层 `logs` 数组加完成值或失败消息载荷的 JSON 序列化;固定的 `CodeRunResult` 字段名、花括号、有界错误 kind 标签,以及后续呈现空白不计入这份可变载荷账本。未超过上限时会返回精确值;有损完成值属于 `invalid-output`,组合溢出属于 `output-limit`,不会用 inspected string 代替。失败会保留能容纳的已捕获前缀,之后按普通外层 `run_code` 落盘策略处理。
- **空环境**worker 使用 `env: {}``execArgv: []`,既没有环境凭据(比 spawn 命令的清理环境规则更严格),也不会继承 loader 标志。
- **释放资源时等待完全停稳**:清理会进行中的运行标记为 `abort`,并在 resolve 前等待每个 worker 退出。
- **绑定调用被拒绝时使用的异常类属于请求数据**:可选命名空间描述符会指定构造器全局变量,以及用于接收调用失败成员名称的自有属性。worker 会创建并注入该真实类,使 `instanceof` 生效,同时无需硬编码 `tools``ToolCallError`;全局变量无效或冲突的声明会在启动 worker 前失败。失败路径使用模块捕获的错误 intrinsic 与属性定义 intrinsic,以及 null-prototype 描述符,因此模型之后的修改无法把被拒绝的绑定变成 worker 崩溃。
- **两个独立预算,因为对端不可信**:`computeMs` 统计 worker 实际测得的忙碌时间(轮询 `worker.performance.eventLoopUtilization()`);热循环无法借助待完成的诱饵 dispatch 隐藏,程序等待慢工具时则不累计。`maxWallMs` 为忙碌时间无法观测的情况兜底(例如等待永远不会 resolve 的 promise)。二者最终都会调用 `worker.terminate()`,连同步热循环也能终止;堆溢出会表现为 worker 的 OOM 退出(`kind: 'worker-exit'`)。`maxWallMs` 在加载时会对照 `MAX_TIMER_DELAY_MS` 做范围校验:`setTimeout` 会把更长的延迟限制为 1 ms,仅有正数校验会放行一个在第一个 tick 就到期的上限。`computeMs` 不需要这道上界,因为它对照的是实测占用率,而不是喂给定时器。
- **中间绑定值是完整 JSON**:绑定参数与 resolve 值会接受迭代式无损 JSON 验证。程序执行前,worker 会捕获自己 realm 中的普通容器原型身份,以及只用于外部 realm 的原生函数源码检查,因此构造器槽修改和用户编写的仿冒对象都无法改变容器分类。它还会捕获该 JSON 边界使用的每一个结构与计量 intrinsic,以无原型对象创建属性描述符,并绕过可变集合原型管理私有遍历状态;因此,模型对全局对象、原型方法或 `Object.prototype` 上形似描述符字段的修改,都无法改变验证、wire 传输或字节计量。值会展平为自身嵌套深度有界的前序 wire 值,供 structured clone 使用,并在另一侧迭代式重建。它们没有字节、JavaScript 调用栈或嵌套 structured-clone 深度上限,绝不会进入外层输出账本或模型上下文;上限仍来自提供方/执行器获取限制与进程/worker 内存。
- **日志主动流入一个外层账本**consolestdoutstderr 文本按产生顺序经端口传输,因此超时或被终止的程序仍会显示已经打印的内容。worker 会精确统计 JSON 字符串的字节数,并在发送完成值和异常诊断前,根据组合预算的剩余量预检;因此,抛出的百万字节 stack 会在 worker 边界变成固定的 `output-limit` 诊断。绕过补丁 stream 槽的原生写入会到达独立于完成端口的 pipe,因此宿主会针对这些字节和不可信伪造通信再次执行账本统计;在物化结果前,结算过程会持续进行有界 pipe 捕获,直到 worker 完成终止。`maxOutputBytes` 统计外层 `logs` 数组加完成值或失败消息载荷的 JSON 序列化;固定的 `CodeRunResult` 字段名、花括号、有界错误 kind 标签,以及后续呈现空白不计入这份可变载荷账本。未超过上限时会返回精确值;有损完成值属于 `invalid-output`,组合溢出属于 `output-limit`,不会用 inspected string 代替。失败会保留能容纳的已捕获前缀,之后按普通外层 `run_code` 落盘策略处理。
- **空环境**worker 使用 `env: {}``execArgv: []`,既不会获得环境变量中的凭据(比 spawn 命令的清理环境规则更严格),也不会继承 loader 标志。
- **dispose(资源释放)时等待完全停稳**:清理会使进行中的运行 `abort` 失败,并会等待每个 worker 退出后再完成
## 未构建与已构建的 worker 入口
源代码模式通过 Node 原生类型剥离加载只包含可擦除语法的 `src/worker.ts`。其传递运行时闭包只包含 Node 内置模块和相对源模块,因此全新 checkout 绝不需要兄弟工作区包尚未构建的 `lib/` 导出。worker 本地 JSON 快照器会与会话自有的规范边界执行一致性测试;消息端口两侧都会展平并重建已验证值,使应用嵌套永远不会进入 structured clone。构建模式会把兄弟文件 `lib/worker.cjs` 作为文件系统路径传入,因为 pkg 的 VFS Worker hook 要求 CommonJS;同一路径也可在普通 Node 下使用。`tests/built-lib.e2e.ts` 固定了 [docs/testing.md](../../../docs/testing.md) 要求的真实加载路径
源代码模式通过 Node 原生类型剥离加载只包含可擦除语法的 `src/worker.ts`。其传递运行时闭包只包含 Node 内置模块和相对源模块,因此全新 checkout 绝不需要兄弟工作区包尚未构建的 `lib/` 导出。worker 本地和会话自有的 JSON 边界都会在消息端口周围展平并重建已验证值,使应用嵌套永远不会进入 structured clone。构建模式会把兄弟文件 `lib/worker.cjs` 作为文件系统路径传入,因为 pkg 的虚拟文件系统(VFSWorker hook 要求 CommonJS;同一路径也可在普通 Node 下使用。演练这个已发布入口路径的仓库级要求由[测试策略](../../../docs/testing.md)规定
SDK 接口是默认具名 `WorkerCodeRuntime` `Config`可操作`./worker` 子路径仅作为打包后的 spawn 入口存在;wire 协议与启动辅助模块是源代码私有的实现细节。
SDK 对外提供默认具名导出的 `WorkerCodeRuntime`,以及 `Config`运行所用`./worker` 子路径仅作为打包后的 spawn 入口存在;wire 协议与启动辅助模块是源代码私有的实现细节。
## 模型体验
@@ -42,13 +42,13 @@ SDK 接口是默认/具名 `WorkerCodeRuntime` 类与 `Config`。可操作的
#### KV Cache 影响
不会直接失效;由具名消费方负责请求前缀变更。
不会直接失效;由上述消费方负责请求前缀变更。
## 已知限制与暂缓工作
## 已知限制与暂缓事项
- **程序 spawn 的 OS 进程在终止后仍会存活**`worker.terminate()` 只结束线程;需要清理进程树的部署应选择 `dsh-code-runtime-subprocess`,由其挂载的子进程提供方负责该清理
- **类型剥离依赖 Node 的实验性 `stripTypeScriptTypes` API**:依赖的行为由单元测试固定;如其发生变化,amarosucrase 是已经点名的直接替代品。
- **程序派生的 OS 进程在程序终止后仍会存活**`worker.terminate()` 只结束线程,比 bash-local 的进程组终止更弱;在容器后端出现前,孤儿进程清理属于部署职责
- **类型剥离依赖 Node 的实验性 `stripTypeScriptTypes` API**依赖的行为发生变化,amarosucrase 是已经点名的直接替代品。
- **`computeMs` 到期最多可能超过一个轮询间隔**:系统每 25 ms 采样一次忙碌时间(内部常量,有意不做成配置)。
- **程序获得一个含 5 方法的 `console` shim**`log``info``warn``error``debug`):有意不提供 Node 的完整 console 接口。
- **程序获得一个含 5 方法的 `console` shim**`log``info``warn``error``debug`):有意不提供 Node 的完整 console 接口。
- **中间绑定值没有字节上限**:程序可以用永远不会成为外层输出的值耗尽进程或 worker 内存。
- **默认 64 MiB 是拒绝边界,不是可恢复存储**:外层落盘只能保存发生 `output-limit` 后返回的有界日志和诊断;在运行时上限之外被拒绝的字节永远不会到达落盘层。
@@ -21,10 +21,6 @@ import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from
import { decodeWorkerJson, encodeWorkerJson } from './worker-json.ts'
import type { WorkerJsonWire } from './worker-json.ts'
export { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts'
export { decodeWorkerJson, encodeWorkerJson } from './worker-json.ts'
export type { WorkerJsonWire } from './worker-json.ts'
/** Plugin config: every execution cap, changeable from `cordis.yml` (no hardcoded tunables). */
export interface Config {
/**
@@ -169,19 +165,14 @@ function parseWorkerMessage(raw: unknown): WorkerToHost | undefined {
}
/** Shared outer-output accounting for one isolated run; binding values never enter it. */
export class OutputLedger {
/** One run's combined outer-output ledger; binding values never enter it. */
class OutputLedger {
private bytes = 2 // JSON serialization of the empty logs array: []
private entries = 0
constructor(private readonly maxBytes: number) {}
/**
* Admit one exact log entry, or report that the hard cap was crossed.
* @param text - Candidate log entry.
* @param sink - Accepted log entries for the current run.
* @returns Whether the complete entry fits the remaining outer-output budget.
*/
/** Admit one exact log entry, or report that the hard cap was crossed. */
admit(text: string, sink: string[]): boolean {
const separatorBytes = this.entries > 0 ? 1 : 0
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
@@ -192,33 +183,19 @@ export class OutputLedger {
return true
}
/**
* Finalize a successful absent-or-JSON completion against the combined cap.
* @param logs - Already accepted log entries.
* @param value - Optional lossless-JSON completion value.
* @returns A success result or an output-limit failure.
*/
/** Finalize a successful absent-or-JSON completion against the combined cap. */
success(logs: string[], value?: CodeJsonValue): CodeRunResult {
if (value !== undefined && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, ...value !== undefined ? { value } : {} }
}
/**
* Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap.
* @param logs - Already accepted log entries.
* @param error - Candidate failure diagnostic.
* @returns The diagnostic result or an output-limit failure.
*/
/** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
failure(logs: string[], error: CodeRunFailure): CodeRunResult {
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === undefined) return this.limit(logs)
return { logs, error }
}
/**
* Build the explicit output-limit failure while retaining a fitting prefix of the final log.
* @param logs - Candidate log entries in original order.
* @returns A capped output-limit result.
*/
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
limit(logs: string[]): CodeRunResult {
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`
// The fixed diagnostic is ASCII, so every character is one byte plus the quotes.