Add E2B PTY, LSP, and code runtime providers

This commit is contained in:
Tianyi Cui
2026-07-28 13:59:47 +08:00
parent e7b682f1f6
commit 6667102890
82 changed files with 5462 additions and 644 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/README.md
README.md: f20a287419b94b1a9dc1d8da7303fc4d3032cfd3
README.zh.md: f5cd4c9949f2bd7a7d6d7cd078144910712a3819
README.md: f59fc3b15331b4799cbc7a3fbe27ce8cd9a12e51
README.zh.md: 02ade97887fab8a8a0c22efcf73bb2dd1f2cf33c

View File

@@ -2,11 +2,12 @@
English | [中文](README.zh.md)
The code-execution capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the SDK generated in the loaded runtime's `language`); design in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). **Product** packages.
The code-execution capability seam (see [capability seams](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)): an abstract runtime interface for executing one model-written program against host-provided async bindings, capturing what it printed and returned. The consumer is the tool registry's [Code Mode](../core/tools/README.md) (`tools: { mode: code }` — the `run_code` tool and the generated TypeScript SDK); design in the [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md). **Product** packages.
| Package | Role | ctx key |
|---|---|---|
| [`code-runtime/`](code-runtime/README.md) | Code-execution seam and shared vocabulary | `ctx.codeRuntime` |
| [`code-runtime-worker/`](code-runtime-worker/README.md) | Worker-thread backend | registers `ctx.codeRuntime` |
| `code-runtime/` | Abstract code-execution seam (interface + vocabulary) | `ctx.codeRuntime` |
| [`code-runtime-worker/`](code-runtime-worker/README.md) | Worker-thread backend: fresh worker per run, TypeScript via host-side type-strip (annotations advisory, never type-checked), port-bridged bindings, budget/heap containment | registers `ctx.codeRuntime` |
| [`code-runtime-e2b/`](code-runtime-e2b/README.md) | E2B backend: host type-strip and bindings, fresh remote runner/worker, framed bridge, remote process-group cleanup | registers `ctx.codeRuntime` |
Backends register the seam without changing its consumer. The child READMEs own language, isolation, and execution-budget details.
Backends differ by execution substrate and source language—both readonly descriptors on the service—and register `ctx.codeRuntime` without touching the interface or its consumer. The E2B ownership split is recorded in the [remote extension note](../../.agents/notes/implemented/feature/2026-07-28-e2b-interactive-semantic-code-runtime-poc.md).

View File

