使用 WorkerJavaScriptBackend 隔离运行 JavaScript 模块:@cloudflare/computer 隔离运行时实战指南
【免费下载链接】computerGive your agent a computer 👾项目地址: https://gitcode.com/GitHub_Trending/computer1/computer
导读
@cloudflare/computer为 Durable Object 提供了一套开箱即用的虚拟文件系统与执行后端,而 WorkerJavaScriptBackend 是其核心能力之一:在每次执行时创建一个全新的 Cloudflare Dynamic Worker,在隔离的 ECMAScript 模块运行时中运行用户代码。它支持静态导入、字面量动态导入、顶层 await、持久化相对导入、宿主安装的配置模块,以及由 Workspace 提供的持久化node:fs/promises和受信任的ws:git/ws:artifacts能力模块。阅读本文后,你将掌握该后端的完整配置项、执行模型、资源限制、取消语义与隔离边界,能够为你的 Agent 搭建安全、可回放、受约束的 JavaScript 代码执行环境。
前置说明:
@cloudflare/computer目前处于PREVIEW(预览)阶段(见 docs/README.md),API 不稳定、设计可能调整,适合实验与原型,暂不适合生产环境;本文描述的是当前仓库(spec 目录为前瞻性说明,代码以 packages/computer 实际实现为准)。
一、整体架构:一次执行 = 一个全新的 Dynamic Worker
WorkerJavaScriptBackend(源码位于 packages/computer/src/backends/worker-javascript/worker-javascript.ts,类型定义与导出见 packages/computer/src/backends/worker-javascript/index.ts)的运行模型是:
- 调用方通过
Workspace.runtime.exec()传入一段真实 ES 模块源码; - 后端先用 acorn 解析模块图(见 packages/computer/src/backends/worker-javascript/module-graph.ts),收集所有静态导入、导出与字面量动态导入;
- 把源码与依赖打包成一个模块图,通过
loader.load()启动一个全新的 Dynamic Worker; - Worker 加载宿主生成的
workspace-runtime-runner.js运行器,动态import入口模块并调用其default导出函数; - 运行器通过
WorkspaceRuntimeBridge(见 packages/computer/src/runtime/bridge.ts)以 RPC 方式访问宿主侧的 Workspace 文件系统与能力模块; - stdout/stderr 以帧的形式实时流回宿主,追加到执行事件流;结果与退出事件在输出流关闭后发布。
最小可运行示例
import { Workspace } from "@cloudflare/computer"; import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript"; const workspace = new Workspace({ storage: ctx.storage, backends: [ new WorkerJavaScriptBackend({ loader: env.LOADER, // 动态 Worker 加载器(Worker Loader binding) root: "/workspace", // 代码可见的文件系统根 access: "read-write", // "read" | "read-write" defaultTimeoutMs: 60_000, // 默认超时 maxTimeoutMs: 180_000, // 单次执行允许的最大超时 globalOutbound: null, // 默认关闭出站网络 modules: { "math-kit": `export const double = value => value * 2;`, // 配置模块(bare import) }, }), ], });执行一个模块:
const handle = await workspace.runtime.exec( ` import { double } from "math-kit"; import fs from "node:fs/promises"; export default async function main(input) { const value = double(input.value); await fs.writeFile("/workspace/result.txt", String(value)); return { value, persisted: await fs.readFile("/workspace/result.txt", "utf8") }; } `, { backend: "worker-javascript", input: { value: 21 }, encoding: "utf8", }, ); const result = await handle.result(); // result.value = { value: 42, persisted: "42" }这里的源码是真正的 ES 模块:import { double } from "math-kit"命中后端构造时配置的modules映射,import fs from "node:fs/promises"命中宿主安装的持久化文件系统。如果模块default导出一个函数,Workspace 会用options.input调用它;否则模块求值完成时返回一个null的结构化结果(对应运行器源码中的typeof module.default === "function" ? await module.default(input) : module.default ?? null,见 worker-javascript.ts)。
调用返回时机与执行生命周期
runtime.exec()在 Dynamic Worker完成之前就会返回:运行会在其事件流被消费、且宿主对 Dynamic Worker 的调用仍处于 in-flight 状态时持续推进。这部分挂起的工作本身就能让 Durable Object 保持驻留(resident)。
- 如果拿到了 handle 却从不读取事件流,一旦对象转为空闲,运行可能被驱逐;
- 因此:持续排空事件流(或调用
result())来保持运行存活; - 对于必须跨驱逐存活的持久化工作,通过
ctx.storage.setAlarm()调度 alarm,而不是依赖挂起调用。
执行事件流的类型定义见 packages/computer/src/runtime/types.ts:包含stdout、stderr与携带code(以及可选result)的exit事件。
二、持久化相对导入:模块图在宿主侧解析
相对导入从cwd出发,通过持久化的 Workspace 文件系统解析。先在 Workspace 里写入一个任务模块,再通过相对路径导入它:
await workspace.fs.writeFile( "/workspace/task.js", ` import fs from "node:fs/promises"; export default input => fs.writeFile("/workspace/value.txt", String(input.value)); `, ); await workspace.runtime.exec( `import task from "./task.js"; export default task;`, { backend: "worker-javascript", cwd: "/workspace", input: { value: 42 }, }, );模块图构建(module-graph.ts)在加载 Worker 之前就完成了全部静态分析:
- 加载前解析整个图:用 acorn 把源码解析成 AST,遍历
ImportDeclaration、ExportNamedDeclaration、ExportAllDeclaration与ImportExpression; - 约束每个持久化路径:所有相对导入都先经
WorkspaceRuntimeCapability.resolveConfined()归一化并约束在root之内(见 capability.ts),越界路径直接抛错; - 拒绝符号链接穿越:
#assertSafeComponents会逐段lstat检查路径组件,任何中间符号链接都会被拒绝(见 capability.ts); - 动态导入必须使用字符串字面量:
import(\./${name}.js`)` 这类模板字符串在解析阶段就会抛错(见 module-graph.ts); - 绝对导入不被支持:
import "/abs/path.js"会被拒绝,必须改用相对 Workspace 导入。
此外,模块图受聚合源码大小(maxSourceBytes)、模块数量(默认maxModules128,见 module-graph.ts)与导入深度(默认maxDepth32)约束;加载到 Dynamic Worker 的完整模块图还被assertLoaderGraph限制为最多 256 个模块(见 worker-javascript.ts)。
三、执行限额与记录保留
并发执行上限
后端默认同时接纳最多24 个执行(maxConcurrentExecutions,构造器默认值见 worker-javascript.ts)。超过上限的并发启动会以EEXEC_BUSY错误失败,而不是无限创建 Dynamic Worker。同一执行 id 已存在也会抛出EEXEC_BUSY/EEXEC_EXISTS(见 worker-javascript.ts)。
在调整
maxConcurrentExecutions之前,请先实测部署环境的 Durable Object 与 Worker Loader 限额,再据此上调。
单次执行的字节与数量限额
每次执行都会约束以下资源(可分别下调,公共负载场景建议收紧):
| 选项 | 默认值 | 作用 |
|---|---|---|
maxSourceBytes | 1 MiB | 源码与模块图聚合大小 |
maxInputBytes | 1 MiB | 结构化输入序列化后的字节数 |
maxResultBytes | 1 MiB | 结构化结果字节数(宿主侧assertResult校验) |
maxStdinBytes | 256 KiB | 标准输入字节数 |
maxEnvBytes | 1 MiB | env记录总字节数 |
maxStdioBytes | 1 MiB | stdout + stderr 合计字节数 |
maxCapabilityBytes | 1 MiB | 单次能力调用请求/响应载荷(最小 256 字节) |
maxHostCallMs | 默认maxTimeoutMs | 单次宿主能力调用的调用方可见期限 |
maxConcurrentCapabilityCalls | 32 | 并发的宿主能力调用数 |
maxCapabilityCalls | 256 | 累计能力调用次数 |
maxCapabilityRequestBytes | 8 MiB | 能力调用累计请求字节 |
maxCapabilityResponseBytes | 8 MiB | 能力调用累计响应字节 |
maxDirectoryEntries | 1024 | 单次目录读取返回的最大条目数 |
maxExecutionSubscribers | 8 | 单次执行的实时事件订阅者数 |
(以上默认值全部来自 worker-javascript.ts 的构造器解析逻辑。)
实现细节值得注意:
- 目录读取限额在 SQLite 层生效:
readdir会以maxDirectoryEntries + 1作为 limit 请求,超过即抛错,避免物化超量行(见 capability.ts); - 请求在隔离内、Workers RPC 前检查一次,宿主再检查一次:隔离侧
workspace-capabilities.js先校验序列化后的请求大小(见 module-graph.ts),宿主侧WorkspaceRuntimeBridge.call()再对载荷、并发、总数与累计字节做二次校验(见 bridge.ts)。
已完成执行的保留与回放
- 默认保留60 分钟(
retentionMs),同一后端最多保留100 条完成记录(maxRetainedExecutions,见 worker-javascript.ts); - 完成记录立即离开内存中的活跃集合,回放时从 SQLite 读取:后端在连接时创建
workspace_runtime_executions与workspace_runtime_events两张表,事件按(backend, execution_id, seq)持久化(见 worker-javascript.ts); - 内存清理与 SQLite 清理都由
#prune()驱动,按finished_at与条数双条件删除过期记录(见 worker-javascript.ts)。
取消语义
取消(killExec/disposeExec/ Workspace 关闭)会:
- 停止新的宿主能力调用:
cancelAndDrain()置#cancelled = true并 abort 所有在途AbortController(见 bridge.ts); - dispose 掉 Dynamic Worker;
- 等待已接受的宿主调用 settle 之后才发布 exit 130。
正常完成同样遵循这条 drain 规则,因此未 await 的能力调用不可能在 exit 0 之后继续改动 Workspace。宿主调用存在调用方可见的截止期限(maxHostCallMs):错过期限会让该能力调用失败并把执行标记为 failed——即使调用方代码 catch 了这个错误。但执行仍会等待已接受的宿主操作本身完成后再发布终态事件,因为许多宿主 API 在派发后无法回滚外部副作用。
受信任模块会收到可选的{ signal, deadline }上下文,必须在 signal abort 时及时停止;一个无视取消、永不 settle 的受信任模块会让执行停留在 finalizing 状态。
运行期配置
compatibilityDate与compatibilityFlags控制 Dynamic Worker 运行时,默认分别为"2026-05-23"与["nodejs_compat"](见 worker-javascript.ts),即包内测试过的设置;日期必须符合YYYY-MM-DD格式,否则构造抛错。
四、环境变量、标准输入与processshim
每次执行都会安装一个极小的node:processshim,让普通模块代码可以读取自己的环境与标准流。shim 只暴露调用方为该次执行提供的内容,宿主环境完全不可见:
process.env:是 exec options 中env记录的快照。未传入的变量不存在,Durable Object 自身环境从不合并进去——模块无法通过process.env读取宿主 binding 或 secrets;process.stdin:一个非交互式async-iterable,按for await消费调用方传入的stdin字节一次后即结束;由于 evaluate-once 执行没有会话可等待,不存在阻塞读取更多输入。isTTY恒为false。输入受maxStdinBytes约束,超限会以明确错误失败;process.stdout/process.stderr:可写流,写入会流向实时输出(见下文"隔离与生命周期")。console.log/console.info路由到 stdout,console.warn/console.error路由到 stderr,两者共享同一个maxStdioBytes上限;process.argv、process.cwd()、process.platform:返回惰性值——cwd()反映该次执行的cwd,而argv(固定为["workspace", <entryName>])与platform(固定为"linux")是占位符,不描述宿主进程。
上述行为对应运行器源码(worker-javascript.ts):nextProcess只包含env、固定argv、cwd()、固定platform与一次性stdin迭代器,且安装时会尽力覆盖globalThis.process(不可覆盖时则只回填env/stdin/stdout/stderr)。
示例:读取 stdin 与环境
const handle = await workspace.runtime.exec( ` export default async function main() { let piped = ""; for await (const chunk of process.stdin) piped += new TextDecoder().decode(chunk); console.log("received", piped.length, "bytes"); return { who: process.env.WHO, piped }; } `, { backend: "worker-javascript", env: { WHO: "demo" }, stdin: "hello", encoding: "utf8", }, );env必须是 string-to-string 记录,超出maxEnvBytes会抛错(assertEnv,见 worker-javascript.ts);stdin只接受字符串或Uint8Array(normalizeStdin)。
五、配置模块(Configured modules)
bare imports 在后端构造时安装,而不是在单次执行时传递:
new WorkerJavaScriptBackend({ loader: env.LOADER, modules: { "tar-stream": TAR_STREAM_BUNDLE, }, });- 未知的 bare import 会在创建 Worker 之前失败:模块图构建时,凡是既非相对路径、又非
node:*/ws:*/ 内部模块名的 specifier,都必须命中configuredModules,否则抛Module "X" is not configured for the worker-javascript backend.(见 module-graph.ts); node:fs与node:fs/promises是宿主安装的例外,由持久化 Workspace 背书;- 配置模块只是代码,不是宿主权限:它们不得使用保留的
ws:命名空间,也不得遮蔽node:fs、node:fs/promises两个文件系统 specifier(配置模块名含/、ws:前缀或与内部模块名冲突都会在 module-graph.ts 中被拒绝)。
六、受信任的 Workspace 模块
node:fs/node:fs/promises:持久化文件系统
文件系统访问使用熟悉的异步 Node API,但底层是持久化的 Workspace而非 isolate 本地文件系统。两种写法都自动安装:
import fs from "node:fs/promises"; // or: import { promises as fs } from "node:fs"; const text = await fs.readFile("/workspace/input.txt", "utf8"); await fs.writeFile("/workspace/output.txt", text.toUpperCase());受支持的 promise API 为:readFile、writeFile、mkdir、rm、chmod、symlink、readlink、readdir、stat、lstat、access(实现见 module-graph.ts)。行为边界:
readFile省略 encoding 时返回字节(Uint8Array),仅支持"utf8"/"utf-8"文本编码,其他编码(如"base64")被拒绝;writeFile支持默认"w"标志与独占"wx";其他 Node 标志被拒绝;与 Node 一致,父目录必须已存在(writeFileNode只放行w/wx,见 capability.ts);readlink保留相对符号链接目标;通过符号链接的读写被 Workspace 约束边界拒绝;- 同步与回调式 Node 文件系统 API刻意不可用,因为每个操作都要跨越 isolate → Workspace 的能力边界(隔离侧通过
workspace-capabilities.js安装的全局分发器路由到宿主WorkspaceRuntimeBridge.call,见 module-graph.ts)。
ws:命名空间:宿主能力模块
整个ws:命名空间为 Workspace 维护的宿主能力保留;内置运行时安装ws:git与ws:artifacts。调用方模块与持久化文件都不能遮蔽node:fs、node:fs/promises或ws:*。未知的ws:导入在模块图构建时即失败(Unknown trusted Workspace module,见 module-graph.ts)。
ws:git
import { clone, diff, status, log, cli } from "ws:git";ws:git是显式宿主权限而非环境化的隔离区联网。clone、fetch、pull、push、ls-remote与 submodule 等命令即使 Dynamic Worker 配置了globalOutbound: null也能发起宿主侧请求,因此默认一律拒绝;只有在受信任的后端构造上显式设置allowGitNetwork: true才可用,本地 Git 操作无需该权限。实现上,bridge.#callGit对网络命令调用#requireGitNetwork,对写操作调用#requireWrite,并对cli参数做-C/--git-dir/--work-tree路径覆盖防护(见 bridge.ts)。
ws:artifacts
import { create, get, list, importArtifact, deleteArtifact, } from "ws:artifacts";这些模块是沙箱侧的 shim,背后是宿主 RPC。Loader binding、凭据、Durable Object 存储与不受限的 Workspace 对象永不进入用户代码。宿主桥接层在每次变更上都校验后端固定的 read / read-write 权限(#requireWrite);未配置 Artifacts binding 时,Artifacts 方法会明确失败(Workspace Artifacts are not configured for this execution.,见 bridge.ts)。远程ws:artifacts.importArtifact()独立于 Git 网络权限,默认也被拒绝,需allowArtifactNetwork: true。
路径约束的安全边界
路径约束会拒绝词法逃逸(..、NUL 字节等)与操作前路径中的每一个符号链接组件(见 capability.ts)。但要注意:这些检查不是原子化的 inode 式"root 之下"原语——不要把单个 isolate 能力当作针对另一个、可在同一可变 Workspace 中并发替换路径的更高权限主体的安全边界。需要对抗这种并发替换的部署,应等待未来的事务性 DOFS 原语,或使用独立的 Workspace 身份。
七、隔离与生命周期
每次执行都会获得一个全新的 Dynamic Worker,并伴随:
- 显式的 Worker Loader CPU 限额(
limits: { cpuMs: timeoutMs },见 worker-javascript.ts); - 宿主墙钟期限(
timeoutMs与maxTimeoutMs约束); - 默认
globalOutbound: null(无出站网络;也可通过egress策略或globalOutboundFetcher 配置网关); - 有限、无环、JSON 兼容的结构化输入与结果校验:
assertRuntimeValue拒绝循环、非平凡原型与非有限数值(见 capability.ts); - 可配置的 source / module graph / input / result / stdin / stdio / 文件与能力调用请求 / 响应字节限额;
- 显式的 entrypoint 与 Worker dispose(
disposeQuietly,见 worker-javascript.ts); - 宿主持有的取消(host-owned cancellation);
- 事件与结果行保留在 Workspace 数据库中。
实时输出管道
stdout / stderr实时流式输出:
- 隔离侧运行器把
IdentityTransformStream的可读端通过host.attachOutput(readable)交给宿主(见 worker-javascript.ts); - 宿主在用户代码仍在运行时逐帧排空该流(
#pumpFrames,见 worker-javascript.ts),每帧到达即追加到执行事件流(帧编解码见 packages/computer/src/backends/worker-javascript/frames.ts),而不是缓存到运行结束再发布; - 结构化结果与退出事件在输出流关闭后 settle,因此终态事件总是跟随最后一行输出;
- 输出由
maxStdioBytes在两条流上共同限定;超过后隔离侧会以...[stdio truncated]标记截断并停止记录(见 worker-javascript.ts)。
已完成写入立即可持久化;失败或取消不会回滚已完成的文件系统效果。
八、受信任的集成扩展(Trusted integrations)
宿主可以通过WorkerJavaScriptBackend.trustedModules配置额外的保留能力模块:
new WorkerJavaScriptBackend({ loader: env.LOADER, trustedModules: { "ws:my-service": { async call(method, args, context) { // 实现你的宿主能力 }, }, }, });这些模块在后端构造时固定,调用方源码无法提供或替换(类型定义WorkspaceTrustedModule见 packages/computer/src/runtime/types.ts)。受信任模块名必须以ws:开头、使用唯一简单的保留名,且不得与内置的node:fs、node:fs/promises、ws:git、ws:artifacts冲突(校验见 module-graph.ts)。宿主桥接层在分发trusted/<specifier>.call时同样会校验参数与返回值均为 JSON 兼容的 Workspace 值(assertBridgeValues),并为受信任模块传入{ signal, deadline }取消上下文(见 bridge.ts)。
九、相关文档与源码索引
- 运行时入口与后端路由:05. Runtime Interface、16. Execution runtime architecture
- 后端类型与选项定义:packages/computer/src/runtime/types.ts、packages/computer/src/backends/worker-javascript/worker-javascript.ts
- 模块图构建与能力 shim:packages/computer/src/backends/worker-javascript/module-graph.ts
- 帧编解码:packages/computer/src/backends/worker-javascript/frames.ts
- 宿主能力桥接与限额执行:packages/computer/src/runtime/bridge.ts
- 路径约束与结构化值校验:packages/computer/src/runtime/capability.ts
- 后端行为测试:packages/computer/src/backends/worker-javascript/worker-javascript.test.ts
- 包级总览与安装方式:docs/README.md(
npm install @cloudflare/computer,入口子路径@cloudflare/computer/backends/worker-javascript)
综上所述,WorkerJavaScriptBackend的价值在于把"运行一段不可信模块代码"与"访问持久化 Workspace 能力"之间,建立了一条显式、可配限额、可审计、可回放的受控边界:模块图在加载前静态解析并约束,能力调用在隔离内与宿主双重校验,输出实时回流且终态严格落库。它是构建 Agent 代码执行、插件沙箱、脚本化任务等场景的可靠基础件。
【免费下载链接】computerGive your agent a computer 👾项目地址: https://gitcode.com/GitHub_Trending/computer1/computer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考