Task 3.6 of novox/hq ADR 0116. A module is still written against request,
handle, publish, subscribe, close; only what is underneath changes. main.ts
still selects the AMQP client — steps 1 to 4 leave every node on AMQP, so
this ships beside it and is selected at the rollout.
Round-tripped against a real server (test/roundtrip.mjs): a tool answered
across two connections, a throwing handler reaching the caller as an error
rather than a timeout, an event delivered once with its key, body, node and
event id intact, and an event landing under its emitter's own namespace.
Three things the compiler and the server corrected:
- the envelope's field is `key`, not `type`, and the payload is `env.body`
with metadata in headers — not the whole envelope re-encoded. An
implementation that nested the envelope would pass all its own tests and
agree with nobody, which is what the conformance suite exists to stop.
- the NATS client's TLS options are PEM strings with no verify hook, so the
AMQP client's `checkServerIdentity: () => undefined` has no equivalent.
The fingerprint check still happens and is still the guarantee, but the
bus's certificate must now carry a SAN matching the address nodes dial.
That is a constraint on the mesh's certificates, recorded where it bites.
- a durable consumer is bound, never created: a module's account cannot
reach the JetStream API, and a runtime creating its own would be a module
choosing its own delivery semantics.
package.json names @novox/mesh-sdk by version and the install is a
buildkit-secret-mounted resolve from the mesh's package registry, closing the
git-URL half of issue 053. The lock is regenerated against the registry (a
follow-up switches install to ci with a committed lock).
Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
It copied in a compiled directory that is not in source control and resolved
the toolkit to a sibling checkout, so only a workstation with two repositories
side by side could produce it — and its fingerprint was then typed into every
module by hand. Nothing could rebuild it, so nothing could check it, and the
rule that catches a base moving had no version on the far end of its edge.
The recipe now also says what the image in service actually is. It claimed
Alpine and has been serving Debian for as long as nobody could rebuild it.
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