@@ -1,12 +1,13 @@
# code-runtime/代码执行能力家族
# code-runtime/代码执行能力家族
[English](README.md) | 中文
代码执行能力 seam参见[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):一个抽象运行时接口,用于对宿主提供的异步绑定执行模型编写的程序,并捕获打印和返回的内容。消费方是工具注册表的 [Code Mode](../core/tools/README.md)`tools: { mode: code }`,即 `run_code` 工具和按所加载运行时 `language` 生成的 SDK设计 [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。这些是**产品**包。
代码执行能力 seam参见[能力 seam](../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):一个抽象运行时接口,用于对宿主提供的异步绑定执行一段模型编写的程序,并捕获程序打印和返回的内容。消费方是工具注册表的 [Code Mode](../core/tools/README.md)`tools: { mode: code }`,即 `run_code` 工具与生成的 TypeScript SDK设计记录在 [Code Mode Agent Note](../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)。这些是**产品** 包。
| 包 | 职责 | ctx key |
| 包 | 职责 | ctx |
|---|---|---|
| [`code-runtime/`](code-runtime/README.md) | 代码执行 seam 与共享词汇 | `ctx.codeRuntime` |
| [`code-runtime-worker/`](code-runtime-worker/README.md) | Worker 线程后端 | 注册 `ctx.codeRuntime` |
| `code-runtime/` | 抽象代码执行 seam(接口 + 词汇 | `ctx.codeRuntime` |
| [`code-runtime-worker/`](code-runtime-worker/README.md) | worker 线程后端:每次运行使用全新 worker由宿主侧剥离 TypeScript 类型(类型注解仅供参考,绝不执行类型检查)、端口桥接绑定、预算/堆隔离 | 注册 `ctx.codeRuntime` |
| [`code-runtime-e2b/`](code-runtime-e2b/README.md) | E2B 后端:宿主侧类型剥离与绑定、全新远程 runnerworker、分帧桥、远程进程组清理 | 注册 `ctx.codeRuntime` |
后端在不改变消费方的情况下注册该 seam。子 README 负责语言、隔离和执行预算细节
不同后端的执行基底和源语言各异,二者都是服务上的只读描述符;后端注册 `ctx.codeRuntime`无需修改接口或消费方。E2B 所有权拆分记录在[远程扩展 Agent Note](../../.agents/notes/implemented/feature/2026-07-28-e2b-interactive-semantic-code-runtime-poc.md) 中

View File

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

View File

@@ -0,0 +1,44 @@
# @deepseek-ai/dsh-code-runtime-e2b
English | [中文](README.zh.md)
E2B implementation of [`ctx.codeRuntime`](../code-runtime/README.md). Each run executes one model-written TypeScript program in a fresh remote Node worker while binding functions, type stripping, output accounting, and lifecycle orchestration remain on the host.
## Configuration
| Key | Default | Meaning |
|---|---|---|
| `computeMs` | `60000` | Remote worker event-loop busy-time budget. |
| `maxWallMs` | `600000` | Host-observed wall-clock ceiling. |
| `maxOutputBytes` | `67108864` | Combined serialized outer logs/value/diagnostic cap. |
| `maxOldGenerationSizeMb` | `512` | Remote worker old-generation heap cap in MiB. |
| `maxFrameBytes` | `268435456` | Largest decoded bridge frame, including binding traffic. |
| `killGraceMs` | `2000` | Remote process-group TERM-to-KILL grace. |
Every value is a positive safe integer. `maxOutputBytes` is at least four bytes, `maxWallMs` cannot exceed Node's maximum timer delay, and `maxFrameBytes` cannot be smaller than `maxOutputBytes`. The service requires the concrete `dsh-subprocess-e2b` backend so run cleanup has remote process-group semantics.
## Execution and bridge contract
Setup uploads one dependency-free runner under `ctx.e2b.runtimeRoot` and resolves remote Node. For each run, the host wraps and type-strips erasable TypeScript with Node's `stripTypeScriptTypes`, then starts the runner in `ctx.e2b.cwd`. The runner creates a fresh worker thread with an empty environment and heap limit, measures active event-loop time, and destroys that worker after one completion. The enclosing E2B subprocess group is terminated and awaited after every result, timeout, abort, or disposal, so ordinary child processes in that group stop with the run.
The bridge uses validated newline-delimited base64 JSON frames because E2B subprocess callbacks expose decoded text. Binding arguments and resolutions use the worker runtime's iterative lossless-JSON wire shape; binding functions execute on the host and typed rejection classes are materialized inside the remote worker. The worker captures the JavaScript intrinsics that its adapter boundary invokes before model code runs, hardening binding transport, output accounting, and completion validation against mutation of those references. The host repeats message validation, call-id deduplication, lossless-JSON checks, and the outer-output ledger.
Program failures resolve as `CodeRunResult.error`; only seam misuse rejects. `isolation` is reported as `container`, which is a deployment descriptor rather than a security claim.
## Model Experience
Indirectly, through Code Mode in `dsh-tools`, which returns program logs, values, or typed failures through the existing `run_code` result contract.
#### KV Cache effect
No direct invalidation; Code Mode owns request-prefix changes.
## Known Limitations and Deferred Work
- **Not a whole-agent runtime** — Cordis, sessions, LLM calls, binding dispatch, TypeScript stripping, output ledgers, and E2B SDK state remain on the host.
- **No reconnectable runs** — retaining a sandbox preserves files but not worker/subprocess handles, binding calls, timers, or output cursors.
- **Node worker internals share the model realm** — mutating realm-wide globals or prototypes that Node itself uses can terminate the worker; captured adapter intrinsics are not a separate JavaScript realm or a security boundary.
- **Deliberate process-group escape is not captured** — model code can create a new POSIX session; that unmanaged process is outside this backend's cleanup identity.
- **Intermediate binding traffic is memory-bounded only per frame** — it does not enter model context or the outer-output ledger, but aggregate host/remote process memory remains the limit.
- **Experimental type stripping** — the backend shares the worker implementation's reliance on Node's experimental erasable-syntax API.
- **Sandbox policy is template-owned** — this package adds no network, volume, snapshot, or workspace-synchronization policy.

View File

@@ -0,0 +1,44 @@
# @deepseek-ai/dsh-code-runtime-e2b
[English](README.md) | 中文
[`ctx.codeRuntime`](../code-runtime/README.md) 的 E2B 实现。每次运行都会在全新的远程 Node worker 中执行一段模型编写的 TypeScript 程序;绑定函数、类型剥离、输出记账和生命周期编排仍保留在宿主侧。
## 配置
| 配置键 | 默认值 | 含义 |
|---|---|---|
| `computeMs` | `60000` | 远程 worker 的事件循环忙碌时间预算。 |
| `maxWallMs` | `600000` | 宿主观测到的墙钟时间上限。 |
| `maxOutputBytes` | `67108864` | 外层日志、值和诊断合计的序列化上限。 |
| `maxOldGenerationSizeMb` | `512` | 远程 worker 的老生代堆上限MiB。 |
| `maxFrameBytes` | `268435456` | 已解码桥接帧的最大大小,包括绑定流量。 |
| `killGraceMs` | `2000` | 远程进程组 TERM 到 KILL 的宽限期。 |
每个值都必须是正的安全整数。`maxOutputBytes` 必须至少为 4 字节,`maxWallMs` 不得超过 Node 的最大定时器延迟,且 `maxFrameBytes` 不得小于 `maxOutputBytes`。本服务要求使用具体的 `dsh-subprocess-e2b` 后端,使运行清理具备远程进程组语义。
## 执行与桥接契约
设置阶段会在 `ctx.e2b.runtimeRoot` 下上传一个无依赖的 runner并解析远程 Node。每次运行时宿主会包装仅使用可擦除语法的 TypeScript再用 Node 的 `stripTypeScriptTypes` 剥离类型,然后在 `ctx.e2b.cwd` 中启动 runner。runner 会创建一个具有空环境与堆上限的全新 worker 线程,测量事件循环活跃时间,并在一次运行结算后销毁该 worker。每当运行返回结果、超时、中止或因资源释放终止时系统都会终止外围的 E2B 进程组并等待其退出,因此组内的普通子进程会随本次运行一同停止。
由于 E2B 进程管理回调公开的是已解码文本,桥接层使用经过验证、以换行分隔的 base64 JSON 帧。绑定参数与 resolve 值使用 worker 运行时的迭代式无损 JSON wire 形状;绑定函数在宿主执行,类型化的 reject 类则在远程 worker 内物化。worker 会在模型代码运行前捕获其适配器边界调用的 JavaScript intrinsic从而增强绑定传输、输出记账与完成值验证对这些引用修改的抵御能力。宿主会再次执行消息验证、调用 id 去重和无损 JSON 检查,并用外层输出账本再次计量。
程序失败会 resolve 为 `CodeRunResult.error`;只有 seam 误用才会 reject。`isolation` 报告为 `container`;这是部署描述符,不构成安全声明。
## 模型体验
通过 `dsh-tools` 中的 Code Mode 间接影响模型;它会通过现有 `run_code` 结果契约返回程序日志、值或类型化失败。
#### KV Cache 影响
不会直接失效;请求前缀变更由 Code Mode 负责。
## 已知限制与暂缓工作
- **并非完整的 agent智能体运行时**Cordis、会话、LLM大语言模型调用、绑定分发、TypeScript 类型剥离、输出账本和 E2B SDK 状态仍保留在宿主侧。
- **运行不可重连**:保留沙箱会保留文件,但不会保留 worker进程管理句柄、绑定调用、定时器或输出游标。
- **Node worker 内部机制与模型共享同一 realm**:修改 Node 自身使用、影响整个 realm 的全局对象或原型可能会终止 worker已捕获的适配器 intrinsic 并不构成独立的 JavaScript realm 或安全边界。
- **不会捕获有意逃逸进程组的行为**:模型代码可以创建新的 POSIX 会话;该非受管进程不属于此后端的清理身份范围。
- **中间绑定流量的内存边界仅适用于单帧**:它不会进入模型上下文或外层输出账本,但其总量仍只受宿主/远程进程内存限制。
- **实验性类型剥离**:该后端与 worker 实现一样,依赖 Node 的实验性可擦除语法 API。
- **沙箱策略归模板负责**:本包不会额外增加网络、卷、快照或工作区同步策略。

View File

@@ -0,0 +1,52 @@
{
"name": "@deepseek-ai/dsh-code-runtime-e2b",
"description": "E2B code-runtime implementation for DeepSeek Harness",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-code-runtime": "^0.0.1",
"@deepseek-ai/dsh-code-runtime-worker": "^0.0.1",
"@deepseek-ai/dsh-e2b": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-subprocess-e2b": "^0.0.1",
"@deepseek-ai/dsh-timeout": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"dependencies": {
"schemastery": "^3.18.0"
},
"devDependencies": {
"@deepseek-ai/dsh-code-runtime": "workspace:^",
"@deepseek-ai/dsh-code-runtime-worker": "workspace:^",
"@deepseek-ai/dsh-e2b": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-subprocess-e2b": "workspace:^",
"@deepseek-ai/dsh-timeout": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -0,0 +1,441 @@
/** E2B process/worker implementation of the harness code-runtime seam. */
import { posix } from 'node:path'
import { stripTypeScriptTypes } from 'node:module'
import type { Context } from 'cordis'
import z from 'schemastery'
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
import type {
CodeBindingNamespace,
CodeJsonValue,
CodeRunFailure,
CodeRunRequest,
CodeRunResult,
} from '@deepseek-ai/dsh-code-runtime'
import {
E2BFrameDecoder,
encodeE2BFrame,
quoteE2BShellArg,
resolveE2BExecutable,
} from '@deepseek-ai/dsh-e2b'
import {
decodeWorkerJson,
encodeWorkerJson,
OutputLedger,
} from '@deepseek-ai/dsh-code-runtime-worker'
import type { WorkerJsonWire } from '@deepseek-ai/dsh-code-runtime-worker'
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b'
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { CODE_RUNNER_SOURCE } from './runner-source.ts'
/** Runtime configuration; every execution and bridge bound is deployment-tunable. */
export interface Config {
/** Remote worker measured event-loop busy-time budget. */
computeMs?: number
/** Host-observed wall-clock ceiling. */
maxWallMs?: number
/** Combined serialized outer logs/value/diagnostic cap. */
maxOutputBytes?: number
/** Remote worker old-generation heap cap in MiB. */
maxOldGenerationSizeMb?: number
/** Largest decoded bridge frame, including binding traffic. */
maxFrameBytes?: number
/** Remote process-group TERM-to-KILL grace. */
killGraceMs?: number
}
type ResolvedConfig = Required<Config>
interface LiveRun {
settle(failure: CodeRunFailure): void
finished: Promise<void>
}
interface CallMessage {
type: 'call'
id: number
global: string
name: string
args: WorkerJsonWire
}
interface LogMessage {
type: 'log'
text: string
}
interface DoneMessage {
type: 'done'
value?: WorkerJsonWire
error?: CodeRunFailure
}
type RunnerMessage = CallMessage | LogMessage | DoneMessage | { type: 'output-limit' }
const STRIP_WRAP = { prefix: 'async function __dsh_program__() {\n', suffix: '\n}' } as const
const MIN_OUTPUT_BYTES = 4
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
/* jscpd:ignore-start -- Backends enforce the same injected-global vocabulary without coupling lifecycle implementations. */
const RESERVED_WORDS = new Set([
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
'private', 'protected', 'public', 'arguments', 'eval',
])
const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack'])
/* jscpd:ignore-end */
const FAILURE_KINDS = new Set<CodeRunFailure['kind']>([
'exception', 'timeout', 'abort', 'worker-exit', 'invalid-output', 'output-limit',
])
function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
function parseRunnerMessage(raw: unknown): RunnerMessage | undefined {
if (typeof raw !== 'object' || raw === null) return undefined
const record = raw as Record<string, unknown>
if (record.type === 'output-limit') return { type: 'output-limit' }
if (record.type === 'log') return typeof record.text === 'string' ? { type: 'log', text: record.text } : undefined
if (record.type === 'call') {
if (!Number.isSafeInteger(record.id) || (record.id as number) < 1 || typeof record.global !== 'string' || typeof record.name !== 'string' || !Array.isArray(record.args)) return undefined
return { type: 'call', id: record.id as number, global: record.global, name: record.name, args: record.args as WorkerJsonWire }
}
if (record.type !== 'done') return undefined
if (record.error === undefined) {
return { type: 'done', ...record.value === undefined ? {} : { value: record.value as WorkerJsonWire } }
}
if (typeof record.error !== 'object' || record.error === null) return undefined
const error = record.error as Record<string, unknown>
if (typeof error.kind !== 'string' || !FAILURE_KINDS.has(error.kind as CodeRunFailure['kind']) || typeof error.message !== 'string') return undefined
return { type: 'done', error: { kind: error.kind as CodeRunFailure['kind'], message: error.message } }
}
/** E2B-backed runtime: host-side type stripping, remote worker execution, host binding dispatch. */
export class E2BCodeRuntime extends CodeRuntime {
static inject = ['e2b', 'subprocess']
static Config: z<Config> = z.object({
computeMs: z.number().default(60_000),
maxWallMs: z.number().default(600_000),
maxOutputBytes: z.number().default(67_108_864),
maxOldGenerationSizeMb: z.number().default(512),
maxFrameBytes: z.number().default(268_435_456),
killGraceMs: z.number().default(2_000),
})
readonly language = 'typescript'
readonly isolation = 'container'
private readonly config: ResolvedConfig
private readonly ready: Promise<{ node: string; runner: string }>
private readonly live = new Set<LiveRun>()
private readonly subprocess: E2BSubprocessService
private disposed = false
constructor(ctx: Context, config: Config) {
super(ctx)
if (!(ctx.subprocess instanceof E2BSubprocessService)) {
throw new Error('code-runtime-e2b requires @deepseek-ai/dsh-subprocess-e2b as ctx.subprocess')
}
this.subprocess = ctx.subprocess
this.config = config as ResolvedConfig
for (const [key, value] of Object.entries(this.config)) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`code-runtime-e2b: config.${key} must be a positive safe integer`)
}
}
if (this.config.maxOutputBytes < MIN_OUTPUT_BYTES) {
throw new Error(`code-runtime-e2b: config.maxOutputBytes must be at least ${MIN_OUTPUT_BYTES}`)
}
if (this.config.maxWallMs > MAX_TIMER_DELAY_MS) {
throw new Error(`code-runtime-e2b: config.maxWallMs must be at most ${MAX_TIMER_DELAY_MS}`)
}
if (this.config.maxFrameBytes < this.config.maxOutputBytes) {
throw new Error('code-runtime-e2b: config.maxFrameBytes must be at least maxOutputBytes')
}
this.ready = this.prepare()
void this.ready.catch(() => {})
ctx.effect(() => () => this.teardown(), 'E2B code-runtime teardown')
}
/* jscpd:ignore-start -- Seam-level abort and type-strip results remain identical across execution substrates. */
/** Execute one type-stripped program in a fresh E2B worker process. */
async run(request: CodeRunRequest): Promise<CodeRunResult> {
if (this.disposed) throw new Error('code-runtime-e2b: run() after disposal')
const bindings = this.validateBindings(request)
if (request.signal?.aborted === true) {
return this.failure({ kind: 'abort', message: String(request.signal.reason) })
}
let code: string
try {
const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + request.program + STRIP_WRAP.suffix)
code = stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length)
} catch (error: unknown) {
return this.failure({ kind: 'exception', message: messageOf(error) })
}
let runtime: Awaited<typeof this.ready>
try {
runtime = await this.ready
} catch (error: unknown) {
return this.failure({ kind: 'worker-exit', message: `E2B runtime setup failed: ${messageOf(error)}` })
}
// Disposal can race the awaited remote setup after the pre-await check.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.disposed) return this.failure({ kind: 'abort', message: 'runtime disposed' })
return await this.execute(request, code, bindings, runtime)
}
/* jscpd:ignore-end */
private async prepare(): Promise<{ node: string; runner: string }> {
const sandbox = await this.ctx.e2b.getSandbox()
const runner = posix.join(this.ctx.e2b.runtimeRoot, 'code-runtime-runner.mjs')
await sandbox.files.write([{ path: runner, data: CODE_RUNNER_SOURCE }])
await sandbox.commands.run(`chmod 600 -- ${quoteE2BShellArg(runner)}`)
const node = await resolveE2BExecutable(sandbox, 'node')
return { node, runner }
}
private failure(error: CodeRunFailure): CodeRunResult {
return new OutputLedger(this.config.maxOutputBytes).failure([], error)
}
/* jscpd:ignore-start -- Binding names have one seam contract while dispatch and teardown remain backend-owned. */
private validateBindings(request: CodeRunRequest): Map<string, CodeBindingNamespace> {
const bindings = new Map<string, CodeBindingNamespace>()
for (const namespace of request.bindings) {
if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) {
throw new Error(`code-runtime-e2b: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
}
if (namespace.global === 'console' || bindings.has(namespace.global)) {
throw new Error(`code-runtime-e2b: duplicate binding global ${JSON.stringify(namespace.global)}`)
}
bindings.set(namespace.global, namespace)
}
const errorClassNames = new Set<string>()
for (const namespace of request.bindings) {
const descriptor = namespace.errorClass
if (descriptor === undefined) continue
if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) {
throw new Error(`code-runtime-e2b: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
}
if (descriptor.name === 'console' || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
throw new Error(`code-runtime-e2b: duplicate injected global ${JSON.stringify(descriptor.name)}`)
}
if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) {
throw new Error(`code-runtime-e2b: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
}
errorClassNames.add(descriptor.name)
}
return bindings
}
/* jscpd:ignore-end */
private async execute(
request: CodeRunRequest,
code: string,
bindings: Map<string, CodeBindingNamespace>,
runtime: { node: string; runner: string },
): Promise<CodeRunResult> {
const handle = this.subprocess.spawn({
argv: [runtime.node, runtime.runner],
cwd: this.ctx.e2b.cwd,
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: this.config.maxOutputBytes } },
graceMs: this.config.killGraceMs,
...request.signal === undefined ? {} : { signal: request.signal },
env: {},
})
if (handle.stdin === undefined || handle.stdout === undefined) {
handle.terminate()
await Promise.allSettled([handle.done])
try {
await handle.waitForExit()
} catch (error: unknown) {
return this.failure({ kind: 'worker-exit', message: `E2B runtime cleanup failed: ${messageOf(error)}` })
}
return this.failure({ kind: 'worker-exit', message: 'E2B subprocess dropped a piped runtime stream' })
}
const stdin = handle.stdin
const stdout = handle.stdout
return new Promise<CodeRunResult>((resolve) => {
const output = new OutputLedger(this.config.maxOutputBytes)
const logs: string[] = []
const answered = new Set<number>()
const decoder = new E2BFrameDecoder(this.config.maxFrameBytes)
let settled = false
let finishResolve!: () => void
const finished = new Promise<void>((done) => { finishResolve = done })
const wallTimer: { current: NodeJS.Timeout | undefined } = { current: undefined }
const live: LiveRun = {
finished,
settle: (failure) => { finish(() => output.failure(logs, failure)) },
}
const finish = (result: CodeRunResult | (() => CodeRunResult)): void => {
if (settled) return
settled = true
clearTimeout(wallTimer.current)
request.signal?.removeEventListener('abort', onAbort)
this.live.delete(live)
void new Promise<void>((resume) => { setImmediate(resume) }).then(async () => {
handle.terminate()
await handle.done.catch(() => {})
let cleanupError: unknown
try {
await handle.waitForExit()
} catch (error: unknown) {
cleanupError = error
}
try {
decoder.finish()
} catch (error: unknown) {
result = output.failure(logs, { kind: 'worker-exit', message: messageOf(error) })
}
if (cleanupError !== undefined) {
result = output.failure(logs, { kind: 'worker-exit', message: `E2B runtime cleanup failed: ${messageOf(cleanupError)}` })
}
const final = typeof result === 'function' ? result() : result
finishResolve()
resolve(final)
})
}
const sendReply = (message: unknown): void => {
if (settled) return
stdin.write(encodeE2BFrame(message), (error?: Error | null) => {
if (error !== undefined && error !== null) {
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime bridge write failed: ${error.message}` }))
}
})
}
/* jscpd:ignore-start -- Host binding resolution mirrors worker semantics over a different transport. */
const onCall = (message: CallMessage): void => {
if (answered.has(message.id)) return
answered.add(message.id)
const functions = bindings.get(message.global)?.functions
const fn = functions !== undefined && Object.hasOwn(functions, message.name) ? functions[message.name] : undefined
if (typeof fn !== 'function') {
sendReply({ type: 'reply', id: message.id, ok: false, message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}` })
return
}
const args = decodeWorkerJson(message.args)
if (args === undefined) {
sendReply({ type: 'reply', id: message.id, ok: false, message: 'binding arguments must be lossless JSON' })
return
}
void (async () => {
try {
const resolved = await fn(args)
let value: CodeJsonValue | undefined
try {
value = snapshotJsonValue(resolved)
} catch {
value = undefined
}
if (value === undefined) {
sendReply({ type: 'reply', id: message.id, ok: false, message: 'binding resolution must be lossless JSON' })
} else {
sendReply({ type: 'reply', id: message.id, ok: true, value: encodeWorkerJson(value) })
}
} catch (error: unknown) {
sendReply({ type: 'reply', id: message.id, ok: false, message: messageOf(error) })
}
})()
}
/* jscpd:ignore-end */
const onMessage = (raw: unknown): void => {
if (settled) return
const message = parseRunnerMessage(raw)
if (message === undefined) return
if (message.type === 'log') {
if (!output.admit(message.text, logs)) finish(output.limit([...logs, message.text]))
return
}
if (message.type === 'output-limit') {
finish(output.limit(logs))
return
}
if (message.type === 'call') {
onCall(message)
return
}
if (message.error !== undefined) {
finish(() => output.failure(logs, message.error as CodeRunFailure))
} else if (message.value === undefined) {
finish(() => output.success(logs))
} else {
const value = decodeWorkerJson(message.value)
if (value === undefined) finish(() => output.failure(logs, { kind: 'invalid-output', message: 'program completion must be lossless JSON' }))
else finish(() => output.success(logs, value))
}
}
stdout.on('data', (chunk: Buffer) => {
if (settled) return
try {
for (const frame of decoder.push(chunk.toString('utf8'))) onMessage(frame)
} catch (error: unknown) {
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime bridge failed: ${messageOf(error)}` }))
}
})
stdout.on('error', (error: Error) => {
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime stdout failed: ${error.message}` }))
})
stdin.on('error', (error: Error) => {
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime stdin failed: ${error.message}` }))
})
void handle.done.then(
() => {
if (!settled) {
const stderr = handle.collected.stderr?.readFrom(0).text.trim()
finish(() => output.failure(logs, { kind: 'worker-exit', message: stderr === undefined || stderr === '' ? 'E2B runtime exited before completing' : `E2B runtime exited before completing: ${stderr}` }))
}
},
(error: unknown) => {
finish(() => output.failure(logs, { kind: 'worker-exit', message: `E2B runtime spawn failed: ${messageOf(error)}` }))
},
)
const onAbort = (): void => {
finish(() => output.failure(logs, { kind: 'abort', message: String(request.signal?.reason) }))
}
request.signal?.addEventListener('abort', onAbort, { once: true })
wallTimer.current = setTimeout(() => {
finish(() => output.failure(logs, { kind: 'timeout', message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)` }))
}, this.config.maxWallMs)
this.live.add(live)
if (request.signal?.aborted === true) {
onAbort()
return
}
sendReply({
type: 'boot',
code,
namespaces: [...bindings].map(([global, namespace]) => ({
global,
names: Object.keys(namespace.functions),
...namespace.errorClass === undefined ? {} : { errorClass: namespace.errorClass },
})),
computeMs: this.config.computeMs,
maxOutputBytes: this.config.maxOutputBytes,
maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb,
})
})
}
/* jscpd:ignore-start -- Code-runtime backends share the service lifecycle but own different child identities. */
private async teardown(): Promise<void> {
this.disposed = true
const runs = [...this.live]
for (const run of runs) run.settle({ kind: 'abort', message: 'runtime disposed' })
await Promise.all(runs.map(run => run.finished))
}
/* jscpd:ignore-end */
}
export default E2BCodeRuntime

View File

@@ -0,0 +1,20 @@
/** Package-owned invariant companion for `@deepseek-ai/dsh-code-runtime-e2b`. */
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-code-runtime-e2b'
/** Cordis companion plugin name. */
export const name = 'code-runtime-e2b-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/** No runtime invariant: the service owns every one-shot remote run. */
const install: InvariantInstaller = () => {}
/** Register this package's invariant companion. */
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,460 @@
/** Dependency-free remote code runner installed inside the E2B sandbox. */
/** Node program that runs one model program in a fresh remote worker thread. */
export const CODE_RUNNER_SOURCE = String.raw`import { Buffer } from 'node:buffer'
import { inspect } from 'node:util'
import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads'
import { createInterface } from 'node:readline'
const emitFrame = message => {
process.stdout.write(Buffer.from(JSON.stringify(message)).toString('base64') + '\n')
}
const parseFrame = line => JSON.parse(Buffer.from(line, 'base64').toString('utf8'))
if (isMainThread) {
const input = createInterface({ input: process.stdin, crlfDelay: Infinity })
let worker
let finished = false
let computeTimer
const finish = message => {
if (finished) return
finished = true
clearInterval(computeTimer)
emitFrame(message)
const current = worker
worker = undefined
Promise.resolve(current ? current.terminate() : undefined).finally(() => {
input.close()
process.stdin.destroy()
})
}
input.on('line', line => {
let message
try {
message = parseFrame(line)
} catch (error) {
process.stderr.write('code-runtime-e2b frame error: ' + String(error) + '\n')
finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote runner received a malformed frame' } })
return
}
if (!worker) {
if (!message || message.type !== 'boot' || typeof message.code !== 'string' || !Array.isArray(message.namespaces)) {
finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote runner received an invalid boot frame' } })
return
}
worker = new Worker(new URL(import.meta.url), {
workerData: message,
env: {},
stdout: true,
stderr: true,
resourceLimits: { maxOldGenerationSizeMb: message.maxOldGenerationSizeMb },
})
worker.stdout.on('data', data => { emitFrame({ type: 'log', text: data.toString('utf8') }) })
worker.stderr.on('data', data => { emitFrame({ type: 'log', text: data.toString('utf8') }) })
worker.on('message', raw => {
if (!raw || typeof raw !== 'object') return
if (raw.type === 'call' && typeof raw.id === 'number' && typeof raw.global === 'string' && typeof raw.name === 'string' && Array.isArray(raw.args)) {
emitFrame({ type: 'call', id: raw.id, global: raw.global, name: raw.name, args: raw.args })
} else if (raw.type === 'log' && typeof raw.text === 'string') {
emitFrame({ type: 'log', text: raw.text })
} else if (raw.type === 'output-limit') {
finish({ type: 'output-limit' })
} else if (raw.type === 'done') {
if (raw.error && typeof raw.error === 'object' && typeof raw.error.kind === 'string' && typeof raw.error.message === 'string') {
finish({ type: 'done', error: { kind: raw.error.kind, message: raw.error.message } })
} else if (raw.value === undefined || Array.isArray(raw.value)) {
finish({ type: 'done', ...(raw.value === undefined ? {} : { value: raw.value }) })
}
}
})
worker.on('error', error => {
finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote worker error: ' + error.message } })
})
worker.on('exit', code => {
if (!finished) finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote worker exited with code ' + code + ' before completing' } })
})
computeTimer = setInterval(() => {
if (!worker) return
if (worker.performance.eventLoopUtilization().active > message.computeMs) {
finish({ type: 'done', error: { kind: 'timeout', message: 'compute budget exhausted (' + message.computeMs + 'ms busy)' } })
}
}, 25)
return
}
if (message && message.type === 'reply' && typeof message.id === 'number' && typeof message.ok === 'boolean') {
worker.postMessage(message.ok
? { type: 'reply', id: message.id, ok: true, value: message.value }
: { type: 'reply', id: message.id, ok: false, message: String(message.message) })
}
})
input.on('close', () => { if (worker && !finished) void worker.terminate() })
} else {
const port = parentPort
if (!port) throw new Error('remote worker requires parentPort')
const CapturedError = Error
const ArrayIsArray = Array.isArray
const ArrayPrototype = Array.prototype
const ObjectPrototype = Object.prototype
const ObjectCreate = Object.create
const ObjectDefineProperty = Object.defineProperty
const ObjectGetPrototypeOf = Object.getPrototypeOf
const ObjectHasOwn = Object.hasOwn
const ObjectKeys = Object.keys
const ObjectIs = Object.is
const ObjectPropertyIsEnumerable = Object.prototype.propertyIsEnumerable
const ReflectOwnKeys = Reflect.ownKeys
const ReflectApply = Reflect.apply
const NumberIsFinite = Number.isFinite
const NumberIsSafeInteger = Number.isSafeInteger
const PromiseCtor = Promise
const PromiseReject = Promise.reject
const QueueMicrotask = queueMicrotask
const BufferByteLength = Buffer.byteLength
const SetCtor = Set
const SetAdd = Set.prototype.add
const SetDelete = Set.prototype.delete
const SetHas = Set.prototype.has
const MapDelete = Map.prototype.delete
const MapGet = Map.prototype.get
const MapSet = Map.prototype.set
const ArrayJoin = Array.prototype.join
const ArrayPop = Array.prototype.pop
const StringCharCodeAt = String.prototype.charCodeAt
const StringSlice = String.prototype.slice
const JSONStringify = JSON.stringify
const StringValue = String
const define = (target, key, value) => {
const descriptor = ObjectCreate(null)
descriptor.value = value
descriptor.enumerable = true
descriptor.configurable = true
descriptor.writable = true
ObjectDefineProperty(target, key, descriptor)
}
const append = (target, value) => { define(target, target.length, value) }
const pop = target => ReflectApply(ArrayPop, target, [])
const setAdd = (target, value) => { ReflectApply(SetAdd, target, [value]) }
const setDelete = (target, value) => { ReflectApply(SetDelete, target, [value]) }
const setHas = (target, value) => ReflectApply(SetHas, target, [value])
const mapDelete = (target, key) => { ReflectApply(MapDelete, target, [key]) }
const mapGet = (target, key) => ReflectApply(MapGet, target, [key])
const mapSet = (target, key, value) => { ReflectApply(MapSet, target, [key, value]) }
const plainObject = value => {
const prototype = ObjectGetPrototypeOf(value)
return prototype === null || prototype === ObjectPrototype
}
const ownEnumerableStringKeys = value => {
const keys = ReflectOwnKeys(value)
for (let index = 0; index < keys.length; index++) {
const key = keys[index]
if (typeof key !== 'string' || !ReflectApply(ObjectPropertyIsEnumerable, value, [key])) return undefined
}
return keys
}
const assign = (destination, value) => {
if (destination.kind === 'root') destination.holder.value = value
else define(destination.target, destination.key, value)
}
const snapshot = input => {
const active = new SetCtor()
const holder = ObjectCreate(null)
const tasks = [{ kind: 'visit', value: input, destination: { kind: 'root', holder } }]
while (tasks.length) {
const task = pop(tasks)
if (task.kind === 'leave') { setDelete(active, task.source); continue }
const candidate = task.value
if (candidate === null || typeof candidate === 'boolean' || typeof candidate === 'string') {
assign(task.destination, candidate); continue
}
if (typeof candidate === 'number') {
if (!NumberIsFinite(candidate) || ObjectIs(candidate, -0)) return undefined
assign(task.destination, candidate); continue
}
if (typeof candidate !== 'object' || setHas(active, candidate)) return undefined
if (ArrayIsArray(candidate)) {
if (ObjectGetPrototypeOf(candidate) !== ArrayPrototype || ReflectOwnKeys(candidate).length !== candidate.length + 1) return undefined
const target = []
assign(task.destination, target)
setAdd(active, candidate)
append(tasks, { kind: 'leave', source: candidate })
for (let index = candidate.length - 1; index >= 0; index--) {
if (!ObjectHasOwn(candidate, index)) return undefined
append(tasks, { kind: 'visit', value: candidate[index], destination: { kind: 'slot', target, key: index } })
}
continue
}
if (!plainObject(candidate)) return undefined
const keys = ownEnumerableStringKeys(candidate)
if (!keys) return undefined
const target = {}
assign(task.destination, target)
setAdd(active, candidate)
append(tasks, { kind: 'leave', source: candidate })
for (let index = keys.length - 1; index >= 0; index--) {
const key = keys[index]
append(tasks, { kind: 'visit', value: candidate[key], destination: { kind: 'slot', target, key } })
}
}
return holder.value
}
const encodeWire = value => {
const wire = []
const pending = [value]
while (pending.length) {
const current = pop(pending)
if (current === null || typeof current === 'boolean' || typeof current === 'number' || typeof current === 'string') {
append(wire, current); continue
}
if (ArrayIsArray(current)) {
append(wire, { kind: 'array', length: current.length })
for (let index = current.length - 1; index >= 0; index--) append(pending, current[index])
} else {
const keys = ObjectKeys(current)
append(wire, { kind: 'object', keys })
for (let index = keys.length - 1; index >= 0; index--) append(pending, current[keys[index]])
}
}
return wire
}
const decodeWire = wire => {
if (!ArrayIsArray(wire) || wire.length === 0) return undefined
const frames = []
let root
let assigned = false
const attach = value => {
const parent = frames[frames.length - 1]
if (!parent) {
if (assigned) return false
root = value; assigned = true; return true
}
if (parent.kind === 'array') append(parent.target, value)
else define(parent.target, parent.keys[parent.index], value)
parent.index += 1
return true
}
for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
const token = wire[tokenIndex]
let value
let frame
if (token === null || typeof token === 'boolean' || typeof token === 'string') value = token
else if (typeof token === 'number') {
if (!NumberIsFinite(token) || ObjectIs(token, -0)) return undefined
value = token
} else {
if (!plainObject(token)) return undefined
const keys = ownEnumerableStringKeys(token)
if (!keys || keys.length !== 2 || keys[0] !== 'kind') return undefined
if (token.kind === 'array' && keys[1] === 'length' && NumberIsSafeInteger(token.length) && token.length >= 0) {
value = []
if (token.length > wire.length - tokenIndex - 1) return undefined
if (token.length) frame = { kind: 'array', target: value, length: token.length, index: 0 }
} else if (token.kind === 'object' && keys[1] === 'keys' && ArrayIsArray(token.keys)) {
const unique = new SetCtor()
const objectKeys = []
for (const key of token.keys) {
if (typeof key !== 'string' || setHas(unique, key)) return undefined
setAdd(unique, key); append(objectKeys, key)
}
if (objectKeys.length > wire.length - tokenIndex - 1) return undefined
value = {}
if (objectKeys.length) frame = { kind: 'object', target: value, keys: objectKeys, index: 0 }
} else return undefined
}
if (!attach(value)) return undefined
if (frame) append(frames, frame)
while (frames.length) {
const current = frames[frames.length - 1]
const length = current.kind === 'array' ? current.length : current.keys.length
if (current.index < length) break
pop(frames)
}
}
return frames.length === 0 ? root : undefined
}
const byteLength = text => ReflectApply(BufferByteLength, Buffer, [text])
const jsonStringBytes = text => byteLength(JSONStringify(text))
const jsonValueBytes = value => {
let bytes = 0
const tasks = [{ kind: 'value', value }]
while (tasks.length) {
const task = pop(tasks)
if (task.kind === 'separator') { bytes += 1; continue }
if (task.kind === 'key') { bytes += jsonStringBytes(task.value) + 1; continue }
const current = task.value
if (current === null) bytes += 4
else if (typeof current === 'string') bytes += jsonStringBytes(current)
else if (typeof current === 'number' || typeof current === 'boolean') bytes += byteLength(StringValue(current))
else if (ArrayIsArray(current)) {
bytes += 2
for (let index = current.length - 1; index >= 0; index--) {
append(tasks, { kind: 'value', value: current[index] })
if (index > 0) append(tasks, { kind: 'separator' })
}
} else {
bytes += 2
const keys = ObjectKeys(current)
for (let index = keys.length - 1; index >= 0; index--) {
const key = keys[index]
append(tasks, { kind: 'value', value: current[key] })
append(tasks, { kind: 'key', value: key })
if (index > 0) append(tasks, { kind: 'separator' })
}
}
}
return bytes
}
const truncate = (text, available) => {
if (available < 2) return ''
let result = ''
let bytes = 2
let index = 0
while (index < text.length) {
const first = ReflectApply(StringCharCodeAt, text, [index])
let end = index + 1
if (first >= 0xd800 && first <= 0xdbff && end < text.length) {
const second = ReflectApply(StringCharCodeAt, text, [end])
if (second >= 0xdc00 && second <= 0xdfff) end += 1
}
const character = ReflectApply(StringSlice, text, [index, end])
const cost = jsonStringBytes(character) - 2
if (bytes + cost > available) break
bytes += cost
result += character
index = end
}
return result
}
let logBytes = 2
let logEntries = 0
let limited = false
const pushLog = text => {
if (limited) return
const separator = logEntries > 0 ? 1 : 0
const available = workerData.maxOutputBytes - logBytes - separator
const cost = jsonStringBytes(text)
if (cost > available) {
const prefix = truncate(text, available)
if (prefix) {
logBytes += jsonStringBytes(prefix) + separator
logEntries += 1
port.postMessage({ type: 'log', text: prefix })
}
limited = true
port.postMessage({ type: 'output-limit' })
return
}
logBytes += cost + separator
logEntries += 1
port.postMessage({ type: 'log', text })
}
const originalStdout = process.stdout.write
const originalStderr = process.stderr.write
process.stdout.write = (chunk, ...rest) => {
pushLog(typeof chunk === 'string' ? chunk : StringValue(chunk))
let callback
for (let index = 0; index < rest.length; index++) {
if (typeof rest[index] === 'function') { callback = rest[index]; break }
}
if (callback) QueueMicrotask(() => { callback(null) })
return true
}
process.stderr.write = process.stdout.write
const consoleShim = ObjectCreate(null)
for (const level of ['log', 'info', 'warn', 'error', 'debug']) {
define(consoleShim, level, (...args) => {
const rendered = []
for (let index = 0; index < args.length; index++) {
const value = args[index]
append(rendered, typeof value === 'string' ? value : inspect(value, { depth: 4, maxArrayLength: 100, maxStringLength: 10000 }))
}
pushLog(ReflectApply(ArrayJoin, rendered, [' ']))
})
}
const pending = new Map()
let nextId = 1
const errorClasses = new Map()
for (const namespace of workerData.namespaces) {
if (!namespace.errorClass) continue
const descriptor = namespace.errorClass
mapSet(errorClasses, namespace.global, class BindingCallError extends CapturedError {
constructor(memberName, message) {
super(message)
ObjectDefineProperty(this, 'name', { value: descriptor.name, enumerable: true })
ObjectDefineProperty(this, descriptor.memberNameProperty, { value: memberName, enumerable: true })
}
})
}
port.on('message', message => {
if (!message || message.type !== 'reply' || typeof message.id !== 'number') return
const entry = mapGet(pending, message.id)
if (!entry) return
mapDelete(pending, message.id)
if (!message.ok) { entry.reject(new CapturedError(StringValue(message.message))); return }
const value = decodeWire(message.value)
if (value === undefined) entry.reject(new CapturedError('binding resolution must be lossless JSON'))
else entry.resolve(value)
})
const namespaces = workerData.namespaces.map(namespace => {
const target = ObjectCreate(null)
const ErrorClass = mapGet(errorClasses, namespace.global)
for (const name of namespace.names) {
define(target, name, args => {
const detached = snapshot(args)
if (detached === undefined) {
return ReflectApply(PromiseReject, PromiseCtor, [ErrorClass ? new ErrorClass(name, 'binding arguments must be lossless JSON') : new CapturedError('binding arguments must be lossless JSON')])
}
return new PromiseCtor((resolve, reject) => {
const id = nextId++
mapSet(pending, id, {
resolve,
reject: error => { reject(ErrorClass ? new ErrorClass(name, error.message) : error) },
})
port.postMessage({ type: 'call', id, global: namespace.global, name, args: encodeWire(detached) })
})
})
}
return target
})
const errorClassNames = []
const errorClassValues = []
for (const namespace of workerData.namespaces) {
if (!namespace.errorClass) continue
append(errorClassNames, namespace.errorClass.name)
append(errorClassValues, mapGet(errorClasses, namespace.global))
}
const AsyncFunction = ObjectGetPrototypeOf(async function () {}).constructor
try {
const fn = new AsyncFunction(...workerData.namespaces.map(value => value.global), ...errorClassNames, 'console', '"use strict";\n' + workerData.code)
const value = await fn(...namespaces, ...errorClassValues, consoleShim)
if (!limited) {
if (value === undefined) port.postMessage({ type: 'done' })
else {
const detached = snapshot(value)
if (detached === undefined) {
const message = 'program completion must be lossless JSON'
if (jsonStringBytes(message) > workerData.maxOutputBytes - logBytes) port.postMessage({ type: 'output-limit' })
else port.postMessage({ type: 'done', error: { kind: 'invalid-output', message } })
} else if (jsonValueBytes(detached) > workerData.maxOutputBytes - logBytes) {
port.postMessage({ type: 'output-limit' })
} else {
port.postMessage({ type: 'done', value: encodeWire(detached) })
}
}
}
} catch (error) {
if (!limited) {
let message
try { message = error instanceof CapturedError ? error.stack || error.message : StringValue(error) }
catch { message = 'program threw an unrenderable value' }
if (jsonStringBytes(message) > workerData.maxOutputBytes - logBytes) port.postMessage({ type: 'output-limit' })
else port.postMessage({ type: 'done', error: { kind: 'exception', message } })
}
} finally {
process.stdout.write = originalStdout
process.stderr.write = originalStderr
}
}
`

View File

@@ -0,0 +1,460 @@
import { PassThrough, Writable } from 'node:stream'
import { Context } from 'cordis'
import { describe, expect, it, vi } from 'vitest'
import type { Sandbox } from '@deepseek-ai/dsh-e2b'
import {
E2BFrameDecoder,
encodeE2BFrame,
} from '@deepseek-ai/dsh-e2b'
import type E2BSandboxService from '@deepseek-ai/dsh-e2b'
import type {
SubprocessHandle,
SubprocessOutcome,
SubprocessSpawnSpec,
} from '@deepseek-ai/dsh-subprocess'
import E2BSubprocessService from '@deepseek-ai/dsh-subprocess-e2b'
import {
encodeWorkerJson,
} from '@deepseek-ai/dsh-code-runtime-worker'
import E2BCodeRuntime from '@deepseek-ai/dsh-code-runtime-e2b'
import * as E2BCodeRuntimeInvariant from '../src/invariant.ts'
import { CODE_RUNNER_SOURCE } from '../src/runner-source.ts'
import InvariantService from '@deepseek-ai/dsh-invariants'
class FakeHandle implements SubprocessHandle {
readonly pid = 123
readonly stdin: Writable | undefined
readonly stdout: PassThrough | undefined
readonly stderr = undefined
readonly collected: SubprocessHandle['collected']
readonly done: Promise<SubprocessOutcome>
readonly writes: unknown[] = []
readonly result = Promise.withResolvers<SubprocessOutcome>()
terminated = 0
waitCalls = 0
private readonly decoder = new E2BFrameDecoder(10_000_000)
private readonly waitError: Error | undefined
private settled = false
constructor(
private readonly onMessage: (message: unknown, handle: FakeHandle) => void = () => {},
options: { stdin?: boolean; stdout?: boolean; stderr?: string; writeError?: Error; waitError?: Error } = {},
) {
this.waitError = options.waitError
this.stdin = options.stdin === false
? undefined
: options.writeError === undefined
? new PassThrough()
: new Writable({ write: (_chunk, _encoding, callback) => { callback(options.writeError) } })
this.stdout = options.stdout === false ? undefined : new PassThrough()
this.collected = options.stderr === undefined
? {}
: { stderr: { readFrom: () => ({ text: options.stderr as string, nextOffset: 0, lossy: false }) } }
this.done = this.result.promise
this.stdin?.on('data', (chunk: Buffer) => {
for (const message of this.decoder.push(chunk.toString('ascii'))) {
this.writes.push(message)
this.onMessage(message, this)
}
})
}
emit(message: unknown): void {
this.stdout?.write(encodeE2BFrame(message))
}
emitRaw(text: string): void {
this.stdout?.write(text)
}
exit(outcome: SubprocessOutcome = { exitCode: 0, signal: null }): void {
if (this.settled) return
this.settled = true
this.stdout?.end()
this.result.resolve(outcome)
}
crash(error: unknown): void {
if (this.settled) return
this.settled = true
this.stdout?.end()
this.result.reject(error)
}
terminate(): void {
this.terminated += 1
this.exit({ exitCode: null, signal: 'SIGTERM' })
}
async waitForExit(): Promise<boolean> {
this.waitCalls += 1
if (this.waitError !== undefined) throw this.waitError
return true
}
}
interface RuntimeFixture {
ctx: Context
fiber: Awaited<ReturnType<Context['plugin']>>
runtime: E2BCodeRuntime
sandbox: Sandbox
spawn: ReturnType<typeof vi.fn<(spec: SubprocessSpawnSpec) => SubprocessHandle>>
write: ReturnType<typeof vi.fn>
run: ReturnType<typeof vi.fn>
}
async function setup(
handles: FakeHandle[] = [],
config: Record<string, number> = {},
sandboxOverrides: Partial<Sandbox> = {},
getSandbox?: () => Promise<Sandbox>,
): Promise<RuntimeFixture> {
const write = vi.fn().mockResolvedValue([])
const run = vi.fn().mockImplementation(async (command: string) => ({
exitCode: 0,
stdout: command.startsWith('command -v') ? '/usr/bin/node\n' : '',
stderr: '',
}))
const sandbox = {
files: { write },
commands: { run },
...sandboxOverrides,
} as unknown as Sandbox
const e2b = {
cwd: '/workspace',
runtimeRoot: '/workspace/.dsh-e2b',
getSandbox: getSandbox ?? (async () => sandbox),
} as unknown as E2BSandboxService
const spawn = vi.fn<(spec: SubprocessSpawnSpec) => SubprocessHandle>(() => {
const handle = handles.shift()
if (handle === undefined) throw new Error('no fake handle queued')
return handle
})
const subprocess = Object.create(E2BSubprocessService.prototype) as E2BSubprocessService
Object.defineProperty(subprocess, 'spawn', { value: spawn })
const ctx = new Context()
ctx.provide('e2b', e2b)
ctx.provide('subprocess', subprocess)
const fiber = await ctx.plugin(E2BCodeRuntime, config)
return { ctx, fiber, runtime: ctx.codeRuntime as E2BCodeRuntime, sandbox, spawn, write, run }
}
function request(program = 'return 1') {
return { program, bindings: [] }
}
describe('E2BCodeRuntime', () => {
it('prepares the remote runner and returns logs and a lossless completion', async () => {
const handle = new FakeHandle((message, current) => {
if ((message as { type?: string }).type !== 'boot') return
current.emit({ type: 'log', text: 'remote 你好' })
current.emitRaw(
encodeE2BFrame({ type: 'done', value: encodeWorkerJson({ answer: 42 }) })
+ encodeE2BFrame({ type: 'log', text: 'ignored after done' }),
)
current.emit({ type: 'log', text: 'also ignored after done' })
})
const fixture = await setup([handle])
await expect(fixture.runtime.run(request('const answer: number = 42; return { answer }')))
.resolves.toEqual({ logs: ['remote 你好'], value: { answer: 42 } })
expect(fixture.runtime.language).toBe('typescript')
expect(fixture.runtime.isolation).toBe('container')
expect(fixture.write).toHaveBeenCalledWith([{ path: '/workspace/.dsh-e2b/code-runtime-runner.mjs', data: CODE_RUNNER_SOURCE }])
expect(fixture.run).toHaveBeenCalledWith("chmod 600 -- '/workspace/.dsh-e2b/code-runtime-runner.mjs'")
expect(fixture.spawn).toHaveBeenCalledWith(expect.objectContaining({
argv: ['/usr/bin/node', '/workspace/.dsh-e2b/code-runtime-runner.mjs'],
cwd: '/workspace',
env: {},
}))
expect(handle.terminated).toBe(1)
expect(handle.waitCalls).toBe(1)
await fixture.fiber.dispose()
})
it('bridges binding success, host rejection, unknown members, and invalid values', async () => {
const replies: unknown[] = []
const handle = new FakeHandle((message, current) => {
const record = message as { type?: string; id?: number; ok?: boolean }
if (record.type === 'boot') {
current.emit({ type: 'call', id: 1, global: 'bridge', name: 'double', args: encodeWorkerJson({ value: 4 }) })
current.emit({ type: 'call', id: 2, global: 'bridge', name: 'fail', args: encodeWorkerJson(null) })
current.emit({ type: 'call', id: 3, global: 'bridge', name: 'missing', args: encodeWorkerJson(null) })
current.emit({ type: 'call', id: 4, global: 'bridge', name: 'double', args: [] })
current.emit({ type: 'call', id: 5, global: 'bridge', name: 'invalid', args: encodeWorkerJson(null) })
current.emit({ type: 'call', id: 6, global: 'bridge', name: 'throwing', args: encodeWorkerJson(null) })
current.emit({ type: 'call', id: 1, global: 'bridge', name: 'double', args: encodeWorkerJson({ value: 99 }) })
return
}
if (record.type === 'reply') {
replies.push(message)
if (replies.length === 6) current.emit({ type: 'done', value: encodeWorkerJson('done') })
}
})
const fixture = await setup([handle])
const result = await fixture.runtime.run({
program: 'return await bridge.double({ value: 4 })',
bindings: [
{
global: 'bridge',
errorClass: { name: 'BridgeError', memberNameProperty: 'member' },
functions: {
double: async args => (args as { value: number }).value * 2,
fail: async () => { throw 'nope' },
invalid: (async () => undefined) as never,
throwing: async () => Object.defineProperty({}, 'value', {
enumerable: true,
get: () => { throw new Error('getter failed') },
}),
},
},
{ global: 'plain', functions: {} },
],
})
expect(result).toEqual({ logs: [], value: 'done' })
expect(replies.sort((left, right) => (left as { id: number }).id - (right as { id: number }).id)).toEqual([
{ type: 'reply', id: 1, ok: true, value: encodeWorkerJson(8) },
{ type: 'reply', id: 2, ok: false, message: 'nope' },
{ type: 'reply', id: 3, ok: false, message: 'unknown binding "bridge.missing"' },
{ type: 'reply', id: 4, ok: false, message: 'binding arguments must be lossless JSON' },
{ type: 'reply', id: 5, ok: false, message: 'binding resolution must be lossless JSON' },
{ type: 'reply', id: 6, ok: false, message: 'binding resolution must be lossless JSON' },
])
await fixture.fiber.dispose()
})
it('ignores malformed runner traffic and classifies terminal runner messages', async () => {
const ignored = [
null, 1, {}, { type: 'log' }, { type: 'call' },
{ type: 'call', id: 0, global: 'x', name: 'y', args: [] },
{ type: 'call', id: 1, global: 1, name: 'y', args: [] },
{ type: 'call', id: 1, global: 'x', name: 1, args: [] },
{ type: 'call', id: 1, global: 'x', name: 'y', args: {} },
{ type: 'done', error: null },
{ type: 'done', error: { kind: 'invented', message: 'x' } },
{ type: 'done', error: { kind: 'exception', message: 1 } },
]
const handles = [
new FakeHandle((message, current) => {
if ((message as { type?: string }).type !== 'boot') return
for (const item of ignored) current.emit(item)
current.emit({ type: 'done' })
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emit({ type: 'done', error: { kind: 'exception', message: 'boom' } })
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emit({ type: 'done', value: [] })
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emit({ type: 'output-limit' })
}),
]
const fixture = await setup(handles, { maxOutputBytes: 64, maxFrameBytes: 128 })
await expect(fixture.runtime.run(request())).resolves.toEqual({ logs: [] })
await expect(fixture.runtime.run(request())).resolves.toEqual({ logs: [], error: { kind: 'exception', message: 'boom' } })
await expect(fixture.runtime.run(request())).resolves.toEqual({ logs: [], error: { kind: 'invalid-output', message: 'program completion must be lossless JSON' } })
await expect(fixture.runtime.run(request())).resolves.toEqual({ logs: [], error: { kind: 'output-limit', message: 'outer output exceeded 64 bytes' } })
await fixture.fiber.dispose()
})
it('enforces the host output ledger and catches malformed bridge output', async () => {
const handles = [
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emit({ type: 'log', text: 'x'.repeat(1_000) })
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emitRaw('not-base64\n')
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emitRaw('é')
}),
new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.stdout?.emit('error', new Error('stdout broke'))
}),
]
const fixture = await setup(handles, { maxOutputBytes: 128, maxFrameBytes: 4_096 })
expect((await fixture.runtime.run(request())).error?.kind).toBe('output-limit')
const malformed = (await fixture.runtime.run(request())).error
expect(malformed?.kind).toBe('worker-exit')
expect(malformed?.message).toContain('bridge failed')
expect((await fixture.runtime.run(request())).error?.message).toContain('non-ASCII')
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime stdout failed: stdout broke' })
await fixture.fiber.dispose()
})
it('contains stdin errors, process exits, spawn failures, and missing pipes', async () => {
const writeError = new FakeHandle(() => {}, { writeError: new Error('write callback broke') })
const stdinError = new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.stdin?.emit('error', new Error('stdin broke'))
})
const earlyExit = new FakeHandle(() => {}, { stderr: 'remote diagnostic' })
const quietExit = new FakeHandle()
const emptyStderrExit = new FakeHandle(() => {}, { stderr: '' })
const spawnFailure = new FakeHandle()
const missingStdin = new FakeHandle(() => {}, { stdin: false })
const missingStdout = new FakeHandle(() => {}, { stdout: false, waitError: new Error('missing-stream process query failed') })
const truncated = new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') {
current.emitRaw('YQ==')
setImmediate(() => { current.exit() })
}
})
const cleanupFailure = new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') current.emit({ type: 'done' })
}, { waitError: new Error('process query failed') })
const fixture = await setup([
writeError, stdinError, earlyExit, quietExit, emptyStderrExit,
spawnFailure, missingStdin, missingStdout, truncated, cleanupFailure,
])
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime bridge write failed: write callback broke' })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime stdin failed: stdin broke' })
setImmediate(() => { earlyExit.exit() })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime exited before completing: remote diagnostic' })
setImmediate(() => { quietExit.exit() })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime exited before completing' })
setImmediate(() => { emptyStderrExit.exit() })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime exited before completing' })
setImmediate(() => { spawnFailure.crash('spawn rejected') })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime spawn failed: spawn rejected' })
expect((await fixture.runtime.run(request())).error?.message).toContain('dropped a piped runtime stream')
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime cleanup failed: missing-stream process query failed' })
expect(missingStdin.terminated).toBe(1)
expect(missingStdin.waitCalls).toBe(1)
expect(missingStdout.terminated).toBe(1)
expect(missingStdout.waitCalls).toBe(1)
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B frame stream ended mid-frame' })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'worker-exit', message: 'E2B runtime cleanup failed: process query failed' })
await fixture.fiber.dispose()
})
it('reports wall timeout, abort, pre-abort, type-strip failure, and disposal', async () => {
const timeout = new FakeHandle()
const abort = new FakeHandle()
const disposing = new FakeHandle()
const fixture = await setup([timeout, abort, disposing], { maxWallMs: 20 })
expect((await fixture.runtime.run(request())).error).toEqual({ kind: 'timeout', message: 'wall-clock ceiling reached (20ms)' })
const controller = new AbortController()
const aborting = fixture.runtime.run({ ...request(), signal: controller.signal })
controller.abort('stop')
expect((await aborting).error).toEqual({ kind: 'abort', message: 'stop' })
expect((await fixture.runtime.run({ ...request(), signal: AbortSignal.abort('already') })).error)
.toEqual({ kind: 'abort', message: 'already' })
expect((await fixture.runtime.run(request('enum E { A }'))).error?.kind).toBe('exception')
const live = fixture.runtime.run(request())
await new Promise(resolve => setImmediate(resolve))
await fixture.fiber.dispose()
expect((await live).error).toEqual({ kind: 'abort', message: 'runtime disposed' })
await expect(fixture.runtime.run(request())).rejects.toThrow('after disposal')
})
it('drops binding replies that settle after abort', async () => {
const controller = new AbortController()
const resolution = Promise.withResolvers<string>()
const invoked = Promise.withResolvers<undefined>()
const handle = new FakeHandle((message, current) => {
if ((message as { type?: string }).type === 'boot') {
current.emit({ type: 'call', id: 1, global: 'bridge', name: 'late', args: encodeWorkerJson(null) })
}
})
const fixture = await setup([handle])
const running = fixture.runtime.run({
program: 'return await bridge.late(null)',
bindings: [{
global: 'bridge',
functions: {
late: async () => {
invoked.resolve(undefined)
return await resolution.promise
},
},
}],
signal: controller.signal,
})
await invoked.promise
controller.abort('stop')
expect((await running).error).toEqual({ kind: 'abort', message: 'stop' })
resolution.resolve('late')
await new Promise(resolve => setImmediate(resolve))
expect(handle.writes).toHaveLength(1)
await fixture.fiber.dispose()
})
it('validates binding and runtime configuration before remote execution', async () => {
const fixture = await setup([])
const invalidRequests = [
{ global: 'not-valid!', functions: {} },
{ global: 'await', functions: {} },
{ global: 'console', functions: {} },
{ global: 'same', functions: {} },
{ global: 'same', functions: {} },
{ global: 'ok', functions: {}, errorClass: { name: 'not-valid!', memberNameProperty: 'member' } },
{ global: 'ok', functions: {}, errorClass: { name: 'await', memberNameProperty: 'member' } },
{ global: 'Clash', functions: {}, errorClass: { name: 'Clash', memberNameProperty: 'member' } },
{ global: 'one', functions: {}, errorClass: { name: 'Err', memberNameProperty: 'member' } },
{ global: 'two', functions: {}, errorClass: { name: 'Err', memberNameProperty: 'member' } },
{ global: 'ok', functions: {}, errorClass: { name: 'Err', memberNameProperty: '' } },
{ global: 'ok', functions: {}, errorClass: { name: 'Err', memberNameProperty: 'message' } },
]
for (const bindings of [
[invalidRequests[0]], [invalidRequests[1]], [invalidRequests[2]],
invalidRequests.slice(3, 5), [invalidRequests[5]], [invalidRequests[6]],
[invalidRequests[7]], invalidRequests.slice(8, 10), [invalidRequests[10]], [invalidRequests[11]],
]) {
await expect(fixture.runtime.run({ program: 'return 1', bindings: bindings as never })).rejects.toThrow()
}
await fixture.fiber.dispose()
for (const config of [
{ computeMs: 0 }, { computeMs: 1.5 }, { maxOutputBytes: 3 },
{ maxWallMs: 2_147_483_648 }, { maxFrameBytes: 10, maxOutputBytes: 20 },
]) {
const ctx = new Context()
const subprocess = Object.create(E2BSubprocessService.prototype) as E2BSubprocessService
ctx.provide('e2b', { getSandbox: async () => ({}) } as never)
ctx.provide('subprocess', subprocess)
await expect(ctx.plugin(E2BCodeRuntime, config)).rejects.toThrow()
}
const wrong = new Context()
wrong.provide('e2b', { getSandbox: async () => ({}) } as never)
wrong.provide('subprocess', {} as never)
await expect(wrong.plugin(E2BCodeRuntime, {})).rejects.toThrow('dsh-subprocess-e2b')
})
it('turns asynchronous runtime preparation failure into a run result', async () => {
const sandbox = {
files: { write: vi.fn().mockRejectedValue(new Error('upload failed')) },
commands: { run: vi.fn() },
} as unknown as Sandbox
const fixture = await setup([], {}, sandbox)
expect((await fixture.runtime.run(request())).error).toEqual({
kind: 'worker-exit',
message: 'E2B runtime setup failed: upload failed',
})
await fixture.fiber.dispose()
})
it('returns disposal when remote preparation completes after teardown', async () => {
const gate = Promise.withResolvers<Sandbox>()
const fixture = await setup([], {}, {}, () => gate.promise)
const running = fixture.runtime.run(request())
await (fixture.runtime as unknown as { teardown(): Promise<void> }).teardown()
gate.resolve(fixture.sandbox)
expect((await running).error).toEqual({ kind: 'abort', message: 'runtime disposed' })
await fixture.fiber.dispose()
})
it('registers the package-owned invariant companion', async () => {
const ctx = new Context()
await ctx.plugin(InvariantService, { enabled: true })
const fiber = await ctx.plugin(E2BCodeRuntimeInvariant).await()
await fiber.dispose()
})
})

View File

@@ -0,0 +1,20 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../code-runtime" },
{ "path": "../code-runtime-worker" },
{ "path": "../../e2b/e2b" },
{ "path": "../../core/session" },
{ "path": "../../subprocess/subprocess-e2b" },
{ "path": "../../util/timeout" },
{ "path": "../../support/invariants" }
]
}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-worker/README.md
README.md: 590b79dcd1bc322350060b55767b09c6305edacc
README.zh.md: 12c25f892bd20892cb47e593f16b6aadd9ffa84c
README.md: 4b14c6fad5d1e491faeb54c9cb0e4403c8e2d8dd
README.zh.md: c522917835b562cf7648d8bc78f0315b7c52d2d2

View File

@@ -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 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).
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).
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, 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.
- **OS processes a program spawns survive this backend's termination** — `worker.terminate()` ends only the thread; deployments needing remote process-group cleanup can select the E2B backend, whose separate limitations still apply.
- **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.
- **`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.

View File

@@ -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__` 的绑定名称只是普通键。
- **绑定调用被拒绝时使用的异常类属于请求数据**:可选命名空间描述符会指定构造器全局变量,以及用于接收调用失败成员名称的自有属性。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 退出后再完成
- **绑定 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 入口
源代码模式通过 Node 原生类型剥离加载只包含可擦除语法的 `src/worker.ts`。其传递运行时闭包只包含 Node 内置模块和相对源模块,因此全新 checkout 绝不需要兄弟工作区包尚未构建的 `lib/` 导出。worker 本地和会话自有的 JSON 边界都会在消息端口周围展平并重建已验证值,使应用嵌套永远不会进入 structured clone。构建模式会把兄弟文件 `lib/worker.cjs` 作为文件系统路径传入,因为 pkg 的虚拟文件系统(VFSWorker hook 要求 CommonJS同一路径也可在普通 Node 下使用。演练这个已发布入口路径的仓库级要求由[测试策略](../../../docs/testing.md)规定
源代码模式通过 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) 要求的真实加载路径
SDK 对外提供默认具名导出的 `WorkerCodeRuntime`,以及 `Config`运行所用`./worker` 子路径仅作为打包后的 spawn 入口存在wire 协议与启动辅助模块是源代码私有的实现细节。
SDK 接口是默认具名 `WorkerCodeRuntime` `Config`可操作`./worker` 子路径仅作为打包后的 spawn 入口存在wire 协议与启动辅助模块是源代码私有的实现细节。
## 模型体验
@@ -42,13 +42,13 @@ SDK 对外提供默认及具名导出的 `WorkerCodeRuntime` 类,以及 `Confi
#### KV Cache 影响
不会直接失效;由上述消费方负责请求前缀变更。
不会直接失效;由具名消费方负责请求前缀变更。
## 已知限制与暂缓事项
## 已知限制与暂缓工作
- **程序派生的 OS 进程在程序终止后仍会存活**`worker.terminate()` 只结束线程,比 bash-local 的进程组终止更弱;在容器后端出现前,孤儿进程清理属于部署职责
- **类型剥离依赖 Node 的实验性 `stripTypeScriptTypes` API**依赖的行为发生变化amarosucrase 是已经点名的直接替代品。
- **程序 spawn 的 OS 进程在该后端终止后仍会存活**`worker.terminate()` 只结束线程;需要清理远程进程组的部署可以选择 E2B 后端,但该后端自身的限制仍然适用
- **类型剥离依赖 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` 后返回的有界日志和诊断;在运行时上限之外被拒绝的字节永远不会到达落盘层。

View File

@@ -21,6 +21,10 @@ 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 {
/**
@@ -165,14 +169,19 @@ function parseWorkerMessage(raw: unknown): WorkerToHost | undefined {
}
/** One run's combined outer-output ledger; binding values never enter it. */
class OutputLedger {
/** Shared outer-output accounting for one isolated run; binding values never enter it. */
export 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. */
/**
* 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(text: string, sink: string[]): boolean {
const separatorBytes = this.entries > 0 ? 1 : 0
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes)
@@ -183,19 +192,33 @@ class OutputLedger {
return true
}
/** Finalize a successful absent-or-JSON completion against the combined cap. */
/**
* 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.
*/
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. */
/**
* 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.
*/
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. */
/**
* 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.
*/
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.

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime/README.md
README.md: bb1c20d00a260f643f601c42c6e48722437d5aab
README.zh.md: 15fbcecf77b2318acf3b09101802cd032ae426d2
README.md: c7690412d0f1bc8556fc758da4e94c6ce5a08d8f
README.zh.md: ecfa48a97c46113c52f13c7a6edfc5d6fbbc2285

View File

@@ -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)).
- **Only the worker-thread backend ships** — `'process'`/`'container'` are declared well-known `isolation` values with no implementation; a hard security boundary awaits a container backend.
- **Isolation is backend-specific** — the worker backend is process-local, while the E2B backend reports `container` and keeps orchestration and bindings on the host; the descriptor remains informational rather than a security claim.
- **Intermediate binding values have no byte cap** — implementations remain subject to structured-clone cost and process memory, while a provider or executor may already have imposed its own acquisition bound.

View File

@@ -4,23 +4,21 @@
这是**代码执行 seam**:抽象的 `CodeRuntime` 服务(`ctx.codeRuntime`)只定义代码运行时做什么,即针对宿主提供的一组异步绑定运行一段模型编写的程序,并报告 `{ value, logs, error? }`,而不规定如何实现。
此包承担该能力三个组成部分中的接口职责(以 bash 三包结构为模板,参见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):实现通过继承 `CodeRuntime` 并注册服务接入;消费方是工具注册表的 Code Mode它生成面向模型的 SDK并桥接工具分发。这两项职责均由 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) 规定,首个实现是 Node worker 线程后端。运行时不了解工具或会话:调用方只向它提供具名异步函数与程序字符串;所有工具有关的内容都留在消费方。
此包该能力的接口(以 bash 三包结构为模板,参见[能力 seam](../../../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)):实现通过继承 `CodeRuntime` 并注册服务接入;消费方是工具注册表的 Code Mode它生成面向模型的 SDK并桥接工具分发。两者都由 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md) 规定,首个实现是 Node worker 线程后端。运行时不了解工具或会话:调用方只向它提供具名异步函数与程序字符串;所有工具形状的内容都留在消费方。
## 服务 API`ctx.codeRuntime`
| 成员 | 语义 |
|---|---|
| `run(request)` | 针对请求的绑定执行一段程序。**所有程序失败结果都通过 resolve 结果中的 error 字段报告**:包括解析/转换失败、抛出异常、无效完成值、输出溢出、预算到期、中止或执行基底终止(由 `CodeRunFailure` 的正交 `kind` 分类表示);只有调用方误用 seam 本身时才 reject例如 dispose资源释放后仍提交运行)。程序作为异步函数的函数体运行,因此顶层 `await``return` 可用,无损 JSON 完成值会成为 `result.value`。 |
| `language` | 只读描述符:`run` 期望的源语言已知值为 `'typescript'``'python'`——`dsh-tools` 能呈现的那些;其中只有 `'typescript'` 有已发布的后端。仅供参考,不作门禁;生成语言专用呈现的消费方会根据该值选择分支,遇到无法呈现的语言时明确失败。 |
| `run(request)` | 针对请求的绑定执行一段程序。**每一种程序结果都通过 error 字段完成 resolve**:包括解析/转换失败、抛出异常、无效完成值、输出溢出、预算到期、中止或执行基底死亡(由 `CodeRunFailure` 的正交 `kind` 分类表示);只有调用方误用 seam 本身时才 reject例如资源释放后仍提交运行。程序作为异步函数的函数体运行因此顶层 `await``return` 可用,无损 JSON 完成值会成为 `result.value`。 |
| `language` | 只读描述符:`run` 期望的源语言已知值为 `'typescript'`。仅供参考,不作门禁;生成语言专用呈现的消费方会对该值执行分支,遇到无法呈现的语言时明确失败。 |
| `isolation` | 只读描述符:执行基底(`'worker-thread'``'process'``'container'`)。供部署与诊断使用,**不构成安全声明**。 |
每个实现都必须遵守以下语义(完整契约见类 JSDoc绑定调用会桥接完整的无损 JSON 参数与 resolve 值seam 层不设字节上限;程序被视为敌对对等方(任意绑定名称都会成为自有属性,格式错误的通信绝不能使宿主崩溃);不同运行之间不保留任何状态;dispose 会终止进行中的运行,并且在完成前等待其退出。
每个实现都必须遵守以下语义(完整契约见类 JSDoc绑定调用会桥接完整的无损 JSON 参数与 resolve 值seam 层不设字节上限;程序被视为不可信对等方(任意绑定名称都自有属性,格式错误的通信绝不能使宿主崩溃);不同运行之间不保留任何状态;资源释放会终止进行中的运行,并且在完成前等待其退出。
## 词汇
`CodeRunRequest``program``bindings``signal?`)携带运行时操作所需的全部内容;默认值解析(时间预算与外层输出上限)属于实现的已验证配置,绝不能是隐藏的 `??`,更不能藏在 `run()` 内部。`bindings``CodeBindingNamespace` 列表(`global` + `functions` + 可选 `errorClass`);每个命名空间会作为一个由异步可调用函数组成的全局对象公开给程序,这些函数返回 `CodeJsonValue`。后者是 seam 本地、与规范 `JsonValue` 结构等价的类型,使接口包保持独立于会话。`errorClass` 描述符点名真实的程序全局构造器,以及用于接收被拒绝成员名称的自有属性;运行时不依赖 `ToolCallError` 等消费方术语。`CodeRunResult` 报告无损 JSON 完成值 `value?`、有序的 `logs: string[]``error?``CodeRunFailure``kind` + 可反馈给模型的 `message`)。完整契约见 `src/types.ts`
binding-global 与 error-class 名称是**语言可移植**的:必须匹配标识符子集 `[A-Za-z_][A-Za-z0-9_]*`(不含 JS 专有的 `$`)并通过 seam 导出的排除集,因此同一份 `bindings` 列表对每个后端都有效,无论其 `language` 为何。本包导出每个后端都执行的契约——`PORTABLE_RESERVED_WORDS`ECMAScript Python 保留字)、`RESERVED_BINDING_GLOBALS`(如 `console` 等后端拥有的 global`RESERVED_ERROR_MEMBERS``DUNDER_MEMBER`error-member 排除)——因此 `$tools``lambda``__dsh_main__` 之类的名称会让 `run()` 在任何后端上作为 seam 误用而 reject而非只在某些后端。确切集合与理由见 `src/index.ts`
`CodeRunRequest``program``bindings``signal?`)携带运行时操作所需的全部内容;默认值解析(时间预算与外层输出上限)属于实现的已验证配置,绝不能是隐藏的 `??`,更不能藏在 `run()` 内部。`bindings``CodeBindingNamespace` 列表(`global` + `functions` + 可选 `errorClass`);每个命名空间会作为一个由异步可调用函数组成的全局对象公开给程序,这些函数返回 `CodeJsonValue`。后者是 seam 本地、与规范 `JsonValue` 结构等价的类型,使接口包保持独立于会话。`errorClass` 描述符点名真实的程序全局构造器,以及用于接收 reject 成员名称的自有属性;运行时不依赖 `ToolCallError` 等消费方术语。`CodeRunResult` 报告无损 JSON 完成值 `value?`、有序的 `logs: string[]``error?``CodeRunFailure``kind` + 可反馈给模型的 `message`)。完整契约见 `src/types.ts`
## 模型体验
@@ -28,11 +26,11 @@ binding-global 与 error-class 名称是**语言可移植**的:必须匹配标
#### KV Cache 影响
不会直接失效;由上述消费方负责请求前缀变更。
不会直接失效;由具名消费方负责请求前缀变更。
## 已知限制与暂缓事项
## 已知限制与暂缓工作
- **`run()` 是一次性的**`logs` 只有在 `CodeRunResult` resolve 后才能获得seam 不提供正在运行的程序所产生输出的流式日志或进度接口。
- **`run()` 是一次性的**`logs` 只有在 `CodeRunResult` resolve 后才能获得seam 不提供活跃程序输出的流式日志或进度接口。
- **持久 REPL 风格内核已记录为未来工作**:在持久内核后端带来自己的日志方案前,运行之间不保留状态的契约继续有效(参见 [Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md))。
- **目前只提供 worker 线程后端**`'process'``'container'` 是已经声明但没有实现的已知 `isolation` 值;强安全边界需要等待容器后端
- **隔离方式由后端决定**worker 后端位于宿主进程内,而 E2B 后端报告 `container`,并把编排与绑定留在宿主;该描述符仍只提供信息,不构成安全声明
- **中间绑定值没有字节上限**:实现仍受 structured-clone 成本与进程内存约束,而提供方或执行器可能已经应用自己的获取上限。