Skip to content

这里的「代码运行时」指什么

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服务定义(CodeRuntimectx.codeRuntime
@deepseek-ai/dsh-code-runtime-worker-threadProvider:每次运行一个全新 worker 线程(TypeScript)
@deepseek-ai/dsh-e2bCore:共享 E2B 沙箱所有者(ctx.e2b
@deepseek-ai/dsh-fs-e2bProvider:ctx.fs 的 E2B 文件系统后端
@deepseek-ai/dsh-subprocess-e2bProvider:ctx.subprocess 的 E2B 进程后端
@deepseek-ai/dsh-subprocess服务定义(SubprocessRuntimectx.subprocess
@deepseek-ai/dsh-subprocess-localProvider:本地 OS 进程生成

接缝定义

CodeRuntimepackages/code-runtime/code-runtime/src/index.ts):

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 关键字/软关键字之并(lambdamatchtype_,……)。新增一种语言意味着扩大这个并集(对既有绑定名的破坏性审查,而非受设计之选择)。

请求/结果结构

CodeRunRequest 携带运行时据以行动的一切(显式优先于隐式——没有隐藏的 ?? 调参旋钮):

ts
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 专属拼写出于设计被拒绝);functionsRecord<string, CodeBindingFunction>——把可调用成员名映射到 (args: unknown) => Promise<CodeJsonValue> 函数的对象,其中 CodeJsonValue 是无损 JSON 类型;errorClass 命名一个运行时注入到 name 下、携带 memberNameProperty 的、程序可见的类型化拒绝构造器。

CodeRunResult = 可选 value(顶层 return,若它跨过了无损 JSON 边界)、有序的 logs: string[]、可选 error。错误分类之一:

ts
export type CodeRunFailureKind =
  | 'exception'      // 程序抛出或解析/变换失败
  | 'timeout'        // 某实现预算到期
  | 'abort'          // 请求信号触发
  | 'worker-exit'    // 执行底座未结算即亡(如 OOM)
  | 'invalid-output' // 完成值不是无损 JSON
  | 'output-limit'   // 序列化后的 logs/值/诊断超出上限

worker 线程 Provider

@deepseek-ai/dsh-code-runtime-worker-threadpackages/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 数组/完成值/失败消息负载的硬上限。
maxOldGenerationSizeMbworker 最大老生代堆(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_GLOBALSPORTABLE_RESERVED_WORDS 在请求期强制,因此在本后端有效的命名空间列表对任何未来后端都有效。

worker→host 线上协议

packages/code-runtime/code-runtime-worker-thread/src/protocol.ts版本无关、结构化克隆的协议,跑在共同发布的宿主与 worker 代码之间。信任方向不对称且被明言:宿主把入站流量当作敌意(模型代码可伪造 parentPort 消息),因此逐条重校验并重建;worker 则信任宿主回复。宿主在 spawn 时经 workerDataWorkerBootData 交给 worker——类型剥离后的程序 code、要实例化的绑定命名空间(函数体留在宿主侧)、以及合并的 maxOutputBytes 上限。

消息联合(WorkerToHost):

消息负载含义
call{ id, global, name, args }一次桥接绑定调用;宿主对每个 id 至多回答一次并忽略重复
log{ text }捕获到的文本,急切地流式发送,使输出在一次中途终止(超时、中止、OOM)后仍然存活
output-limitworker 侧捕获或完成值计量超出外层上限
done{ value? / error? }程序已结算;logs 随之携带(它们已流式发送)

宿主用一个 ReplyMessage 回答 callok: true + 无损值,或 ok: false + 消息)。因为预算、中止与底座死亡在宿主侧观测,done 只携带程序异常/无效输出/输出超限错误;timeoutabortworker-exit 由宿主围绕已结算的 worker 分类。

E2B:远程沙箱化后端

packages/e2b/e2b 持有一个共享的 E2B 沙箱ctx.e2b),两个基础 E2B Provider 共用,使文件系统与进程操作落在同一个远程 Linux 运行时中。配置:

