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>
202 lines
5.4 KiB
TypeScript
202 lines
5.4 KiB
TypeScript
// 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<string, Module>();
|
|
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<string, unknown>,
|
|
): Promise<unknown> {
|
|
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,
|
|
}));
|
|
}
|
|
}
|