Merge main: renumber this branch's records around the trunk's

Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
This commit is contained in:
2026-09-27 18:23:41 +02:00
36 changed files with 1489 additions and 104 deletions
+8 -1
View File
@@ -7,11 +7,12 @@ code:
- mesh-controller internal/identity/authority.go
- mesh-host internal/identity/serving.go
- mesh-host internal/apply (the service that reflects a rule set)
updated: 2026-09-25
updated: 2026-09-27
decisions:
- 02-DECISIONS/0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0105-the-mesh-adopts-the-predecessors-tunnel-in-place.md
- 02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md
- 02-DECISIONS/0108-a-route-carries-the-policy-applied-to-a-request.md
- 02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md
- 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md
@@ -768,6 +769,12 @@ where a found tunnel is left running beside the mesh's; where it is adopted ther
The guard admits the mesh's ports from that one interface, and the predecessor's peers arrive on
it.
*2026-09-27, [ADR 0127](../../02-DECISIONS/0119-a-taken-tunnels-predecessor-is-retired.md).* The
found configuration is kept only until the take is proven — the found unit down, the mesh's
interface up and handshaking with a peer. Then it is removed from where the found unit reads it
(its original stays kept), the hold ends, and the predecessor's tunnel cannot be raised again by
anything but a person restoring it by hand. Undeclaring the private network does not bring it back.
## The bus is NATS
*2026-09-23, [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md). Architecture to be written
+2 -2
View File
@@ -9,7 +9,7 @@ updated: 2026-09-26
decisions:
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
@@ -78,7 +78,7 @@ which is what the identity rule below exists to prevent.
The credential itself is fetched, never carried in a declaration: a declaration is persisted as
state and a sealed secret in a stream is an archive rather than a moment
([design 29](29-what-a-module-declares.md) §10).
([design 29](32-what-a-module-declares.md) §10).
### Connecting
+2 -2
View File
@@ -6,7 +6,7 @@ updated: 2026-09-25
decisions:
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
---
# 23 — Choosing a provider
@@ -54,7 +54,7 @@ to that provider and not to whichever one is nearest.
**A seat names the mesh's one provider of a kind.** Where a seat delivers the provision, its holder
answers for it when several providers exist and the consumer named none. That is not picking: the
choice was made once, mesh-wide, by assigning the holder, rather than once per consumer by naming it
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)),
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)),
[26 — The seats](26-the-seats.md)). A named provider still wins over the seat, because a consumer
coupled to particular contents has said so.
+11 -11
View File
@@ -10,7 +10,7 @@ code:
updated: 2026-09-27
decisions:
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
- 02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md
@@ -18,8 +18,8 @@ decisions:
- 02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- 02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md
- 02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0122-the-predecessor-is-ending-and-its-broker-goes-with-it.md
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md
---
# 25. The bus on NATS
@@ -49,7 +49,7 @@ mesh's own state lives, and where what a module may say is decided by what it de
The last two rows are the ones worth dwelling on, because they are not messaging in the sense of
carrying bytes from A to B. **A role is addressable**, so a caller names the capability and never
the module or the node — and the implementation can be replaced under it without a caller
changing ([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md)). That is a
changing ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). That is a
property of the mesh's architecture that happens to be expressed in subjects.
And more of the mesh lands here as it is built: conditions and observed state in key-value
@@ -60,7 +60,7 @@ is a message being moved; all of it is the bus being the mesh's centre.
**What a module sees of it is small and derived.** It declares what it emits, consumes, serves
and uses, and the subjects, streams, consumers and permissions all follow from that
([design 29](29-what-a-module-declares.md)). The sdk's contract — `request`, `handle`, `publish`,
([design 29](32-what-a-module-declares.md)). The sdk's contract — `request`, `handle`, `publish`,
`subscribe`, `close` — is the whole surface, and it does not change
([ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md)).
@@ -82,7 +82,7 @@ mesh.seat.<seat>.tool.<verb> a role's tool (core request/rep
mesh.ask.<node>.<command> the controller's command api (core request/reply)
```
**Revised 2026-09-27** ([ADR 0121](../../02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md)):
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
holders, which is what a build queue shared by several machines *is*. Keeping a second mechanism for it
@@ -91,7 +91,7 @@ seat's own event — which is why the control branch loses its copy too: one pub
asked, the controller that records it and the catalogue that places it — the fan-out a shared exchange gave for free, as a derived subject rather than a
configured topology.
**Revised 2026-09-26** ([design 29](29-what-a-module-declares.md)): a module's events and tools
**Revised 2026-09-26** ([design 29](32-what-a-module-declares.md)): a module's events and tools
moved from `mesh.events.*` / `mesh.tools.*` into one namespace per module, `mesh.mod.<module>.>`,
so a module's authority over its own name is a single subject pattern the server enforces — and
each carries a **kind token**, without which an events stream's filter would capture tool calls.
@@ -303,7 +303,7 @@ the private network. It is raised at genesis like the store, adopted as a module
phase.
**The deprecated broker is an ordinary module, not a compatibility layer.** Revision, second review
([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)): earlier text here, and
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)): earlier text here, and
ADR 0106 before it, called it `lavinmq-compat` — one purpose, the predecessor's clients, and a
retirement condition of no client connected for a period the operator sets. It is none of those.
A module may legitimately need an AMQP broker as a **backing service**, the way it needs a
@@ -312,11 +312,11 @@ foundation, never raised at genesis, installed when something wants it and absen
that does not. There is no retirement condition, because the day its last client disappears is
not a day anything is waiting for.
**Revised 2026-09-27** ([ADR 0122](../../02-DECISIONS/0122-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
**Revised 2026-09-27** ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
**that day is coming.** The predecessor is deprecated — some of it still running, none of it being
migrated, left to stop rather than moved — so the broker retires once nothing requires `amqp`. Still no
retirement *condition* and no end-date machinery: a provision with no consumers has its provider
unassigned, which is the ordinary mechanism and is ADR 0119 being paid off rather than revised. What
unassigned, which is the ordinary mechanism and is ADR 0127 being paid off rather than revised. What
also goes with it is the tooling that reaches this installation's machines remotely, because the
predecessor's own mesh talks over that broker — so the rollout is driven from the node, or before the
broker stops.
@@ -534,7 +534,7 @@ find what changed and why.
**Still open:**
- ~~Whether EVENTS should be one stream or one per emitting module.~~ **Closed**
([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md))): one stream, and not as a
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md))): one stream, and not as a
preference — the streams the bus is made of are composed as configuration before any module
runs, because a provisioner is itself a module that needs a bus account to start. Bootstrapping
decides it.
+20 -6
View File
@@ -8,12 +8,13 @@ code:
- mesh-controller cmd/mesh-controller/source.go
- mesh-controller internal/inventory/migrations/0032-a-source-may-live-on-a-seat.sql
- mesh-catalog modules/gitea/module.json
updated: 2026-09-26
updated: 2026-09-27
decisions:
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md
- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
---
# 26 — The seats
@@ -49,13 +50,26 @@ nobody argued for is an entry nobody can explain.
## The set
**The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), superseding
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), superseding
[ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)): a module
declares its own seats with their protocols, so the seats a mesh has are the mesh's own **plus
every registered module's**. The set is still closed — a seat named nowhere is refused — but it is
computed from the catalogue rather than maintained by hand, which is the property 0110 actually
needed and the table could not keep.
**And the mesh's own half is data, named for its scope.** Revision, 2026-09-27, reconciling two
records made in parallel: [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
names a system seat for the scope it is held at — `mesh-*` for one per mesh, `node-*` for one per
machine — and [ADR 0122](../../02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md) moves
the set out of compiled code into a table the controller owns, so a rename is one write rather than a
rebuild of everything that names one.
So the set has two halves and neither is written out here: the mesh's own, which the controller holds
as rows, and every registered module's, which is computed from the catalogue. What this document keeps
is what a seat *is* — the rest would be a third copy, stale the first time somebody renamed one, which
is the fault ADR 0122 exists about.
**Every seat below is named `mesh-*`, and the prefix is the reservation rule**: a module declaring
any `mesh-*` name is refused at registration, so there is no reserved-names list to drift. Ten of
these are renamed to restore [ADR 0079](../../02-DECISIONS/0079-the-foundation-seats-are-named-after-their-servers.md)'s
@@ -82,7 +96,7 @@ convention, which later seats departed from.
The controller holds **the mesh's own** entries in code, and a test asserts their size and that
every one names the record that made it a seat. A module's seats are not here and never will be —
they are read from the catalogue. **This table and
[ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) govern, and code that
[ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) govern, and code that
disagrees is what is wrong.** The implementation in progress predates several
things here: seats held by assignments rather than claimed by definitions, the `mesh-vault` seat and
its reservation, and the foundation's seats delivering nothing. It is brought to this table before it
@@ -171,7 +185,7 @@ still to take.
## How it is checked
The rules here are [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md)'s —
The rules here are [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)'s —
which supersedes [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
and keeps every rule below except how the set is formed — and
[ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)'s. Each is
@@ -8,7 +8,7 @@ decisions:
- 02-DECISIONS/0115-one-assignment-of-a-module-per-node.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md
- 02-DECISIONS/0084-which-provider-serves-a-consumer.md
- 02-DECISIONS/0046-a-module-configuration-is-its-assignments-not-its-manifest.md
@@ -64,7 +64,7 @@ Which module answers, in order:
1. **the holder of the seat the requirement names.** A requirement may name a seat instead of leaving
the provider open. It asks for *the mesh's* one, and the mesh answers with whichever assignment
holds that seat, with nothing asked of anyone. Unheld, the requirement is refused, naming the seat
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)),
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)),
[26 — The seats](26-the-seats.md)). Only a seat that delivers a provision can be named; naming a
foundation seat is refused, because it delivers nothing. A `secret` requirement always names
`mesh-vault`, because that provision is reserved;
@@ -199,7 +199,7 @@ containers, login, broker account and settings are keyed by it, as today, and a
tightest backend ([ADR 0049](../../02-DECISIONS/0049-a-consumers-identity-fits-the-tightest-backend.md)).
**A module may run on many nodes, and one assignment may hold a seat**
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md))). The definition
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md))). The definition
says which seats the module can hold; the assignment says which it does. So the store module can run
on every node, one of those assignments holds `mesh-store`, and moving that role changes an
assignment, not a definition.
+15 -15
View File
@@ -9,13 +9,13 @@ 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/0127-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0126-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
- 02-DECISIONS/0122-the-predecessor-is-ending-and-its-broker-goes-with-it.md
- 02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md
---
# 28. Building the bus
@@ -117,8 +117,8 @@ 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
> **Revised 2026-09-26** ([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md),
> [design 29](32-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
@@ -139,7 +139,7 @@ paper is wrong until there is a second mesh to find out.
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
[design 29](32-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
@@ -444,7 +444,7 @@ pays for itself furthest away.
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
- [x] 3.8 **the declaration model** of [design 29](32-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
@@ -469,7 +469,7 @@ pays for itself furthest away.
- [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
name (recorded as a progressive insight on ADR 0126). 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.
@@ -520,7 +520,7 @@ it, and the beds that need a mesh living on NATS can finally run.
is where the code stops and the lab starts
- [x] 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 — **a build is work submitted to a role now**
([ADR 0121](../../02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md)). Both sides
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)). Both sides
are behind a seam with an implementation per bus, and on the bus being built one publish does
what two did: the outcome is the role's own event, so the asker matches it by the id its
request carried, the controller records it and the catalogue places it in the graph. A build
@@ -652,7 +652,7 @@ reserves them for after the move, and a flow built ahead of its design would be
> **What this costs if it goes wrong, measured rather than assumed.** On the installation this is
> for, the old broker is also what a whole automation layer outside the mesh connects to — so it
> stays, as an ordinary provider of `amqp` ([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)),
> stays, as an ordinary provider of `amqp` ([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)),
> and this step is not its retirement. Nothing in a served request's path goes over the mesh's own
> bus: modules serve from their own containers. What a failed move costs is the mesh's ability to
> *change* anything — pushes, tool calls, new provisioning — until it is finished or undone. That
@@ -660,10 +660,10 @@ reserves them for after the move, and a flow built ahead of its design would be
> keep running" is a reasonable position rather than a gamble.
- [ ] 5.3 the mesh's own accounts removed from the deprecated broker, and then the broker itself:
after the rollout nothing of the mesh speaks to it, and an account nothing uses is one nobody
rotates. **It finishes now** ([ADR 0122](../../02-DECISIONS/0122-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
rotates. **It finishes now** ([ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)):
the predecessor is deprecated rather than kept, so once its remnants have stopped the module is
unassigned and the port is free. No retirement machinery — a provision with no consumers has its
provider unassigned, which is ADR 0119 being paid off rather than revised.
provider unassigned, which is ADR 0127 being paid off rather than revised.
Retiring with it: the build outcome's second announcement under the module's own name, which
exists only so a catalogue deployed before the rename and one deployed after both hear it.
@@ -673,10 +673,10 @@ reserves them for after the move, and a flow built ahead of its design would be
> The rollout has to be driven from the node, or driven before the broker stops — which is a
> sequencing constraint on 5.2 and not an afterthought.
> **5.4 is gone, and was wrong from ADR 0119 onward.** It read "the deprecated broker retires
> **5.4 is gone, and was wrong from ADR 0127 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
> [ADR 0127](../../02-DECISIONS/0127-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
@@ -699,7 +699,7 @@ itself moves once, at the end, on one day.
- **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 and it is not moving —
[ADR 0122](../../02-DECISIONS/0122-the-predecessor-is-ending-and-its-broker-goes-with-it.md): it is
[ADR 0130](../../02-DECISIONS/0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md): it is
deprecated, some of it is still running, and it is being left to stop rather than migrated. Its
broker goes with it, unassigned like any provider whose provision nothing requires.
@@ -0,0 +1,178 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
- 02-DECISIONS/0051-shared-data-is-the-operators.md
- 02-DECISIONS/0113-the-vault-makes-every-secret.md
- 02-DECISIONS/0117-a-machines-uplink-is-a-seat.md
---
# 29 — A node has operator accounts, and the mesh owns what lives under a home
**The mesh models machines but not the people on them.** A node record holds its name, its
address, its mode — and nothing about *who a person is* on it: `jochens` on novox, `ace` on ace,
`jochen` on shanks and g14. That username is not incidental. It decides who a file under `~` is
owned by, who a user service runs as, and — the case that surfaced this — which account `ssh
<node>` logs in as. The predecessor knew it (its per-node `user:`, and the modules that wrote a
person's `~/.ssh/config`, `~/.zshrc`, `~/.config`); the mesh, taking those over, kept the machine
facts and dropped the human one.
Several things are missing, and they are one idea.
## 1. The account is a node fact
A node has one or more **operator accounts**: the human logins on it. At minimum a name; the
mesh already knows the node and its address, so `<account>@<node>` is then a complete answer to
"who am I, where." It is the mesh's to hold because everything below is derived from it, and
because it is exactly the fact that was silently lost — `ssh ace` failed to `ace` because nothing
in the mesh said ace's account is `ace`.
## 2. A resource may live under a home, owned by its account
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) placed a
module's *system* data — `<root>/<module>`, owned by the module. It has no analog for the other
half of the filesystem: the things that belong under a person's home and are owned by that
person. `~/.ssh/config`, `~/.zshrc`, `~/.config/hal` — every one of these is a resource the mesh
should be able to place and own, resolved against **the account's home** rather than a system
root, and chowned to **the account** rather than to root or a module uid.
This is the same move as `${dir:…}`, one level over: a resource says `home: <account>` (or names
an account requirement), and the mesh resolves the home directory and the owning uid on the node
that account lives on. A module that writes operator config — the eventual replacements for
`hal/terminal`, `hal/claude-code`, `hal/secrets` — declares its files this way and names no
`/home/...` path, exactly as a system module now names no `/var/lib` path.
These are a **family**, not one module: an `ssh-client` module, a shell module, a `~/.config`
module, each a *universal-tier* consumer of the account fact — assigned wherever a person logs in,
which is every node, unlike the graphical stack that a capability gates.
## 3. The whole of `~/.ssh` is the mesh's — with one boundary drawn inside it
The predecessor owned a single file (`~/.ssh/config`) and left the rest alone; it drifted, because
owning one file beside foreign ones is not owning anything. The mesh should own **the directory**:
create `~/.ssh` at `0700`, chown it to the account, and own the files it places there —
- **`config`** (or the mesh's region of it): the `Host` blocks for every other node, composed
from the roster;
- **`known_hosts`**: authoritative, so the "Host key verification failed / accept-new" dance that
cost real time during enrolment simply ends;
- **`authorized_keys`**: who may log into this account, governed centrally rather than by whichever
key happened to be pasted where.
**The boundary — and it is the reason this is safe:** `~/.ssh` is the one directory where a wrong
declaration locks a person out of their own machine. So the mesh's *found-vs-owned* semantics
([ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md),
adoption) apply *inside* the home directory. The mesh **owns** the directory and the files above; it
**holds as found — never rewrites, never removes** — the operator's own contents: their **private
keys** and their **personal drop-ins** (`config.d/personal`, the personal `Host` aliases a
workstation carries, exactly as `hosts.local` is the home the mesh never rewrites for `/etc/hosts`).
Reconcile removing an unassigned `config.d/mesh` is fine; the same logic aimed at `id_ed25519` or an
operator's own `authorized_keys` entry is a lockout. This is the login-channel cousin of the rule
[ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md) draws for the uplink and the sshd
module draws for the firewall: **the mesh must never be able to arrange the one failure that severs
its own way back in.** The carve-out is not a convenience; it is that rule, in `~/.ssh`.
## 4. Keys are the mesh's to generate — through a CA, and existing keys are adopted, not replaced
Key *generation* is the mesh's, not each node's improvising its own. The clean form is an **SSH
certificate authority as a seat**, the sibling of the TLS internal CA the mesh already runs:
- **Host certs.** The mesh signs each node's host key. Every node's `known_hosts` becomes one line
— `@cert-authority *.<suffix> <mesh-CA-key>` — and nothing is distributed per node; a new node is
trusted the instant its host key is signed.
- **User certs.** The mesh signs a cert naming the principals (accounts) allowed. Every node's
`authorized_keys` / sshd `TrustedUserCAKeys` becomes one trust line — no N×N key spraying — and
short-lived certs give rotation for free
([ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)).
- The **CA private key is the mesh's**, a secret the vault makes
([ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md)).
**Three kinds of key, and only one is never minted.** Host keys (server identity) and pure
machine-to-machine keys the mesh may generate end to end. The operator's **personal** private key —
possibly on a hardware token, possibly used from an off-mesh laptop — the mesh **signs into a cert
but never generates**; that, and only that, is the residue of
[ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md). So "keys are mesh-owned" and
"the operator's login key is the operator's" reconcile: the mesh owns the CA and the signing; it
holds the human's private half, never mints it.
**Existing keys are not lost.** Taking ownership is *adoption*, not regeneration: a key already on a
machine is recorded and signed, not overwritten. The mesh gains authority over `~/.ssh` — it does
not clear it. An enrolling node's host key and the operator's existing key are carried forward; the
found-vs-owned boundary of §3 is exactly what guarantees nothing already there is destroyed.
## 5. How it is distributed: the controller composes, the node applies
None of this needs a node to discover the mesh, and none of it needs a control-plane module of its
own. The ssh files are **roster facts**
([ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md)): once the
roster view carries a node's **host key** and its **account** beside its name and address, the
`ssh-client` module ships a template for `known_hosts`, `config` and `authorized_keys`, and the
controller renders each node's copy from the full roster and pushes it. The mesh owns the data; the
module owns ssh's format; the control plane gains no ssh syntax. It is the same act as composing a
peer list or `/etc/hosts` — which is why there is **no novox-only "mesh-ssh" module**: the
centralization is the controller's composition, not a module that runs somewhere. Only non-secret
facts travel (names, addresses, accounts, host keys, the CA public key); the private key stays the
operator's, placed as an operator-owned file, referenced by path.
## 6. The two modules, and the seat between them
- **`sshd`** (server, every node) — manages sshd, owns and **reports** its host key so the roster
carries it, and trusts the user CA.
- **`ssh-client`** (client, every node) — owns `~/.ssh` per §3, consumes the roster and the CA
public key.
- **`the-ssh-ca`** (a seat, held on the control node) — signs host and user certs.
They meet at the account and the CA, not at a bespoke module. The `sshd` server side already exists;
the client/identity side and the CA are the open pieces.
## Why now, and why not yet
**Why it matters:** when HAL retires, the generators that keep `~/.ssh`, shell config and the
operator's `~/.config/hal` current retire with it. Without this, adding a node stops adding its ssh
alias and its trust, and a fresh machine has no operator dotfiles at all — the mesh would run every
service and leave the human unable to work on the box.
**Why not build it reflexively:** it is a real addition to the node model, the resource model, and
the seat set, and must be gotten right. The mechanism half is now settled — ADR 0128 is what lets
the ssh files be templates with no control-plane format — so what remains to decide here is the
model:
- **One account or several per node?** A workstation has one human; a shared box might have more.
Allow more than one without forcing the common case to name it.
- **The CA's shape.** Host-cert and user-cert principals, cert lifetime and renewal, where the CA
runs (a seat on the control node). The one thing fixed: the operator's personal key is signed,
never minted.
- **Adoption of existing keys.** How an enrolling node's host key and an operator's existing key are
recorded and signed rather than replaced — the found-vs-owned boundary, made concrete for keys.
- **The `sshd` boundary.** Server side exists; this is the client, the identity, and the CA.
- **The ssh-agent.** An agent is a *user-scoped service running as the account* — the first concrete
case of the user services §2 anticipates. It holds the operator's private key in memory; the mesh
declares the unit and sets `AddKeysToAgent`/`IdentityAgent` in `config`, and still never sees the
private half. Agent *forwarding* wants a policy, not a default: with user certs it is largely
unnecessary, and forwarding an agent into a node exposes the operator's keys to that node's root —
so prefer certificates and `ProxyJump` over forwarding.
**Not urgent, not blocking.** ssh and dotfiles work today because HAL's generators still run as the
substrate. This becomes load-bearing in the node-by-node retirement phase, not before — which is the
right time to build it, once the account and CA model are decided here.
## References
- The gap was found generating `~/.ssh/config` from the *HAL* registry (`hal/terminal`'s
postConfigure hook), which the nox mesh has no equivalent for.
- [ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the roster
fact mechanism that renders the ssh files, format owned by the module.
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — the
system-path placement this mirrors for home paths.
- [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) — why the operator's personal
key is signed, never minted.
- [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md) — the CA key is a secret the
vault makes; [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md)
— short-lived certs as rotation.
- [ADR 0125](../../02-DECISIONS/0117-a-machines-uplink-is-a-seat.md),
[ADR 0126](../../02-DECISIONS/0118-undeclaring-gives-a-unit-back-the-state-it-was-found-in.md) —
the never-sever-the-channel rule and the found-vs-owned semantics, applied here to `~/.ssh`.
@@ -0,0 +1,137 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
decisions:
- 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md
- 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md
---
# 30 — The mesh updates itself on a push
**Today the mesh does not update itself; a person drives the pipeline by hand, and one class of
change freezes it.** A code change lands in `mesh-controller` or `mesh-catalog`, and getting it onto
the machines is a sequence somebody types. The predecessor's pipelines rebuilt and redeployed on a
push without anyone watching; the successor should too. This records the process as it is done by
hand now — so it can be read, and then coded — and the two things that make it more than "add a
webhook".
## The process, as done by hand
**An ordinary (non-breaking) change** — new module code, a bug fix, a manifest tweak that changes no
seat or schema:
1. `module moved <module> <commit>` — tell the mesh its source advanced (the controller repo has no
trigger, so this is manual; the catalogue's webhook does it automatically — see below).
2. `build --behind` (or `build <repo> [--ref] [--path <subdir>]`) — the build machine rebuilds and
records the new image.
3. The mesh **reconciles on its own**: the module's declaration now names the new image, the next
push/heartbeat sends it, and the host swaps the container. For the control plane this is a
self-upgrade — the running controller composes its own new image and the host replaces it. No
restart is typed.
**A breaking change** — a manifest schema the controller parses differently (a fact's shape, a
seat's name), where the new control plane cannot read the manifests the old one stored:
4. Land the code (controller + catalogue together — they are one change).
5. Rebuild + deploy the new controller (steps 1–3). **The moment it is live it refuses the
still-old-shape stored manifests, and composition freezes for every node that runs an affected
module.** Running services are untouched; only new declarations stop.
6. **Re-register each affected manifest under the new shape**, which the *new* controller accepts —
`module add <file> -source <repo> -ref <ref> -commit <commit>`. This writes the manifest to the
store without a build, so it is the fast way to lift the freeze. (The controller container is
distroless: `docker cp` the file to the container root `/x.json`; `/tmp` does not exist; the
root filesystem is writable. The file is lost when the container is recreated on the next image
swap, so copy it *after* the swap.)
7. `push --behind`, then verify `status` is clean and `seats` (or the relevant surface) shows the
new shape held by the right holders.
The freeze in a breaking change has been paid three times in one session (a fact-shape change, the
`/etc/hosts` region, a seat rename); each time it lasted seconds and no service dropped. It is
recoverable, but it is not something a push should trigger unwatched — which is the crux of what
automating this must solve.
## Why it is more than "add a webhook"
### 1. The trigger today is HAL's, not the mesh's
Build-on-push works for the catalogue because its repository has a Gitea webhook pointing at
`http://host.docker.internal:9877/webhook/gitea` — and **that receiver is `hal-gitea-tools.service`**
(`~/.hal/modules/hal/gitea/tools/server.js`), a *predecessor* component. The nox builder consumes
build work; it does not receive Git events. So the mesh's own build pipeline currently rides on a
HAL service, and:
- the `mesh-controller` repository was never wired to it, which is why the control plane is the one
thing that does **not** self-update — every controller deploy this session was `module moved` +
`build` by hand;
- when HAL is retired, build-on-push stops for the whole mesh.
**The mesh needs its own forge-webhook→build trigger**, a nox component (a module, and likely a
seat — `mesh-forge-trigger` or folded into the git seat's holder) that receives Git events and turns
them into build work over the broker, for **every** repository including `mesh-controller`. Replacing
`hal-gitea-tools` is the concrete first build. Its logic already exists to copy: match the pushed
repository (and changed paths, for a monorepo like the catalogue) against the build-context
repository of every registered module, and rebuild the matches.
### 2. The builder validates too — and a breaking change deadlocks it
The build machine embeds the same catalogue package the controller does, so **it validates a
manifest against its own compiled-in seat/schema set**. A breaking change therefore couples *four*
things, not two: the controller, the **builder**, every affected manifest, and every node's host.
This session's seat rename rebuilt the controller but not the builder, and the stale builder then
refused every manifest claiming a renamed seat.
Worse, one rename **deadlocked** the builder: the build machine's own seat was renamed
(`the-build-machine` → `mesh-build-machine`). To refresh the builder you must build it; to build it
the *running* (old) builder must accept the new builder's manifest — which claims the new name it
does not know. The old builder cannot build the new builder. Escapes:
- **Never rename a seat whose holder validates manifests** in an ordinary pass — the build machine's
seat belongs with the deferred delivering seats (ADR 0121). Reverting `mesh-build-machine` to
`the-build-machine` (deferred) lets the old builder build the new builder, which then knows the
new names.
- Or bootstrap a new builder image **out of band** (build locally, publish to the registry, register
the module at that digest), the way genesis loads the first builder — bypassing the old builder's
validation once.
Either way, self-update for breaking changes needs a **transition discipline** so a push does not
auto-freeze: the new control plane (and builder) should accept the *old and new* shape together for
one release — deprecated aliases in the seat set, a schema that reads both — then a later release
drops the old. With that, a breaking change rolls out on a push like any other: everything reads
both, the manifests migrate, the compatibility is removed. Without it, self-update would simply
automate the freeze.
## What to build
- **A nox forge-webhook trigger** (replaces `hal-gitea-tools`): receives Git events for every mesh
repository, dispatches build work to the builder over the broker, and records `module moved`
automatically. Wire `mesh-controller` to it so the control plane self-updates like everything else.
- **A transition discipline for breaking changes**: the control plane and builder accept old+new for
one release; the tooling that lands a schema/seat change emits the compatibility shim and the
follow-up that removes it. This is what makes step 4–7 above safe to trigger unwatched.
- **Config/package modules need no builder** — `module add` registers their manifest directly
(this is how the uplink managers and the re-registrations above were done). Only image-bearing
modules need the build machine, which narrows what the deadlock above can block.
## Why now, and why not yet
**Why it matters:** self-update is the difference between a mesh a person maintains by typing
pipeline steps and one that maintains itself, and it is a stated goal (parity with the predecessor's
pipelines). The HAL trigger dependency also makes it a retirement blocker: build-on-push dies with
HAL.
**Why not reflexively:** the trigger is a new component with the broker and forge in its blast
radius, and the transition discipline changes how every breaking change is written. Both should be
designed, not bolted on beside a freeze. The manual process above is the interim, and it works.
## References
- [ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)
— the seat rename whose migration and builder deadlock this record is drawn from
- [ADR 0128](../../02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md) — the
fact-shape change that first showed the breaking-change freeze
- `hal-gitea-tools.service` (`~/.hal/modules/hal/gitea/tools/server.js`) — the predecessor webhook
receiver on `:9877` the mesh currently rides on
- mesh-controller `cmd/mesh-builder` (the build machine), `internal/catalogue` (the seat/schema
validation the builder shares with the controller)
@@ -0,0 +1,65 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-27
decisions:
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
---
# 31 — A module declares its fail2ban jail, and the mesh composes them per node
**A node's intrusion filter should be composed from the modules it runs, the same way its firewall
is.** The mesh already derives a node's nftables ruleset from every assigned module's `listens` and
`guards` (the `Filtering` mechanism). fail2ban is the same shape and is not modelled: a module that
runs an authenticating service — postgres, mssql, mailu — has a jail (a filter that reads its log
and a jail stanza that bans on it), and which jails a node's fail2ban runs should be exactly the
jails of the modules assigned to that node.
The predecessor did this with per-module files: `postgres` shipped `postgres-auth.conf`, `mssql`
shipped `mssql-auth.conf`, `mailu` shipped `mailu.conf`, and the node's fail2ban read whichever were
present. When HAL retired on novox those became dangling symlinks — fail2ban ran the jails only from
memory, and a restart would have dropped them. The base was salvaged (the fail2ban module now ships
`sshd`, `recidive`, and the `ignoreip` that spares the mesh's own range), but the **service jails
are gone**, because no nox module declares one yet.
## The shape
- **A module declares its jail in its manifest**, naming no node and no path (ADR 0112): the filter
(the failregex, or a stock filter it uses) and the jail stanza (port, logpath, maxretry, bantime).
The `postgres` module says what a postgres brute-force looks like and how to ban it; it does not
say on which machine, because it does not know.
- **The mesh composes them per node.** For each node, the jails of its assigned modules are gathered
and written into the fail2ban holder's `jail.d/` (and filters into `filter.d/`), exactly as
`listens`/`guards` are gathered into the node's firewall. So a node running postgres gets the
postgres jail; a node not running it does not. The `node-intrusion-prevention` holder receives
them the way a provider receives its consumers' contributions.
- **The base stays the fail2ban module's**: `sshd`, `recidive`, and the `ignoreip` naming
`${machine:mesh-range}` so a tunnel peer is never banned.
## Why this, and not the module writing the file itself
A module could declare a `file` resource at `/etc/fail2ban/jail.d/<x>.conf` directly. Rejected: the
path is the fail2ban holder's to own (one module owns `jail.d`, as one module owns the firewall
table), the jail's logpath and defaults want the mesh's composition (the `ignoreip`, the ban action
the node uses), and two modules writing into one directory is the collision the seat/holder model
exists to prevent. The module declares *what its jail is*; the holder's composition decides *how it
lands* — the same split as `listens` (the module says the port; the mesh says the rule).
## Why now
fail2ban on novox currently runs the service jails from memory only; the next restart drops them
(the `ignoreip` is safe on disk, so the mesh-partition risk is closed, but postgres/mssql/mailu
auth-banning would be lost). This is the mechanism that restores them properly, and it is needed as
each of those modules migrates to the other nodes — ace running postgres should get the postgres
jail, composed from the postgres module's manifest, without anyone editing a node.
## References
- [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) — a module
names no node or path; its jail is declared the same way its `listens` are
- mesh-controller `internal/catalogue/adoption.go` (`Filtering` — the firewall composition this
mirrors), `internal/catalogue/manifest.go` (`Listens`/`Guards`, the fields a jail field sits
beside)
- mesh-catalog `modules/fail2ban` (the base: sshd, recidive, ignoreip); the service modules
(`postgres`, `mssql`, `mailu`) that will declare jails
@@ -4,20 +4,20 @@ status: proposed
code: []
updated: 2026-09-27
decisions:
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0041-events-are-a-relationship.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- 02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
---
# 29. What a module declares, and what the bus makes of it
# 32. What a module declares, and what the bus makes of it
**A module that speaks to the mesh requires the bus, and receives what it needs to connect**
([ADR 0120](../../02-DECISIONS/0120-the-mesh-bus-is-required-not-ambient.md)). What a module
([ADR 0128](../../02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md)). What a module
declares are *relationships*; subjects, streams, consumers and permissions are all derived from
those, and a manifest never contains one.
@@ -132,7 +132,7 @@ and the controller stays the only writer of stream and consumer definitions
([design 25](25-the-bus-on-nats.md) §3).
**The mesh's own seats carry protocol too.** *Added 2026-09-27,
[ADR 0121](../../02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md).* A seat declared by a
[ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md).* A seat declared by a
module says what it accepts, emits and serves; the `mesh-*` set said only who does a job. So the mesh
had roles it could not describe — a build machine with three audiences for one outcome and no way to
derive a grant for any of them, and an event genuinely about a role with nowhere to live but the
@@ -219,7 +219,7 @@ That is the wire-level answer to
## 5. Seats
A module declares a seat with its protocol, and the mesh enforces one holder at its scope
([ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md)). A caller declares that
([ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md)). A caller declares that
it uses the *seat*, never the module, so the implementation can be replaced under it.
- The set of seats is **derived** — the mesh's own, plus every registered module's — so it is both
@@ -357,7 +357,7 @@ controller — because the bus's accounts are configuration rather than somethin
creates, so there is no provisioner process in the path and nothing waiting on a bus account in
order to make bus accounts. A module that runs a NATS server of its own and offers it as a
backing service provides **`nats`**, exactly as the deprecated broker provides `amqp`
([ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md)).
([ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md)).
They are never the same name. A manifest saying `nats` could otherwise mean either the mesh's
nervous system or a private queue, and the difference between those is the whole architecture.
@@ -427,7 +427,7 @@ it as an ordinary module once the registry exists.
So there are exactly two things the normal path cannot make, both at genesis, both ending the
moment the mesh can mint for itself: **the bus's own accounts** (§the bootstrap argument in
[ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)) — a provisioner is a module and
[ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)) — a provisioner is a module and
needs an account before it can run) and **the vault's own credential**. Any third exception is a
design failure, and naming these two is what makes a third one visible.
+4 -3
View File
@@ -35,11 +35,12 @@ document is written and this one's status becomes `implemented`.
| [`23-choosing-a-provider.md`](23-choosing-a-provider.md) | Which of several providers of a kind serves a consumer, and when a module carries its own instead | [ADR 0084](../../02-DECISIONS/0084-which-provider-serves-a-consumer.md), [ADR 0027](../../02-DECISIONS/0027-a-provision-names-what-the-consumer-is-coupled-to.md) |
| [`24-the-secrets-vault.md`](24-the-secrets-vault.md) | The module that owns a secret — a `secret` provision, and the boundary of what it owns | [ADR 0085](../../02-DECISIONS/0085-a-secret-is-a-provision.md), [ADR 0031](../../02-DECISIONS/0031-the-control-plane-authenticates-nobody.md), [ADR 0048](../../02-DECISIONS/0048-a-provider-creates-the-credential-the-mesh-minted.md) |
| [`25-the-bus-on-nats.md`](25-the-bus-on-nats.md) | **Proposed.** The architecture of the mesh's bus on NATS: what rides which subject under which guarantee and whose account, how a node joins, how a person reaches a tool, and how the mesh moves from the bus it has | [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0043](../../02-DECISIONS/0043-a-module-broker-account-is-scoped-by-emits-and-consumes.md), [ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md) |
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
| [`26-the-seats.md`](26-the-seats.md) | **Proposed.** What a mesh can have one of, who fills each, and a seat's holder answering for the provision it delivers — including the `git` seat a build's source can live on | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)), [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md), [ADR 0109](../../02-DECISIONS/0109-a-package-registry-seat-is-one-per-ecosystem.md) |
| [`27-a-module-requires-the-mesh-resolves.md`](27-a-module-requires-the-mesh-resolves.md) | **Proposed.** One concept for everything a module needs: a requirement with a contract, answered by one of four kinds of provider, resolved at assignment or refused. Retires settings, placeholders, facts and paths in definitions | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0113](../../02-DECISIONS/0113-the-vault-makes-every-secret.md), [ADR 0114](../../02-DECISIONS/0114-a-shared-credential-rotates-over-two-credentials.md), [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md) (superseding [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)) |
| [`28-building-the-bus.md`](28-building-the-bus.md) | **Proposed.** The five steps of the bus work in the order their dependencies allow, each ending at a bed — with the surface measured, so no step's size is a guess | [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md), [ADR 0106](../../02-DECISIONS/0106-the-bus-is-nats.md), [ADR 0074](../../02-DECISIONS/0074-the-wire-is-specified-not-the-types.md) |
| [`29-a-node-has-operator-accounts.md`](29-a-node-has-operator-accounts.md) | **Proposed.** The mesh models machines but not the humans on them: a node gains operator accounts, and a resource may live under a home owned by its account — what would own ~/.ssh, dotfiles and ~/.config when HAL retires | [ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md) |
| [`29-what-a-module-declares.md`](29-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0118](../../02-DECISIONS/0118-a-module-declares-its-own-seats.md), [ADR 0119](../../02-DECISIONS/0119-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
| [`32-what-a-module-declares.md`](32-what-a-module-declares.md) | **Proposed.** What a module declares and what the bus derives from it: three namespaces, subjects from local names, queues never declared, the five relationships, and the build-publish-deploy lifecycle on one bus | [ADR 0126](../../02-DECISIONS/0126-a-module-declares-its-own-seats.md), [ADR 0127](../../02-DECISIONS/0127-amqp-is-a-provision-not-the-bus.md) (superseding [ADR 0125](../../02-DECISIONS/0125-the-bus-is-the-only-broker.md)), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
## Not yet written