--- 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.