Correcting a tick and a claim I made one commit ago. 4.1 does not wait on an enrolment user per live token; it waits on the whole composition, of which that user is one input. Tasks 1.3 and 1.4 are honest about what they built — the composer, the derivation, the permission model, the stream and consumer definitions, the asserter, all pure and held by unit tests and a golden composition. Nobody wrote the caller. Measured: outside the package that defines them there is not one use of the composer, the permission derivation, the stream set, the stream asserter or the principal type. Step 1's "done when" claims every account and permission composed from the manifests, and a mesh raised today would stand up a server with no user list at all. It also needs state the mesh does not keep. Design 25 §4 says the file holds bcrypt hashes and that passwords are minted and sealed exactly as today — but today the mesh mints one, hands it to the broker through a management call, seals the plaintext to the holder and keeps nothing. With no management call the hash has to survive every later recomposition, because the first thing a new module or a person's access change touches is a file that must still hold every other user's password. No bcrypt hash is stored anywhere in the controller. Named as its own task rather than folded into 1.3, so the gap between "the parts of step 1 exist" and "the mesh does any of it" is visible.
514 lines
36 KiB
Markdown
514 lines
36 KiB
Markdown
---
|
|
layer: to-be
|
|
status: in-progress
|
|
code:
|
|
- mesh-catalog modules/nats
|
|
- mesh-controller internal/catalogue
|
|
- mesh-lab scenarios
|
|
updated: 2026-09-27
|
|
decisions:
|
|
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
|
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
|
- 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
|
|
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
|
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
|
|
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
|
|
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
|
|
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
|
|
---
|
|
|
|
# 28. Building the bus
|
|
|
|
**The work of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)'s five steps,
|
|
in the order its dependencies allow, with what each ends at.**
|
|
[Design 25](25-the-bus-on-nats.md) is the architecture and stays the authority on *what is built*;
|
|
this document holds only the order, the sizes and the proofs, and it is wrong the moment it
|
|
disagrees with design 25 rather than the other way round.
|
|
|
|
Each step ends at something runnable. A step that cannot name what its bed proves is not a step,
|
|
and is divided further before it is started.
|
|
|
|
## How this is built, and when it is run
|
|
|
|
**Written as code with unit tests, committed per change, and taken to the lab once the pieces that
|
|
would change the outcome are in place.** The mistake this avoids is the one
|
|
[design 22](22-the-work-ahead.md) records: running a long bed against a mesh mid-transformation and
|
|
debugging paths the next step deletes. Where a fault can be reasoned out of the code path, it is —
|
|
reading, not running.
|
|
|
|
So the beds below are acceptance tests at the end of assembled work, not the tool for finding each
|
|
bug, and a step's bed is run when that step is finished rather than while it is being written.
|
|
|
|
## What the work is, measured
|
|
|
|
Counted 2026-09-26, non-test source only. The point of counting is that none of this is unknown
|
|
territory: every piece has a shape already standing beside it.
|
|
|
|
| Piece | Today | Size | Becomes |
|
|
|---|---|---|---|
|
|
| the controller's link | Go, one package | ~1 800 lines | the same package on NATS |
|
|
| the host's link | Go, one package, mirroring the contracts rather than importing them | ~1 000 lines | the same, on NATS |
|
|
| the tool runtime's client | TypeScript, one file | ~390 lines | the same, on NATS |
|
|
| the sdk's messaging surface | TypeScript: messaging, events, tools, contracts, primitives | ~360 lines across five | **unchanged**, see below |
|
|
| the broker module | the adopted AMQP broker: client, tools, provisioner, bootstrap, image, manifest | ~340 lines of module code | the `nats` module, same shape |
|
|
| the beds | 39 lab scenarios, including a broker bed, an adoption bed, a genesis bed and a store-window bed | — | four analogues and one new |
|
|
|
|
**Two measurements are worth stating on their own, because they change what the steps are.**
|
|
|
|
**The sdk speaks no AMQP, and never did.** The word appears in its source three times, in three
|
|
comments; its messaging module says in as many words that it "carries the contract, not a specific
|
|
AMQP client build." [ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) put the
|
|
client in the runtime, and the payoff is collected here: **no module is rebuilt for this change, and
|
|
the sdk's own diff is three comments.** That is the whole reason a bus can be replaced under a live
|
|
mesh at all.
|
|
|
|
**The wire therefore has three implementations, not two, and no suite pins any of them.**
|
|
[ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) spoke of "the existing
|
|
two implementations" — Go and TypeScript. Measured, the Go side is *two separate packages* that
|
|
mirror rather than share (the host imports nothing, by
|
|
[ADR 0005](../../02-DECISIONS/0005-the-node-host.md)), so the count is the controller's link, the
|
|
host's link, and the runtime's client. And a search for conformance fixtures finds none anywhere in
|
|
the four repositories: design 22's Phase 1.2 — the suite — has not been built.
|
|
|
|
> **This corrected the record.** ADR 0116 said step 3's fixtures were *recaptured* on NATS. There
|
|
> is nothing to recapture, so step 3 **builds** the suite, and its first job is to pin the wire the
|
|
> mesh has before changing it — a suite written only against the new bus certifies whatever the new
|
|
> bus happens to do. A fact went stale while the decision stood, which is a **progressive insight**
|
|
> ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)): it is marked and dated in ADR 0116
|
|
> itself rather than left to be discovered here.
|
|
|
|
## The order the work actually allows
|
|
|
|
**The five steps are chunks of capability; the build order is not simply 1 to 5, and pretending
|
|
otherwise would put two beds where they cannot run.** Three edges decide it:
|
|
|
|
- **A specification precedes the implementations it governs.** ADR 0074's whole argument is that
|
|
agreement is specified and checked, not hoped for. So the wire's NATS binding is written *before*
|
|
the three implementations are, even though it is step 3 — and its conformance half can only
|
|
*finish* once two implementations exist to disagree.
|
|
- **A mesh cannot be raised on a bus nothing speaks.** A bed that raises a mesh on NATS from
|
|
genesis — enrolling a node, holding a push while the store restarts, rolling out an upgrade —
|
|
needs the controller and the host to speak NATS already. That is the implementations, and they
|
|
arrive with step 3.
|
|
- **Adoption needs the module and nothing else.** Step 2 puts a correctly configured server into a
|
|
running mesh that continues to ignore it, which depends on no link at all.
|
|
|
|
So step 1's bed proves *the server, from genesis, configured* — not a mesh living on it. The full
|
|
genesis bed is step 4's, where it can first run.
|
|
|
|
> **This corrected the record too.** ADR 0116 first attributed "a mesh raised on NATS from genesis"
|
|
> to step 1. That bed cannot run until the links exist, and a step whose proof cannot run is the
|
|
> exact failure the record was written to prevent — so step 1 now ends at the server standing,
|
|
> correctly configured and carrying nothing, and the full bed is named under step 4. The five
|
|
> steps, their names, their order and the single rollout are unchanged; only where two beds run has
|
|
> moved. Marked and dated in ADR 0116 as a progressive insight, with what the record said before.
|
|
|
|
```
|
|
step 1 module, genesis places it ──┐
|
|
step 2 adoption into a running mesh ──┤ neither needs a link
|
|
│
|
|
step 3 the wire specified ──► three implementations ──► the suite
|
|
│
|
|
step 4 the flows, and the full genesis bed
|
|
│
|
|
step 5 the rollout
|
|
```
|
|
|
|
## Step 1 — the module, and genesis raises it
|
|
|
|
> **Revised 2026-09-26** ([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md),
|
|
> [design 29](29-what-a-module-declares.md)). Tasks 1.3 and 1.4 said the controller composes every
|
|
> account and creates *the four streams* at genesis, from a fixed set. That is only the mesh's own
|
|
> half. A module declares seats with their protocols, so streams are created **at registration**
|
|
> and durable consumers **at assignment** — neither of which has happened at genesis. The fixed
|
|
> foundation set stays here; the derived machinery moves to step 3, where the declaration model it
|
|
> reads from is specified. Tasks 1.1 and 1.2, already done, are untouched by this: the module and
|
|
> its reload mechanism do not care what the configuration says.
|
|
|
|
**Why here.** Everything else needs a server to talk to, and genesis is where the foundation is
|
|
defined. The mesh this is for will never travel this path — it is already running, and takes step 2
|
|
— but genesis is the definition every other path is measured against, and one that exists only on
|
|
paper is wrong until there is a second mesh to find out.
|
|
|
|
- [x] 1.1 the `nats` module: manifest, image, one container, its client, TLS and monitoring ports,
|
|
JetStream on a named volume — the shape of design 25 §5, and the same shape the broker module
|
|
beside it already has
|
|
- [x] 1.2 the composed configuration as a **directory** resource, and the entrypoint that watches
|
|
the one file and signals the server itself — design 25 §5's correction, kept inside the module
|
|
because a container has no reload and a recreate would drop every connection the mesh has
|
|
- [x] 1.3 the controller composes that file: accounts, permissions, TLS, JetStream — a user's
|
|
permissions derived from its declaration and nothing else, over the three namespaces of
|
|
[design 29](29-what-a-module-declares.md) §2, plus its own ack subject and its own inbox
|
|
prefix (design 25 §4)
|
|
- [x] 1.4 the mesh's own streams, created at genesis and asserted idempotently on start, by the
|
|
controller as their only writer — **the mesh's own, not all of them**: a seat's streams are
|
|
created when the module declaring it is registered, and a module's durable consumers when it
|
|
is assigned, so this task is the fixed foundation set and 3.x carries the derived rest
|
|
- [x] 1.5 genesis raises it as foundation, claiming the seat **`mesh-broker`** — the seat is the
|
|
server's role, not the product. **Already true of the controller and needed no change**: it
|
|
resolves the broker by seat ("that is where the broker is, whatever else the topology says")
|
|
and names no broker module anywhere in its source. What remains is naming `nats` instead of
|
|
the deprecated broker where a genesis module set is declared, which is scenario and installer
|
|
configuration — carried with 1.6 rather than before it.
|
|
- [ ] 1.7 **the composition, delivered** — the controller gathering its principals, composing the
|
|
file, and asserting the streams and consumers on start.
|
|
|
|
> **This corrects a tick, not a decision.** Tasks 1.3 and 1.4 are ticked and they are honest
|
|
> about what they built — the composer, the derivation, the permission model, the stream and
|
|
> consumer definitions, the asserter, all pure and held by unit tests and a golden
|
|
> composition. What nobody wrote is the *caller*. Measured on the feature branch: outside the
|
|
> package that defines them, there is **not one** use of the composer, the permission
|
|
> derivation, the stream set, the stream asserter or the principal type. Step 1's "done when"
|
|
> claims "every account and permission composed from the manifests", and a mesh raised today
|
|
> would stand up a server with no user list at all.
|
|
>
|
|
> It also needs state the mesh does not keep. Design 25 §4 says the file holds bcrypt
|
|
> hashes, and passwords are "minted and sealed exactly as today" — but today the mesh mints a
|
|
> password, hands it to the broker through a management call, seals the plaintext to the
|
|
> holder and **keeps nothing**. There is no management call here, so the hash has to survive
|
|
> for every later recomposition: the first thing a person's access change or a new module
|
|
> touches is a file that must still contain every other user's password. No bcrypt hash is
|
|
> stored anywhere in the controller today.
|
|
>
|
|
> Named as its own task rather than folded into 1.3 so the gap is visible: the parts of
|
|
> step 1 exist and the mesh does not yet do any of it.
|
|
|
|
- [ ] 1.6 the genesis-broker bed — **deferred**: beds are run once, at the end, rather than per
|
|
step (novox/hq design 22's rule, and the operator's instruction). Every claim step 1 makes
|
|
is covered by a unit test or was demonstrated against the real server; what the bed adds is
|
|
the claims that need a mesh.
|
|
|
|
> **Not done here, deliberately.** The controller builds a module's broker credential as an
|
|
> `amqps://` URL and defaults a portless genesis address to 5671. Those are correct until the
|
|
> rollout and must not move: steps 1 to 4 leave every node on AMQP
|
|
> ([ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md)), so changing the
|
|
> credential's shape now would break the running bus to serve a bus nothing speaks yet. They
|
|
> change with the links, in step 3.
|
|
|
|
**Done when.** A mesh raised from nothing has the server standing with the streams asserted and
|
|
every account and permission composed from the manifests; a user cannot publish outside its
|
|
`emits`, subscribe outside its `consumes`, ack another user's delivery or subscribe another's inbox
|
|
prefix; the monitoring port is refused from anything but the private network; a change to the
|
|
composed file is live within one watcher interval without a restart, and the container is not
|
|
recreated by it.
|
|
|
|
**The permission checks belong here rather than later** because the server enforces them itself — a
|
|
plain client proves them, no link required — and they are the whole of what ADR 0043 asks for. No
|
|
mesh traffic is on the bus yet; that is step 4's bed, not this one's.
|
|
|
|
## Step 2 — adoption puts it in the seat
|
|
|
|
**Why here.** It needs only step 1's module, it is the path the mesh that exists will actually take,
|
|
and it is what makes steps 3 and 4 safe to develop against a live mesh. A running mesh does not get
|
|
a foundation module by being raised again; it adopts one in place
|
|
([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)).
|
|
|
|
- [ ] 2.1 the server raised beside the existing broker on its own ports, carrying nothing —
|
|
installer-side, from the upstream image
|
|
- [ ] 2.2 the `nats` module assigned, which **recreates the container once, deliberately** (see
|
|
below), keeping its JetStream directory
|
|
- [x] 2.3 the seat claim, and the resolver's refusal of a second holder mesh-wide — **already
|
|
true and now proved**: the refusal is generic to any mesh-scoped seat, and three tests pin
|
|
what matters for this one — a second bus anywhere is refused naming the seat, a *different*
|
|
bus implementation is refused for the same reason (which is what lets the bus be replaced
|
|
at all), and the deprecated broker no longer contends for it, so both run on one mesh
|
|
- [ ] 2.4 the adoption bed — deferred with the other beds
|
|
|
|
> **Adoption here is not a no-op, and pretending it would be is the trap.** The host keeps an
|
|
> existing container only when its spec matches the declaration exactly
|
|
> ([`apply.go`](https://git.novox.be/novox/mesh-host): *existed && before.Spec == want && running*
|
|
> → unchanged; anything else is `rm -f` and recreate). Genesis raises the server from the
|
|
> **upstream** image, because nothing has been built yet; the module declares the **mesh-built**
|
|
> artifact, which carries the entrypoint that reloads configuration in place. Those two specs
|
|
> differ, so assigning the module recreates the container.
|
|
>
|
|
> That is correct, and it is [ADR 0067](../../02-DECISIONS/0067-genesis-is-a-pivot.md)'s pivot
|
|
> exactly: raise a temporary thing, then reinstall it as an ordinary module. It is safe **only
|
|
> because it happens while the bus carries nothing** — which is what 2.1 means by "carrying
|
|
> nothing", and why step 2 comes before anything speaks NATS rather than after. One recreate, at
|
|
> the one moment it costs nothing.
|
|
>
|
|
> **After that, never again.** The configuration is a directory mount rather than a file, so
|
|
> rewriting accounts does not change the container's spec and the entrypoint reloads the server in
|
|
> place. That is the whole point of task 1.2, and this is the moment it pays: every later account,
|
|
> permission or person's access change touches a running bus with connections on it.
|
|
|
|
**Done when.** A mesh already running has the server adopted, holding `mesh-broker`; a second
|
|
assignment anywhere is refused at resolution — *one per mesh*; and every node is still on the old
|
|
bus with nothing routed to the new one. **That last check is the point of the step**: adoption that
|
|
quietly carried traffic would be step 5 arriving early and unrehearsed.
|
|
|
|
## Step 3 — the protocol on NATS
|
|
|
|
**Why here.** The implementations cannot be written against an unwritten wire, and this is the step
|
|
that decides what "agreeing" means for everything after it. It is the largest step and the one that
|
|
pays for itself furthest away.
|
|
|
|
- [x] 3.1/3.3 **the fixtures** — one directory in the sdk, read by each implementation's own
|
|
runner rather than copied into either, because a fixture copied twice is two fixtures. The
|
|
Go emitter and the runtime's NATS client both pass the first: every required header set,
|
|
each value in the pinned shape, the subject derived the same way, and the payload the body
|
|
alone.
|
|
|
|
**The suite also had to settle what "byte-for-byte" can mean**, which ADR 0074 stated and
|
|
nothing had yet had to implement. The envelope is exact — subject, required headers, names
|
|
and formats — because that is what two implementations get wrong invisibly. The body is
|
|
not: Go sorts a map's keys and JavaScript keeps insertion order, so identical bytes would
|
|
commit every implementation to a canonical JSON encoder, to buy a property the mesh never
|
|
uses. Read strictly it would have sent somebody writing one.
|
|
|
|
Still to capture: a served tool call, a grant and its answer, and the contributions file —
|
|
the other three ADR 0074 names.
|
|
- [x] 3.2 design 19 rewritten from exchanges, queues and routing keys to the subjects and streams of
|
|
design 25 §2–§3, per capability, with ADR 0074's model untouched: floor plus capabilities, an
|
|
implementation legitimate when it claims less, identity from the sealed credential, dedup on
|
|
`x-event-id`. Claims checked against a running server are marked *verified* in the text, so
|
|
a reader can tell what was measured from what was reasoned. One limitation lifts with the
|
|
transport: a module may now call another's tool, which issue 049 recorded it could not.
|
|
- [x] 3.4 the controller's link on NATS — **both halves are through the seam, and the store
|
|
window is the server's.** `Bus` states the outbound in the mesh's words (publish an event,
|
|
declare to a node) and `Control` states the inbound (took it, dropped it, held it for the
|
|
store); each has an AMQP and a NATS implementation, and both ship, because steps 1 to 4
|
|
leave every node on AMQP and both shipping is what holds them to one envelope.
|
|
|
|
The outbound seam turned out to be eight call sites; the inbound was the larger half, and
|
|
the reason: every handler took the transport's own delivery type, so the loop could not move
|
|
without moving enrolment, reports, builds, upgrades and catch-up with it in one breath.
|
|
|
|
**The window (ADR 0083) is now what decides, once, for both.** On the bus the mesh has,
|
|
holding a message means an unacknowledged delivery kept in the controller, bounded by the
|
|
prefetch and lost if it stops. On the bus being built it is a `nak` with a delay: the
|
|
message stays the server's and the controller keeps only the moment it first could not take
|
|
it, so one that restarts mid-window has nothing to lose. Seven claims about that were asked
|
|
of a running server rather than reasoned — a report heard and gone from the work queue, one
|
|
held through a store outage and recorded when it returned, one let go once the bound passed,
|
|
a superseded one settled without being acted on, a heartbeat heard and nothing persisted,
|
|
both followed events acknowledged on a stream the controller had no ack subject for, and the
|
|
enrolment answer arriving at the address the request carried in its payload.
|
|
|
|
**Three things the wiring forced into the open.**
|
|
|
|
*Supersession is asked before the store, not after.* A report about a declaration the mesh
|
|
has moved past would otherwise wait out a restarting store to be written, and then overwrite
|
|
what the node is doing now.
|
|
|
|
*Half of a report is not about a declaration, and that half is never stale.* What the machine
|
|
**is** — the tunnel it took over, the ports its own bundle holds, what an adopted node found,
|
|
a node moving its overlay key — reaches the mesh on a report and nowhere else. A rekey set
|
|
aside as stale is a node whose overlay key never moves, and no retry is coming, because the
|
|
node said it once. So staleness is asked only of a report that is purely an apply's account.
|
|
|
|
*The controller could not have consumed a module event at all.* Its account granted no event
|
|
subject to subscribe and no ack subject on the events stream, so every announcement would
|
|
have been redelivered for ever, refused by the permission list it already had. Both are now
|
|
granted, each subject named rather than by pattern — a controller subscribing every event in
|
|
the mesh is a permission list that has stopped saying what it is for. Its consumers are
|
|
**named beside the mesh's own streams rather than derived**, because the controller files no
|
|
manifest and authority cannot come from a declaration that does not exist.
|
|
|
|
Still outstanding: a build's own shape, which travels with the builder in step 4.
|
|
- [x] 3.5 the host's link on NATS — **all three halves are through seams**, mirroring the
|
|
controller's and still importing nothing of the mesh's own (ADR 0005): the host's own
|
|
interfaces over its own libraries, agreeing with the controller only because a fixture holds
|
|
both to one envelope. A report goes through JetStream because it is the message the
|
|
store-window guarantee is about; a heartbeat stays on core, because a heartbeat in a stream is
|
|
the mesh's least valuable message competing for retention with its most valuable.
|
|
|
|
`Link` is dialling, hearing and saying in one interface, because **dialling is where the
|
|
transport is chosen** and choosing it twice is how one half of a node ends up on a different
|
|
bus from the other. `Asking` is the enrolment conversation, and it is separate for the
|
|
opposite reason: almost nothing about it is the same, and a node that fails there is not in
|
|
the mesh at all.
|
|
|
|
**What the new bus took away, and what it would not give.** A host declares nothing here: on
|
|
the old bus it declares its own queue, because a queue that is not there means a node that
|
|
hears nothing, but the object it reads through now is a durable consumer and a host's account
|
|
reaches no part of the JetStream API. So it **binds** to one the mesh made, and a missing one
|
|
is said as the mesh's to answer rather than quietly created with whatever the client defaults
|
|
to. Two things that had to be built for that: a node's declaration consumer (named after the
|
|
node, because its ack grant is derived from the node's name, so any other name is a delivery
|
|
it cannot acknowledge), and the enrolment user's **inbox** — design 25 §6 names it and the
|
|
composer granted none, so an enrolling node would have published its request and waited out
|
|
its timeout against a mesh that answered.
|
|
|
|
**The reply address travels in the payload, and that is now proved from both ends.** The
|
|
controller reads it from there (3.4) and the host writes it there and waits on it, and the
|
|
test asserts the transport's own reply field held the *consumer's ack address* by the time the
|
|
request arrived — so a future server that stopped claiming that field fails a test rather than
|
|
letting the reason quietly become folklore.
|
|
|
|
The host's **"newest wins" window narrows at the rollout rather than disappearing**, and that
|
|
is now measured rather than predicted: three declarations pushed to an absent node leave one
|
|
on the stream and it is the newest, so the catch-up half is the stream's — but three pushes to
|
|
a connected node are still three deliveries, which is the half that stays.
|
|
|
|
**The pin turned out easier here than in the tool runtime, not harder.** The Go client takes a
|
|
`*tls.Config`, so the same pinned configuration with the same verify callback does the work;
|
|
the subject-alternative-name constraint recorded under 3.6 is that client's, because it takes
|
|
PEM strings with no verify hook. A host checks the fingerprint and nothing else.
|
|
|
|
Nothing here composes an enrolment user per live token, and that is **1.7's**, not this
|
|
task's: it is one input to a composition that does not happen at all yet.
|
|
- [x] 3.6 the tool runtime's client on NATS, behind the unchanged sdk contract — round-tripped
|
|
against a real server: 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. Ships beside the AMQP client and is selected at the rollout,
|
|
because steps 1 to 4 leave every node on AMQP.
|
|
|
|
**A constraint it surfaced, recorded where somebody issuing a certificate will look.** The
|
|
AMQP client pinned the exact certificate and switched hostname verification off, which is
|
|
sound because a fingerprint is stronger than a name. The NATS client exposes no equivalent
|
|
hook — its TLS options are PEM strings with no verify callback — so the pin still happens
|
|
before dialling and the library's own name check happens beside it. **The bus's certificate
|
|
must carry a subject-alternative name matching the address nodes dial it by**, or the
|
|
connection is refused by a library error rather than by anything the mesh says.
|
|
- [x] 3.7 the sdk's three stale comments, and nothing else in it — three lines, which is the
|
|
whole of the sdk's diff for the bus change, and the measurement that predicted it
|
|
- [x] 3.8 **the declaration model** of [design 29](29-what-a-module-declares.md): local names
|
|
derived to subjects, the three namespaces, permissions computed from a declaration, and a
|
|
manifest that contains no subject. Done in the controller's composer (permissions, streams,
|
|
consumers), in the runtime's client (subjects derived from the credential, never named by a
|
|
module), and as a catalogue test asserting all 72 manifests hold no subject — because the
|
|
rule held by construction, and a rule held by construction is one a later field breaks
|
|
quietly.
|
|
- [x] 3.9 **seats declared by modules** — the manifest now carries `seats` (name, scope,
|
|
accepts/emits/serves, retention) and `uses`, and registration refuses a `mesh-*` name, a
|
|
duplicate declarer, an undeclared `uses` or claim, a seat with no protocol, a scope
|
|
mismatch, and a holder that does not answer what its seat promises. **Still to do**:
|
|
creating a seat's streams at registration and its holder's work-queue consumer at
|
|
assignment, which need the JetStream client wired in.
|
|
|
|
The refusal for an unknown claim *moved* rather than disappeared — the parser cannot judge
|
|
it from one manifest any more, because another module may legitimately declare that seat,
|
|
so it is registration's. The test that encoded the old rule was rewritten rather than
|
|
deleted, and a second one pins the case the parser could not distinguish.
|
|
|
|
**Done**: a seat's work queue is derived and created, and a holder's worker with it. The
|
|
JetStream client behind them is wired and verified against a running server, which also
|
|
completes 1.4's missing half — the pure `Asserter` had no implementation until now.
|
|
- [x] 3.10 **the ten seat renames** — done in the controller's table, the ten manifests that
|
|
claim them, the controller's own shipped manifests, and every test. Not a migration after
|
|
all: a holding is derived at resolution, never stored, so nothing recorded points at an old
|
|
name (recorded as a progressive insight on ADR 0118). A **kept** rename table tells a
|
|
manifest written against an old name what it became, because a module lives in its own
|
|
repository and may be registered long after the catalogue stopped using one.
|
|
|
|
**A seat and the interface it delivers are different names.** The `git` seat became
|
|
`mesh-git` while the `git` *provision* it delivers did not change, and the same for the
|
|
package registry. A blanket replace got this wrong first and the failure read "the package
|
|
registry is served on `<nil>`", which does not say "you renamed an interface" — so a test
|
|
now pins every seat against the interface it delivers.
|
|
|
|
**Done when.** The fixtures are produced and consumed byte for byte by every implementation that
|
|
claims the capability, and a module built before any of this serves its tools unchanged on the new
|
|
runtime. **The step is not done when the code runs** — two implementations that disagree about an
|
|
envelope do not fail to compile, they ignore each other while both keep running, which is the
|
|
failure ADR 0074 exists to catch.
|
|
|
|
**And the shared library gained nothing but the binding.** A new transport is when the pressure to
|
|
add conveniences is highest, and ADR 0039's rule does not bend for it: a helper that arrives with
|
|
the bus is a review failure, not a detail. Code shared among a module's own features stays in that
|
|
module.
|
|
|
|
## Step 4 — the core speaks it
|
|
|
|
**Why here.** The links exist from step 3, so the flows that are not on the bus at all can move onto
|
|
it, and the beds that need a mesh living on NATS can finally run.
|
|
|
|
> **A blocker surfaced here that is not this step's to fix.** Every event name in the catalogue is
|
|
> still written the way a routing key on the bus the mesh has is written, so the derivation design 29
|
|
> §1 specifies turns a consumer's declaration into a subject **no emitter publishes** — thirty-seven
|
|
> manifests, and one that cannot be composed at all. Nothing fails on the bus the mesh runs on
|
|
> today, where a routing key is matched literally; it fails on the first mesh raised on the new bus
|
|
> and not before, which is why wiring the controller's own subscription is what found it. Opened as
|
|
> [issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md).
|
|
> It holds 4.2, 4.3 and the catch-up half of 4.5; the node-facing flows — enrolment, reports,
|
|
> heartbeats, a build's outcome — are unaffected, because those subjects are the mesh's own and
|
|
> derive from nothing a module declares.
|
|
|
|
- [ ] 4.1 **the full genesis bed** — a mesh raised on NATS from nothing and living on it: a node
|
|
enrols over TLS with a claimed token and the enrolment user cannot read a declaration; a push
|
|
is held while the store restarts and applies after, nothing lost or duplicated; a node that
|
|
was away gets exactly the newest declaration and refuses a replayed older one by sequence; an
|
|
upgrade rolls out to two nodes; an event dead-letters after `max-deliver`; and an enrolment
|
|
held by a `nak`-with-delay cycle still reaches the enrolling node, proving the reply travels
|
|
in the payload and not the transport field the consumer's ack has claimed. The server-enforced
|
|
permissions were proved at step 1 and are not re-proved here
|
|
— **waiting on 1.7, the composition.** Both links speak NATS and every claim above has a unit
|
|
test or a check against a running server behind it; what none of them needs is a bus that
|
|
composed its own accounts, because each supplies its own. A mesh raising itself does need one
|
|
- [ ] 4.2 a build source's change reaches the builder over the bus, and the build that follows is
|
|
the one the change asked for — **blocked by
|
|
[issue 127](../../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)**
|
|
- [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces —
|
|
**blocked by the same**
|
|
- [~] 4.4 a person's client — **the account is done**: a person is not a module and holds no
|
|
seat, so their authority is a list of tools (or `*` for an administrator) and nothing else.
|
|
Held to four properties, each a way of being wrong that would not announce itself: nothing
|
|
but tools, so a person cannot claim a module said something; no ack subject, because
|
|
authority over a consumer that does not exist is authority nobody audits; no ability to
|
|
answer, because a person who can answer a request is impersonating a module on a bus where
|
|
anyone may serve a tool; and two people do not share an inbox.
|
|
|
|
Still to build: the client program itself — the command line and the MCP surface over it.
|
|
It needs nothing from the consume side, so it is not blocked by step 3.
|
|
- [~] 4.5 reports and catch-up: a node that was unreachable catches up rather than losing them —
|
|
**the reports half is in and proved against a server** (3.4): held through the store's
|
|
absence by the server rather than by the controller, superseded ones settled by the digest
|
|
they carry. The catch-up half is where issue 127 bites hardest: the controller replays a
|
|
build announcement under its **own** name rather than the builder's, so a catalogue
|
|
filtering the builder's subject hears nothing. Whether the controller may sign an event as
|
|
another module is a design question, not a wiring one, and it is open in that issue.
|
|
|
|
**Done when.** Each converted flow is proved against the behaviour it replaced, and the full genesis
|
|
bed is green. **Observation is not in this step** — heartbeats, conditions and key-value state are
|
|
[research 017](../../01-RESEARCH/017-a-mesh-that-heals-itself/00-overview.md)'s, that effort already
|
|
reserves them for after the move, and a flow built ahead of its design would be rebuilt.
|
|
|
|
## Step 5 — the rollout
|
|
|
|
**Why here.** It is the only step that moves a node's bus, and it moves every node's at once.
|
|
|
|
- [ ] 5.1 the cutover bed: a mesh on AMQP with a predecessor stand-in on the deprecated broker
|
|
moves its bus in one rollout, every node reporting on NATS afterwards, the stand-in's own
|
|
client still connected throughout
|
|
- [ ] 5.2 the rollout: accounts composed, then the controller, every host and every runtime
|
|
together; every node confirmed heard before AMQP stops
|
|
- [ ] 5.3 the mesh's own accounts removed from the deprecated broker: after the rollout nothing
|
|
of the mesh speaks to it, and an account nothing uses is one nobody rotates
|
|
|
|
> **5.4 is gone, and was wrong from ADR 0119 onward.** It read "the deprecated broker retires
|
|
> when its condition holds — no client connected for the period the operator sets", which is
|
|
> ADR 0106's framing of it as a compatibility module with an end date.
|
|
> [ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) settled that it is an
|
|
> **ordinary provider** of the `amqp` provision, like any module answering a backing service:
|
|
> no seat, not foundation, and **no retirement condition**, because the day its last client
|
|
> disappears is not a day anything is waiting for. Removed rather than reworded — a step that
|
|
> waits for a condition nobody set would sit open forever.
|
|
|
|
**Done when.** Every node reports on NATS, and nothing of the mesh's own is left connected to the
|
|
deprecated broker.
|
|
|
|
## The through-line
|
|
|
|
The order is dependency, not preference. **Steps 1 to 4 leave every node on AMQP**, so the cost of
|
|
being wrong is bounded until the last step: a step may be abandoned, or reordered after step 2,
|
|
without a rollback. The server stands before anything speaks to it; the wire is specified before it
|
|
is implemented three times; the flows move once there is something to move them onto; and the bus
|
|
itself moves once, at the end, on one day.
|
|
|
|
## What is deliberately not here
|
|
|
|
- **Observation** — research 017's, after the move, by its own design.
|
|
- **Leaf nodes** — design 25 §11 keeps this out of scope and says so; a leaf per machine is a later
|
|
question, noted so it is not forgotten.
|
|
- **The predecessor's world.** It is AMQP, it cannot move, and it does not need to: its broker is
|
|
the compatibility module until its last client is gone.
|
|
|
|
## How this list is kept true
|
|
|
|
A task is ticked when its change is committed, not when it is written. A step is done when its bed
|
|
is green, not when its tasks are ticked. If a step's tasks are all ticked and its bed has not run,
|
|
the step is **in progress** and this document says so — that gap is the thing the whole shape is
|
|
built to make visible.
|