tools: serve each tool on its own module-namespaced key (ADR 0052)
serveTools now serves each tool on serve.<module>.<tool> instead of one tools.invoke that dispatched by name — so a module's account is scoped to serve.<module>.* 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.
This commit is contained in:
+32
-9
@@ -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.
|
* 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> {
|
export async function serveTools(broker: Broker, env: NodeJS.ProcessEnv = process.env): Promise<() => void> {
|
||||||
const byName = new Map<string, ToolDefinition>();
|
// Each tool is served on its own key, namespaced by its module (novox/hq ADR 0052): a caller
|
||||||
|
// invokes `<module>.<tool>`, only the module that serves it answers, and the module's account is
|
||||||
|
// scoped to serve.<module>.* — 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)) {
|
for (const { module, tools } of collectTools(env)) {
|
||||||
|
const seen = new Set<string>();
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
if (byName.has(t.name)) {
|
if (seen.has(t.name)) {
|
||||||
throw new Error(`tool name ${t.name} is exposed by two modules (one is ${module}) — refused`);
|
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<Readonly<Record<string, unknown>>, unknown>(
|
||||||
|
toolKey(module, t.name),
|
||||||
|
(args) => t.run(args ?? {}),
|
||||||
|
);
|
||||||
|
stops.push(stop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return broker.handle<Invocation, unknown>("tools.invoke", async (inv) => {
|
return () => {
|
||||||
const tool = byName.get(inv.tool);
|
for (const stop of stops) stop();
|
||||||
if (!tool) throw new Error(`no such tool: ${inv.tool}`);
|
};
|
||||||
return tool.run(inv.args ?? {});
|
}
|
||||||
});
|
|
||||||
|
/** 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<Record<string, unknown>> = {},
|
||||||
|
): Promise<unknown> {
|
||||||
|
return broker.request(toolKey(module, tool), args);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Testing/inspection: drop all registrations. */
|
/** Testing/inspection: drop all registrations. */
|
||||||
|
|||||||
Reference in New Issue
Block a user