mesh-tools — the tool runtime and AMQP broker binding

The per-node process that makes a module's tools serve on the mesh:
- broker-amqp.ts: a concrete AMQP implementation of the sdk's Broker
  contract (request/reply over a reply queue + correlation id, publish/
  subscribe over a topic exchange). Kept here, not in the sdk, so a
  broker-client change never rebuilds a module (ADR 0044).
- runtime.ts: bind the broker, import the assigned modules' tool
  entrypoints (each registers as it loads), serveTools. The thin wrapper.
- main.ts: the entrypoint, configured by MESH_BROKER_URL + MESH_TOOL_MODULES.
- Dockerfile: the container a node runs it as.

Verified over a REAL broker: the test spins LavinMQ (the mesh's broker),
the runtime serves a registered tool, a separate connection invokes it by
name over AMQP and gets the result, and an unknown tool is refused over
the wire. So 'modules can serve' is now running-on-the-mesh, not just
proven in a mock.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-03 23:21:01 +02:00
commit 9eddcdb0a0
10 changed files with 421 additions and 0 deletions
+44
View File
@@ -0,0 +1,44 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { registerModuleTools, resetTools } from "@novox/mesh-sdk/tools";
import { connectAmqp } from "../dist/broker-amqp.js";
import { runTools } from "../dist/runtime.js";
// Requires a real broker at $MESH_BROKER_URL. The test harness spins LavinMQ (the mesh's broker)
// and points this at it; skipped if it is not set, never failed for the environment.
const url = process.env.MESH_BROKER_URL;
test("a module's tool serves and is invoked over a real AMQP broker", { skip: !url }, async () => {
resetTools();
// A module registers a real tool, exactly as umami does.
registerModuleTools("demo", () => [
{
name: "greet",
description: "return a greeting",
input: { who: { type: "string" } },
run: async (args) => ({ hello: String(args.who), from: "the tool runtime" }),
},
]);
// The runtime binds the broker and serves the module (no module entrypoints to import here — the
// tool is registered in-process — but this is the exact runtime that serves on a node).
const serverBroker = await connectAmqp(url!);
const stop = await runTools({ broker: serverBroker, moduleEntrypoints: [] });
// A separate connection — a caller, like mesh-control's command API — invokes over the broker.
const caller = await connectAmqp(url!);
const result = await caller.request<{ tool: string; args: Record<string, unknown> }, { hello: string }>(
"tools.invoke",
{ tool: "greet", args: { who: "mesh" } },
);
assert.equal(result.hello, "mesh");
// An unknown tool is refused over the wire, not silently dropped.
await assert.rejects(caller.request("tools.invoke", { tool: "nope", args: {} }), /no such tool/);
stop();
await caller.close();
await serverBroker.close();
});