922a57fe34
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>
143 lines
5.3 KiB
TypeScript
143 lines
5.3 KiB
TypeScript
/**
|
|
* arcrun logic component Worker
|
|
*
|
|
* POST / → JSON input → WASM (WASI preview1 stdin/stdout) → JSON output
|
|
*
|
|
* WASM is statically bundled at build time via wrangler.toml [[wasm_modules]].
|
|
* Each logic component gets its own Worker at {name}.arcrun.dev.
|
|
*/
|
|
|
|
import componentWasm from '../component.wasm' assert { type: 'webassembly' };
|
|
|
|
import { Hono } from 'hono';
|
|
import { cors } from 'hono/cors';
|
|
|
|
const app = new Hono();
|
|
|
|
app.use('*', cors());
|
|
|
|
app.get('/', (c) => c.json({ ok: true, component: COMPONENT_ID }));
|
|
|
|
app.post('/', async (c) => {
|
|
let input: unknown;
|
|
try {
|
|
input = await c.req.json();
|
|
} catch {
|
|
return c.json({ success: false, error: 'request body must be JSON' }, 400);
|
|
}
|
|
|
|
try {
|
|
const result = await runWasm(componentWasm, input);
|
|
return c.json(result);
|
|
} catch (e) {
|
|
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 500);
|
|
}
|
|
});
|
|
|
|
export default app;
|
|
|
|
// ── WASM runner (WASI preview1 stdin/stdout) ─────────────────────────────────
|
|
|
|
declare const COMPONENT_ID: string; // injected via [vars] in wrangler.toml
|
|
|
|
async function runWasm(wasmModule: WebAssembly.Module, input: unknown): Promise<unknown> {
|
|
const stdinBytes = new TextEncoder().encode(JSON.stringify(input));
|
|
let stdinOffset = 0;
|
|
|
|
const stdoutChunks: Uint8Array[] = [];
|
|
let memory: WebAssembly.Memory | null = null;
|
|
|
|
const getView = () => new DataView(memory!.buffer);
|
|
|
|
const wasi: WebAssembly.Imports = {
|
|
wasi_snapshot_preview1: {
|
|
fd_write(fd: number, iovs: number, iovs_len: number, nwritten_ptr: number): number {
|
|
if (fd !== 1 && fd !== 2) return 76; // ENOSYS
|
|
const view = getView();
|
|
let total = 0;
|
|
for (let i = 0; i < iovs_len; i++) {
|
|
const base = view.getUint32(iovs + i * 8, true);
|
|
const len = view.getUint32(iovs + i * 8 + 4, true);
|
|
if (len === 0) continue;
|
|
const chunk = new Uint8Array(memory!.buffer, base, len);
|
|
const copy = new Uint8Array(len);
|
|
copy.set(chunk);
|
|
if (fd === 1) stdoutChunks.push(copy);
|
|
total += len;
|
|
}
|
|
view.setUint32(nwritten_ptr, total, true);
|
|
return 0;
|
|
},
|
|
fd_read(fd: number, iovs: number, iovs_len: number, nread_ptr: number): number {
|
|
if (fd !== 0) return 76;
|
|
const view = getView();
|
|
let total = 0;
|
|
for (let i = 0; i < iovs_len; i++) {
|
|
const base = view.getUint32(iovs + i * 8, true);
|
|
const len = view.getUint32(iovs + i * 8 + 4, true);
|
|
const remaining = stdinBytes.length - stdinOffset;
|
|
if (remaining <= 0) break;
|
|
const toCopy = Math.min(len, remaining);
|
|
new Uint8Array(memory!.buffer, base, toCopy).set(
|
|
stdinBytes.subarray(stdinOffset, stdinOffset + toCopy)
|
|
);
|
|
stdinOffset += toCopy;
|
|
total += toCopy;
|
|
}
|
|
view.setUint32(nread_ptr, total, true);
|
|
return 0;
|
|
},
|
|
proc_exit(code: number): never { throw new Error(`wasm exit: ${code}`); },
|
|
random_get(ptr: number, len: number): number {
|
|
crypto.getRandomValues(new Uint8Array(memory!.buffer, ptr, len));
|
|
return 0;
|
|
},
|
|
fd_seek: () => 76, fd_close: () => 0,
|
|
fd_fdstat_get: () => 76, fd_prestat_get: () => 76,
|
|
fd_prestat_dir_name: () => 76, environ_get: () => 0,
|
|
environ_sizes_get: (cp: number, sp: number) => {
|
|
if (memory) { const v = getView(); v.setUint32(cp,0,true); v.setUint32(sp,0,true); }
|
|
return 0;
|
|
},
|
|
args_get: () => 0,
|
|
args_sizes_get: (ap: number, bp: number) => {
|
|
if (memory) { const v = getView(); v.setUint32(ap,0,true); v.setUint32(bp,0,true); }
|
|
return 0;
|
|
},
|
|
clock_time_get: (_id: number, _prec: bigint, tp: number) => {
|
|
if (memory) getView().setBigUint64(tp, BigInt(Date.now()) * 1_000_000n, true);
|
|
return 0;
|
|
},
|
|
clock_res_get: () => 76, poll_oneoff: () => 76, sched_yield: () => 0,
|
|
proc_raise: () => 76, sock_accept: () => 76, sock_recv: () => 76,
|
|
sock_send: () => 76, sock_shutdown: () => 76,
|
|
path_open: () => 76, path_create_directory: () => 76,
|
|
path_remove_directory: () => 76, path_rename: () => 76,
|
|
path_unlink_file: () => 76, path_filestat_get: () => 76,
|
|
path_readlink: () => 76, path_symlink: () => 76, path_link: () => 76,
|
|
},
|
|
// u6u host functions (no-op for pure logic components)
|
|
u6u: { http_request: () => 1 },
|
|
};
|
|
|
|
const instance = await WebAssembly.instantiate(wasmModule, wasi);
|
|
memory = instance.exports.memory as WebAssembly.Memory;
|
|
|
|
const start = (instance.exports._start ?? instance.exports.main) as () => void;
|
|
if (typeof start !== 'function') throw new Error('WASM missing _start or main export');
|
|
|
|
try { start(); } catch (e) {
|
|
if (!(e instanceof Error && e.message === 'wasm exit: 0')) throw e;
|
|
}
|
|
|
|
const decoder = new TextDecoder();
|
|
const total = stdoutChunks.reduce((n, c) => n + c.length, 0);
|
|
const merged = new Uint8Array(total);
|
|
let off = 0;
|
|
for (const chunk of stdoutChunks) { merged.set(chunk, off); off += chunk.length; }
|
|
const stdout = decoder.decode(merged).trim();
|
|
|
|
if (!stdout) throw new Error('WASM component produced no output');
|
|
return JSON.parse(stdout);
|
|
}
|