@dinosaur-mod/mod-runtime
v0.1.1
Published
mod 執行 runtime:**isolate host + per-org 容器分片調度**。
Readme
@dinosaur-mod/mod-runtime
mod 執行 runtime:isolate host + per-org 容器分片調度。
狀態:M1 已實作 isolate host 核心(QuickJS-WASM 沙箱,藏在引擎中立縫後面;待人工安全審)。 per-org 容器分片=部署拓撲層 TODO(理由見下)。 真相來源=
docs/mod-platform.md(尤其 §三 執行模型、§十四 repo 擺放、§二十 實作落差盤點)。
M1 選型決策:執行原語=QuickJS-WASM(取代 M0 的「V8 isolate 待拍板」)
M0 README 把引擎選型(isolated-vm vs workerd)留給 M1。M1 拍板=QuickJS-WASM
(quickjs-emscripten-core + @jitl/quickjs-ng-wasmfile-release-asyncify,ASYNCIFY 變體):
- §三 已把「執行原語」標為實作細節、可換 → 不違契約。
- 選 QuickJS-WASM 的關鍵好處:WASM 無 ambient authority——沙箱內沒有 fs / socket / 原生記憶體
任意存取,逃逸面與「拿到 host 能力」之間隔著 WASM 邊界本身。故 MOD-2 的「per-org 容器分片」
從「承重的核心級邊界」降為選配的部署縱深防禦(把整個 runtime 進程依 org 分到不同容器/機器),
屬部署編排、不是這支 code 的職責(見
runtime.ts檔頭 TODO)。 - ASYNCIFY 變體:讓 mod 在 guest 內同步呼叫
ctx.*(host I/O glue),平台代發真 async; 代價=一顆 WASM 模組同時只能一個 async 呼叫 → 並發靠 module pool(每槽一顆模組、序列化共享)。
可逆性:引擎藏在 sandbox.ts 的引擎中立縫(ModSandbox/ModModule/ModInstance + HostBridge)
後面。將來要換 workerd / 原生 V8 =再寫一份 ModSandbox 實作,runtime.ts 與上層一行不改。
檔案地圖(M1)
sandbox.ts— 引擎中立縫(純介面,不依賴 quickjs)。跨界只准傳可序列化值 + async 函式呼叫, 絕不漏引擎 handle 給呼叫端。quickjs-adapter.ts— QuickJS-WASM 實作。安全邊界:每次 invoke 全新 realm、只注入ctx(無 host globals)、記憶體上限(預設 64MB)、CPU deadline(預設 2s)、嚴格 handle dispose。 檔頭列人工安全審查必查項。runtime.ts— invocation-driven 生命週期:module pool(原子佔用、序列化共享模組)+ 每次全新 realm- idle reaper + 中毒模組退役(OOM/中斷腐化的 WASM 模組用完即丟、連 sandbox 換新)。
index.ts— 對外門面:M0 契約信封(ModInvocation/ModInvocationResult)+ModRuntime串接。
這層是什麼
平台可信區(apps/web 的 Nuxt/Nitro 編排)在「送出那刻」綁 (org, identity) + 注入該安裝憑證,再把 mod handler 交給最內層的執行沙箱跑。本套件=那個沙箱的 host:
- 執行原語=V8 isolate(TS-only、~5ms 起,MOD-2):mod 後端是 I/O glue(收參數→經 connection 取資料→整理→回傳),不碰檔案/不重運算 → 用 isolate,非整個容器 per mod。
- 縱深防禦=per-org 容器分片(MOD-2):isolate 關進「活躍中的 org」共住的容器;萬一 V8 0-day 逃逸仍困在該 org 的容器內、碰不到 host 或別的 org。容器數量=同時活躍的 org 數(非安裝數)。
- 生命週期=invocation-driven(MOD-3):生死只看「有無請求」+ idle 計時器,與頁面開不開無關。
- 跨 invocation 不殘留(§三):重用昂貴的 isolate 分片,但每次 invocation 開全新 realm、清掉上次全域/模組級狀態。
一次調用的骨架:取分片 → 取 isolate(warm pool 重用)→ 開新 realm → 注入 ctx → 跑 handler → idle 回收。
邊界(不在這層)
- 不持有第三方憑證、不做 egress:isolate 無 socket;對外唯一通道=平台代發的
ctx.connections[X](憑證由平台整合層持有/注入,mod 永不見 raw token)。整合層=docs/connections.md。 - 不渲染 UI:宣告式 UI 由
apps/web第一方渲染器處理(MOD-5)。 - 不定義
ctx型別 / dev runner:那是packages/mod-sdk(M1+)。 - 不解析/驗證 manifest:那是
packages/mod-schema(M1+)。本層只「取已驗證的安裝 + 執行」。 - 不跑 agent 容器:mod agent run 走既有平台 agent 脊椎(
apps/web/server/utils/agent-run.ts的runDriver+AgentSpec,§六/§二十);本層只跑 isolate handler。
M1 已完成 / 後續里程碑要補
M1 已完成(本套件):
- ✅ isolate 引擎選型拍板=QuickJS-WASM(見上方選型決策);依賴已入
package.json。 - ✅ realm 清理:每次 invocation 開全新
QuickJSAsyncContext(realm)、跑完 dispose(§三「跨 invocation 不殘留」);測過跨 invoke 全域不串味。 - ✅ 限制:記憶體上限(預設 64MB)+ CPU/wall-clock deadline(預設 2s,interrupt handler)+ 嚴格 handle dispose。
- ✅ module pool + idle reaper(invocation-driven,MOD-3)+ 中毒模組退役。
- ✅ 最小
ctx(ctx.callasyncify 橋 +ctx.log)橋接到HostBridge;無 host globals。
後續里程碑要補:
- per-org 容器分片(部署拓撲,非此 code):QuickJS-WASM 無 ambient authority → 容器降為選配縱深防禦;
要做=把 runtime 進程依 org 分容器/機器(部署編排)。若日後改用原生 V8/workerd(更大逃逸面),
才需在此/部署層補 per-org 邊界、pool key 改 orgId(見
runtime.ts檔頭 TODO)。 - 真實
ctx能力面(M2):在HostBridge之上接connections/store/blobs/docs/agent/log真實作(§五),平台在送出那刻綁(org, workspace, identity)+ 注入該安裝憑證,mod 永不見 raw token。 形狀對齊@dinosaur-mod/mod-sdk的Ctx。 - 計帳/稽核:每次 invocation 落
mod_audit(trigger/handler/asker/status/duration_ms,§十四,append-only、對齊 D31)。index.ts的ModInvocationResult已備status/durationMs/error.code供平台落帳。 - idle reaper 與
ctx.agent.run阻塞語意:ctx.agent.run阻塞 await 視為「有請求」、不可回收(§十九)—— M2 接 agent 能力時,把其 in-flight 納入 reaper 判據。
相依
quickjs-emscripten-core+@jitl/quickjs-ng-wasmfile-release-asyncify(0.32.0,鎖定)— QuickJS-WASM 沙箱。@dinosaur/db(query()/uuidv7())、@dinosaur-mod/shared(Zod schema+型別)— M2+ 才實際用到(mod_audit 落帳 / ctx 真實作);現於package.json預留。
慣例
@dinosaur/*套件:private、type: module、main/types直指src/index.ts、無 build step(消費端用 tsx/bundler)。- ESM:相對 import 一律帶
.js副檔名(即使來源是.ts)。 - workspace glob 已含
apps/*,新增此套件免改 rootpackage.json。
