# `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_request` no-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-sync` variant(wasm 內嵌 base64、同步載入) —— CF Workers 相容 loading 路徑(不靠 fetch/fs 取 .wasm),bundler 友善、無需 `[[wasm_modules]]`。 - **`sha256` curated 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. 安全性質(總結) 1. user code **無網路**:沒有 fetch/XHR,QuickJS wasm 也沒有 WASI socket import。 2. user code **無檔案**:沒有 fs,WASI path_* 一律 ENOSYS。 3. user code **無 env/secret**:沒有 process,Worker 的 `env`(bindings/secret)不進 QuickJS context。 4. user code **碰不到 Worker 物件圖**:獨立 wasm 線性記憶體 + 獨立 QuickJS heap + JSON 邊界。 5. **可終止**:timeout/記憶體/輸出上限,跑飛也不拖垮 Worker。 6. **決定性**(除 `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`。