Commit Graph
11 Commits
Author SHA1 Message Date
jschoubben 3d55aeb1d8 The old bus's wire is pinned unchanged, because this has to merge to a running mesh
Every module's event names were converted from the old bus's routing keys to local
names, and this client maps them back. If that mapping is wrong anywhere a live mesh's
events stop being delivered — silently, because a binding that matches nothing is not an
error.

So the mapping is pinned against the literal routing keys the mesh published before,
taken from the manifests as they were: what each module now emits, what each now binds,
and that a handler still matches what the bus delivers. Including the audit logger's
"everything", which must stay `#` on this bus.

And a key already in the old form is left alone, so a module built from an older manifest
keeps working beside one built from a current manifest — which is the state the mesh will
actually be in between deployments.
2026-09-27 17:37:19 +02:00
jschoubben fbeb373d1a Both clients map local event names to their own wire
A module names its events locally and each transport works out where they land.
That is what design 29 says and what neither client did: both passed the name
straight through, which happened to be right on the old bus because modules were
writing routing keys, and wrong on the new one (novox/hq 04-ISSUES/127).

The old bus's client now turns a local name into `module.<emitter>.<event>` on the
way out and back on the way in. Without that, converting the modules to local
names would have broken the mesh that is actually running.

**A handler and a manifest now say the same thing.** The key a module sees was the
event name alone, so a manifest declaring `consumes: builder.built` produced a
pattern that could never match what it was compared against — and a module
consuming one event from two emitters could only tell them apart by reading a
header. The subject already carries the emitter, so naming it in the key makes a
mismatch between manifest and code a typo instead of a category error.

Both matchers accept `**` for the rest of a name, which is how a manifest spells
it; the old bus's `#` still works, because both buses ship until the rollout.
2026-09-27 14:42:40 +02:00
jschoubben 5b111da4a9 The patient reconnect gives up on permanent failures (issue 058 review)
The retry loop treated everything but a cert-pin mismatch as transient,
so a refused login (revoked/mis-sealed credential) or a malformed broker
URL retried for ever logging 'not reachable yet' — the silent
non-progress the fix set out to remove, and a contradiction of its own
docstring. fatalBrokerReason now classifies those three as fatal and
everything else (connection refused, timeout, DNS) as retryable, with a
unit test covering the split — the honest proof the bed cannot give,
since it only ever starts the consumer after the broker is up.

The pin case is now a typed PinMismatchError caught by instanceof, not a
prose substring a reword could silently downgrade to an infinite retry
against an impostor. Added a little jitter so modules do not stampede a
recovering broker in lockstep.
2026-09-20 13:30:52 +02:00
jschoubben 813f2e0db3 Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:40:40 +02:00
jschoubben ef540042bd Resync hq ADR references 0044-0054 -> 0039-0049 after the hq record reconciliation 2026-09-05 12:45:25 +02:00
jschoubben 4558248f16 runtime: RPC replies ride mesh.rpc; an invoke subcommand (ADR 0052)
Replies go through the RPC exchange keyed by the caller's reply-queue name, not
the default exchange — so a serving module's scoped account answers with write
on mesh.rpc alone, never the default exchange (which would let it publish into
any queue). 'mesh-tools invoke <module> <tool> [args]' is the caller's side, the
sibling of emit. Verified against a real broker: a scoped account serves its
tool and is refused another module's serve queue.
2026-09-04 21:56:04 +02:00
jschoubben bf5339cec2 runtime: take node and module identity from the sealed credential
The mesh scoped the account to a node and module; the credential now carries
both, so the runtime names its queue and stamps its events as the mesh
authorised without a manifest interpolating a node the vocabulary has no token
for. Verified: with only MESH_BROKER_FILE, the audit logger consumed as
anchor/audit-logger.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 01:53:23 +02:00
jschoubben 04a689e008 runtime: connect with a sealed credential, scoped, over pinned amqps (ADR 0048)
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
2026-09-04 01:51:17 +02:00
jschoubben f85d1dc303 broker: the ADR 0047 wire shape, and the event failure paths
The AMQP adapter now honours the full contract: events published
persistent with metadata in headers; a durable per-consumer queue
(<node>.<module>.events) with prefetch and a dead-letter exchange
(mesh.events.dead); manual ack for at-least-once.

Failure paths, not just the happy one:
- a confirm channel, so a publish the broker never accepted fails the
  emit rather than vanishing — at-least-once starts at the emitter;
- a handler that keeps failing is requeued once, then dead-lettered
  (poison set aside, never looping);
- an undecodable body is dead-lettered at once — it never decodes on
  redelivery, and must not wedge the queue.

Binding-conformance tests against a disposable broker (a stand-in for the
mesh-hosted broker, ADR 0001): headers on the wire with a pure body, the
redelivery-limit dead-letter, and the poison-body dead-letter.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-04 00:26:09 +02:00
jschoubben 201a413f69 broker: split events (mesh.events) from tool RPC (mesh.rpc)
Per ADR 0047: two topic exchanges kept apart, so a '#' subscription on
mesh.events is a clean audit of module/mesh/node events without the
request/reply tool traffic. Header-in-AMQP, persistent messages, per-
consumer durable dead-lettered queues and prefetch are the remaining
alignment to 0047.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-03 23:50:56 +02:00
jschoubben 9eddcdb0a0 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
2026-09-03 23:21:01 +02:00