默认含义
apiKey$E2B_API_KEYE2B API 密钥;绝不转发进沙箱
cwd/home/user/workspace共享远程工作目录,在适配器收到沙箱前创建
timeoutMs300_000沙箱生命周期;到期必然删除沙箱

E2BRuntime 暴露 getSandbox()——创建在插件构造时开始;适配器 await 同一个句柄。它为该 SDK 硬编码的 /bin/bash -l -c 层给参数加引号(quoteE2BShellArg),并隔离在全新随机的 HOME 后面的登录 shell(e2bControlEnvs)。在销毁或超时时删除沙箱。

两个 E2B 后端是 E2B 世界下同一文件底座的一对:

  • fs-e2bpackages/e2b/fs-e2b)——在远程沙箱文件系统之上实现 FileSystem,因此 ctx.fs 的读/写变成远程 Linux 文件操作,processPath 返回远程沙箱内的路径。
  • subprocess-e2bpackages/e2b/subprocess-e2b)——在 Sandbox.command/PTY 之上实现 SubprocessRuntime,把 CommandHandleCommandResult 词汇表(output.tsprocess.tsenvironment.tsremote.tsterminal.ts)映射过来。

组合 fs-e2b + subprocess-e2b 得到一个连贯的远程执行世界:shell 命令的 workdir 路径与 ctx.fs 的一次 writeText 目标解析到同一个沙箱内文件系统。

subprocess 服务

packages/subprocess/subprocess 定义 ctx.subprocess——shell 执行器、PTY 后端、LSP 宿主以及进程外 subagent 传输都经它生成进程的接缝。它是 bash-localbash-sandboxterminal-bash 扎根的底座(它们的 env 分层、输出上限与杀死升级都来自这里)。

SubprocessRuntime 语义(摘自 packages/subprocess/subprocess/src/index.ts):

  • 一个执行世界,与挂载的文件系统 Provider 共享——可执行文件路径与文件路径一致。
  • spawn 立即返回一个 SubprocessHandledone 在进程关闭时以退出事实 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 匹配),于是 PATHHOMElocale/代理得以保留,但 harness 身份从不隐式泄漏。显式 env 层在清理之后合并,因此有意传递的条目得以幸存。

subprocess-local

@deepseek-ai/dsh-subprocess-local 是本地 OS 进程 Provider(spawn.tsterminal.tsprocess-inspector.ts)。进程坐标(process-inspector)使它能够提供按 PGD 的 tree 作用终止。bash-localLocalBashExecutor 注入 subprocess,并交给它一份完全指定的 SubprocessSpawnSpec(argv、cwd、逐流 collect 预算、graceMs、signal、分层 env)。

与沙箱的关系

  • subprocess-local 生成不受约束的 OS 进程;bash 层决定策略。bash-local 是裸的,而 bash-sandboxctx.sandbox 包裹同一 argv(见 Shell 与终端沙箱架构)。
  • fs-sandbox 按共享沙箱模式围栏 ctx.fs变更(见 文件系统文件系统观察与沙箱策略)。
  • E2B 本身就是一个沙箱边界——一个远程、可销毁的 Linux VM/容器——因此 subprocess-e2bfs-e2b 无需本地 OS 沙箱包裹;底座约束。ctx.e2bdocs/capability-seams.md 中被列为 core(非接缝)。
  • code-runtime-worker-thread隔离,不是安全边界——worker 做隔离,但代码携带与 bash 同等的信任;真正不可信的程序请优先使用沙箱化或 E2B 的 composition。

延伸阅读

  • Shell 与终端 —— 作为 ctx.subprocess 消费者的 bash-localbash-sandbox
  • 文件系统工具与策略 —— 作为远程 ctx.fs 后端的 fs-e2b
  • 沙箱架构 —— E2B 参与其中的模式/强制执行故事。
  • 权限与审批 —— 各执行器共享的升级流程。
  • packages/code-runtime/code-runtime/src/index.ts —— CodeRuntime 定义与可移植性契约。
  • packages/subprocess/subprocess/src/index.ts —— SubprocessRuntime 定义与清理规则。