go/pkg/coredeno/runtime/worker-entry.ts
Claude ad6a466459
feat(coredeno): Tier 3 Worker isolation — sandboxed module loading with I/O bridge
Each module now runs in a real Deno Worker with per-module permission
sandboxing. The I/O bridge relays Worker postMessage calls through the
parent to CoreService gRPC, so modules can access store, files, and
processes without direct network/filesystem access.

- Worker bootstrap (worker-entry.ts): sets up RPC bridge, dynamically
  imports module, calls init(core) with typed I/O object
- ModuleRegistry rewritten: creates Workers with Deno permission
  constructor, handles LOADING → RUNNING → STOPPED lifecycle
- Structured ModulePermissions (read/write/net/run) replaces flat
  string array in Go→Deno JSON-RPC
- I/O bridge: Worker postMessage → parent dispatchRPC → CoreClient
  gRPC → response relayed back to Worker
- Test module proves end-to-end: Worker calls core.storeSet() →
  Go verifies value in store

40 unit tests + 3 integration tests (Tier 1 boot + Tier 2 bidir + Tier 3 Worker).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 00:48:16 +00:00

79 lines
2.3 KiB
TypeScript

// Worker bootstrap — loaded as entry point for every module Worker.
// Sets up the I/O bridge (postMessage ↔ parent relay), then dynamically
// imports the module and calls its init(core) function.
//
// The parent (ModuleRegistry) injects module_code into all gRPC calls,
// so modules can't spoof their identity.
// I/O bridge: request/response correlation over postMessage
const pending = new Map<number, { resolve: Function; reject: Function }>();
let nextId = 0;
function rpc(
method: string,
params: Record<string, unknown>,
): Promise<unknown> {
return new Promise((resolve, reject) => {
const id = ++nextId;
pending.set(id, { resolve, reject });
self.postMessage({ type: "rpc", id, method, params });
});
}
// Typed core object passed to module's init() function.
// Each method maps to a CoreService gRPC call relayed through the parent.
const core = {
storeGet(group: string, key: string) {
return rpc("StoreGet", { group, key });
},
storeSet(group: string, key: string, value: string) {
return rpc("StoreSet", { group, key, value });
},
fileRead(path: string) {
return rpc("FileRead", { path });
},
fileWrite(path: string, content: string) {
return rpc("FileWrite", { path, content });
},
processStart(command: string, args: string[]) {
return rpc("ProcessStart", { command, args });
},
processStop(processId: string) {
return rpc("ProcessStop", { process_id: processId });
},
};
// Handle messages from parent: RPC responses and load commands
self.addEventListener("message", async (e: MessageEvent) => {
const msg = e.data;
if (msg.type === "rpc_response") {
const p = pending.get(msg.id);
if (p) {
pending.delete(msg.id);
if (msg.error) p.reject(new Error(msg.error));
else p.resolve(msg.result);
}
return;
}
if (msg.type === "load") {
try {
const mod = await import(msg.url);
if (typeof mod.init === "function") {
await mod.init(core);
}
self.postMessage({ type: "loaded", ok: true });
} catch (err) {
self.postMessage({
type: "loaded",
ok: false,
error: err instanceof Error ? err.message : String(err),
});
}
return;
}
});
// Signal ready — parent will respond with {type: "load", url: "..."}
self.postMessage({ type: "ready" });