A module reads its broker credential from MESH_BROKER_FILE — the sealed
{url,fingerprint} the mesh delivered — and connects over amqps pinned to
exactly that certificate. The pin is two-phase (fetch cert, verify, then
trust only it), because Node's checkServerIdentity does not run under
rejectUnauthorized:false, so a naive connect-then-check would already have
sent the password to whoever answered.
A scoped module (assumeExchanges) never declares the exchanges (its account
may not) nor its own queue with a dead-letter (the broker refuses that to a
non-administrator) — the mesh pre-declared the queue, so it passively checks
it, binds and consumes. The RPC reply queue is lazy, and a module that
registered no tools serves none: a pure-events consumer touches only what its
account allows.
Verified end-to-end against a real broker as the scoped account: the audit
logger consumes # and records events, over an account that is not the
broker's own.
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
36 lines
1.8 KiB
TypeScript
36 lines
1.8 KiB
TypeScript
// The tool runtime — the thin per-node process that makes a module's tools actually serve. It
|
|
// binds the mesh broker, loads the assigned modules' tool entrypoints (each of which calls
|
|
// registerModuleTools as it imports), and hands them to the sdk's serving harness. Everything hard
|
|
// — dispatch, collection, duplicate-name safety — is the sdk's; this is the wrapper.
|
|
|
|
import { pathToFileURL } from "node:url";
|
|
import { resolve } from "node:path";
|
|
import { useBroker } from "@novox/mesh-sdk/messaging";
|
|
import { serveTools, listTools } from "@novox/mesh-sdk/tools";
|
|
import type { Broker } from "@novox/mesh-sdk/messaging";
|
|
|
|
export interface RuntimeOptions {
|
|
/** The mesh broker to serve over. */
|
|
broker: Broker;
|
|
/** Absolute paths to the assigned modules' compiled tool entrypoints (e.g. .../umami/tools/index.js). */
|
|
moduleEntrypoints: string[];
|
|
}
|
|
|
|
/** Load the modules, bind the broker, and serve. Returns a stop function that unhooks serving. */
|
|
export async function runTools(opts: RuntimeOptions): Promise<() => void> {
|
|
useBroker(() => opts.broker);
|
|
|
|
for (const entry of opts.moduleEntrypoints) {
|
|
// Importing the entrypoint runs its registerModuleTools(...) — that is the whole handshake.
|
|
await import(pathToFileURL(resolve(entry)).href);
|
|
}
|
|
|
|
// Serve the RPC endpoint only if a module actually registered a tool. A pure-events module (the
|
|
// audit logger) registers none, and its scoped account may not declare the serve queue — so a
|
|
// runtime that always served would fail for exactly the modules that never needed it.
|
|
const tools = listTools();
|
|
const stop = tools.length > 0 ? await serveTools(opts.broker) : () => {};
|
|
console.log(`[mesh-tools] serving ${tools.length} tool(s): ${tools.map((t) => t.name).join(", ") || "(none)"}`);
|
|
return stop;
|
|
}
|