// arcrun `code` 零件 —— 沙箱核心(PoC 參考實作) // --------------------------------------------------------------------------- // 語義:n8n Code node 式。config 帶一段 inline user JS,stdin 帶 input JSON。 // user code 只能:讀 `input`(已解析的 stdin JSON)、回傳一個 JSON-able 值。 // user code 碰不到:網路 / 檔案 / env / secret / Worker 物件圖。 // // 隔離機制:user JS 跑在 QuickJS(JS 直譯器)編成的 wasm sandbox 內。QuickJS // context 的 global 只有純 ECMAScript 內建(Object/Array/JSON/Math/Date/String…), // 沒有 fetch / process / require / globalThis.env / WebAssembly / 任何 host binding // —— 「無 ambient 能力(no ambient capability by construction)」。要給的能力, // 只能由 host 明確、逐一注入為 curated builtin(本檔 = 一個純函式 sha256)。 // // 對齊 arcrun 契約:io_model=stdin_stdout_json、no_network_syscall、 // no_filesystem_syscall。與其他零件唯一差別=直譯器是 QuickJS-wasm 而非 TinyGo-wasm。 import { getQuickJS } from 'quickjs-emscripten'; import { createHash } from 'node:crypto'; export const DEFAULT_LIMITS = { timeout_ms: 1000, // 執行牆鐘上限(interrupt handler 逐指令檢查 deadline) memory_bytes: 16 * 1024 * 1024, // QuickJS runtime 記憶體硬上限 max_stack_bytes: 512 * 1024, // 遞迴/深堆疊上限 max_output_bytes: 1024 * 1024, // stdout JSON 大小上限(防跑飛) max_code_bytes: 256 * 1024, // user code 本身大小上限 }; // curated 安全 builtin:純、決定性、零 ambient 能力(不能碰網路/檔案/secret)。 // PoC 用 Node crypto 實作 sha256;live(Worker)要換成 Web Crypto 的 // crypto.subtle.digest('SHA-256', …)(見 DESIGN.md「curated builtins」)。 function hostSha256(s) { return createHash('sha256').update(s, 'utf8').digest('hex'); } /** * 在沙箱內跑一段 user code。 * @param {string} code user 的 inline JS(函式體:可含宣告、以 `return` 回值) * @param {*} input 已解析的 stdin JSON(會以 `input` 綁進沙箱) * @param {object} [opts] { limits, builtins } * @returns {Promise<{success:true,data:*}|{success:false,error:string,error_type?:string}>} */ export async function runCode(code, input, opts = {}) { const limits = { ...DEFAULT_LIMITS, ...(opts.limits || {}) }; if (typeof code !== 'string') { return err('code must be a string', 'ContractError'); } if (Buffer.byteLength(code, 'utf8') > limits.max_code_bytes) { return err(`code exceeds max_code_bytes (${limits.max_code_bytes})`, 'ResourceError'); } const QuickJS = await getQuickJS(); const runtime = QuickJS.newRuntime(); runtime.setMemoryLimit(limits.memory_bytes); runtime.setMaxStackSize(limits.max_stack_bytes); const deadline = Date.now() + limits.timeout_ms; let interrupted = false; runtime.setInterruptHandler(() => { if (Date.now() > deadline) { interrupted = true; return true; } return false; }); const ctx = runtime.newContext(); try { // 注入 curated builtin:sha256(單一純函式;其餘一律不給) const shaFn = ctx.newFunction('sha256', (argHandle) => { const s = ctx.getString(argHandle); return ctx.newString(hostSha256(s)); }); ctx.setProp(ctx.global, 'sha256', shaFn); shaFn.dispose(); // 綁 input:以 JSON 字串安全穿越邊界,沙箱內 JSON.parse(不共享物件圖) const inputJson = JSON.stringify(input === undefined ? null : input); // 包裝:user code 當成函式體跑,回值 JSON.stringify 後交回 host。 // `input` 為唯一綁定;`sha256` 為唯一注入 builtin。 const wrapped = `(() => { "use strict"; const input = JSON.parse(${JSON.stringify(inputJson)}); const __run = (input) => { ${code} }; const __out = __run(input); return JSON.stringify(__out === undefined ? null : __out); })()`; const evalResult = ctx.evalCode(wrapped, 'user-code.js'); if (evalResult.error) { const detail = ctx.dump(evalResult.error); evalResult.error.dispose(); if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError'); const msg = typeof detail === 'object' && detail ? `${detail.name || 'Error'}: ${detail.message || ''}`.trim() : String(detail); return err(msg, 'UserCodeError'); } const outJson = ctx.getString(evalResult.value); evalResult.value.dispose(); if (Buffer.byteLength(outJson, 'utf8') > limits.max_output_bytes) { return err(`output exceeds max_output_bytes (${limits.max_output_bytes})`, 'ResourceError'); } return { success: true, data: JSON.parse(outJson) }; } catch (e) { if (interrupted) return err(`execution timed out after ${limits.timeout_ms}ms`, 'TimeoutError'); const m = e instanceof Error ? e.message : String(e); if (/out of memory|memory/i.test(m)) return err('out of memory', 'ResourceError'); return err(m, 'SandboxError'); } finally { ctx.dispose(); runtime.dispose(); } } function err(message, error_type) { return { success: false, error: message, error_type }; }