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.
133 lines
9.3 KiB
Markdown
133 lines
9.3 KiB
Markdown
---
|
|
topic: what runs on it
|
|
status: accepted
|
|
date: 2026-09-27
|
|
deciders: jochen
|
|
reconstructed: false
|
|
extends: 0110-a-seat-is-a-module-assignment-from-a-closed-set.md
|
|
---
|
|
|
|
# 121. A system seat is named for its scope, and a module may define its own
|
|
|
|
## Context
|
|
|
|
[ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) made seats a closed set the
|
|
control plane defines: a well-formed name no longer becomes a seat by being claimed, so a person can
|
|
read what a mesh can have and who fills each role. It left two things unsettled that the growing set
|
|
now exposes:
|
|
|
|
- **The names carry no rule.** `mesh-controller`, `mesh-store`, `mesh-broker` are named for the mesh;
|
|
beside them sit `the-artifact-store`, `the-build-machine`, `the-dns-port`, `the-showcase`,
|
|
`the-uplink` — a second naming style with no principle behind it. A reader cannot tell a seat's
|
|
scope from its name, and the mesh's own roles do not look like the mesh's.
|
|
- **The set is the *only* place a seat may be defined.** A module claiming any name not in the
|
|
control plane's set is refused. That is right for *system* roles — one broker, one packet filter
|
|
per node — but it means a module can never define a role of its own: a demo module's
|
|
`the-showcase`, a future application's coordination role, must be smuggled into the control plane's
|
|
set or not exist. The control plane ends up holding roles that are not the mesh's to define.
|
|
|
|
Reviewing the set against these also found seats whose *scope* or *membership* is wrong, not just
|
|
their name — the review is the occasion to fix those too.
|
|
|
|
## Decision
|
|
|
|
**A system seat — one the control plane defines — is named for its scope:**
|
|
|
|
- **`mesh-*`** for a mesh-scoped seat: one holder in the whole mesh, a role the mesh has once
|
|
(`mesh-controller`, `mesh-store`, `mesh-broker`, `mesh-git`, …). A `mesh-*` seat is always held by
|
|
a module **on a named node** — `mesh-git` is gitea *on novox*, not "gitea"; another node running
|
|
gitea does not hold `mesh-git` unless it is the holder. The seat is the mesh's single answer for
|
|
the role, and which node answers is part of what the seat records.
|
|
- **`node-*`** for a node-scoped seat: one holder per node, a role each machine has at most once
|
|
(`node-packet-filter`, `node-intrusion-prevention`, `node-uplink`, …).
|
|
|
|
The three already-`mesh-*` seats keep their names; the rest are renamed by this rule. The scope a
|
|
name declares must match the seat's actual scope — a `mesh-*` seat at node scope, or the reverse, is
|
|
a contradiction the reader is entitled to trust is impossible.
|
|
|
|
**The control plane defines only system seats. A module may define its own.** A seat named `mesh-*`
|
|
or `node-*` is the control plane's, and claiming one the control plane does not define is refused as
|
|
before. Any *other* name is a **module-defined seat**: valid when the module declaring the claim also
|
|
declares the seat (its name, scope, and — if any — the protocol its holder speaks). The control plane
|
|
enforces one-holder-per-scope for it exactly as for its own, but does not otherwise know what it
|
|
means. So an application can coordinate its own instances through a seat of its own, and the mesh's
|
|
closed set stays what its name says it is: the *system's* roles, not everyone's.
|
|
|
|
**Specific seats this settles:**
|
|
|
|
- **`the-build-machine` → `mesh-build-machine`, and its scope becomes mesh.** There is one build
|
|
machine in the mesh (the builder on novox), not one per node. Node scope said the opposite. It
|
|
delivers no provision; it is the mesh's single build machine.
|
|
- **`the-private-network` → `mesh-private-network`, held by the network *server* on one node.** Today
|
|
it is node-scoped and held on every node, with a stated (untested) story that a different VPN could
|
|
hold it per machine — which would force every provider module to independently implement receiving
|
|
and applying the controller-composed configuration. The mesh does not work that way and should not
|
|
pretend to: **one mesh decides one private network.** The seat is mesh-scoped, held by the server
|
|
module (WireGuard on the hub, novox). A machine that joins is given a **client module** that
|
|
receives the composed configuration and applies it; when a node joins, the mesh emits each node's
|
|
configuration so all of them know each other at once. This drops per-node VPN choice deliberately —
|
|
the private network is nox-mesh's own, and it defines the nodes' configuration rather than being
|
|
assembled from each node's opinion. (Implementation: the overlay generator's per-node computation
|
|
is unchanged; what changes is the seat's scope and the server/client split of the module.)
|
|
- **`the-showcase` → removed from the set; it becomes a module-defined seat.** It is a demo module's
|
|
own coordination role, claimed by nothing else and held nowhere. It is the first module-defined
|
|
seat, and the reason the rule above is needed rather than hypothetical.
|
|
- **`the-dns-port` → `node-dns-resolver`** (the daemon that binds `:53`), kept distinct from
|
|
**`the-resolver-configuration` → `node-resolver-config`** (what writes `resolv.conf`). Two roles,
|
|
two seats; the rename must not blur them.
|
|
- **`the-packet-filter` → `node-packet-filter`** and **`the-intrusion-prevention` →
|
|
`node-intrusion-prevention`** — names kept as-is but for the prefix. "Packet filter" stays distinct
|
|
from "firewall", which would swallow intrusion-prevention too.
|
|
- **`the-uplink` → `node-uplink`** ([ADR 0125](0117-a-machines-uplink-is-a-seat.md)). Unheld, so it
|
|
renames with no migration.
|
|
- **The registry seats — `the-artifact-store`, `npm-package-registry` (→ `mesh-artifact-store`,
|
|
`mesh-npm-package-registry`) — and `git` (→ `mesh-git`) — are decided but deferred.** They each
|
|
*deliver* a provision, so renaming them is a delivering-seat migration: a holder that stops
|
|
resolving mid-flight takes a provision away from every consumer. That risk is not worth carrying in
|
|
the same pass as the node-* renames, so they keep their names until done deliberately.
|
|
|
|
**`distribution` stays the mesh's registry; only `verdaccio` is retired.** An earlier draft of this
|
|
record had the registry consolidating onto gitea and `distribution` retired — that was reversed:
|
|
`distribution` is the standalone OCI registry serving every `artifact-store://…@sha256` image (the
|
|
control plane's own included), and the mesh keeps it. `verdaccio` was a *second* npm registry;
|
|
gitea already provides `npm-package-registry`, so verdaccio is redundant and is removed. It is only
|
|
in the catalogue (never registered in the running mesh), so removing it is deleting the module — no
|
|
migration, nothing to strand.
|
|
|
|
## Consequences
|
|
|
|
- **A reader learns a seat's scope from its name.** `mesh-*` is mesh-wide and one; `node-*` is
|
|
per-machine. The mesh's own roles finally look like the mesh's.
|
|
- **Applications get their own seats** without the control plane learning their meaning. The closed
|
|
set shrinks to what it should be — the system's roles — and stops being where unrelated roles hide.
|
|
- **The renames are a coordinated migration, not a rename.** A held seat's name lives in three places
|
|
that must move together: the control plane's set (`seats.go`), every claiming manifest, and what
|
|
each node reports it holds (re-derived by re-registering the manifest and re-pushing). A seat
|
|
renamed in one place and not the others stops resolving to its holder — and for a *delivering* seat
|
|
(`mesh-store`→postgres, `mesh-broker`→amqp, the registry seats) that is a mesh-wide provision
|
|
outage, the same failure mode as a schema change hitting an old manifest. So: the non-delivering
|
|
`node-*` seats and `mesh-build-machine` migrate as one tested controller+catalogue change;
|
|
`node-uplink` is free (unheld); the delivering registry seats are deferred to their own pass.
|
|
- **The node-* migration was done as one controlled step, and it froze briefly.** Deploying the new
|
|
controller made it reject the still-old-named claims in the stored manifests, so composition stopped
|
|
for the affected nodes until each manifest was re-registered under its new name; running services
|
|
were untouched, and the window was seconds. This is the coordinated-migration cost named above,
|
|
paid once — and the reason the *delivering* registry seats, whose freeze would be a provision
|
|
outage rather than a compose pause, are not folded into the same pass.
|
|
- **`distribution` is not retired.** It stays as the registry; only `verdaccio` (a redundant second
|
|
npm registry) is removed. The mesh keeps one OCI registry (`distribution`) and gitea for npm/git —
|
|
the "one registry, on gitea" idea was considered and dropped.
|
|
- **The private network stops pretending to be swappable per node.** The gain is a coherent
|
|
server/client model matching how the controller already composes configuration; the cost is that
|
|
choosing a different VPN is now a mesh-wide change, not a per-node one — accepted.
|
|
|
|
## References
|
|
|
|
- [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md) — the closed set this refines
|
|
- [ADR 0125](0117-a-machines-uplink-is-a-seat.md) — `the-uplink`, renamed here to `node-uplink`
|
|
- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md) — the original `mesh-*` seats
|
|
whose naming this generalises
|
|
- [to-be 26](../03-DESIGN/01-to-be/26-the-seats.md) — the seat table, updated by this
|
|
- mesh-controller `internal/catalogue/seats.go` (the set and claim validation),
|
|
`internal/overlay/generator.go` (the private network as server + client)
|