这里的「代码运行时」指什么
packages/code-runtime/code-runtime 定义 ctx.codeRuntime——在宿主提供的异步绑定之上运行一段模型所写的程序的接缝。决定性契约(取自类文档):"Runtimes know nothing about tools or sessions; consumers own those concerns."(运行时对工具或会话一无所知;那些关切实属消费者)。消费者(如工具注册表为「Code Mode」所用)传入程序源码外加一组绑定命名空间,运行时把它当作敌意对等方执行——把程序故障作为结果字段上报,而非异常。
该接缝刻意是可移植标识符驱动的:为一个后端写的程序在所有后端都须有效。今天只有 'typescript' 有已发布后端,但 'python' 是一等移植目标,因此标识符校验是两个语言保留字之并。
各包版本
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-code-runtime | 服务定义(CodeRuntime、ctx.codeRuntime) |
@deepseek-ai/dsh-code-runtime-worker-thread | Provider:每次运行一个全新 worker 线程(TypeScript) |
@deepseek-ai/dsh-e2b | Core:共享 E2B 沙箱所有者(ctx.e2b) |
@deepseek-ai/dsh-fs-e2b | Provider:ctx.fs 的 E2B 文件系统后端 |
@deepseek-ai/dsh-subprocess-e2b | Provider:ctx.subprocess 的 E2B 进程后端 |
@deepseek-ai/dsh-subprocess | 服务定义(SubprocessRuntime、ctx.subprocess) |
@deepseek-ai/dsh-subprocess-local | Provider:本地 OS 进程生成 |
接缝定义
CodeRuntime(packages/code-runtime/code-runtime/src/index.ts):
export abstract class CodeRuntime extends Service {
abstract readonly language: string // 'typescript' | 'python'(信息性)
abstract readonly isolation: string // 'worker-thread' | 'process' | 'container'(信息性)
constructor(ctx: Context) { super(ctx, 'codeRuntime') }
abstract run(request: CodeRunRequest): Promise<CodeRunResult>
}run resolve 出一个 CodeRunResult——错误是已 resolve 结果上的字段,绝不是异常。只有服务定义层面违约调用会 reject。可移植标识符契约通过三个共享集合强制:
RESERVED_BINDING_GLOBALS——{ console, __dsh_main__, __builtins__, __name__, __debug__ }:某些后端私有的槽位;共享集合保证在某一后端有效的命名空间列表在所有后端都有效。RESERVED_ERROR_MEMBERS——{ name, message, stack, args, with_traceback, add_note },外加整体拒绝任何 dunder 形态(__x__)。PORTABLE_RESERVED_WORDS——ECMAScript 保留字与 Python 3 关键字/软关键字之并(lambda、match、type、_,……)。新增一种语言意味着扩大这个并集(对既有绑定名的破坏性审查,而非受设计之选择)。
请求/结果结构
CodeRunRequest 携带运行时据以行动的一切(显式优先于隐式——没有隐藏的 ?? 调参旋钮):
export interface CodeRunRequest {
program: string // 作为 async 函数体运行:顶层 return/await 合法
bindings: CodeBindingNamespace[] // 宿主函数,每个命名空间一个全局对象
signal?: AbortSignal // 中止会强制停止程序,即使在中途循环
}一个 CodeBindingNamespace 是 { global, functions, errorClass? }:global 必须匹配可移植标识符子集 [A-Za-z_][A-Za-z0-9_]*(如 $tools 这类 JS 专属拼写出于设计被拒绝);functions 是 Record<string, CodeBindingFunction>——把可调用成员名映射到 (args: unknown) => Promise<CodeJsonValue> 函数的对象,其中 CodeJsonValue 是无损 JSON 类型;errorClass 命名一个运行时注入到 name 下、携带 memberNameProperty 的、程序可见的类型化拒绝构造器。
CodeRunResult = 可选 value(顶层 return,若它跨过了无损 JSON 边界)、有序的 logs: string[]、可选 error。错误分类之一:
export type CodeRunFailureKind =
| 'exception' // 程序抛出或解析/变换失败
| 'timeout' // 某实现预算到期
| 'abort' // 请求信号触发
| 'worker-exit' // 执行底座未结算即亡(如 OOM)
| 'invalid-output' // 完成值不是无损 JSON
| 'output-limit' // 序列化后的 logs/值/诊断超出上限worker 线程 Provider
@deepseek-ai/dsh-code-runtime-worker-thread(packages/code-runtime/code-runtime-worker-thread/src/index.ts)是唯一已发布的后端。它自己的文档对信任模型的描述很直白:"This is containment, not a security boundary: model code has bash-equivalent trust."(这是隔离,不是安全边界:模型代码拥有与 bash 同等的信任)。它每次运行一个全新的 Worker,把程序包裹进 async function __dsh_program__() { … },用保位的原生类型剥离移除 TypeScript 类型,并经 worker 的消息端口桥接绑定。
配置(每个执行上限都声明在 provider 的 Config schema 中——可从 cordis.yml 调整):
| 键 | 默认 | 含义 |
|---|---|---|
computeMs | — | 忙时预算:当 worker 测得的事件循环活跃时间(eventLoopUtilization)超过它时,以 kind:'timeout' 失败。计量测得忙时(而非墙钟时间)使预算既公平(await 慢工具的程序不累积)又难以钻营。 |
maxWallMs | — | 墙钟上限(对无人 resolve 的 promise 的兜底),最大 2_147_483_647(Node 的 setTimeout 最大延迟)。 |
maxOutputBytes | — | 序列化 logs 数组/完成值/失败消息负载的硬上限。 |
maxOldGenerationSizeMb | — | worker 最大老生代堆(MiB,resourceLimits);溢出杀死 worker → kind:'worker-exit'。 |
值得注意的运行时机制:
- 每次运行全新 worker 隔离各次运行,而且终止也能停掉同步循环(
worker.terminate())。 - worker 入口未构建直接跑
src/worker.ts(erasable-only,仅类型导入);构建后的包把它作为兄弟 CommonJS 包(lib/worker.cjs)发布。 - 入站端口流量被逐字段重校验并重建。文档直言:the peer runs MODEL CODE and can post anything——所以编译期
WorkerToHost类型没有任何意义,伪造的额外字段永远不会随之通过。 - 绑定经
snapshotJsonValue穿越端口;只有无损 JSON 的解析结果才合法,否则以invalid-output失败。 RESERVED_BINDING_GLOBALS/PORTABLE_RESERVED_WORDS在请求期强制,因此在本后端有效的命名空间列表对任何未来后端都有效。
worker→host 线上协议
packages/code-runtime/code-runtime-worker-thread/src/protocol.ts 是版本无关、结构化克隆的协议,跑在共同发布的宿主与 worker 代码之间。信任方向不对称且被明言:宿主把入站流量当作敌意(模型代码可伪造 parentPort 消息),因此逐条重校验并重建;worker 则信任宿主回复。宿主在 spawn 时经 workerData 把 WorkerBootData 交给 worker——类型剥离后的程序 code、要实例化的绑定命名空间(函数体留在宿主侧)、以及合并的 maxOutputBytes 上限。
消息联合(WorkerToHost):
| 消息 | 负载 | 含义 |
|---|---|---|
call | { id, global, name, args } | 一次桥接绑定调用;宿主对每个 id 至多回答一次并忽略重复 |
log | { text } | 捕获到的文本,急切地流式发送,使输出在一次中途终止(超时、中止、OOM)后仍然存活 |
output-limit | — | worker 侧捕获或完成值计量超出外层上限 |
done | { value? / error? } | 程序已结算;logs 不随之携带(它们已流式发送) |
宿主用一个 ReplyMessage 回答 call(ok: true + 无损值,或 ok: false + 消息)。因为预算、中止与底座死亡在宿主侧观测,done 只携带程序异常/无效输出/输出超限错误;timeout、abort、worker-exit 由宿主围绕已结算的 worker 分类。
E2B:远程沙箱化后端
packages/e2b/e2b 持有一个共享的 E2B 沙箱(ctx.e2b),两个基础 E2B Provider 共用,使文件系统与进程操作落在同一个远程 Linux 运行时中。配置:
| 键 | 默认 | 含义 |
|---|---|---|
apiKey | $E2B_API_KEY | E2B API 密钥;绝不转发进沙箱 |
cwd | /home/user/workspace | 共享远程工作目录,在适配器收到沙箱前创建 |
timeoutMs | 300_000 | 沙箱生命周期;到期必然删除沙箱 |
E2BRuntime 暴露 getSandbox()——创建在插件构造时开始;适配器 await 同一个句柄。它为该 SDK 硬编码的 /bin/bash -l -c 层给参数加引号(quoteE2BShellArg),并隔离在全新随机的 HOME 后面的登录 shell(e2bControlEnvs)。在销毁或超时时删除沙箱。
两个 E2B 后端是 E2B 世界下同一文件底座的一对:
fs-e2b(packages/e2b/fs-e2b)——在远程沙箱文件系统之上实现FileSystem,因此ctx.fs的读/写变成远程 Linux 文件操作,processPath返回远程沙箱内的路径。subprocess-e2b(packages/e2b/subprocess-e2b)——在Sandbox.command/PTY 之上实现SubprocessRuntime,把CommandHandle/CommandResult词汇表(output.ts、process.ts、environment.ts、remote.ts、terminal.ts)映射过来。
组合 fs-e2b + subprocess-e2b 得到一个连贯的远程执行世界:shell 命令的 workdir 路径与 ctx.fs 的一次 writeText 目标解析到同一个沙箱内文件系统。
subprocess 服务
packages/subprocess/subprocess 定义 ctx.subprocess——shell 执行器、PTY 后端、LSP 宿主以及进程外 subagent 传输都经它生成进程的接缝。它是 bash-local/bash-sandbox/terminal-bash 扎根的底座(它们的 env 分层、输出上限与杀死升级都来自这里)。
SubprocessRuntime 语义(摘自 packages/subprocess/subprocess/src/index.ts):
- 一个执行世界,与挂载的文件系统 Provider 共享——可执行文件路径与文件路径一致。
spawn立即返回一个SubprocessHandle;done在进程关闭时以退出事实 resolve,只对 spawn 级失败 reject。- collect 模式读取器按偏移且非消耗,因此独立读取器互不消耗对方的输出;有损读取会报告截断以及保存完整流的 spill 文件。
terminate(以及 spec 的中止信号)SIGTERM → 宽限 → SIGKILL 升级,在各平台按 tree 作用——这是唯一的终止动词。- 管道 stdio 原样交给调用方,在此绝不缓冲。
- 销毁会终止所有仍在运行的受管进程并等待其退出。
环境卫生:scrubbedParentEnv() 是每个 harness 子进程从之出发的规范基准——它丢弃凭据形态的名字(SENSITIVE_ENV_PATTERN = /KEY|PASSWORD|SECRET|TOKEN/i)以及所有残留的 DSH_* 名(按 DSH_ENV_PREFIX 匹配),于是 PATH/HOME/locale/代理得以保留,但 harness 身份从不隐式泄漏。显式 env 层在清理之后合并,因此有意传递的条目得以幸存。
subprocess-local
@deepseek-ai/dsh-subprocess-local 是本地 OS 进程 Provider(spawn.ts、terminal.ts、process-inspector.ts)。进程坐标(process-inspector)使它能够提供按 PGD 的 tree 作用终止。bash-local 的 LocalBashExecutor 注入 subprocess,并交给它一份完全指定的 SubprocessSpawnSpec(argv、cwd、逐流 collect 预算、graceMs、signal、分层 env)。
与沙箱的关系
subprocess-local生成不受约束的 OS 进程;bash 层决定策略。bash-local是裸的,而bash-sandbox经ctx.sandbox包裹同一 argv(见 Shell 与终端 与 沙箱架构)。fs-sandbox按共享沙箱模式围栏ctx.fs的变更(见 文件系统 与 文件系统观察与沙箱策略)。- E2B 本身就是一个沙箱边界——一个远程、可销毁的 Linux VM/容器——因此
subprocess-e2b/fs-e2b无需本地 OS 沙箱包裹;底座即约束。ctx.e2b在docs/capability-seams.md中被列为core(非接缝)。 code-runtime-worker-thread是隔离,不是安全边界——worker 做隔离,但代码携带与 bash 同等的信任;真正不可信的程序请优先使用沙箱化或 E2B 的 composition。