arcrun — AI workflow execution engine (clean history)

Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-06-03 15:52:38 +08:00
commit 922a57fe34
485 changed files with 89356 additions and 0 deletions
+741
View File
@@ -0,0 +1,741 @@
/**
* WASI preview1 輕量 shim
* 只實作 stdin/stdout/stderr 所需的最小 syscall 集合。
* 其餘 syscall 一律回傳 ENOSYS(76),確保零件無法呼叫網路或檔案系統。
*
* 不依賴任何外部套件(不使用 @cloudflare/workers-wasi)。
* Requirements: 3.1, 3.3
*/
/**
* createArcrunHostFunctions 所需的最小 env 子集。
* 不直接依賴 cypher-executor 的 Bindings,讓 auth primitive Worker 這類
* 只綁 CREDENTIALS_KV / RECIPES / ENCRYPTION_KEY 的獨立 Worker 也能用。
*/
export interface ArcrunHostEnv {
CREDENTIALS_KV: KVNamespace;
RECIPES: KVNamespace;
ENCRYPTION_KEY: string;
}
const WASI_ESUCCESS = 0;
const WASI_ENOSYS = 76;
// fd 常數
const FD_STDIN = 0;
const FD_STDOUT = 1;
const FD_STDERR = 2;
export interface WasiShim {
/** WebAssembly.Imports 物件,傳入 WebAssembly.instantiate */
imports: WebAssembly.Imports;
/** 取得 stdout 的完整輸出(合併所有 chunks) */
getStdout(): string;
/** 取得 stderr 的完整輸出 */
getStderr(): string;
/** 注入 WebAssembly.Memoryinstantiate 後呼叫) */
setMemory(memory: WebAssembly.Memory): void;
/**
* 執行 WASM _start,自動使用 WebAssembly.promisingJSPI)讓 async host
* function 能正確 suspend/resume。若 JSPI 不可用則 fallback 同步執行。
* 必須在 setMemory() 之後呼叫。
*/
run(instance: WebAssembly.Instance): Promise<void>;
}
/**
* Host function 注入介面
* 讓 .wasm 零件能透過 host function 呼叫外部服務,而不需要網路 syscall
*
* 嚴格邊界:
* - encryption key 只在 `crypto_decrypt` host function 內部使用,永遠不傳給 WASM
* - `kv_get` 必須在 Worker 側檢查 key 前綴以防越權(見 auth-dispatcher.ts
*/
export interface WasiHostFunctions {
/** HTTP 請求 host function.wasm 呼叫此函數發出 HTTP 請求 */
http_request?: (url: string, method: string, headers: string, body: string) => Promise<string>;
/** KV 讀取:key 前綴由 Worker 路由到對應 binding,並做越權檢查 */
kv_get?: (key: string) => Promise<string | null>;
/** KV 寫入:用於快取 access_token 等短效值,ttlSeconds=0 表示不設 TTL */
kv_put?: (key: string, value: string, ttlSeconds: number) => Promise<void>;
/** AES-GCM 解密:encryption key 由 Worker 保管,不暴露給 WASM */
crypto_decrypt?: (encryptedB64: string, ivB64: string) => Promise<string>;
/** RS256 簽章:用 crypto.subtle 做 RSASSA-PKCS1-v1_5 + SHA-256 */
crypto_sign_rs256?: (data: Uint8Array, pkcs8: Uint8Array) => Promise<Uint8Array>;
/** HMAC-SHA256(data, ENCRYPTION_KEY) → raw bytes */
crypto_hmac_sha256?: (data: Uint8Array) => Promise<Uint8Array>;
/** AES-GCM 加密(plaintext, ENCRYPTION_KEY) → {encryptedB64, ivB64} */
crypto_aes_encrypt?: (plaintext: Uint8Array) => Promise<{ encryptedB64: string; ivB64: string }>;
/** crypto random bytes → hex string */
crypto_random_bytes?: (numBytes: number) => string;
}
/**
* 建立 WASI shim 實例
* @param stdinData - 要寫入 stdin 的 UTF-8 字串(通常是 JSON.stringify(input)
* @param hostFunctions - 可選的 host function 注入(讓 .wasm 呼叫外部服務)
*/
export function createWasiShim(stdinData: string, hostFunctions?: WasiHostFunctions): WasiShim {
const stdinBytes = new TextEncoder().encode(stdinData);
let stdinOffset = 0;
const stdoutChunks: Uint8Array[] = [];
const stderrChunks: Uint8Array[] = [];
let memory: WebAssembly.Memory | null = null;
function getMemoryView(): DataView {
if (!memory) throw new Error('WASI memory not set — call setMemory() after instantiate');
return new DataView(memory.buffer);
}
// 寫入結果到 WASM 的 outPtr bufferhost function 共用)
// 回傳 0 = 成功,1 = memory 不可用
function writeOut(buf: ArrayBuffer, outPtr: number, outLenPtr: number, data: Uint8Array): number {
try {
new Uint8Array(buf, outPtr, data.length).set(data);
new DataView(buf).setUint32(outLenPtr, data.length, true);
return 0;
} catch {
return 1;
}
}
/**
* fd_write: 將 iovec 陣列的資料寫入 fdstdout=1 或 stderr=2
* iovec 結構:{ buf: i32, buf_len: i32 }(各 4 byteslittle-endian
*/
function fd_write(fd: number, iovs: number, iovs_len: number, nwritten_ptr: number): number {
if (fd !== FD_STDOUT && fd !== FD_STDERR) return WASI_ENOSYS;
const view = getMemoryView();
const buf = memory!.buffer;
let totalWritten = 0;
for (let i = 0; i < iovs_len; i++) {
const iov_base = view.getUint32(iovs + i * 8, true);
const iov_len = view.getUint32(iovs + i * 8 + 4, true);
if (iov_len === 0) continue;
const chunk = new Uint8Array(buf, iov_base, iov_len);
const copy = new Uint8Array(iov_len);
copy.set(chunk);
if (fd === FD_STDOUT) stdoutChunks.push(copy);
else stderrChunks.push(copy);
totalWritten += iov_len;
}
view.setUint32(nwritten_ptr, totalWritten, true);
return WASI_ESUCCESS;
}
/**
* fd_read: 從 stdin 讀取資料到 iovec 陣列
*/
function fd_read(fd: number, iovs: number, iovs_len: number, nread_ptr: number): number {
if (fd !== FD_STDIN) return WASI_ENOSYS;
const view = getMemoryView();
const buf = memory!.buffer;
let totalRead = 0;
for (let i = 0; i < iovs_len; i++) {
const iov_base = view.getUint32(iovs + i * 8, true);
const iov_len = view.getUint32(iovs + i * 8 + 4, true);
if (iov_len === 0) continue;
const remaining = stdinBytes.length - stdinOffset;
if (remaining <= 0) break;
const toCopy = Math.min(iov_len, remaining);
const dest = new Uint8Array(buf, iov_base, toCopy);
dest.set(stdinBytes.subarray(stdinOffset, stdinOffset + toCopy));
stdinOffset += toCopy;
totalRead += toCopy;
}
view.setUint32(nread_ptr, totalRead, true);
return WASI_ESUCCESS;
}
/**
* proc_exit: 零件呼叫 exit(),拋出 Error 中止執行
*/
function proc_exit(code: number): never {
throw new Error(`wasm exit: ${code}`);
}
/**
* random_get: 填充隨機 bytes(使用 Web Crypto API
*/
function random_get(buf_ptr: number, buf_len: number): number {
const view = new Uint8Array(memory!.buffer, buf_ptr, buf_len);
crypto.getRandomValues(view);
return WASI_ESUCCESS;
}
// ── Asyncify protocol ──────────────────────────────────────────────────────
// TinyGo WASI target 永遠使用 asyncify scheduler。Asyncify 讓 WASM 能在呼叫 host
// function 時「unwind」(保存 call stack),待 async 工作完成後再「rewind」(恢復)。
//
// 協議流程(每次 async host function 呼叫):
// 1. WASM 呼叫 host import(例如 http_request
// 2. Host 檢查 asyncify_get_state()
// - state=1Unwinding: 正在展開,host 應直接回傳 0(佔位值)
// - state=2Rewinding: 正在恢復,host 應回傳上一次 async 結果(已存在 asyncifyResult
// - state=0Normal: 正常執行,host 啟動 async 工作並呼叫 asyncify_start_unwind
// 3. WASM 的 _start 控制流回到 run()asyncify 讓 _start 提前返回)
// 4. run() await async 工作,呼叫 asyncify_start_rewind,再次呼叫 _start
// 5. WASM 從 host import 返回點繼續執行,host 回傳儲存的結果
//
// 注意:每次 _start 呼叫只能處理一個 async 中斷點。若 WASM 有多個連續的 async host call
// run() 會在 while 迴圈裡重複 rewind 直到 asyncify_get_state() == 0Normal)。
// Asyncify 資料緩衝區設定(TinyGo asyncify 用於保存 call stack
// 位址在 run() 中設定(WASM memory 末尾分配 1MB
let asyncifyDataPtr = 0;
const ASYNCIFY_BUF_SIZE = 1024 * 1024; // 1MB stack buffer
// 儲存 async host function 的結果和 Promise
let asyncifyPendingPromise: Promise<number> | null = null;
let asyncifyResult: number = 0;
// asyncify exportsrun() 設定後才可用)
let asyncifyExports: {
get_state: () => number;
start_unwind: (ptr: number) => void;
stop_unwind: () => void;
start_rewind: (ptr: number) => void;
stop_rewind: () => void;
} | null = null;
// JSPI helper:若環境支援 WebAssembly.Suspending,用它包裝 async import function
// 用於 scheduler=none 編譯的 WASM(無 asyncify exports
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function jspiSuspending<T extends (...args: any[]) => Promise<unknown>>(fn: T): T {
const SuspendingCtor = (WebAssembly as unknown as Record<string, unknown>)['Suspending'] as
(new (fn: T) => T) | undefined;
return SuspendingCtor ? new SuspendingCtor(fn) : fn;
}
// 建立一個 asyncify-aware 的 host function wrapper
// 協議:Normal 時啟動 async 工作並呼叫 start_unwindRewinding 時回傳已存的結果
// 用於 scheduler=asyncify 編譯的 WASM(有 asyncify exports
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function asyncifyWrap(fn: (...args: any[]) => Promise<number>): (...args: any[]) => number {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (...args: any[]): number => {
if (!memory) return 1;
const ax = asyncifyExports;
if (!ax) return 0; // asyncify 尚未初始化(sync fallback
const state = ax.get_state();
if (state === 2) {
// Rewinding:回傳上次 async 的真實結果
return asyncifyResult;
}
if (state === 1) {
// Unwinding 中:直接回傳 0WASM 在 unwind,不使用此值)
return 0;
}
// Normalstate=0):啟動 async 工作,觸發 asyncify unwind
asyncifyPendingPromise = fn(...args);
// asyncify_start_unwind 設定 WASM 內部 unwind flag
// host function 返回後 WASM 開始保存 call stack,最終 _start() 返回
ax.start_unwind(asyncifyDataPtr);
return 0; // WASM 忽略此值(正在 unwind
};
}
// 根據 WASM 是否有 asyncify exports 決定使用哪種包裝方式
// JSPI mode: scheduler=none WASM + WebAssembly.Suspending
// asyncify mode: scheduler=asyncify WASM + asyncify protocol
// 初始化時先用 asyncifyWraprun() 後若沒有 asyncify exports 就切換到 jspiSuspending
// 但因為 imports 在 instantiate 前就需要確定,這裡統一先用 asyncifyWrap
// run() 時若發現沒有 asyncify exports 且有 JSPI,則使用 JSPI 模式
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function hostWrap(fn: (...args: any[]) => Promise<number>): (...args: any[]) => number | Promise<number> {
// 嘗試使用 JSPI Suspending(若環境支援)
const SuspendingCtor = (WebAssembly as unknown as Record<string, unknown>)['Suspending'] as
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(new (fn: any) => any) | undefined;
if (SuspendingCtor) {
// JSPI 可用:包裝為 Suspending,讓 WASM 能 suspend 等待 async 結果
// 這適用於 scheduler=none 的 WASM(無 asyncify 干擾)
return new SuspendingCtor(fn);
}
// fallbackasyncify 協議(scheduler=asyncify WASM
return asyncifyWrap(fn);
}
const shim: WasiShim = {
imports: {
wasi_snapshot_preview1: { fd_write,
fd_read,
proc_exit,
random_get,
// 其餘 syscall 回傳 ENOSYS(不允許網路/檔案系統操作)
fd_seek: () => WASI_ENOSYS,
fd_close: () => WASI_ESUCCESS,
fd_fdstat_get: () => WASI_ENOSYS,
fd_prestat_get: () => WASI_ENOSYS,
fd_prestat_dir_name: () => WASI_ENOSYS,
environ_get: () => WASI_ESUCCESS,
environ_sizes_get: (count_ptr: number, size_ptr: number) => {
if (memory) {
const view = getMemoryView();
view.setUint32(count_ptr, 0, true);
view.setUint32(size_ptr, 0, true);
}
return WASI_ESUCCESS;
},
args_get: () => WASI_ESUCCESS,
args_sizes_get: (argc_ptr: number, argv_buf_size_ptr: number) => {
if (memory) {
const view = getMemoryView();
view.setUint32(argc_ptr, 0, true);
view.setUint32(argv_buf_size_ptr, 0, true);
}
return WASI_ESUCCESS;
},
clock_time_get: (id: number, precision: bigint, time_ptr: number) => {
if (memory) {
const view = getMemoryView();
const now = BigInt(Date.now()) * 1_000_000n;
view.setBigUint64(time_ptr, now, true);
}
return WASI_ESUCCESS;
},
clock_res_get: () => WASI_ENOSYS,
poll_oneoff: () => WASI_ENOSYS,
sched_yield: () => WASI_ESUCCESS,
proc_raise: () => WASI_ENOSYS,
sock_accept: () => WASI_ENOSYS,
sock_recv: () => WASI_ENOSYS,
sock_send: () => WASI_ENOSYS,
sock_shutdown: () => WASI_ENOSYS,
path_open: () => WASI_ENOSYS,
path_create_directory: () => WASI_ENOSYS,
path_remove_directory: () => WASI_ENOSYS,
path_rename: () => WASI_ENOSYS,
path_unlink_file: () => WASI_ENOSYS,
path_filestat_get: () => WASI_ENOSYS,
path_readlink: () => WASI_ENOSYS,
path_symlink: () => WASI_ENOSYS,
path_link: () => WASI_ENOSYS,
},
// u6u host functions:讓 .wasm 零件透過 host function 呼叫外部服務
// .wasm 零件用 //go:wasmimport u6u <name> 宣告
// 所有 async host function 透過 asyncifyWrap 包裝,實作 asyncify 協議
u6u: {
http_request: hostFunctions?.http_request
? hostWrap(async (urlPtr: number, urlLen: number, methodPtr: number, methodLen: number,
headersPtr: number, headersLen: number, bodyPtr: number, bodyLen: number,
outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) return 1;
// 在 await 前讀完所有輸入(memory.buffer 在 await 後可能因 grow 而失效)
const snapBuf = memory.buffer;
const dec = new TextDecoder();
const url = dec.decode(new Uint8Array(snapBuf, urlPtr, urlLen));
const method = dec.decode(new Uint8Array(snapBuf, methodPtr, methodLen));
const headers = dec.decode(new Uint8Array(snapBuf, headersPtr, headersLen));
const body = dec.decode(new Uint8Array(snapBuf, bodyPtr, bodyLen));
try {
const result = await hostFunctions!.http_request!(url, method, headers, body);
// await 後重新拿 memory.buffergrow 會產生新的 ArrayBuffer
return writeOut(memory.buffer, outPtr, outLenPtr, new TextEncoder().encode(result));
} catch {
return 1;
}
})
: () => 1,
// kv_get(keyPtr, keyLen, outPtr, outLenPtr) → 0 成功;1 錯誤;2 找不到 key
kv_get: hostFunctions?.kv_get
? hostWrap(async (keyPtr: number, keyLen: number, outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) { console.error('[kv_get] memory null'); return 1; }
const key = new TextDecoder().decode(new Uint8Array(memory.buffer, keyPtr, keyLen));
console.error(`[kv_get] key="${key}" keyPtr=${keyPtr} keyLen=${keyLen} outPtr=${outPtr} outLenPtr=${outLenPtr}`);
try {
const result = await hostFunctions!.kv_get!(key);
console.error(`[kv_get] result=${result === null ? 'null' : result.slice(0, 80)}`);
if (result === null) return 2;
const encoded = new TextEncoder().encode(result);
const status = writeOut(memory.buffer, outPtr, outLenPtr, encoded);
console.error(`[kv_get] writeOut status=${status} encodedLen=${encoded.length} memBufLen=${memory.buffer.byteLength}`);
return status;
} catch (e) {
console.error(`[kv_get] error: ${e}`);
return 1;
}
})
: () => 1,
// kv_put(keyPtr, keyLen, valPtr, valLen, ttlSeconds) → 0 成功;1 錯誤
kv_put: hostFunctions?.kv_put
? hostWrap(async (keyPtr: number, keyLen: number, valPtr: number, valLen: number, ttlSeconds: number): Promise<number> => {
if (!memory) return 1;
const dec = new TextDecoder();
const key = dec.decode(new Uint8Array(memory.buffer, keyPtr, keyLen));
const value = dec.decode(new Uint8Array(memory.buffer, valPtr, valLen));
try {
await hostFunctions!.kv_put!(key, value, ttlSeconds);
return 0;
} catch {
return 1;
}
})
: () => 1,
// crypto_decrypt(encPtr, encLen, ivPtr, ivLen, outPtr, outLenPtr) → 0 成功
// 輸入皆為 base64 字串(WASM 從 KV 讀到什麼就送什麼)
crypto_decrypt: hostFunctions?.crypto_decrypt
? hostWrap(async (encPtr: number, encLen: number, ivPtr: number, ivLen: number,
outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) return 1;
const dec = new TextDecoder();
const encB64 = dec.decode(new Uint8Array(memory.buffer, encPtr, encLen));
const ivB64 = dec.decode(new Uint8Array(memory.buffer, ivPtr, ivLen));
try {
const plaintext = await hostFunctions!.crypto_decrypt!(encB64, ivB64);
return writeOut(memory.buffer, outPtr, outLenPtr, new TextEncoder().encode(plaintext));
} catch {
return 1;
}
})
: () => 1,
// crypto_sign_rs256(dataPtr, dataLen, pkcs8Ptr, pkcs8Len, outPtr, outLenPtr) → 0 成功
crypto_sign_rs256: hostFunctions?.crypto_sign_rs256
? hostWrap(async (dataPtr: number, dataLen: number, pkcs8Ptr: number, pkcs8Len: number,
outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) return 1;
// await 前複製 typed array(避免 memory grow 後 buffer 失效)
const data = new Uint8Array(new Uint8Array(memory.buffer, dataPtr, dataLen));
const pkcs8 = new Uint8Array(new Uint8Array(memory.buffer, pkcs8Ptr, pkcs8Len));
try {
const sig = await hostFunctions!.crypto_sign_rs256!(data, pkcs8);
return writeOut(memory.buffer, outPtr, outLenPtr, sig);
} catch {
return 1;
}
})
: () => 1,
// crypto_hmac_sha256(dataPtr, dataLen, outPtr, outLenPtr) → 0 成功,output = raw bytes
crypto_hmac_sha256: hostFunctions?.crypto_hmac_sha256
? hostWrap(async (dataPtr: number, dataLen: number, outPtr: number, outLenPtr: number): Promise<number> => {
if (!memory) return 1;
const data = new Uint8Array(new Uint8Array(memory.buffer, dataPtr, dataLen));
try {
const sig = await hostFunctions!.crypto_hmac_sha256!(data);
return writeOut(memory.buffer, outPtr, outLenPtr, sig);
} catch {
return 1;
}
})
: () => 1,
// crypto_aes_encrypt(plaintextPtr, plaintextLen, outEncPtr, outEncLenPtr, outIvPtr, outIvLenPtr) → 0 成功
crypto_aes_encrypt: hostFunctions?.crypto_aes_encrypt
? hostWrap(async (plaintextPtr: number, plaintextLen: number,
outEncPtr: number, outEncLenPtr: number,
outIvPtr: number, outIvLenPtr: number): Promise<number> => {
if (!memory) return 1;
const plaintext = new Uint8Array(new Uint8Array(memory.buffer, plaintextPtr, plaintextLen));
try {
const { encryptedB64, ivB64 } = await hostFunctions!.crypto_aes_encrypt!(plaintext);
const encBytes = new TextEncoder().encode(encryptedB64);
const ivBytes = new TextEncoder().encode(ivB64);
const s1 = writeOut(memory.buffer, outEncPtr, outEncLenPtr, encBytes);
const s2 = writeOut(memory.buffer, outIvPtr, outIvLenPtr, ivBytes);
return s1 !== 0 ? s1 : s2;
} catch {
return 1;
}
})
: () => 1,
// crypto_random_bytes(numBytes, outPtr, outLenPtr) → 0 成功,output = hex string
crypto_random_bytes: hostFunctions?.crypto_random_bytes
? (numBytes: number, outPtr: number, outLenPtr: number): number => {
if (!memory) return 1;
try {
const hexStr = hostFunctions!.crypto_random_bytes!(numBytes);
return writeOut(memory.buffer, outPtr, outLenPtr, new TextEncoder().encode(hexStr));
} catch {
return 1;
}
}
: () => 1,
},
},
setMemory(mem: WebAssembly.Memory) {
memory = mem;
},
async run(instance: WebAssembly.Instance): Promise<void> {
const exp = instance.exports as Record<string, unknown>;
const startFn = (exp._start ?? exp.main) as (() => void) | undefined;
if (typeof startFn !== 'function') throw new Error('WASM missing _start or main export');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const promisingFn = (WebAssembly as unknown as Record<string, unknown>)['promising'] as
((fn: () => void) => () => Promise<void>) | undefined;
// 若環境支援 JSPICloudflare Workers 2025+),優先使用 WebAssembly.promising
// hostWrap() 已將 imports 包裝為 WebAssembly.Suspending,不需要 asyncify 協議
if (promisingFn) {
try {
await promisingFn(startFn)();
} catch (e) {
if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e;
}
return;
}
// JSPI 不可用:使用 asyncify 協議(需要 WASM 有 asyncify exports
const asyncifyGetState = exp.asyncify_get_state as (() => number) | undefined;
const asyncifyStartUnwind = exp.asyncify_start_unwind as ((ptr: number) => void) | undefined;
const asyncifyStopUnwind = exp.asyncify_stop_unwind as (() => void) | undefined;
const asyncifyStartRewind = exp.asyncify_start_rewind as ((ptr: number) => void) | undefined;
const asyncifyStopRewind = exp.asyncify_stop_rewind as (() => void) | undefined;
if (asyncifyGetState && asyncifyStartUnwind && asyncifyStopUnwind &&
asyncifyStartRewind && asyncifyStopRewind) {
asyncifyExports = {
get_state: asyncifyGetState,
start_unwind: asyncifyStartUnwind,
stop_unwind: asyncifyStopUnwind,
start_rewind: asyncifyStartRewind,
stop_rewind: asyncifyStopRewind,
};
const mallocFn = exp.malloc as ((size: number) => number) | undefined;
if (mallocFn && memory) {
const totalSize = ASYNCIFY_BUF_SIZE;
asyncifyDataPtr = mallocFn(totalSize);
const view = new DataView(memory.buffer);
view.setInt32(asyncifyDataPtr, asyncifyDataPtr + 8, true);
view.setInt32(asyncifyDataPtr + 4, asyncifyDataPtr + totalSize, true);
} else if (memory) {
const memBytes = memory.buffer.byteLength;
asyncifyDataPtr = memBytes - ASYNCIFY_BUF_SIZE;
if (asyncifyDataPtr > 8) {
const view = new DataView(memory.buffer);
view.setInt32(asyncifyDataPtr, asyncifyDataPtr + 8, true);
view.setInt32(asyncifyDataPtr + 4, asyncifyDataPtr + ASYNCIFY_BUF_SIZE, true);
}
}
}
// JSPI 不可用且無 asyncify exports:同步執行(host function 不能 async
if (!asyncifyExports) {
try { startFn(); } catch (e) {
if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e;
}
return;
}
// 主執行迴圈:每次呼叫 _start,若 asyncify 捕捉到 pending promise 就 await 再 rewind
let rewinding = false;
while (true) {
asyncifyPendingPromise = null;
try {
if (rewinding) {
asyncifyExports.start_rewind(asyncifyDataPtr);
startFn();
asyncifyExports.stop_rewind();
} else {
startFn();
}
} catch (e) {
if (e instanceof Error && e.message === 'wasm exit: 0') break;
throw e;
}
// 若 asyncifyWrap 觸發了 unwind_start 會因 unwind 返回(沒有 exit
// asyncifyWrap 已呼叫 start_unwind,這裡只需 stop_unwind 並 await promise
if (asyncifyPendingPromise !== null) {
asyncifyExports.stop_unwind();
asyncifyResult = await asyncifyPendingPromise;
asyncifyPendingPromise = null;
rewinding = true;
continue;
}
// 沒有 pending promise 且沒有 exit → 正常完成
break;
}
},
getStdout(): string {
if (stdoutChunks.length === 0) return '';
const total = stdoutChunks.reduce((n, c) => n + c.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of stdoutChunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(merged);
},
getStderr(): string {
if (stderrChunks.length === 0) return '';
const total = stderrChunks.reduce((n, c) => n + c.length, 0);
const merged = new Uint8Array(total);
let offset = 0;
for (const chunk of stderrChunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(merged);
},
};
return shim;
}
// ── Worker 端 host function 實作(Phase 0.6)──────────────────────────────────
//
// 唯一合法位置:AES-GCM 解密與 RS256 簽章只准出現在本檔(02-forbidden.md §2.2)。
// 由 component-loader 的 WASM runner 路徑呼叫,注入進 createWasiShim。
//
// 安全邊界:
// 1. `ENCRYPTION_KEY` 只在 `crypto_decrypt` 內部讀 env,絕不經 stdin/回傳值傳給 WASM
// 2. `kv_get` 依 key 前綴路由,且 `{api_key}:cred:*` 必須符合 stdin 傳入的 api_key(越權檢查)
// 3. 未知前綴回傳 nullWASM 收到 kv_get 回傳 2 = 找不到)
function hexToUint8Array(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
return bytes;
}
function base64ToUint8Array(b64: string): Uint8Array {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
return bytes;
}
/**
* 依 key 前綴路由到對應 KV binding,並做越權檢查。
* - `auth_recipe:{service}` → env.RECIPES
* - `{apiKey}:cred:{name}` → env.CREDENTIALS_KV(前綴必須等於 caller 的 apiKey
* - 其他前綴 → null(拒絕)
*/
async function routedKvGet(env: ArcrunHostEnv, apiKey: string, key: string): Promise<string | null> {
if (key.startsWith('auth_recipe:')) {
return env.RECIPES.get(key);
}
const credMatch = key.match(/^([^:]+):cred:.+$/);
if (credMatch) {
if (credMatch[1] !== apiKey) {
// 越權:WASM 嘗試讀其他租戶的 credential
return null;
}
return env.CREDENTIALS_KV.get(key);
}
return null;
}
/**
* 依 key 前綴路由寫入 KV。只允許寫 oauth2 cache key(短效 access_token)。
* - `{apiKey}:oauth2:{service}:*` → env.CREDENTIALS_KV(越權檢查)
*/
async function routedKvPut(env: ArcrunHostEnv, apiKey: string, key: string, value: string, ttlSeconds: number): Promise<void> {
const oauth2Match = key.match(/^([^:]+):oauth2:.+$/);
if (oauth2Match && oauth2Match[1] === apiKey) {
const opts = ttlSeconds > 0 ? { expirationTtl: ttlSeconds } : undefined;
await env.CREDENTIALS_KV.put(key, value, opts);
return;
}
// 其他 key 前綴拒絕寫入(安全邊界)
}
/**
* AES-GCM 解密。encryption key 由 env.ENCRYPTION_KEY 在本 function 內讀取,
* 永不傳給 WASM。輸入為 base64 字串,輸出為 UTF-8 plaintext。
*/
async function aesGcmDecrypt(env: ArcrunHostEnv, encryptedB64: string, ivB64: string): Promise<string> {
const keyBytes = hexToUint8Array(env.ENCRYPTION_KEY);
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, false, ['decrypt'],
);
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToUint8Array(ivB64) },
cryptoKey,
base64ToUint8Array(encryptedB64),
);
return new TextDecoder().decode(plaintext);
}
/**
* RSASSA-PKCS1-v1_5 + SHA-256 簽章。private key 以 PKCS8 bytes 傳入(由 WASM 零件解析 PEM 後送進來)。
*/
async function rsaPkcs1Sha256Sign(data: Uint8Array, pkcs8: Uint8Array): Promise<Uint8Array> {
const cryptoKey = await crypto.subtle.importKey(
'pkcs8',
pkcs8,
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['sign'],
);
const sig = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', cryptoKey, data);
return new Uint8Array(sig);
}
/**
* 建立 arcrun host function 組合(kv_get / crypto_decrypt / crypto_sign_rs256)。
* 由 WASM runnercomponent-loader 的 WASM 路徑)呼叫,與 api_key 綁定以做越權檢查。
*
* http_request 不由本 factory 提供 — auth primitive WASM 與 API WASM 零件若需要
* 發 HTTP,由呼叫者(component-loader)另外注入,以便個別限制可達主機。
*/
export function createArcrunHostFunctions(env: ArcrunHostEnv, apiKey: string): WasiHostFunctions {
return {
kv_get: (key: string) => routedKvGet(env, apiKey, key),
kv_put: (key: string, value: string, ttlSeconds: number) => routedKvPut(env, apiKey, key, value, ttlSeconds),
crypto_decrypt: (encB64: string, ivB64: string) => aesGcmDecrypt(env, encB64, ivB64),
crypto_sign_rs256: (data: Uint8Array, pkcs8: Uint8Array) => rsaPkcs1Sha256Sign(data, pkcs8),
};
}
/**
* 建立 platform_crypto host functions。
* 不需要 apiKey 或 KV routing,只提供加密操作。
* ENCRYPTION_KEY 在 closure 內,永不傳給 WASM。
*/
export function createPlatformCryptoHostFunctions(encryptionKey: string): WasiHostFunctions {
const toB64 = (buf: ArrayBuffer): string => btoa(String.fromCharCode(...new Uint8Array(buf)));
return {
crypto_hmac_sha256: async (data: Uint8Array): Promise<Uint8Array> => {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, data);
return new Uint8Array(sig);
},
crypto_aes_encrypt: async (plaintext: Uint8Array): Promise<{ encryptedB64: string; ivB64: string }> => {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, plaintext);
return { encryptedB64: toB64(enc), ivB64: toB64(iv.buffer) };
},
crypto_random_bytes: (numBytes: number): string => {
const arr = crypto.getRandomValues(new Uint8Array(numBytes));
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
},
};
}