三个运行时跑同一批任务 · 《什么时候,Agent 可以住进浏览器》的第二手材料 ================================================================== 日期:2026-10-20 内容:任务脚本、适配器、调度脚本、云端沙箱的部署配置、原始结果和汇总。 接口:每个运行时一个适配器,只有 boot / put / exec 三个方法;任务(tasks.mjs)对三者完全相同。 浏览器两格(WebContainers、Wasmer)跑的是线上两个页面各自的真实 Bash 工具(本地同源副本,Wasmer 页面为提交 c35595a 之后),不是 SDK 的裸调用。 云端一格是临时部署的 Cloudflare Sandbox(实例档位 lite,镜像为 Debian bookworm + Node 22 + git + python3 + build-essential,允许出站网络),测完已删除 Worker、容器应用和镜像。 每格只跑了一次(冷启动另有重复),不是基准。Wasmer 联网经本机 WISP 中继,用完已关。 调度脚本里的 __CFG__ 等占位符由启动器在运行时填入;令牌从本地文件读取,未包含在本文里。 ================================================================== 一、汇总(results.md) ================================================================== # 三个运行时跑同一批任务(2026-10-20) > 同一套任务、同一个适配器接口(`boot / put / exec`),任务脚本是 `tasks.mjs`,适配器是 `adapters.mjs`,调度是 `run.mjs`,原始结果是 `results-browser.json` 和 `results-cloud.json`。 > 浏览器两格跑的是线上两个页面的真实 `Bash` 工具(本地同源副本,提交 c35595a 之后),不是 SDK 裸调用;云端一格是临时部署的 Cloudflare Sandbox,`instance: lite`、Debian bookworm、Node 22、git、python3、build-essential、允许出站网络,跑完已删除 Worker、容器应用和镜像。 > 环境:macOS,Ego Lite(Chrome 内核),本机走代理;Wasmer 联网经本机 WISP 中继(`allow_private_ips`,用完已关)。每格只跑了一次(冷启动另有重复),不是基准。 ## 结果 | 任务 | WebContainers | Wasmer | Cloudflare Sandbox | |---|---|---|---| | node:test 红→绿(cart,3 个 bug) | ✓ exit 1; fail 4 / exit 0; pass 5 | ✓ exit 1; fail 4 / exit 0; pass 5 | ✓ exit 1; fail 4 / exit 0; pass 5 | | 装一个纯 JS 依赖再 require | ✓ exit 0 | ✓ exit 0 | ✓ exit 0 | | git init / commit / log | ✗ exit 127; jsh: command not found: git | ✗ exit 79; bash: line 2: git: command not found | ✓ exit 0 | | grep / sed / awk / find | ✗ grep✗ sed✗ awk✗ find✗ | ✗ grep✗ sed✗ awk✗ find✗ | ✓ grep✓ sed✓ awk✓ find✓ | | 装原生模块(better-sqlite3)并调用 | ✗ exit 1; Error: Cannot load native addon because loading addons is disabled: /home/project/t05/node_modules/b | ✗ exit 1; Error: Cannot find module '/workspace/t05/node_modules/better-sqlite3/build/Release/better_sqlite3.n | ✓ exit 0 | | Python 跑脚本并 import sqlite3 | ✗ python3 存在,但 `No module named '_sqlite3'` | ✓ sqlite 3.53.3 | ✓ sqlite 3.40.1 | | 出站 HTTPS 到没开 CORS 的站点(example.com) | ✗ ERR UND_ERR_SOCKET | ✓ status=200 | ✓ status=200 | | node 做管道上游(node -e … \| cat) | ✓ 有输出 | ✗ 输出为空 | ✓ 有输出 | | 超时强杀(node 空转 30 秒,限 3 秒) | ✓ 3.1s 返回,exit 143 | ✓ 3.8s 返回,exit 137 | ✓ 3.1s 返回,exit 124 | | 超时强杀(shell 的 sleep 30,限 3 秒) | ✗ 0.1s 返回,exit 127 | ✓ 3.0s 返回,exit 137 | ✓ 3.3s 返回,exit 124 | | CPU 吞吐(3 秒空转的迭代次数,越大越快) | ✓ 121.6M | ✓ 18.0M | ✓ 1.6M | ## 启动与延迟 | | WebContainers | Wasmer | Cloudflare Sandbox | |---|---|---|---| | 冷启动(全新站点分区首次访问) | 7.7 s、7.5 s | 33.7 s、26.7 s | 12.7 s、9.3 s(容器冷启动,第二次是新沙箱名) | | 清掉该来源存储后再来一次(HTTP 缓存还在) | 1.8 s | 8.7 s | — | | 暖命令 `echo x`(页面/客户端端到端,轮询间隔 50 ms) | 约 105 ms | 约 58 ms | 150–615 ms(含到 Cloudflare 的网络往返) | | 3 秒空转迭代次数(越大越快) | 121.6M | 18.0M | 1.6M(lite 实例,第一次跑是 2.9M) | ## 读这张表要小心的地方 - 浏览器两格的“冷启动”只在第一次访问一个新站点分区时才是冷的;清掉来源存储不会清 HTTP 缓存。Wasmer 要下约 89MB,所以冷热差距最大。 - Cloudflare 的 `lite` 是官方最小示例默认的实例档位,3 秒空转只有 1.6–2.9M 次,不代表云端沙箱都这么慢;我没有试更大的实例。 - WebContainers 的 `sleep` 在 jsh 里不存在(exit 127),所以“sleep 30”那一行的 ✗ 是工具缺失,不是超时失效;用 `node` 空转 30 秒的那一行才测到强杀(3.0 s,exit 143)。 - 任务 t05(原生模块):WebContainers 装得上、`require` 时报 `ERR_DLOPEN_DISABLED`;Wasmer 装不出可加载的二进制;云端实例有编译工具,约 37 s 跑通。 - 任务 t06:WebContainers 的 `python3` 存在但没有 `_sqlite3` 模块;Wasmer 的 Python 3.13 有 sqlite3(3.53.3),云端是系统 Python 3.11(sqlite 3.40.1)。 - Wasmer 的 `Bash` 工具里 `sleep 30` 限 3 秒曾经 30 秒才返回:SDK 自带的 `timeoutMs` 要等阻塞的 guest 调用结束才生效,工具已改成自己计时再 `kill()`(提交 c35595a)。这条是这次对照实验顺手发现的。 - 跑这套任务还暴露了两个测试脚本自己的错:没文件的任务没有建目录(`cd: no such file`,让好几格假失败、一格假通过);轮询间隔 700 ms 把暖命令延迟垫高。都已修,上表是修后的结果。 ================================================================== 二、任务(tasks.mjs) ================================================================== // Tasks for the runtime comparison. Each task gets its own directory `/` so the runtimes' shared workspaces do not collide. // A task = { id, title, files, steps: [{ cmd, timeoutMs, expect(result) -> { pass, note } }] }. {{install:pkg}} expands per runtime. const CART_BUGGY = `export function sum(nums) {\n return nums.reduce((a, b) => a + b);\n}\n\nexport function formatPrice(cents) {\n return '$' + Math.floor(cents / 100) + '.' + (cents % 100);\n}\n\nexport function applyDiscount(cents, percent) {\n return Math.round(cents * percent / 100);\n}\n\nexport function cartTotal(items) {\n return sum(items.map((i) => i.cents * i.qty));\n}\n`; const CART_FIXED = `export function sum(nums) {\n return nums.reduce((a, b) => a + b, 0);\n}\n\nexport function formatPrice(cents) {\n return '$' + Math.floor(cents / 100) + '.' + String(cents % 100).padStart(2, '0');\n}\n\nexport function applyDiscount(cents, percent) {\n return cents - Math.round(cents * percent / 100);\n}\n\nexport function cartTotal(items) {\n return sum(items.map((i) => i.cents * i.qty));\n}\n`; const CART_TEST = `import test from 'node:test';\nimport assert from 'node:assert';\nimport { sum, formatPrice, applyDiscount, cartTotal } from './cart.js';\n\ntest('sum adds numbers', () => { assert.strictEqual(sum([1, 2, 3]), 6); });\ntest('sum of an empty list is 0', () => { assert.strictEqual(sum([]), 0); });\ntest('formatPrice pads the cents', () => {\n assert.strictEqual(formatPrice(305), '$3.05');\n assert.strictEqual(formatPrice(1200), '$12.00');\n assert.strictEqual(formatPrice(99), '$0.99');\n});\ntest('applyDiscount returns the discounted price', () => {\n assert.strictEqual(applyDiscount(1000, 20), 800);\n assert.strictEqual(applyDiscount(999, 10), 899);\n});\ntest('cartTotal multiplies by quantity', () => {\n assert.strictEqual(cartTotal([{ cents: 250, qty: 2 }, { cents: 100, qty: 1 }]), 600);\n assert.strictEqual(cartTotal([]), 0);\n});\n`; export { CART_FIXED }; export const tasks = [ { id: "t01", title: "node:test 红→绿(cart,3 个 bug)", files: { "package.json": '{"type":"module"}\n', "cart.js": CART_BUGGY, "cart.test.js": CART_TEST }, steps: [ { cmd: "node --test --test-reporter=spec", timeoutMs: 60000, label: "改前跑测试", expect: (r) => ({ pass: r.code !== 0 && /fail\s+4/.test(r.out) && /pass\s+1/.test(r.out), note: `exit ${r.code}; ${(r.out.match(/fail\s+\d+/) || ["no fail line"])[0]}` }) }, { replaceFile: ["cart.js", CART_FIXED] }, { cmd: "node --test --test-reporter=spec", timeoutMs: 60000, label: "改后跑测试", expect: (r) => ({ pass: r.code === 0 && /pass\s+5/.test(r.out), note: `exit ${r.code}; ${(r.out.match(/pass\s+\d+/) || ["no pass line"])[0]}` }) }, ] }, { id: "t02", title: "装一个纯 JS 依赖再 require", files: { "package.json": '{"name":"lab","version":"1.0.0"}\n' }, steps: [{ cmd: "{{install:is-odd}} && node -e \"console.log('is-odd(3)=' + require('is-odd')(3))\"", timeoutMs: 240000, label: "安装并使用", expect: (r) => ({ pass: r.code === 0 && /is-odd\(3\)=true/.test(r.out), note: `exit ${r.code}${/is-odd\(3\)=true/.test(r.out) ? "" : "; " + r.out.slice(-120).replace(/\s+/g, " ")}` }) }] }, { id: "t03", title: "git init / commit / log", files: {}, steps: [{ cmd: "git init -q . && git config user.email lab@example.com && git config user.name lab && echo x > f.txt && git add f.txt && git commit -q -m init && git log --oneline", timeoutMs: 60000, expect: (r) => ({ pass: r.code === 0 && /\binit\b/.test(r.out), note: `exit ${r.code}${r.code === 0 ? "" : "; " + r.out.slice(-80).replace(/\s+/g, " ")}` }) }] }, { id: "t04", title: "grep / sed / awk / find", files: {}, steps: [{ cmd: "printf 'a\\nb foo\\n' > f.txt; echo \"grep=$(grep -c foo f.txt 2>/dev/null)\"; echo \"sed=$(sed -n 2p f.txt 2>/dev/null)\"; echo \"awk=$(awk 'END{print NR}' f.txt 2>/dev/null)\"; echo \"find=$(find . -name f.txt 2>/dev/null | head -n 1)\"", timeoutMs: 60000, expect: (r) => { const ok = { grep: /grep=1\b/.test(r.out), sed: /sed=b foo/.test(r.out), awk: /awk=2\b/.test(r.out), find: /find=\.\/f\.txt/.test(r.out) }; return { pass: Object.values(ok).every(Boolean), note: Object.entries(ok).map(([k, v]) => `${k}${v ? "✓" : "✗"}`).join(" ") }; } }] }, { id: "t05", title: "装原生模块(better-sqlite3)并调用", files: { "package.json": '{"name":"lab","version":"1.0.0"}\n' }, steps: [{ cmd: "{{install:better-sqlite3}} && node -e \"const D=require('better-sqlite3'); console.log('sqlite=' + JSON.stringify(new D(':memory:').prepare('select 1 as x').get()))\"", timeoutMs: 300000, label: "安装并调用", expect: (r) => ({ pass: r.code === 0 && /sqlite=\{"x":1\}/.test(r.out), note: `exit ${r.code}${/sqlite=\{"x":1\}/.test(r.out) ? "" : "; " + (r.out.match(/ERR_[A-Z_]+|Cannot find module[^\n]*|gyp[^\n]*|error[^\n]*/i) || [r.out.slice(-80).replace(/\s+/g, " ")])[0].slice(0, 100)}` }) }] }, { id: "t06", title: "Python 跑脚本并 import sqlite3", files: {}, steps: [{ cmd: "python3 -c \"import sqlite3; print('sqlite', sqlite3.sqlite_version)\" || python -c \"import sqlite3; print('sqlite', sqlite3.sqlite_version)\"", timeoutMs: 240000, expect: (r) => { const ok = r.code === 0 && /sqlite \d/.test(r.out); return { pass: ok, note: ok ? (r.out.match(/sqlite \S+/) || [""])[0] : `exit ${r.code}; ${r.out.slice(-110).replace(/\s+/g, " ")}` }; } }] }, { id: "t07", title: "出站 HTTPS 到没开 CORS 的站点(example.com)", files: {}, steps: [{ cmd: "node -e \"fetch('https://example.com').then(r => console.log('status=' + r.status)).catch(e => console.log('ERR ' + ((e.cause && e.cause.code) || e.message)))\"", timeoutMs: 90000, expect: (r) => ({ pass: /status=200/.test(r.out), note: (r.out.match(/status=\d+|ERR \S+/) || [`exit ${r.code}`])[0] }) }] }, { id: "t08", title: "node 做管道上游(node -e … | cat)", files: {}, steps: [{ cmd: "node -e \"console.log('piped-ok')\" | cat", timeoutMs: 60000, expect: (r) => ({ pass: /piped-ok/.test(r.out), note: /piped-ok/.test(r.out) ? "有输出" : "输出为空" }) }] }, { id: "t09", title: "超时强杀(node 空转 30 秒,限 3 秒)", files: {}, steps: [{ cmd: "node -e \"setTimeout(() => {}, 30000)\"", timeoutMs: 3000, expect: (r) => ({ pass: r.ms < 12000 && r.code !== 0, note: `${(r.ms / 1000).toFixed(1)}s 返回,exit ${r.code}` }) }] }, { id: "t09b", title: "超时强杀(shell 的 sleep 30,限 3 秒)", files: {}, steps: [{ cmd: "sleep 30", timeoutMs: 3000, expect: (r) => ({ pass: r.ms < 12000 && r.code !== 0 && r.code !== 127, note: `${(r.ms / 1000).toFixed(1)}s 返回,exit ${r.code}` }) }] }, { id: "t10", title: "CPU 吞吐(3 秒空转的迭代次数,越大越快)", files: {}, steps: [{ cmd: "node -e \"let n=0; const t=Date.now(); while (Date.now()-t<3000) n++; console.log('iters=' + n)\"", timeoutMs: 60000, expect: (r) => { const m = r.out.match(/iters=(\d+)/); return { pass: !!m, note: m ? `${(+m[1] / 1e6).toFixed(1)}M` : "无数字", metric: m ? +m[1] : null }; } }] }, ]; ================================================================== 三、适配器(adapters.mjs) ================================================================== // One interface, three runtimes. boot() -> { coldMs, ok }; put(dir, files); exec(cmd, { timeoutMs, cwd }) -> { code, out, ms }; installCmd(pkg). const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const shq = (s) => "'" + s.replace(/'/g, "'\\''") + "'"; function browserAdapter({ name, pm, page, url, flag, relay }) { const a = { name, pm, installCmd: (pkg) => (pm === "pnpm" ? `pnpm add ${pkg}` : `npm install ${pkg} --no-audit --no-fund`), async boot() { await page.cdp("Storage.clearDataForOrigin", { origin: new URL(url).origin, storageTypes: "all" }); // cold: this test origin only await page.goto(url); await page.waitForFunction(() => window.__ca, undefined, { timeout: 30000 }); await page.evaluate(`window.__ca.${flag}.allowed = true; 1`); if (relay) await page.evaluate(`window.__ca.COMMANDS.wisp.run(${JSON.stringify(relay)}); 1`); const t0 = Date.now(); const r = await a.exec("echo boot-ok", { timeoutMs: 240000 }); return { coldMs: Date.now() - t0, ok: /boot-ok/.test(r.out) }; }, async put(dir, files) { await page.evaluate((x) => { const ca = window.__ca; for (const [k, v] of Object.entries(x.files)) ca.ws.files.set(x.dir + "/" + k, v); return 1; }, { dir, files }); }, async exec(cmd, { timeoutMs = 60000, cwd } = {}) { const full = cwd ? `cd ${cwd} && ${cmd}` : cmd; const t0 = Date.now(); await page.evaluate(`window.__r = null; window.__ca.TOOLS.Bash.run({ command: ${JSON.stringify(full)}, timeout_ms: ${timeoutMs} }).then((r) => { window.__r = { r }; }, (e) => { window.__r = { err: String(e) }; }); 1`); let st; while (Date.now() - t0 < timeoutMs + 120000) { await sleep(50); st = await page.evaluate(() => window.__r); if (st) break; } const ms = Date.now() - t0; if (!st) return { code: -2, out: "(harness gave up waiting)", ms }; if (st.err) return { code: -3, out: st.err, ms }; const r = st.r; if (r.error) return { code: -1, out: r.error, ms }; return { code: r.exit_code, out: r.output || "", ms, toolMs: r.elapsed_ms, warning: r.warning }; }, async close() {}, }; return a; } function cloudAdapter({ url, token, sandbox }) { const H = { Authorization: `Bearer ${token}`, "content-type": "application/json" }; const a = { name: "cloud", pm: "npm", installCmd: (pkg) => `npm install ${pkg} --no-audit --no-fund`, async boot() { const t0 = Date.now(); const r = await a.exec("echo boot-ok", { timeoutMs: 240000 }); return { coldMs: Date.now() - t0, ok: /boot-ok/.test(r.out) }; }, async put(dir, files) { await a.exec(`mkdir -p ${dir}`); for (const [k, v] of Object.entries(files)) { const res = await fetch(`${url}/sandboxes/${sandbox}/files/${dir}/${k}`, { method: "PUT", headers: { Authorization: `Bearer ${token}` }, body: v }); if (!res.ok) throw new Error(`put ${dir}/${k}: ${res.status}`); } }, async exec(cmd, { timeoutMs = 60000, cwd } = {}) { const secs = Math.max(1, Math.ceil(timeoutMs / 1000)); const wrapped = `cd /workspace${cwd ? "/" + cwd : ""} && timeout -k 1 ${secs} bash -lc ${shq(cmd)}`; const t0 = Date.now(); const res = await fetch(`${url}/sandboxes/${sandbox}/exec`, { method: "POST", headers: H, body: JSON.stringify({ argv: ["bash", "-lc", wrapped] }) }); const ms = Date.now() - t0; if (!res.ok) return { code: -1, out: `HTTP ${res.status} ${(await res.text()).slice(0, 120)}`, ms }; const j = await res.json(); return { code: j.exitCode, out: (j.stdout || "") + (j.stderr || ""), ms }; }, async close() { await fetch(`${url}/sandboxes/${sandbox}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` } }).catch(() => {}); }, }; return a; } ================================================================== 四、调度(run.mjs) ================================================================== // Orchestrator. Placeholders (__X__) are filled by the launcher. const fs = await import("node:fs/promises"); const CFG = __CFG__; const results = { startedAt: new Date().toISOString(), adapters: {} }; const OUT = CFG.out; const save = () => fs.writeFile(OUT, JSON.stringify(results, null, 1)); for (const which of CFG.adapters) { let task, page, adapter; if (which === "cloud") { adapter = cloudAdapter({ url: CFG.cloudUrl, token: (await fs.readFile(CFG.tokenFile, "utf8")).trim(), sandbox: CFG.sandbox }); } else { task = await taskSpace("runtime lab " + which); page = task.page("p1"); adapter = which === "wc" ? browserAdapter({ name: "webcontainers", pm: "npm", page, url: CFG.base + "/browser-coding-agent/index.html", flag: "WC" }) : browserAdapter({ name: "wasmer", pm: "pnpm", page, url: CFG.base + "/browser-coding-agent-wasmer/index.html", flag: "WR", relay: CFG.relay }); } const rec = results.adapters[adapter.name] = { tasks: {} }; try { const boot = await adapter.boot(); rec.boot = boot; await save(); console.log(adapter.name, "boot", JSON.stringify(boot)); const warm = []; for (let i = 0; i < 5; i++) warm.push((await adapter.exec("echo x", { timeoutMs: 30000 })).ms); rec.warmEchoMs = warm; for (const t of tasks) { const tr = rec.tasks[t.id] = { title: t.title, steps: [] }; try { await adapter.put(t.id, { ".keep": "\n", ...t.files }); for (const s of t.steps) { if (s.replaceFile) { await adapter.put(t.id, { [s.replaceFile[0]]: s.replaceFile[1] }); continue; } const cmd = s.cmd.replace(/\{\{install:([^}]+)\}\}/g, (_, p) => adapter.installCmd(p)); const r = await adapter.exec(cmd, { timeoutMs: s.timeoutMs, cwd: t.id }); const v = s.expect(r); tr.steps.push({ label: s.label || "", pass: v.pass, note: v.note, metric: v.metric ?? null, code: r.code, ms: r.ms, tail: r.out.slice(-220) }); } tr.pass = tr.steps.every((x) => x.pass); } catch (e) { tr.pass = false; tr.error = String(e && e.message || e).slice(0, 200); } console.log(adapter.name, t.id, tr.pass ? "PASS" : "FAIL", tr.steps.map((x) => x.note).join(" | ").slice(0, 160)); await save(); } } catch (e) { rec.error = String(e && e.message || e); console.log(adapter.name, "ABORTED", rec.error); await save(); } await adapter.close(); if (task) await task.finish({ keep: [] }); } results.finishedAt = new Date().toISOString(); await save(); console.log("DONE"); ================================================================== 五、云端沙箱部署配置(cloudflare-sandbox/Dockerfile) ================================================================== # The donor image ships the shim that @cloudflare/sandbox runs in the container. ARG SANDBOX_TOOLS_IMAGE=docker.io/cloudflare/sandbox:1.0.2 FROM ${SANDBOX_TOOLS_IMAGE} AS sandbox-tools FROM node:22-bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends git python3 python3-pip curl ca-certificates build-essential procps && rm -rf /var/lib/apt/lists/* COPY --from=sandbox-tools /usr/local/bin/sandbox-shim /usr/local/bin/sandbox-shim RUN mkdir -p /workspace CMD ["sleep", "infinity"] ================================================================== 五、云端沙箱部署配置(cloudflare-sandbox/wrangler.jsonc) ================================================================== { "$schema": "node_modules/wrangler/config-schema.json", "name": "runtime-lab-temp", "main": "src/index.ts", "compatibility_date": "2026-09-02", // Files resolves native Linux errno names through node:os. "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [ { "name": "SANDBOX", "class_name": "Sandbox", }, ], }, "containers": [ { "class_name": "Sandbox", "scheduling_policy": "durable_object", "images": { "sandbox": { "dockerfile": "./Dockerfile", }, }, }, ], "exports": { "Sandbox": { "type": "durable-object", "storage": "sqlite", }, }, "observability": { "enabled": true, "traces": { "enabled": true, }, }, } ================================================================== 五、云端沙箱部署配置(cloudflare-sandbox/package.json) ================================================================== { "name": "sandbox-minimal", "private": true, "type": "module", "scripts": { "dev": "wrangler dev", "deploy": "wrangler deploy", "types": "wrangler types --include-runtime=false" }, "dependencies": { "@cloudflare/sandbox": "1.0.2", "zod": "^4.5.4" }, "devDependencies": { "@cloudflare/workers-types": "^5.20261008.1", "typescript": "^6.0.3", "wrangler": "^4.148.0" } } ================================================================== 五、云端沙箱部署配置(cloudflare-sandbox/index.ts) ================================================================== import { Files, SandboxFileError, SandboxProtocolError } from "@cloudflare/sandbox"; import { DurableObject } from "cloudflare:workers"; import { z } from "zod"; const SANDBOX_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/; const WORKSPACE_DIRECTORY = "/workspace"; const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000; const ExecRequest = z.object({ argv: z.array(z.string()).min(1) }); interface Env { SANDBOX: DurableObjectNamespace; LAB_TOKEN: string; } interface CommandResult { exitCode: number; stdout: string; stderr: string; } export class Sandbox extends DurableObject { readonly files: Files; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); const container = this.requireContainer(); this.files = new Files(container); // Each Durable Object instance must set its own timeout; it is not inherited. if (container.running) { void ctx.blockConcurrencyWhile(() => container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS)); } } async writeFile(path: string, contents: ReadableStream): Promise { await this.ensureRunning(); await this.files.writeFile(path, contents); } async readFile(path: string): Promise { await this.ensureRunning(); return this.files.readFile(path); } async exec(argv: string[]): Promise { const container = await this.ensureRunning(); const process = await container.exec(argv, { cwd: WORKSPACE_DIRECTORY }); const output = await process.output(); const decoder = new TextDecoder(); return { exitCode: output.exitCode, stdout: decoder.decode(output.stdout), stderr: decoder.decode(output.stderr), }; } async destroy(): Promise { const container = this.requireContainer(); if (container.running) await container.destroy(); } private async ensureRunning(): Promise { const container = this.requireContainer(); if (!container.running) { container.start({ image: container.images.sandbox, instance: "lite", enableInternet: true, }); await container.setInactivityTimeout(INACTIVITY_TIMEOUT_MS); } return container; } private requireContainer(): Container { const container = this.ctx.container; if (container === undefined) { throw new Error("Container attachment is unavailable"); } return container; } } export default { async fetch(request, env): Promise { const url = new URL(request.url); if (!env.LAB_TOKEN || request.headers.get("authorization") !== `Bearer ${env.LAB_TOKEN}`) return new Response("Unauthorized", { status: 401 }); const match = /^\/sandboxes\/([^/]+)(\/exec|\/files(\/.+))?$/.exec(url.pathname); if (match === null) return new Response("Not found", { status: 404 }); const sandboxName = match[1]; if (!SANDBOX_NAME_PATTERN.test(sandboxName)) { return new Response( "sandbox name must contain 1-63 lowercase letters, digits, or hyphens and start with a letter or digit", { status: 400 }, ); } const sandbox = env.SANDBOX.getByName(sandboxName); const filePath = match[3] === undefined ? undefined : workspacePath(match[3]); if (match[3] !== undefined && filePath === undefined) { return new Response("Invalid file path", { status: 400 }); } try { if (match[2] === "/exec" && request.method === "POST") { const { argv } = ExecRequest.parse(await request.json()); return Response.json(await sandbox.exec(argv)); } if (filePath !== undefined && request.method === "PUT") { if (request.body === null) { return new Response("Request body is required", { status: 400 }); } await sandbox.writeFile(filePath, request.body); return new Response(null, { status: 204 }); } if (filePath !== undefined && request.method === "GET") { return await sandbox.readFile(filePath); } if (match[2] === undefined && request.method === "DELETE") { await sandbox.destroy(); return new Response(null, { status: 204 }); } return new Response("Method not allowed", { status: 405 }); } catch (cause) { if (cause instanceof SyntaxError || cause instanceof z.ZodError) { return new Response('Body must be JSON such as {"argv": ["ls", "-la"]}', { status: 400 }); } console.error({ event: "sandbox.request.failed", sandboxName, path: url.pathname, error: describeError(cause), }); return errorResponse(cause); } }, } satisfies ExportedHandler; // Files follows Linux path rules and enforces no policy, so the Worker keeps paths in /workspace. function workspacePath(routePath: string): string | undefined { let decoded: string; try { decoded = decodeURIComponent(routePath); } catch { return undefined; } const segments = decoded.split("/").slice(1); if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) { return undefined; } return `${WORKSPACE_DIRECTORY}/${segments.join("/")}`; } // Structured logs drop an Error's message and stack because they are not enumerable. function describeError(cause: unknown): string { return cause instanceof Error && cause.stack !== undefined ? cause.stack : String(cause); } function errorResponse(cause: unknown): Response { if (SandboxFileError.is(cause)) { if (cause.code === "ENOENT") return new Response("No such file or directory", { status: 404 }); if (cause.code === "EACCES" || cause.code === "EPERM") { return new Response("Permission denied", { status: 403 }); } return new Response("File operation failed", { status: 500 }); } if (SandboxProtocolError.is(cause)) { return new Response("Sandbox file protocol failed", { status: 500 }); } return new Response("Sandbox request failed", { status: 500 }); } ================================================================== 六、原始结果 · 浏览器两格(results-browser.json) ================================================================== { "startedAt": "2026-10-20T11:20:21.742Z", "adapters": { "webcontainers": { "tasks": { "t01": { "title": "node:test 红→绿(cart,3 个 bug)", "steps": [ { "label": "改前跑测试", "pass": true, "note": "exit 1; fail 4", "metric": null, "code": 1, "ms": 579, "tail": "array with no initial value\n at sum (file:///home/project/t01/cart.js:18:15)\n at Module.cartTotal (file:///home/project/t01/cart.js:30:10)\n at TestContext.eval (file:///home/project/t01/cart.test.js:29:44)" }, { "label": "改后跑测试", "pass": true, "note": "exit 0; pass 5", "metric": null, "code": 0, "ms": 627, "tail": " pads the cents (0.060001ms)\n✔ applyDiscount returns the discounted price (0.11ms)\n✔ cartTotal multiplies by quantity (0.08ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 485.2" } ], "pass": true }, "t02": { "title": "装一个纯 JS 依赖再 require", "steps": [ { "label": "安装并使用", "pass": true, "note": "exit 0", "metric": null, "code": 0, "ms": 3530, "tail": "added 2 packages in 3s\nis-odd(3)=true" } ], "pass": true }, "t03": { "title": "git init / commit / log", "steps": [ { "label": "", "pass": false, "note": "exit 127; jsh: command not found: git", "metric": null, "code": 127, "ms": 106, "tail": "jsh: command not found: git" } ], "pass": false }, "t04": { "title": "grep / sed / awk / find", "steps": [ { "label": "", "pass": false, "note": "grep✗ sed✗ awk✗ find✗", "metric": null, "code": 0, "ms": 158, "tail": "jsh: command not found: printf\ngrep=\nsed=\nawk=\nfind=" } ], "pass": false }, "t05": { "title": "装原生模块(better-sqlite3)并调用", "steps": [ { "label": "安装并调用", "pass": false, "note": "exit 1; Error: Cannot load native addon because loading addons is disabled: /home/project/t05/node_modules/b", "metric": null, "code": 1, "ms": 2522, "tail": " Cannot load native addon because loading addons is disabled: /home/project/t05/node_modules/better-sqlite3/prebuilds/linuxmusl-x64.node\n at eval ([eval]-wrapper:6:24)\n code: 'ERR_DLOPEN_DISABLED'\n}\n\nNode.js v22.22.3" } ], "pass": false }, "t06": { "title": "Python 跑脚本并 import sqlite3", "steps": [ { "label": "", "pass": false, "note": "exit 1; line 27, in ModuleNotFoundError: No module named '_sqlite3' jsh: line 1: \"foo(\" must be followed by )", "metric": null, "code": 1, "ms": 5798, "tail": " last):\n File \"\", line 1, in \n File \"sqlite3\", line 57, in \n File \"sqlite3.dbapi2\", line 27, in \nModuleNotFoundError: No module named '_sqlite3'\njsh: line 1: \"foo(\" must be followed by )" } ], "pass": false }, "t07": { "title": "出站 HTTPS 到没开 CORS 的站点(example.com)", "steps": [ { "label": "", "pass": false, "note": "ERR UND_ERR_SOCKET", "metric": null, "code": 0, "ms": 791, "tail": "ERR UND_ERR_SOCKET" } ], "pass": false }, "t08": { "title": "node 做管道上游(node -e … | cat)", "steps": [ { "label": "", "pass": true, "note": "有输出", "metric": null, "code": 0, "ms": 157, "tail": "piped-ok" } ], "pass": true }, "t09": { "title": "超时强杀(node 空转 30 秒,限 3 秒)", "steps": [ { "label": "", "pass": true, "note": "3.1s 返回,exit 143", "metric": null, "code": 143, "ms": 3058, "tail": "" } ], "pass": true }, "t09b": { "title": "超时强杀(shell 的 sleep 30,限 3 秒)", "steps": [ { "label": "", "pass": false, "note": "0.1s 返回,exit 127", "metric": null, "code": 127, "ms": 106, "tail": "jsh: command not found: sleep" } ], "pass": false }, "t10": { "title": "CPU 吞吐(3 秒空转的迭代次数,越大越快)", "steps": [ { "label": "", "pass": true, "note": "121.6M", "metric": 121631013, "code": 0, "ms": 3142, "tail": "iters=121631013" } ], "pass": true } }, "boot": { "coldMs": 7545, "ok": true }, "warmEchoMs": [ 106, 106, 105, 106, 105 ] }, "wasmer": { "tasks": { "t01": { "title": "node:test 红→绿(cart,3 个 bug)", "steps": [ { "label": "改前跑测试", "pass": true, "note": "exit 1; fail 4", "metric": null, "code": 1, "ms": 320, "tail": "plies by quantity\n TypeError: Reduce of empty array with no initial value\n at Array.reduce ()\n at sum (:5:15)\n at cartTotal (:20:10)\n at TestContext.eval (:21:22)\n …" }, { "label": "改后跑测试", "pass": true, "note": "exit 0; pass 5", "metric": null, "code": 0, "ms": 268, "tail": "mpty list is 0\n✔ formatPrice pads the cents\n✔ applyDiscount returns the discounted price\n✔ cartTotal multiplies by quantity (1ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 33" } ], "pass": true }, "t02": { "title": "装一个纯 JS 依赖再 require", "steps": [ { "label": "安装并使用", "pass": true, "note": "exit 0", "metric": null, "code": 0, "ms": 2773, "tail": "Progress: resolved 1, reused 0, downloaded 0, added 0\n\ndependencies:\n+ is-odd ^3.0.1\n\nPackages: +2\n++\nProgress: resolved 2, reused 0, downloaded 2, added 2, done\nDone in 2.1s using pnpm v10.34.5\nis-odd(3)=true" } ], "pass": true }, "t03": { "title": "git init / commit / log", "steps": [ { "label": "", "pass": false, "note": "exit 79; bash: line 2: git: command not found", "metric": null, "code": 79, "ms": 57, "tail": "bash: line 2: git: command not found" } ], "pass": false }, "t04": { "title": "grep / sed / awk / find", "steps": [ { "label": "", "pass": false, "note": "grep✗ sed✗ awk✗ find✗", "metric": null, "code": 0, "ms": 58, "tail": "grep=\nsed=\nawk=\nfind=" } ], "pass": false }, "t05": { "title": "装原生模块(better-sqlite3)并调用", "steps": [ { "label": "安装并调用", "pass": false, "note": "exit 1; Error: Cannot find module '/workspace/t05/node_modules/better-sqlite3/build/Release/better_sqlite3.n", "metric": null, "code": 1, "ms": 5062, "tail": "e: 'MODULE_NOT_FOUND',\n requireStack: [\n '/workspace/t05/node_modules/better-sqlite3/lib/binding.js',\n '/workspace/t05/node_modules/better-sqlite3/lib/index.js',\n '/workspace/t05/[eval]'\n ]\n}\n\nNode.js v24.13.2" } ], "pass": false }, "t06": { "title": "Python 跑脚本并 import sqlite3", "steps": [ { "label": "", "pass": true, "note": "sqlite 3.53.3", "metric": null, "code": 0, "ms": 18087, "tail": "sqlite 3.53.3" } ], "pass": true }, "t07": { "title": "出站 HTTPS 到没开 CORS 的站点(example.com)", "steps": [ { "label": "", "pass": true, "note": "status=200", "metric": null, "code": 0, "ms": 955, "tail": "status=200" } ], "pass": true }, "t08": { "title": "node 做管道上游(node -e … | cat)", "steps": [ { "label": "", "pass": false, "note": "输出为空", "metric": null, "code": 0, "ms": 165, "tail": "" } ], "pass": false }, "t09": { "title": "超时强杀(node 空转 30 秒,限 3 秒)", "steps": [ { "label": "", "pass": true, "note": "3.8s 返回,exit 137", "metric": null, "code": 137, "ms": 3834, "tail": "" } ], "pass": true }, "t09b": { "title": "超时强杀(shell 的 sleep 30,限 3 秒)", "steps": [ { "label": "", "pass": true, "note": "3.0s 返回,exit 137", "metric": null, "code": 137, "ms": 3018, "tail": "" } ], "pass": true }, "t10": { "title": "CPU 吞吐(3 秒空转的迭代次数,越大越快)", "steps": [ { "label": "", "pass": true, "note": "18.0M", "metric": 17966511, "code": 0, "ms": 3142, "tail": "iters=17966511" } ], "pass": true } }, "boot": { "coldMs": 26714, "ok": true }, "warmEchoMs": [ 57, 58, 59, 58, 58 ] } }, "finishedAt": "2026-10-20T11:21:57.798Z" } ================================================================== 七、原始结果 · 云端一格(results-cloud.json) ================================================================== { "startedAt": "2026-10-20T11:18:13.267Z", "adapters": { "cloud": { "tasks": { "t01": { "title": "node:test 红→绿(cart,3 个 bug)", "steps": [ { "label": "改前跑测试", "pass": true, "note": "exit 1; fail 4", "metric": null, "code": 1, "ms": 882, "tail": "er/test:744:18)\n at Test.postRun (node:internal/test_runner/test:1173:19)\n at Test.run (node:internal/test_runner/test:1101:12)\n at async Test.processPendingSubtests (node:internal/test_runner/test:744:7)\n" }, { "label": "改后跑测试", "pass": true, "note": "exit 0; pass 5", "metric": null, "code": 0, "ms": 524, "tail": "ents (0.22681ms)\n✔ applyDiscount returns the discounted price (0.25355ms)\n✔ cartTotal multiplies by quantity (0.37557ms)\nℹ tests 5\nℹ suites 0\nℹ pass 5\nℹ fail 0\nℹ cancelled 0\nℹ skipped 0\nℹ todo 0\nℹ duration_ms 160.886227\n" } ], "pass": true }, "t02": { "title": "装一个纯 JS 依赖再 require", "steps": [ { "label": "安装并使用", "pass": true, "note": "exit 0", "metric": null, "code": 0, "ms": 1976, "tail": "odd(3)=true\nnpm notice\nnpm notice New major version of npm available! 10.9.9 -> 12.2.0\nnpm notice Changelog: https://github.com/npm/cli/releases/tag/v12.2.0\nnpm notice To update run: npm install -g npm@12.2.0\nnpm notice\n" } ], "pass": true }, "t03": { "title": "git init / commit / log", "steps": [ { "label": "", "pass": true, "note": "exit 0", "metric": null, "code": 0, "ms": 331, "tail": "98c857f init\n" } ], "pass": true }, "t04": { "title": "grep / sed / awk / find", "steps": [ { "label": "", "pass": true, "note": "grep✓ sed✓ awk✓ find✓", "metric": null, "code": 0, "ms": 174, "tail": "grep=1\nsed=b foo\nawk=2\nfind=./f.txt\n" } ], "pass": true }, "t05": { "title": "装原生模块(better-sqlite3)并调用", "steps": [ { "label": "安装并调用", "pass": true, "note": "exit 0", "metric": null, "code": 0, "ms": 36963, "tail": "\nadded 2 packages in 33s\nsqlite={\"x\":1}\n" } ], "pass": true }, "t06": { "title": "Python 跑脚本并 import sqlite3", "steps": [ { "label": "", "pass": true, "note": "sqlite 3.40.1", "metric": null, "code": 0, "ms": 9706, "tail": "py=/usr/bin/python3\nsqlite 3.40.1\n" } ], "pass": true }, "t07": { "title": "出站 HTTPS 到没开 CORS 的站点(example.com)", "steps": [ { "label": "", "pass": true, "note": "status=200", "metric": null, "code": 0, "ms": 6711, "tail": "status=200\n" } ], "pass": true }, "t08": { "title": "node 做管道上游(node -e … | cat)", "steps": [ { "label": "", "pass": true, "note": "有输出", "metric": null, "code": 0, "ms": 1990, "tail": "piped-ok\n" } ], "pass": true }, "t09": { "title": "超时强杀(node 空转 30 秒,限 3 秒)", "steps": [ { "label": "", "pass": true, "note": "3.1s 返回,exit 124", "metric": null, "code": 124, "ms": 3140, "tail": "" } ], "pass": true }, "t09b": { "title": "超时强杀(shell 的 sleep 30,限 3 秒)", "steps": [ { "label": "", "pass": true, "note": "3.3s 返回,exit 124", "metric": null, "code": 124, "ms": 3265, "tail": "" } ], "pass": true }, "t10": { "title": "CPU 吞吐(3 秒空转的迭代次数,越大越快)", "steps": [ { "label": "", "pass": true, "note": "1.6M", "metric": 1588866, "code": 0, "ms": 5290, "tail": "iters=1588866\n" } ], "pass": true } }, "boot": { "coldMs": 9259, "ok": true }, "warmEchoMs": [ 150, 615, 290, 235, 224 ] } }, "finishedAt": "2026-10-20T11:19:47.911Z" }