// Module registry — manages module lifecycle with Deno Worker isolation. // Each module runs in its own Worker with per-module permission sandboxing. // I/O bridge relays Worker postMessage calls to CoreService gRPC. import type { CoreClient } from "./client.ts"; export type ModuleStatus = | "UNKNOWN" | "LOADING" | "RUNNING" | "STOPPED" | "ERRORED"; export interface ModulePermissions { read?: string[]; write?: string[]; net?: string[]; run?: string[]; } interface Module { code: string; entryPoint: string; permissions: ModulePermissions; status: ModuleStatus; worker?: Worker; } export class ModuleRegistry { private modules = new Map(); private coreClient: CoreClient | null = null; private workerEntryUrl: string; constructor() { this.workerEntryUrl = new URL("./worker-entry.ts", import.meta.url).href; } setCoreClient(client: CoreClient): void { this.coreClient = client; } load(code: string, entryPoint: string, permissions: ModulePermissions): void { // Terminate existing worker if reloading const existing = this.modules.get(code); if (existing?.worker) { existing.worker.terminate(); } const mod: Module = { code, entryPoint, permissions, status: "LOADING", }; this.modules.set(code, mod); // Resolve entry point URL for the module const moduleUrl = entryPoint.startsWith("file://") || entryPoint.startsWith("http") ? entryPoint : "file://" + entryPoint; // Build read permissions: worker-entry.ts dir + module source + declared reads const readPerms: string[] = [ new URL(".", import.meta.url).pathname, ]; // Add the module's directory so it can be dynamically imported if (!entryPoint.startsWith("http")) { const modPath = entryPoint.startsWith("file://") ? entryPoint.slice(7) : entryPoint; // Add the module file's directory const lastSlash = modPath.lastIndexOf("/"); if (lastSlash > 0) readPerms.push(modPath.slice(0, lastSlash + 1)); else readPerms.push(modPath); } if (permissions.read) readPerms.push(...permissions.read); // Create Worker with permission sandbox const worker = new Worker(this.workerEntryUrl, { type: "module", name: code, // deno-lint-ignore no-explicit-any deno: { permissions: { read: readPerms, write: permissions.write ?? [], net: permissions.net ?? [], run: permissions.run ?? [], env: false, sys: false, ffi: false, }, }, } as any); mod.worker = worker; // I/O bridge: relay Worker RPC to CoreClient worker.onmessage = async (e: MessageEvent) => { const msg = e.data; if (msg.type === "ready") { worker.postMessage({ type: "load", url: moduleUrl }); return; } if (msg.type === "loaded") { mod.status = msg.ok ? "RUNNING" : "ERRORED"; if (msg.ok) { console.error(`CoreDeno: module running: ${code}`); } else { console.error(`CoreDeno: module error: ${code}: ${msg.error}`); } return; } if (msg.type === "rpc" && this.coreClient) { try { const result = await this.dispatchRPC( code, msg.method, msg.params, ); worker.postMessage({ type: "rpc_response", id: msg.id, result }); } catch (err) { worker.postMessage({ type: "rpc_response", id: msg.id, error: err instanceof Error ? err.message : String(err), }); } } }; worker.onerror = (e: ErrorEvent) => { mod.status = "ERRORED"; console.error(`CoreDeno: worker error: ${code}: ${e.message}`); }; console.error(`CoreDeno: module loading: ${code}`); } private async dispatchRPC( moduleCode: string, method: string, params: Record, ): Promise { const c = this.coreClient!; switch (method) { case "StoreGet": return c.storeGet(params.group as string, params.key as string); case "StoreSet": return c.storeSet( params.group as string, params.key as string, params.value as string, ); case "FileRead": return c.fileRead(params.path as string, moduleCode); case "FileWrite": return c.fileWrite( params.path as string, params.content as string, moduleCode, ); case "ProcessStart": return c.processStart( params.command as string, params.args as string[], moduleCode, ); case "ProcessStop": return c.processStop(params.process_id as string); default: throw new Error(`unknown RPC method: ${method}`); } } unload(code: string): boolean { const mod = this.modules.get(code); if (!mod) return false; if (mod.worker) { mod.worker.terminate(); mod.worker = undefined; } mod.status = "STOPPED"; console.error(`CoreDeno: module unloaded: ${code}`); return true; } status(code: string): ModuleStatus { return this.modules.get(code)?.status ?? "UNKNOWN"; } list(): Array<{ code: string; status: ModuleStatus }> { return Array.from(this.modules.values()).map((m) => ({ code: m.code, status: m.status, })); } }