Modules declare their own seats; the mesh reserves mesh-*

The architecture 0117 opened needs a module to offer a service as a role on
the bus — one holder, addressed by what it does. A closed table in the
controller cannot express that: a capability a module contributes would
require changing the mesh itself.

But 0110 closed the set for a good reason — nothing could say what seats a
mesh had, and the hand count came out at eleven of thirteen. That argues for
enumerable, not hardcoded, and 0110 weighed free-form against a fixed table
without considering a third option: closed at any moment and derived from
the catalogue. A derived list cannot drift, which is how the count broke.

So: the mesh's seats stay the mesh's, reserved by the mesh- prefix so the
prefix is the rule and there is no list to maintain; ten seats are renamed
to restore 0079's convention; everything 0110 decided about what a seat IS
survives untouched.

Design 29 carries the declaration model: three namespaces, subjects derived
from local names so a manifest survives the wire changing, queues never
declared, five relationships (the job and state shapes 0041 had no room
for), and the build-publish-deploy lifecycle with hard, soft and build-time
dependencies distinguished.

0041 gets a progressive insight: "no per-consumer setup, only a
subscription" was a fact about a topic exchange, and a JetStream durable
consumer is a real object someone creates.

WBS 1.3/1.4 were wrong and say so: streams come at registration and
consumers at assignment, so only the foundation set belongs at genesis.
This commit is contained in:
2026-09-26 20:34:32 +02:00
parent b5b68e8852
commit 7b4916e9ec
12 changed files with 461 additions and 45 deletions
+9 -1
View File
@@ -331,7 +331,15 @@ def check_progressive_insights(failures, records):
if any(s <= m.start() and m.end() <= e for s, e in covered):
continue
line = text.rfind("\n", 0, m.start()) + 1
if text[line:m.start()].lstrip().startswith("#"):
end = text.find("\n", m.end())
whole = text[line:end if end != -1 else len(text)]
if whole.lstrip().startswith("#"):
continue
# A line that also carries a link is discussing the rule, not marking a correction:
# a marker never needs to cite anything, and a record that reasons about the policy
# must be able to name it. Bare prose with no citation is the informal marking this
# is here to catch.
if "](" in whole:
continue
if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)):
continue
+1 -1
View File
@@ -47,7 +47,7 @@ another — and a mesh you cannot name precisely is a mesh two people describe d
- **seat** — a named role at a scope (node / site / mesh), held by a module assignment, from a
**closed set** the mesh defines: a claim naming a seat outside the set is refused. A seat may
**deliver a provision**, and its holder is then the mesh's answer for it when several modules
provide it ([ADR 0110](../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)).
provide 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))).
The set, with who holds each seat, is the overview of what a mesh has
([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat has a **capacity**: a
capacity-1 seat is exclusive (one holder); a higher-capacity seat is a **bench** (several holders
@@ -76,6 +76,22 @@ three relationships, one broker, one runtime, all declared on the manifest.
- The runtime must dispatch a module's event handlers as well as its tools; that generalisation is
small (both arrive by importing the module's entrypoint) but it is real work.
## Progressive insight
> **Progressive insight — 2026-09-26.** *"No provisioner and no per-consumer setup" was a fact
> about the transport, and the transport changed.* This record's table says an event's machinery is
> "nothing but the broker's topic routing", and the text that an event needs "no per-consumer setup
> — only a subscription". That was true of a topic exchange, where a binding cost nothing and the
> broker fanned out. On NATS
> ([ADR 0106](0106-the-bus-is-nats.md)) a subscription is a **durable consumer**: a real object
> with a name, an ack policy, a delivery limit and its own ack subject, created when a module is
> assigned and removed when it is not. Per-consumer setup exists, and the controller does it.
>
> The decision is untouched — events are declared on both sides, 1:many, credential-free, and
> still provisioning's lighter sibling; the lightness is now relative rather than absolute.
> [ADR 0118](0118-a-module-declares-its-own-seats.md) adds the relationship this record's two
> columns had no room for: work addressed to a role, where exactly one holder must act.
## References
- [ADR 0002](0002-nodes-communicate-over-a-broker.md) — the broker events ride.
@@ -1,6 +1,7 @@
---
topic: what runs on it
status: accepted
status: superseded
superseded-by: 02-DECISIONS/0118-a-module-declares-its-own-seats.md
date: 2026-09-25
deciders: jochen
reconstructed: false
@@ -0,0 +1,130 @@
---
topic: the tiers
status: accepted
date: 2026-09-26
deciders: jochen
reconstructed: false
supersedes: 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
---
# 118. A module declares its own seats; the mesh reserves its own
## Context
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) closed the set of seats. Its
evidence was strong and still is: nothing could answer *which seats does this mesh have, and who
holds each*. Answering it meant reading every manifest in two repositories and then the
controller's own code, and when that enumeration was done by hand while writing the record, **it
reported eleven claims where there were thirteen.** The fix was a table in the controller, and
adding a seat became a decision.
What that table cannot express is the architecture [ADR 0117](0117-the-bus-is-the-only-broker.md)
opened. With one bus and no private brokers, a module offering a service to other modules offers
it as **a role on the bus**: a set of subjects, exactly one holder, addressed by what it does
rather than by which module or node provides it. A telegram sender, a licensing master, anything
a mesh might want one of. Under a closed table, adding any of those means editing the controller
— so a capability contributed by a module would require a change to the mesh itself, which is the
coupling the module system exists to prevent.
**The two requirements look opposed and are not.** 0110 needs the set *enumerable*. The
architecture needs it *extensible*. Those conflict only if enumerable means *written down in one
place by hand* — which is exactly the property that let the count drift in the first place.
## Considered Options
1. **Keep the closed table, add each new seat by decision.** Rejected. Every capability a module
contributes would need a change to the controller and a record before it could be offered, and
the mesh would carry the names of services it does not itself implement.
2. **Free-form seats, as before 0110.** Rejected for 0110's own reason, unchanged: nothing can
say what a mesh has, and a name invented at a claim site is a name nobody can explain later.
3. **A set that is closed at any moment and derived rather than maintained**, with the mesh's own
seats reserved by name. Adopted. 0110 weighed options 1 and 2 and never considered this one.
## Decision
**A seat may be declared by a module, and the set of seats a mesh has is derived: the mesh's own,
plus those declared by every module it has registered.** The set is still closed — a seat named
nowhere is refused — but it is computed from the catalogue rather than written in the controller.
Everything 0110 decided about what a seat *is* stands untouched: one holder at its scope; a
definition says which seats a module *can* hold and an assignment says which it *does*; holding
one may deliver a provision; a seat makes a role singular, never a module.
**Enumeration is a query, not an inventory.** The catalogue knows every registered manifest, so
"which seats does this mesh have, and who holds each" is answered by asking it. This is a
stronger answer than the table gave, not a weaker one: a derived list cannot drift from reality,
and drift is how the hand-made count came out at eleven of thirteen.
**The mesh's own seats are reserved by prefix.** Every seat the mesh itself defines is named
`mesh-*`, and a module declaring any `mesh-*` name is refused at registration. The prefix *is*
the reservation rule — no list of reserved names to maintain, and no way for the mesh's own
namespace to be colonised by a manifest. This requires renaming the seats that drifted from
[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention: `the-catalogue`
becomes `mesh-catalog`, `git` becomes `mesh-git`, and the node-scoped `the-build-machine`,
`the-dns-port`, `the-intrusion-prevention`, `the-packet-filter`, `the-private-network`,
`the-resolver-configuration`, `the-showcase` take the same prefix.
The mesh's seats stay the mesh's for a reason that does not apply to a module's: **the mesh's own
code looks them up by name.** The resolver *is* the thing that finds the store. `mesh-store` is
not a convention the controller follows, it is an identifier the controller dereferences.
**A declared seat carries a protocol.** A module declaring a seat says what may be sent to it,
what it emits, and what it serves. The holder must satisfy it; a module may not claim a seat whose
protocol it does not implement. Callers declare that they use the *seat*, never the module, so
replacing the implementation changes nothing for any caller.
**A seat is for a role; an event stays addressed to its emitter.** The two are not
interchangeable and the choice is not stylistic. An event is *this happened to me* — the emitter's
identity is the meaning, which is why the envelope carries source, node and time
([ADR 0042](0042-the-shape-of-an-event-on-the-wire.md)); routing it through a role would erase the
provenance an audit needs. A seat is *this capability, whoever provides it* — where not knowing
the holder is the point. Publish an event when the fact is about you; declare a seat when you are
offering something another module could offer instead.
**Two modules declaring the same seat name is refused at registration**, second one loses.
Registration is the last moment the mesh can still say no, and a seat name meaning two different
protocols is the failure nobody could diagnose afterwards.
## Consequences
- **The controller's seat table stops being the set** and becomes the mesh's own reserved entries.
Resolution reads the catalogue for the rest.
- **Ten seats are renamed.** A rename is a migration, not an edit: existing assignments hold the
old names, so the change carries a mapping and is applied once, and the lab beds that name seats
are updated with it.
- **A `uses` naming an undeclared seat is refused at registration**, which is where 0110's
guarantee lands under this model — the same refusal, at the same moment, from a derived set.
- **Adding a capability stops requiring a decision record.** That is a real loss of governance and
the intended trade: the argument for a seat's existence moves into the module that declares it,
where it is reviewed as part of the manifest. The mesh's own seats keep the old bar.
- **[ADR 0041](0041-events-are-a-relationship.md)'s machinery claim is already stale** for a
different reason, and is corrected in place there under the rule in
[`README.md`](README.md) — a progressive insight: on JetStream a subscription is a durable
consumer, a real object someone must create.
- **What got harder:** a seat's protocol is now a compatibility surface between modules that do
not know each other. Changing one breaks callers already bound to it, and nothing here says how
that is versioned. It is the first thing to answer in the design, and the thing most likely to
hurt later rather than now.
## How it is checked
- **The overview answers, and is right.** A command lists every seat, its scope, its protocol and
its holder, derived from the catalogue — and a test asserts the count against a fixture mesh,
because an enumeration nobody checks is how thirteen became eleven.
- **`mesh-*` is refused to a module.** A registration test: a manifest declaring `mesh-anything`
is refused, naming the prefix as the reason.
- **An undeclared seat is refused.** A registration test on `uses`, and a resolution test that
nothing reaches runtime unresolved.
- **A second declarer loses.** A registration test: two manifests, same seat name, the second
refused and the first untouched.
- **A holder must satisfy the protocol.** A claim whose module does not serve what the seat
declares is refused at assignment, not discovered when a caller times out.
## References
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — superseded here; its
requirement is kept and only its mechanism replaced.
- [ADR 0117](0117-the-bus-is-the-only-broker.md) — one bus, which is what makes a role addressable.
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the naming convention
the reserved prefix restores.
- [ADR 0041](0041-events-are-a-relationship.md) — the event half of the boundary drawn here.
+2 -1
View File
@@ -164,6 +164,7 @@ python3 00-META/checks/index.py fail if stale
- **0098** — [A fact a provider makes at first start is fetched from it, not carried in its manifest](0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md)
- **0108** — [A route carries the policy applied to a request, and names a secret rather than holding one](0108-a-route-carries-the-policy-applied-to-a-request.md)
- **0109** — [A package registry seat is one per ecosystem, not one for all of them](0109-a-package-registry-seat-is-one-per-ecosystem.md)
- **0118** — [A module declares its own seats; the mesh reserves its own](0118-a-module-declares-its-own-seats.md)
### What runs on them, and how it gets there
@@ -195,7 +196,7 @@ python3 00-META/checks/index.py fail if stale
- **0087** — [A seeded file is created once, and what grows in it is not the mesh's](0087-a-seeded-file-is-created-once.md)
- **0091** — [A mount is declared, and there are three things it can be](0091-a-mount-is-declared-three-ways.md)
- **0099** — [A step that runs once names what it reads, and runs again when it changed](0099-a-step-that-runs-once-names-what-it-reads.md)
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
- **0110** — [A seat is held by one assignment, from a closed set, and it may deliver a provision](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) *(superseded)*
- **0112** — [A module definition names no node, no mesh and no path: everything it needs is a requirement the mesh resolves](0112-a-module-definition-names-no-node-mesh-or-path.md) *(proposed)*
- **0113** — [The vault makes every shared secret, a provider makes resources and data, and the mesh carries both](0113-the-vault-makes-every-secret.md) *(proposed)*
- **0114** — [A credential two parties hold rotates over two credentials; one a single party holds rotates in place, staged; and retiring a credential never removes what it reached](0114-a-shared-credential-rotates-over-two-credentials.md) *(proposed)*
+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/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0118-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 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.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](26-the-seats.md)). A named provider still wins over the seat, because a consumer
coupled to particular contents has said so.
+50 -29
View File
@@ -10,7 +10,7 @@ code:
- mesh-catalog modules/gitea/module.json
updated: 2026-09-26
decisions:
- 02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0118-a-module-declares-its-own-seats.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
---
@@ -47,27 +47,42 @@ nobody argued for is an entry nobody can explain.
## The set
| seat | scope | delivers | typically held by |
|---|---|---|---|
| `mesh-controller` | mesh | — | the controller |
| `mesh-store` | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | mesh | — | the broker carrying the mesh's own bus |
| `mesh-vault` | mesh | `secret`, reserved | the vault |
| `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `the-catalogue` | mesh | — | the catalogue |
| `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `git` | mesh | `git` | the forge |
| `the-build-machine` | node | — | a builder |
| `the-dns-port` | node | — | the local resolver |
| `the-intrusion-prevention` | node | — | an intrusion-prevention service |
| `the-packet-filter` | node | — | the packet filter |
| `the-private-network` | node | — | the private network the mesh runs over |
| `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
| `the-showcase` | node | — | the showcase module |
**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 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.
The controller holds this set in code, and a test asserts both its size and that every entry names
the record that made it a seat. **This table and [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)
govern, and code that disagrees is what is wrong.** The implementation in progress predates several
**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
convention, which later seats departed from.
| seat | was | scope | delivers | typically held by |
|---|---|---|---|
| `mesh-controller` | — | mesh | — | the controller |
| `mesh-store` | — | mesh | — | the store the mesh's own records live in |
| `mesh-broker` | — | mesh | — | the broker carrying the mesh's own bus |
| `mesh-vault` | — | mesh | `secret`, reserved | the vault |
| `mesh-artifact-store` | `the-artifact-store` | mesh | `artifact-store` | the artifact registry |
| `mesh-catalog` | `the-catalogue` | mesh | — | the catalogue |
| `mesh-npm-package-registry` | `npm-package-registry` | mesh | `npm-package-registry` | the forge |
| `mesh-git` | `git` | mesh | `git` | the forge |
| `mesh-build-machine` | `the-build-machine` | node | — | a builder |
| `mesh-dns-port` | `the-dns-port` | node | — | the local resolver |
| `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service |
| `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter |
| `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |
| `mesh-resolver-configuration` | `the-resolver-configuration` | node | — | whichever of the alternative resolver configurations is chosen |
| `mesh-showcase` | `the-showcase` | node | — | the showcase module |
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
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
merges.
@@ -155,16 +170,22 @@ still to take.
## How it is checked
The rules here are [ADR 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)'s
and [ADR 0111](../../02-DECISIONS/0111-a-build-source-is-on-the-git-seat-or-external.md)'s, and each is
The rules here are [ADR 0118](../../02-DECISIONS/0118-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
checked as their tables say:
| Rule | Checked by |
|---|---|
| The set is closed, and every entry names its decision | 0110: a unit test on the set's size and decisions; manifest tests refusing an unknown seat or the wrong scope. |
| A seat is held by one assignment, and only by one whose module can hold it | 0110: resolution tests for a second holder and for a seat the definition does not name. |
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0110: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
| Several providers and none local is a person's choice | 0110: an assignment test listing candidates with the seat's holder first and recording the pin. |
| `secret` is reserved | 0110: the parser and resolution refusals for another provider and a pin. |
| Holdings are derived, and the overview lists every seat | 0110: the `seats` command test, including an unheld seat. |
| The set is closed, and every mesh entry names its decision | 0118: a unit test on the mesh's own entries; manifest tests refusing an unknown seat or the wrong scope. |
| The set is derived, and enumerating it is a query | 0118: the overview lists the mesh's own plus every registered module's, asserted against a fixture mesh. |
| `mesh-*` is the mesh's, and a module may not declare one | 0118: a registration test refusing a manifest that declares any `mesh-*` seat, naming the prefix. |
| Two modules cannot declare the same seat | 0118: a registration test; the second is refused and the first untouched. |
| A holder satisfies the seat's protocol | 0118: a claim whose module does not serve what the seat declares is refused at assignment. |
| A seat is held by one assignment, and only by one whose module can hold it | 0118: resolution tests for a second holder and for a seat the definition does not name. |
| A requirement naming a seat is answered by its holder; a foundation seat cannot be named | 0118: resolution tests with a second provider on the consumer's node, with the seat unheld, and naming `mesh-store`. |
| Several providers and none local is a person's choice | 0118: an assignment test listing candidates with the seat's holder first and recording the pin. |
| `secret` is reserved | 0118: the parser and resolution refusals for another provider and a pin. |
| Holdings are derived, and the overview lists every seat | 0118: the `seats` command test, including an unheld seat. |
| A build source on the seat records no address; an unheld seat refuses only self-hosted builds | 0111's tests. |
@@ -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/0110-a-seat-is-a-module-assignment-from-a-closed-set.md
- 02-DECISIONS/0118-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 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.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](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 0110](../../02-DECISIONS/0110-a-seat-is-a-module-assignment-from-a-closed-set.md)). The definition
([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
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.
+27 -5
View File
@@ -10,6 +10,7 @@ decisions:
- 02-DECISIONS/0116-the-bus-is-built-in-five-steps.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0117-the-bus-is-the-only-broker.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
@@ -115,6 +116,15 @@ 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
@@ -126,11 +136,14 @@ paper is wrong until there is a second mesh to find out.
- [ ] 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
- [ ] 1.3 the controller composes that file: accounts, permissions, TLS, JetStream — permissions
derived from `emits` and `consumes` and nothing else, plus each user's own ack subject and its
own inbox prefix (design 25 §4)
- [ ] 1.4 the four streams, created at genesis and asserted idempotently on start, by the controller
as their only writer
- [ ] 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)
- [ ] 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
- [ ] 1.5 genesis raises it as foundation, claiming the seat **`mesh-broker`** — the seat is the
server's role, not the product
- [ ] 1.6 the genesis-broker bed
@@ -183,6 +196,15 @@ pays for itself furthest away.
- [ ] 3.5 the host's link on NATS — mirroring, still importing nothing
- [ ] 3.6 the tool runtime's client on NATS, behind the unchanged sdk contract
- [ ] 3.7 the sdk's three stale comments, and nothing else in it
- [ ] 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
- [ ] 3.9 **seats declared by modules** — registration creates a seat's streams and refuses a
`mesh-*` name, a duplicate declarer, an undeclared `uses`, and a holder that does not
satisfy the protocol; assignment creates the holder's work-queue consumer and refuses a
second holder
- [ ] 3.10 **the ten seat renames**, carried as a migration with a mapping rather than an edit,
and the beds that name seats moved with them
**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
@@ -0,0 +1,215 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-09-26
decisions:
- 02-DECISIONS/0118-a-module-declares-its-own-seats.md
- 02-DECISIONS/0117-the-bus-is-the-only-broker.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
---
# 29. What a module declares, and what the bus makes of it
**The bus is ambient.** No module requires it, the way no module requires a filesystem. Every
module gets a connection and an identity whether it asks or not. What a module declares are
*relationships*; subjects, streams, consumers and permissions are all derived from those, and a
manifest never contains one.
This document is the declaration model. [Design 25](25-the-bus-on-nats.md) is the bus itself —
subjects, streams, accounts, enrolment — and stays the authority on the wire.
[Design 19](19-the-module-protocol.md) is the specification an SDK implements, and is rewritten
onto this in step 3 of [ADR 0116](../../02-DECISIONS/0116-the-bus-is-built-in-five-steps.md).
## 1. A module names locally; the mesh derives the subject
This is the load-bearing rule.
[ADR 0112](../../02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md) says a module
definition names no node, mesh or path. A transport address is the same class of thing: if
manifests held literal subjects, reorganising the subject space would mean editing every module in
the catalogue, and the mesh would have hundreds of copies of a decision it made once.
| declared | derived |
|---|---|
| `emits: order.placed` | publish on `mesh.mod.<module>.order.placed` |
| `consumes: billing.order.placed` | durable consumer on `mesh.mod.billing.order.placed` |
| `serves: status` | queue-group subscription on `mesh.mod.<module>.tool.status` |
| seat `telegram-sender`, `accepts: send` | work-queue consumer on `mesh.seat.telegram-sender.send` |
| `uses: telegram-sender` | publish on that seat's `accepts` subjects, and nothing else |
**The test this must pass: the manifest survives the wire changing.** Reorganise the subject space
and every manifest in the catalogue is still correct. That is the property
[ADR 0039](../../02-DECISIONS/0039-what-the-sdk-holds-and-refuses.md) gave the sdk, applied to
declarations.
## 2. Three namespaces, and nothing else
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
so an event's source is a fact the bus enforces rather than a claim in the body.
**Seats it holds** — `mesh.seat.<seat>.>`. Full participation: consume what the seat accepts,
publish what it emits, serve what it serves.
**Seats it uses** — publish only, and only on the `accepts` half. A sender cannot subscribe to a
seat's inbound subject and watch other modules' traffic, and cannot publish the seat's outbound
events and lie about outcomes.
A module naming anything outside these three is refused at registration. The whole permission set
is derivable from the declaration; nobody writes an access rule.
## 3. Queues are derived, never declared
A module says what it reacts to, not how delivery works. Each `consumes` becomes one durable
consumer; a seat's `accepts` becomes one work-queue consumer with a queue group named for the
seat. The module does not name them, does not know their names, and cannot misconfigure them —
and the controller stays the only writer of stream and consumer definitions
([design 25](25-the-bus-on-nats.md) §3).
**Retention belongs to whoever owns the namespace, not to a consumer.** A seat declares how long
its inbound backlog survives, because that is a property of the service:
```
seat: telegram-sender
scope: mesh
accepts: send retain 7d
emits: delivered, failed
serves: status
```
If each consumer could tune it, the mesh's durability would be an emergent property of whichever
manifest was edited last.
**Scope gives per-node workers without a new concept.** A module running on three nodes that each
need their own queue declares a node-scoped seat: one holder per node, three queues, same
machinery. Mesh-scoped and node-scoped seats already exist; here they do the work of "one shared
service" versus "one worker per machine".
## 4. Five relationships
| | provision | event | job | state | tool |
|---|---|---|---|---|---|
| shape | 1:1 resource | 1:many | N:1 | 1:1 | 1:1 |
| addressed to | a provider | the emitter's own namespace | a **seat** | one node | a module or seat |
| who must act | the provider | nobody | exactly one holder | that node | the server |
| credential | sealed, per consumer | none | none | none | none |
| reply | — | none | none, or an event later | a report | awaited |
| retention | — | age and size | work queue, explicit ack | **last per subject** | none |
| declared | `provides`/`requires` | `emits`/`consumes` | seat `accepts` / `uses` | the mesh's own | `serves` |
**Job** is the one [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) had no room
for. Its table has events at 1:many and provisions at 1:1; a module submitting work to a service
is neither. It is not an event, because an event is a broadcast nobody is obliged to act on and a
second holder would do the work twice. It is not a provision, because there is no resource and no
credential. What makes it safe is not cleverness in the subscribe call but the seat: exactly one
holder, so exactly one worker, by construction.
**State** is the shape the deploy path needs and nothing else uses. A declaration is not an event
— replaying yesterday's is actively harmful — and not a job. Only the newest matters, which is
last-per-subject retention, and a node that has seen sequence *n* refuses *n−1* by construction.
That is the wire-level answer to
[issue 107](../../04-ISSUES/107-a-declaration-carries-no-order/00-report.md).
## 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
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
closed and extensible, and enumerating it is a query rather than an inventory.
- `mesh-*` is **reserved**: the prefix is the reservation rule, and a module declaring one is
refused at registration.
- Two modules declaring the same name: the second is refused.
- A module may not claim a seat whose protocol it does not implement.
- **Nobody holding a seat is not an error.** The stream exists from registration, so work queues
until a holder appears. Install the telegram module a week later and the backlog flushes.
## 6. The lifecycle: build, publish, deploy
Every shape above appears once, in order, and no step knows where the next one runs.
**A change lands.** The module holding `mesh-git` emits `pushed` — repository, ref, commit. An
event, because it is a fact about git and git's identity is the meaning.
**The change becomes work.** The controller consumes `pushed`, asks the catalogue which modules
are built from that repository and path
([ADR 0069](../../02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md)), and submits one
**job** per affected module to the `mesh-build-machine` seat. The builder stays simple: it builds
what it is handed, and never resolves anything. A builder that dies mid-build has its job
redelivered, because a work queue with explicit ack is what that means.
**The artifact is published.** The builder pushes to the registry seats and emits `built` —
module, version, digest. An event again: a fact about the builder.
**The build cascade is that event fanning out through a graph the mesh already has.** A module
whose image is built *on* another's artifact declares that in `build.on`. So `built` reaches the
controller, which walks the declared graph and submits rebuild jobs for everything downstream. A
dependency cascade is not special machinery — it is one event, one derived graph, and the same job
queue.
**Deployment is state, not a message.** The controller composes each affected node's declaration
and publishes it last-per-subject. A node that was away gets exactly the current one, never a
queue of superseded ones, and a replayed older one is refused by sequence.
**Applying is reported to a role.** The host applies and reports to the `mesh-controller` seat —
not to an address it was given at genesis. Held and retried while the store restarts
([ADR 0083](../../02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md)).
What disappears across that chain is every address. No webhook URL, no registered callback, no
"which node is the builder on", no controller endpoint baked into a joining node. That is the
class of bug
[issue 102](../../04-ISSUES/102-an-address-recorded-at-genesis-or-build-does-not-follow-the-nodes-ports/00-report.md)
names, dissolved rather than fixed.
## 7. Modules depending on each other
Three kinds, and conflating them is how deployment ordering goes wrong.
**Build-time** — A's image is built on B's artifact. Resolved by the cascade above; nothing at
runtime cares.
**Provision** — A requires a database from B. A **hard** dependency: the credential must exist
before A can start, so resolution gates delivery and A is shown as waiting until B has answered
([design 27](27-a-module-requires-the-mesh-resolves.md)).
**Seat** — A uses B's seat. A **soft** dependency, and this is the one the bus changes. A starts
whether or not anyone holds the seat, because the stream absorbs the gap. Deployment order stops
mattering for everything expressed this way, and a service being restarted, moved or upgraded is
not an outage for its callers — it is latency.
That difference is worth choosing on purpose. A dependency expressed as a provision must be
ordered; the same dependency expressed as a seat need not be.
## 8. Open
**Protocol versioning.** A seat's protocol is a compatibility surface between modules that do not
know each other, and nothing here says what happens when it changes under callers already bound to
it. This is the first thing to answer and the one most likely to hurt in year two rather than week
one.
**Whether a module may declare a seat it does not itself claim** — the contract as one thing, the
implementation as another, which is how two competing implementations would ever exist.
**Whether `consumes` naming another module couples too tightly.** It is kept here deliberately —
an event's provenance is its meaning — but a consumer of `billing.order.placed` does depend on
billing existing under that name.
## 9. How it is checked
- **A manifest holds no subject.** A catalogue test: no manifest contains a string matching the
subject grammar. The rule is worthless if it is followed by convention.
- **Permissions are exactly the three namespaces.** A composition test per module: the derived
permission set equals what its declaration implies, and a hand-written addition to it fails.
- **A sender cannot read the queue it writes to.** A bed: a module declaring `uses` is refused
subscribe on that seat's inbound subject.
- **One holder, one delivery.** A bed: a seat's job delivered once with the holder running, and
a second claim of the seat refused.
- **A queued job survives no holder.** A bed: submit with the seat unheld, assign the holder,
the job is delivered.
- **The cascade rebuilds exactly the dependents.** A bed: publish an artifact two modules build
on, and exactly those two are rebuilt.
- **A stale declaration is refused.** A bed: replay sequence *n−1* after *n*, and the node refuses
it rather than applying it.
+4 -2
View File
@@ -35,10 +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 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 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 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)) |
| [`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-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 0117](../../02-DECISIONS/0117-the-bus-is-the-only-broker.md), [ADR 0041](../../02-DECISIONS/0041-events-are-a-relationship.md) |
## Not yet written
- **The remaining six contexts.**