From f35f3757bb127a6507c5c0634abbb547f1eb3369 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 15:55:15 +0200 Subject: [PATCH] =?UTF-8?q?ADR=200161:=20what=20deserves=20a=20seat=20?= =?UTF-8?q?=E2=80=94=20the=20vault's=20seat,=20the=20hub=20as=20a=20placem?= =?UTF-8?q?ent=20of=20capacity=20one,=20the=20uplink=20holder=20as=20the?= =?UTF-8?q?=20machine's=20dialect;=20design=2026;=20issues=20105,=20106,?= =?UTF-8?q?=20138?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 02-DECISIONS/0161-what-deserves-a-seat.md | 102 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/26-the-seats.md | 20 +++- .../00-report.md | 21 +++- .../106-the-vault-claims-no-seat/00-report.md | 16 ++- .../00-report.md | 17 ++- 6 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 02-DECISIONS/0161-what-deserves-a-seat.md diff --git a/02-DECISIONS/0161-what-deserves-a-seat.md b/02-DECISIONS/0161-what-deserves-a-seat.md new file mode 100644 index 0000000..0941289 --- /dev/null +++ b/02-DECISIONS/0161-what-deserves-a-seat.md @@ -0,0 +1,102 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0126-a-module-declares-its-own-seats.md +--- + +# 161. What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's + +## Context + +Three issues asked the same question from three sides. The vault provides `secret` to the whole +mesh and claims no seat, so nothing refuses a second vault by name +([issue 106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md)). The hub of the private +network is a placement, `overlay place --hub`, and the issue asked whether "there is exactly +one hub" is a seat's shape ([issue 105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md)). +Three modules claim the one uplink seat, one per network manager a machine might run, and nothing +checks that the holder names the manager the machine actually runs +([issue 138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md)). + +Read against the code on the day of deciding: + +- The mesh's own seats are five by [design 26](../03-DESIGN/01-to-be/26-the-seats.md)'s table and + four in the controller's seed: `mesh-vault` is in the table and not in the seed, and the vault's + definition claims nothing. The design also says `secret` is reserved; no parser or resolution rule + reserves it. A second provider of `secret` would be a second candidate, settled by a pin. +- The store already keeps one hub: a unique index since the overlay's first migration, and the + placing command refuses a second hub naming the first. What 105 observed as silent is not. + [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) decided that + the private network becomes a mesh-scoped seat held by a server module, with client modules — + the overlay is the host's own today, so that seat has nothing to be held by yet. +- A machine's capabilities are its profile, detected by the host at enrolment and never since, and + resolution refuses a module on a machine lacking one it declares, naming the capability. The uplink + holders declare `package-manager` and `service-manager`, which every machine has. + +[ADR 0126](0126-a-module-declares-its-own-seats.md) gave the reason the mesh's own seats exist: +**the mesh's own code looks them up by name.** `mesh-store` is an identifier the controller +dereferences, not a convention. That reason decides the first question; the other two are decided +by what a seat is — a role held by a module assignment — and by what the mesh can check. + +## Decision + +**1. A provision the mesh itself dereferences is delivered by a mesh seat its provider claims.** +The vault's `secret` is one: the controller seals every minted credential with it. `mesh-vault` is +the fifth seat of the mesh's own, mesh-scoped, delivering `secret`, under +[ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md)'s convention; the vault's +definition claims it; a second provider of `secret` is a second claimant and refused by name. The +word *reserved* leaves design 26: the effect it described is the seat's. Every other mesh-scoped +provision — `smtp`, `oidc-client`, `s3-bucket`, `route`, `acme-ca` and the rest — may have several +providers, and a consumer with several and none local is a person's choice, as the glossary says. +The test for "deserves a seat" is the question 0126 asked: does the mesh's own code find it by name? + +**2. A singular fact about machines is a placement with a capacity of one; a singular role of a +module is a seat.** A seat is held by a module assignment and points at it; the hub is a machine, +and the private network is the host's own until 0121's server and client modules exist. So the hub +stays a placement, and what a seat would have given — refusal of a second by name, and the one +named when asked — a placement of capacity one gives: the store keeps one (the unique index), the +placing command refuses a second naming the one that stands, and the overlay listing names it. +0121's seat for the private network stands, deferred with the split it needs. The rule generalises: +a fact of the shape *exactly one machine is X* is a placement checked by the store and said by name, +never a seat with no module to hold it. + +**3. A holder of a seat whose role is "speak to what this machine runs" must be the dialect the +machine runs, and the machine says which.** The host's profile gains one capability per network +manager found active — `uplink-networkmanager`, `uplink-systemd-networkd`, `uplink-dhcpcd`, each +`systemctl is-active` of the manager's unit — and each uplink holder declares its own. Assignment +then refuses the wrong holder with the refusal that already exists, naming the capability; nothing +new is judged. The profile is detected again by every apply and travels in the report, and the +controller keeps the latest, so a machine that switches managers is, at its next push, a machine +whose holder lacks a capability: the plan refuses and names it, which is the one thing the machine +is the only one to know. `node-uplink` stays one seat: its three holders are three dialects of one +role, and the capability picks the dialect. One module speaking all three is allowed by this and +built by nobody. + +## Consequences + +- The controller's seed gains `mesh-vault`; the seat table takes it additively at the next start, + as every seed row does. The vault's definition claims it, one release after the controller. +- The uplink definitions declare their capability one release after the host reports it, or they + are refused on every machine in between; the order is controller (the report carries a profile), + host, then catalogue. +- Design 26 loses the word *reserved* for `secret` and states rules 2 and 3; the uplink row of the + seat table names the capability its holders declare. +- Issue 106 is resolved by rule 1, 105 by rule 2 with nothing to build, 138 by rule 3. + +## How this is checked + +| Rule | Checked by | +|---|---| +| `mesh-vault` is in the mesh's own set, mesh-scoped, delivering `secret`, and the vault claims it | a catalogue test on the default seats; registration refuses a second claimant by name (`CanHold`'s existing test, with the vault's seat) | +| A second hub is refused naming the first, and the listing names the hub | the overlay command's test; the store's unique index | +| A machine's profile names the network manager it runs, and is renewed by every report | a host detector test per manager; a controller test that a report carrying a profile replaces the stored one | +| An uplink holder on a machine running another manager is refused, naming the capability | the existing capability refusal, exercised by a resolution test with a networkmanager machine and the systemd-networkd holder | +| Live | `mesh-controller.seats` lists `mesh-vault` held by the vault on the control node; `plan` of a machine refuses the wrong uplink holder naming `uplink-` | + +## References + +- [ADR 0079](0079-the-foundation-seats-are-named-after-their-servers.md), [ADR 0110](0110-a-seat-is-a-module-assignment-from-a-closed-set.md), [ADR 0117](0117-a-machines-uplink-is-a-seat.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md), [ADR 0126](0126-a-module-declares-its-own-seats.md) +- [Design 26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md) +- Issues [105](../04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md), [106](../04-ISSUES/106-the-vault-claims-no-seat/00-report.md), [138](../04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index e6f2c09..da05750 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -174,6 +174,7 @@ python3 00-META/checks/index.py fail if stale - **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md) - **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) - **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) +- **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 115bce4..37804e7 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -10,8 +10,9 @@ 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-27 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0161-what-deserves-a-seat.md - 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md - 02-DECISIONS/0126-a-module-declares-its-own-seats.md - 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md @@ -75,6 +76,17 @@ named at the wrong scope. Adding a seat is a decision, recorded, for the reason host's vocabulary is one: the set is what a person reads to learn what a mesh can have, and an entry nobody argued for is an entry nobody can explain. +**What deserves one** ([ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md)). A provision the +mesh's own code dereferences by name is delivered by a mesh seat its provider claims — the store, the +bus, the vault, the artifact store, the catalogue. A provision a module merely offers may have several +providers, and a consumer with several is a person's choice. A fact of the shape *exactly one machine +is X* — the hub — is not a seat, because a seat is held by a module assignment and points at it; it is +a placement with a capacity of one, kept by the store, refused by name when a second is placed, and +named in the listing. And a seat whose role is to speak to what the machine runs — the uplink — is +held only by the dialect the machine runs: the machine says which in its profile, renewed with every +report, and the holder declares the capability, so the wrong one is refused the way any missing +capability is. + ## The set **The set is derived, and only the mesh's half is written here.** Revision, 2026-09-26 @@ -108,7 +120,7 @@ convention, which later seats departed from. | `mesh-controller` | — | mesh | — | the controller | | `mesh-store` | — | mesh | — | the store the mesh's own records live in | | `mesh-broker` | — | mesh | `mesh-bus` | the broker carrying the mesh's own bus | -| `mesh-vault` | — | mesh | `secret`, reserved | the vault | +| `mesh-vault` | — | mesh | `secret` | the vault (in the seed since [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md); the earlier *reserved* named an effect no rule produced) | | `mesh-artifact-store` | `the-artifact-store` (renamed 2026-09-30, [ADR 0156](../../02-DECISIONS/0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)) | 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 | @@ -240,6 +252,8 @@ checked as their tables say: | A handover replaces the holder as one write, needs an assignment to point at, and goes with it | 0131: store tests — a second handover leaves one row; a handover to a module not assigned where named is refused; unassigning the holder removes the row. | | 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. | +| `secret` has one provider, the holder of `mesh-vault` | 0161: a second claimant of the seat is refused by name (`CanHold`); *correction of fact, 2026-10-01: no parser rule ever reserved the word, the seat does the work*. | +| A singular fact about machines is a placement of capacity one, refused by name | 0161: the overlay command's test for a second hub; the store's unique index. | +| A holder of `node-uplink` is the dialect the machine runs | 0161: the host reports `uplink-` in its profile with every report; a resolution test refuses the other holder naming the capability. | | 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. | diff --git a/04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md b/04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md index 9641d74..a5e898d 100644 --- a/04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md +++ b/04-ISSUES/105-the-hub-of-the-private-network-is-not-a-seat/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: resolved opened: 2026-09-23 -located-in: [] -fixed-by: -amended-design: +located-in: [mesh-controller cmd/mesh-controller/network.go (the placing command), internal/inventory/migrations/0004-the-overlay.sql (one hub)] +fixed-by: nothing to build — ADR 0161 rule 2; the second hub was already refused by name, and the private network's seat waits for ADR 0121's server and client modules +amended-design: [03-DESIGN/01-to-be/26-the-seats.md] --- # 105 — The hub of the private network is a placement, not a seat @@ -32,3 +32,16 @@ a node-scoped seat held by every node, which is true and not what was asked. - Does the per-node seat still say anything once the hub is a seat, or is it the interface's presence restated? - What else in the mesh is "exactly one" and recorded as a placement rather than a seat? + +## Resolved, 2026-10-01 + +Read against the code: the store has kept one hub since the overlay's first migration (a unique +index), and `overlay place --hub` refuses a second naming the first. What this report saw as +silent is not. [ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 2, answers the +question that remained: a singular fact about machines is a placement with a capacity of one, +refused by name and named in the listing — never a seat, because a seat is held by a module +assignment and the private network is the host's own until +[ADR 0121](../../02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md)'s +server and client modules exist. That seat stands, deferred with the split it needs. + +*How it is checked:* the overlay command's test for a second hub, and the index. diff --git a/04-ISSUES/106-the-vault-claims-no-seat/00-report.md b/04-ISSUES/106-the-vault-claims-no-seat/00-report.md index 9f2f9d4..289189d 100644 --- a/04-ISSUES/106-the-vault-claims-no-seat/00-report.md +++ b/04-ISSUES/106-the-vault-claims-no-seat/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] -fixed-by: -amended-design: +located-in: [mesh-controller internal/catalogue/seats.go (the seed lacks mesh-vault), mesh-catalog modules/mesh-vault/module.json (claims nothing)] +fixed-by: +amended-design: [03-DESIGN/01-to-be/26-the-seats.md] --- # 106 — The vault claims no seat, so nothing refuses a second one @@ -31,3 +31,11 @@ others were missed the same way — every provider added after 0079. - A mesh-scoped seat `mesh-vault`, by the 0079 convention — is there any reason not to? - Should a provider of a mesh-scoped provision be required to claim a seat, or say explicitly that more than one is allowed, so the omission cannot recur? + +## Decided, 2026-10-01 + +[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 1: `mesh-vault` joins the mesh's +own set, mesh-scoped, delivering `secret`, and the vault claims it; a second provider is a second +claimant, refused by name. The record also answers the second question: a provision the mesh's own +code dereferences by name gets a seat, every other mesh-scoped provision may have several providers. +Design 26's *reserved* for `secret` named an effect no rule produced; corrected there. diff --git a/04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md b/04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md index 6da3cb1..005c16b 100644 --- a/04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md +++ b/04-ISSUES/138-two-modules-claim-one-seat-and-are-not-interchangeable/00-report.md @@ -1,9 +1,9 @@ --- -status: open +status: located opened: 2026-09-28 -located-in: [mesh-controller internal/catalogue, mesh-catalog] -fixed-by: -amended-design: +located-in: [mesh-host internal/profile/detectors.go (no detector for the network manager), mesh-host cmd/mesh-host (the profile is detected at enrolment only), mesh-controller internal/link (a report carries no profile), mesh-catalog modules/networkmanager, systemd-networkd, dhcpcd (declare no capability of their own)] +fixed-by: +amended-design: [03-DESIGN/01-to-be/26-the-seats.md] --- # 138 — Two modules claim one seat and are not interchangeable, and nothing says so @@ -54,3 +54,12 @@ able to switch the manager, which ADR 0117 refuses for a reason that has not cha alternative is one module that speaks whichever dialect the machine needs, chosen from the report. - What should happen on a machine that switches manager afterwards? The seat would then be held by the wrong module, and the machine is the only place that knows. + +## Decided, 2026-10-01 + +[ADR 0161](../../02-DECISIONS/0161-what-deserves-a-seat.md), rule 3: the host's profile gains one +capability per network manager found active, each holder declares its own, and the existing +capability refusal does the rest, naming it. The profile is detected again by every apply and +travels in the report, so a machine that switches managers is refused at its next push. The uplink +stays one seat; the capability picks the dialect. Order of building: controller (a report may carry +a profile), host, then the three definitions.