# `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`。
