Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HJiLCRUU2o3aSpPEzVCt2o
7.0 KiB
code 零件 —— 沙箱設計小結(Arcrun#10)
狀態:Workers 就緒(裁定 A,可部署),Node/vitest 12/12 綠燈,未部署 leo21c。live 前需總管與 leo 過寫入/部署閘。封裝=A(quickjs-emscripten singlefile variant);B(自建 QuickJS+wasi-sdk)列後續技術債。
0. arcrun 零件 runtime 真相(先摸清再設計)
- 每個 logic 零件 = 一顆 Worker(
{name}.arcrun.dev),POST JSON → 內部跑 wasm → JSON 回傳。 - 零件實作 = Go(
//go:build tinygo)→ TinyGo 編成 WASI-preview1 wasm,build 時靜態 bundle 進 Worker(wrangler.toml [[wasm_modules]])。 - host(
component-worker-template/src/index.ts)用 TS 自己實作一份 WASI-preview1 shim:fd_read餵 stdin(= POST body 的 JSON)、fd_write收 stdout(= 回傳 JSON)。 no_network/no_filesystem不是靠 wasm 自律,是 host 根本不提供那些 import:sock_*/path_*全回ENOSYS(76)、u6u.http_requestno-op。→ 零件對外「無 ambient 能力」是由 host 建構保證的。- runtime 是 workerd(=cf-workers);contract 的
wazero只是相容標記(本機/CLI 測試路徑),生產不經 wazero。
關鍵結論:host 只認 WASI-preview1 + stdin/stdout JSON,與 guest 語言無關。所以載入「QuickJS 編成的 wasm」與載入 TinyGo wasm 在 runtime 層是同一件事 —— QuickJS-wasm 可行,且完全合現有零件模型。差別只在 guest 從「TinyGo」換成「QuickJS」,且 user 的 JS 是「跑時餵進去的資料」而非「build 時編進去的程式」。
1. 沙箱機制
user JS 跑在 QuickJS context 裡,該 context 三層封裝、逐層無逃逸:
Cloudflare Worker isolate(V8)
└─ QuickJS wasm module(線性記憶體沙箱;無 WASI 網路/檔案 import)
└─ QuickJS JS context(global 只有純 ECMAScript 內建)
└─ user code:讀 input、return 值
- 無 ambient 能力(by construction):QuickJS context 起始 global 只有
Object/Array/JSON/Math/Date/String/RegExp…,沒有fetch/process/require/WebAssembly/XMLHttpRequest/globalThis.env。要給的能力必須 host 明確、逐一注入。 - 唯一注入的 curated builtin:
sha256(str)(純、決定性、零能力 —— 不能碰網路/檔案/secret)。card→envelope 的 content_hash 需要它。任何新 builtin 都必須維持「純函式、無 ambient 能力」這條線。 - input 穿越邊界:以 JSON 字串 marshal,沙箱內
JSON.parse—— host 與 guest 不共享物件圖,杜絕 prototype/引用逃逸。 - user code 形狀:當「函式體」跑(可含
const/function宣告、以return回值),綁定唯一入參input。回值JSON.stringify後交回 host。
2. 生產路徑(裁定 A,已就緒)
sandbox.mjs 為 runtime-agnostic 核心,Node 測試與 CF Worker 共用同一份:
- 封裝:
@jitl/quickjs-singlefile-mjs-release-syncvariant(wasm 內嵌 base64、同步載入) —— CF Workers 相容 loading 路徑(不靠 fetch/fs 取 .wasm),bundler 友善、無需[[wasm_modules]]。 sha256curated builtin:以純 JS SHA-256 字串 prelude 注入沙箱(不呼叫 host、不用 async Web Crypto、不需nodejs_compat),Node/Worker 皆決定性;與 Node crypto sha256 逐字等價 (測試 ⑤ 對 card 全文比對 content_hash 相同)。「curated builtin = 純演算法字串」定為往後標準。- Worker host:
index.ts(Hono,POST /→runCode),自足 Worker,不走 TinyGo 模板流程。 - 測試:
sandbox.mjs+test/在 Node/vitest 12/12 全綠(含 card→envelope 全等)。 - 與 B 的差:B(自建 QuickJS+wasi-sdk→preview1 wasm,跑現有 WASI host shim)最同構,但需 wasi-sdk 工具鏈(本環境無 sysroot)+維護 C harness,列後續技術債。
3. 資源限制(防跑飛)
| 限制 | 機制 | 預設 |
|---|---|---|
| 執行 timeout | QuickJS runtime setInterruptHandler(逐指令檢查 wall-clock deadline) |
1000 ms |
| 記憶體 | setMemoryLimit(QuickJS runtime 硬上限) |
16 MiB |
| 堆疊 | setMaxStackSize(防深遞迴) |
512 KiB |
| 輸出大小 | host 量測 stdout JSON bytes,超限即 ResourceError |
1 MiB |
| code 大小 | host 量測 user code bytes,超限即 ResourceError |
256 KiB |
節點 config 可覆蓋(但不得放寬過契約 sandbox_limits 硬上限 —— 由零件在讀 config 時 clamp)。
4. 錯誤處理(Worker 絕不掛)
一律回結構化 envelope,error_type 分類:
UserCodeError:user code 拋錯 / 語法錯誤。TimeoutError:超時被 interrupt。ResourceError:記憶體 / 輸出 / code 超限。ContractError:輸入形狀不合(如 code 非字串)。SandboxError:其餘沙箱層例外。
5. 安全性質(總結)
- user code 無網路:沒有 fetch/XHR,QuickJS wasm 也沒有 WASI socket import。
- user code 無檔案:沒有 fs,WASI path_* 一律 ENOSYS。
- user code 無 env/secret:沒有 process,Worker 的
env(bindings/secret)不進 QuickJS context。 - user code 碰不到 Worker 物件圖:獨立 wasm 線性記憶體 + 獨立 QuickJS heap + JSON 邊界。
- 可終止:timeout/記憶體/輸出上限,跑飛也不拖垮 Worker。
- 決定性(除
Date/Math.random):curated builtin 全是純函式。
6. 設計岔路(給總管/leo 裁)
沙箱主機制 = QuickJS-wasm(本 PoC 已證可行且合模型)。封裝方式有兩條,取捨如下:
- A. QuickJS-emscripten 直接在零件 Worker 用(PoC 走這條,推薦先行)
優點:本環境即可跑、npm 現成、限制 API 齊(timeout/mem/stack)、已在 Workers 驗證過可用。
缺點:不經現有 TinyGo-wasm 的
[[wasm_modules]]+ 自建 WASI shim 路徑,是零件家族裡的「特例封裝」。 - B. 自建 QuickJS+C-harness → wasi-sdk 編成 preview1 wasm,跑在現有 host shim(最「同構」)
優點:與其他零件同一條 runtime(同 WASI shim),
wasi_target: preview1名副其實,「無 ambient 能力」最純。 缺點:需要 wasi-sdk 工具鏈(本環境無 sysroot,無法即刻 build)、要維護一段 C harness。
建議:先以 A live(快、已驗證),把 B 列為後續「收斂到同構 runtime」的技術債。若總管要求所有零件單一 runtime,則走 B,但需先補 wasi-sdk build 基礎設施。此為安全敏感原語,機制選定請總管拍板再 live。
7. 首個消費者(Arcrun#8)
km_wiki_ingest_drain 的 card→envelope 解析,把 card-to-envelope.mjs 的 parseCard/planEnvelopes/planCard 當作 code 節點的 inline JS(去掉 import 'node:crypto' 與 export,改用注入的 sha256)。PoC 測試 ⑤ 已證:沙箱輸出與原始模組 planCard 逐欄全等(含 content_hash)。→ 可丟掉 domain 零件 km_wiki_card_parse。