From 6ef6c761b212ce9f218d2015a3d179c6e8f2ae4b Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 4 Sep 2026 21:56:03 +0200 Subject: [PATCH] tools: serve each tool on its own module-namespaced key (ADR 0052) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serveTools now serves each tool on serve.. instead of one tools.invoke that dispatched by name — so a module's account is scoped to serve..* and one module cannot answer another's calls. toolKey and invokeTool are the caller's side. A tool name need only be unique within its module now, not across the mesh. --- src/tools/index.ts | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index 0fae74f..132cbbc 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -66,20 +66,43 @@ export function listTools(env: NodeJS.ProcessEnv = process.env): { module: strin * other — two tools answering one name is a fault, not a race to resolve. */ export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> { - const byName = new Map(); + // Each tool is served on its own key, namespaced by its module (novox/hq ADR 0052): a caller + // invokes `.`, only the module that serves it answers, and the module's account is + // scoped to serve..* — so one module cannot answer another's calls. The module is the + // namespace, so a tool name need only be unique within its module, not across the whole mesh. + const stops: Array<() => void> = []; for (const { module, tools } of collectTools(env)) { + const seen = new Set(); for (const t of tools) { - if (byName.has(t.name)) { - throw new Error(`tool name ${t.name} is exposed by two modules (one is ${module}) — refused`); + if (seen.has(t.name)) { + throw new Error(`${module} exposes two tools named ${t.name} — refused`); } - byName.set(t.name, t); + seen.add(t.name); + const stop = await broker.handle>, unknown>( + toolKey(module, t.name), + (args) => t.run(args ?? {}), + ); + stops.push(stop); } } - return broker.handle("tools.invoke", async (inv) => { - const tool = byName.get(inv.tool); - if (!tool) throw new Error(`no such tool: ${inv.tool}`); - return tool.run(inv.args ?? {}); - }); + return () => { + for (const stop of stops) stop(); + }; +} + +/** The broker key a tool is served on and invoked by — the module namespaces the tool (ADR 0052). */ +export function toolKey(module: string, tool: string): string { + return `${module}.${tool}`; +} + +/** Invoke a module's tool over the broker — the caller's side of serving. */ +export async function invokeTool( + broker: Broker, + module: string, + tool: string, + args: Readonly> = {}, +): Promise { + return broker.request(toolKey(module, tool), args); } /** Testing/inspection: drop all registrations. */