From 6b49ed613c86be2992f9e46c1b46bd3daa76f6d9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 21:49:57 +0200 Subject: [PATCH 1/2] ADR 0287: a seat names the paths its holder owns, so a file's owner is read from the seat (issue 412, research 023) --- 00-META/glossary.md | 2 + .../00-overview.md | 3 +- ...e-and-a-nodes-hosts-file-is-one-modules.md | 6 + ...ller-writes-no-file-a-seats-holder-owns.md | 5 + ...wns-and-a-claim-without-them-is-refused.md | 176 ++++++++++++++++++ 03-DESIGN/01-to-be/26-the-seats.md | 45 ++++- 6 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md diff --git a/00-META/glossary.md b/00-META/glossary.md index ec656d0b..b3eac862 100644 --- a/00-META/glossary.md +++ b/00-META/glossary.md @@ -55,6 +55,8 @@ is listed under [Homonyms](#homonyms), with the qualified form each domain uses. seat, is the overview of what a mesh has ([26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md)). A seat carries **verbs** — the tools every holder must serve. A verb added to a held seat is first promised as optional, then served, then required ([ADR 0246](../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)). + A node seat may also name **the paths its holder owns**, such as `/etc/hosts`: a claim whose module does + not declare them is refused, and only a claimant declares them ([ADR 0287](../02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md)). - **operator** — the person who runs the mesh, and the one the mesh talks to. *Not:* ~~master~~ (hq) - **person** — any human, as against the mesh acting unattended: *a person's word* releases a held diff --git a/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md b/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md index 7bc80c12..2b96d156 100644 --- a/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md +++ b/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md @@ -1,7 +1,8 @@ --- -status: active +status: graduated initiated: 2026-10-03 touches: [the seats, the seat protocol, the controller's ownership check, 03-DESIGN/01-to-be/26-the-seats.md] +became: [03-DESIGN/01-to-be/26-the-seats.md, 02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md] --- # 023 — A seat protocol that defines what its holder owns diff --git a/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md b/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md index 2ef3f69e..845f397a 100644 --- a/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md +++ b/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md @@ -89,6 +89,12 @@ controller holds none of it:** an operator's line is the machine's, not a record **4. No other module writes `/etc/hosts`.** The private network's region goes, as ADR 0194 already has it; a module that once wrote a line there asks the mesh's resolver instead. +> **The mechanism changed — 2026-10-10, by [ADR 0287](0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md).** +> Decision 3 and 4 stand: `/etc/hosts` is the hosts seat's holder's, and no other module writes it. What moved +> is where that is stated and checked. The seat's definition now names `/etc/hosts` and `/etc/hostname`, a +> claim without them is refused, and a module that does not claim the seat and declares either is refused at +> registration, for every seat that names a path, rather than by this record for one file. + ## Consequences - **The lab's names follow its scenarios.** A scenario raised is resolvable from every node at once; a diff --git a/02-DECISIONS/0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md b/02-DECISIONS/0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md index af65c3dd..034cb170 100644 --- a/02-DECISIONS/0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md +++ b/02-DECISIONS/0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md @@ -94,6 +94,11 @@ leaves the unit as it is. What the record going had found is not the machine's t another resource holds the unit, and restoring it could stop the runtime only for the remaining resource to start it again in the same apply. +> **The mechanism changed — 2026-10-10, by [ADR 0287](0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md).** +> Decision 3 stands. For a path, which file a seat's holder owns is now named by the seat itself, and a +> resource the controller generates on such a path for a module that does not claim the seat is refused at +> composition by ADR 0287's rule 4, beside the collision check of decision 4. + ## Consequences - **Rollout has a fixed order, in three steps.** diff --git a/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md b/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md new file mode 100644 index 00000000..cb750fc5 --- /dev/null +++ b/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md @@ -0,0 +1,176 @@ +--- +topic: what runs on it +status: accepted +date: 2026-10-10 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md +--- + +# 287. A seat names the paths its holder owns, and a claim without them is refused + +Graduates [research 023](../01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md). +The operator left the choices to the agent that graduated it ("I trust your judgement"); they are argued here. + +## Context + +**A seat is a protocol, and a module holds it by implementing the protocol** +([ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md), +[ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). Today the protocol is what the holder +accepts, emits and serves. A seat's definition in the controller carries a name, a scope, what it delivers, +those three, what it receives, whether it is replicated, the seats it needs beside it, and the record that +made it a seat. The store's seat table carries a name, a scope, what it delivers and the record. **Neither +names a path.** + +**Who owns a machine-wide file has been decided one record at a time:** + +- [ADR 0199](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md) + §3 and §4: `/etc/hosts` is held by the holder of the hosts seat (now `node-hostname`), and no other module + writes it. +- [ADR 0222](0222-a-module-is-told-where-a-mesh-seats-holder-is-reached-and-the-controller-writes-no-file-a-seats-holder-owns.md) + §3: the controller writes no file a seat's holder owns. §4 holds what the controller generates to the + same collision check as what a module declares. +- [ADR 0223](0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md): `/etc/resolv.conf` is + written by the holder of `node-uplink`, rendered by the mesh as a file the holder asks for. +- [ADR 0241](0241-a-machine-says-how-its-network-is-and-an-outside-writer-of-a-mesh-file-is-a-finding.md) + §4 and §5: the machine's network statement names "the module whose file it is", and a rewrite is raised + under that module. +- [ADR 0247](0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md): + where `node-resolver` is held, its holder writes `/etc/resolv.conf` and the uplink's holder steps back. + +**The only enforcement is after the fact, and per machine.** The controller's `checkResources` +(mesh-controller `internal/catalogue/resolve.go`, main at `c06a2cd`) refuses two modules on one machine +declaring one path, unit, name or package, at composition. It applies the step-back of ADR 0247 by hand +before comparing. Nothing refuses a module that holds no seat declaring `/etc/hosts` on a machine where the +hosts seat's holder is not assigned: there is no second owner there to collide with. And the question asked +on 2026-10-03, *which module owns `/etc/resolv.conf`?*, had no place to be looked up but every manifest. + +**What the catalogue declares, measured on 2026-10-10** (mesh-catalog main at `61dcdf0`, 125 manifests): + +- 29 modules claim a node seat. +- Three paths that the holders declare are the role's on any machine, whatever program fills the role: + `/etc/hosts` and `/etc/hostname` (declared by `hostname`, holder of `node-hostname`), and + `/etc/resolv.conf` (rendered for `networkmanager` and `systemd-networkd`, holders of `node-uplink`, and for + `systemd-resolved`, holder of `node-resolver`). +- No other module declares any of the three. Two read them (the agent's guard and the container runtime's + tools), which is access, not ownership ([ADR 0051](0051-shared-data-is-the-operators.md)). +- Every other path a node seat's holder declares belongs to its program, not to the role: + `/etc/fail2ban/jail.local`, `/etc/docker/daemon.json`, `/etc/pacman.conf`, `/etc/lemurs/config.toml`, + `/etc/NetworkManager/conf.d/50-mesh.conf`, or a path under the operator's home. + +## Considered Options + +1. **Keep deciding case by case.** Rejected. It takes one record per file, the answer lives in prose, and it + is enforced only on a machine where two owners happen to be assigned together. That is the state the + research was opened about. +2. **The holder names the paths it owns, in its claim.** Rejected. That is the direction the research + turned around: the answer would depend on which module is registered, and a second implementation could + name a different set. The seat is the definition, so the seat says it, before any module is written. +3. **A seat names every path its holder writes, its program's own included** (the research's example of the + intrusion prevention owning its jail file). Rejected. A seat is a protocol any program may implement + (ADR 0132). Naming `/etc/fail2ban/jail.local` would refuse every intrusion prevention that is not fail2ban, + which turns the seat into one module. A program's own file is already one module's, by its path and by + `checkResources`. +4. **No two seats name one path, with no exception.** Rejected. It would refuse the arrangement ADR 0247 + made and the laptop runs. If `node-uplink` lost `/etc/resolv.conf`, a machine without a resolver of its + own would have no owner of the file (against ADR 0223). If `node-resolver` lost it, ADR 0247 would be + undone. +5. **A seat's paths as a column of the store's seat table.** Rejected. Whether a seat is replicated, what it + receives and which seats it needs are compiled with the set and never stored, because each is the mesh's + definition of the role and changes what every holder must satisfy + ([ADR 0220](0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)). A path + is the same kind of fact. A row edited without a release would make claims that were valid refused, with + nothing in review. +6. **Check it only at the merge gate.** Rejected as the only place. The rule is a judgement over the whole + catalogue, which is what registration judges (`CatalogueProblems`). The merge gate already runs + `CatalogueProblems` on the catalogue with the change (mesh-controller `cmd/mesh-controller/merge_gate.go`), + so a refusal at registration is also a red `mesh/merge-gate` before merge, and a manifest registered by + another way (the lab, the catalogue command) is held to it as well. + +## Decision + +**1. A seat's definition may name paths its holder owns.** Each is an absolute path on the machine where +the holder runs, written literally, with no placeholder. Only a node seat names paths: a path is a +machine's, and a mesh seat's holder runs on one machine, so its path would have no owner on every other. +A seat names a path only when the path is the role's on any machine, whatever program fills the role: a +path the operating system's conventions give, not one program's. A seat a module declares for itself +([ADR 0126](0126-a-module-declares-its-own-seats.md)) names paths in its declaration, under the same rules. + +**2. The set this record starts with:** + +| seat | names | +|---|---| +| `node-hostname` | `/etc/hosts`, `/etc/hostname` | +| `node-uplink` | `/etc/resolv.conf`, stepping back for `node-resolver` | +| `node-resolver` | `/etc/resolv.conf` | + +Every other seat names none. Adding a path to a seat, or taking one away, is a decision, recorded, as adding +a seat or a seat's need is. + +**3. A module holds the seat only if it declares every path the seat names.** It declares it as a resource +or as a file the mesh renders for it, which is how the uplink and the resolver hold `/etc/resolv.conf`. A +claim without them is refused by `CanHold`, the one judgement of whether a module can hold a seat (ADR 0131), +at registration and at handover, naming the seat and the missing paths. All four modules that hold one of +these seats today declare what the table names, so the rule refuses nothing registered on 2026-10-10. + +**4. Only a claimant declares a seat's path.** A module that does not claim the seat and declares a path the +seat names is refused at registration, naming the seat. The same refusal applies at composition to what the +controller generates (ADR 0222 §4), so the controller cannot write a seat's path either. Reading a seat's +path is access, and stays allowed. + +**5. No two seats name one path, unless one steps back for the other by definition.** A seat may name a path +*stepping back for* another node seat: where that other seat is held on the machine, its holder owns the path +and this seat's holder does not declare it there. Every path has exactly one seat naming it without stepping +back, and the stepping back does not cycle. This makes the existing rule of ADR 0247 part of the definition, +in the place `checkResources` applies it today. A module declaring a seat that names a path another seat +already names is refused at registration, and the second declaration loses, as for a seat's name. + +**6. The seat listing shows each seat's paths.** *Which module owns `/etc/resolv.conf` on a machine?* is +answered by the controller's `seats` verb: the seat that names the path, and that seat's holder on the +machine, with the step-back applied. + +Building rules 1 to 6 in the controller is tracked by issue 412. The design it builds is +[26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md), *A seat that names the paths its holder owns*. + +## Consequences + +- **The question of 2026-10-03 has a place to be answered**, before any module is written, from the seat's + definition rather than from every manifest. +- **Implementing the seat becomes checkable for files**, as it already is for verbs (ADR 0132): a module that + does not own what the role owns cannot claim the role. +- **ADR 0199 §4 becomes the general rule rather than a case**, and ADR 0222 §3 becomes checked for paths + rather than stated. Both records get a note pointing here. +- **The step-back moves from code into the definition.** `checkResources` keeps comparing what the machine + composes, and reads which seat steps back from the seat instead of from a special case. +- **What got harder.** A new holder of `node-hostname` must own both files, so a program that keeps a + machine's names elsewhere cannot hold the seat until the seat changes by a record. A seat that gains a path + makes every holder that does not declare it refused; the change to the seat and to its holders' manifests + must land together, and the merge gate shows which holders fail. +- **The finding of ADR 0241 is unchanged.** It still names the module whose file was rewritten. For a seat's + path that is now the seat's holder by definition, not by coincidence. + +## How it is checked + +All in mesh-controller, built under issue 412: + +| Rule | Checked by | +|---|---| +| Only a node seat names paths, absolute and without a placeholder | a unit test over the compiled set; a registration test refusing a module's mesh seat that names a path and a path with `${` | +| Every path has one seat naming it without stepping back, and stepping back does not cycle | a unit test over the compiled set; a registration test refusing a module's seat that names `/etc/hosts` | +| A claim without the seat's paths is refused, naming them | `CanHold` tests at registration and at handover, with a holder declaring the path as a resource, as a rendered file, and not at all | +| Only a claimant declares a seat's path | a registration test refusing a module that declares `/etc/hosts` without claiming `node-hostname`; a composition test refusing a generated resource on a seat's path | +| The step-back comes from the seat | the existing composition tests for the uplink and the resolver on one machine pass with the special case removed | +| The listing shows the paths | the `seats` command test | +| The catalogue as it is passes | `CatalogueProblems` over the catalogue's main gives no new problem; the merge gate runs it on every catalogue pull request | + +## References + +- [Research 023](../01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md), the + effort this graduates. +- [26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md), the design amended. +- mesh-controller main at `c06a2cd`: `internal/catalogue/seats.go` (`Seat`, `CanHold`), + `internal/catalogue/resolve.go` (`checkResources`, `steppedBack`), + `internal/catalogue/seats_declared.go` (`CatalogueProblems`), `cmd/mesh-controller/merge_gate.go`. +- mesh-catalog main at `61dcdf0`: the manifests of `hostname`, `networkmanager`, `systemd-networkd` and + `systemd-resolved`. +- Issue 412, the build. diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 626aba48..06543a12 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -15,8 +15,9 @@ code: - mesh-catalog modules/gitea/module.json - mesh-controller internal/inventory/migrations/0063-a-machines-names-are-one-seats.sql - mesh-controller internal/inventory/migrations/0064-the-resolver-file-is-the-uplinks.sql -updated: 2026-10-05 +updated: 2026-10-10 decisions: + - 02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md - 02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md - 02-DECISIONS/0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md - 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md @@ -242,6 +243,45 @@ and refused at composition. Taking the needed seat's last holder from beneath a too. What a seat needs is the mesh's definition of the role, compiled with the set and never stored, and adding a need is a decision. +## A seat that names the paths its holder owns + +*2026-10-10* ([ADR 0287](../../02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md)). +**A node seat may name paths its holder owns**, so that holding the role means owning its files, and *which +module owns this file on that machine?* is read from the seat and its holder rather than from every manifest. + +A seat names a path only when the path is the role's on any machine, whatever program fills the role: a +path the operating system's conventions give, absolute and written literally. A path one program keeps, +such as an intrusion prevention's jail file or a container runtime's daemon file, is not the role's, and a +seat naming it would refuse every other program that could fill the role. Only a node seat names paths; a +mesh seat's holder runs on one machine, and its path would have no owner on the others. What a seat names +is compiled with the set and never stored, like what it needs, and changing it is a decision. + +| seat | names | +|---|---| +| `node-hostname` | `/etc/hosts`, `/etc/hostname` | +| `node-uplink` | `/etc/resolv.conf`, stepping back for `node-resolver` | +| `node-resolver` | `/etc/resolv.conf` | + +**Holding the seat means declaring its paths.** A module that claims the seat declares every path it names, +as a resource or as a file the mesh renders for it. `CanHold` refuses a claim without them, at registration +and at handover, naming the missing paths, beside the verbs it already requires. + +**Only a claimant declares a seat's path.** Any other module declaring it is refused at registration, and +what the controller generates is refused at composition by the same rule, which is how the controller is +held to writing no file a seat's holder owns. Reading the path is access and stays open to every module. + +**One seat per path, with a declared step-back.** Every path has exactly one seat naming it outright. A +second seat may name it only *stepping back for* that seat: where the other seat is held on the machine, its +holder owns the path, and the stepping-back seat's holder does not declare it there. The uplink steps back +for a machine's own resolver this way ([ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)), +and the collision check reads that from the seat rather than from a case of its own. A module declaring a +seat that names a path another seat already names is refused at registration. + +**The listing shows each seat's paths**, so the controller's `seats` verb answers who owns a path on a +machine: the seat that names it, and that seat's holder there. + +Building this is tracked by issue 412. + ## The overview The controller lists every seat in the set with its scope, what it delivers, and its holder as a node @@ -298,5 +338,8 @@ checked as their tables say: | A seat's holder has the seats it needs beside it: the resolver configuration is refused at `assign` without the uplink held on its node, and the uplink's last holder cannot be taken from beneath it | 0220: seat-dependency tests on the definition, on a synthetic claimant, at assign, at unassign and at composition, and one reading the catalogue for the uplink's possible holders. | | A replicated seat has every holder on record and composes on each; a seat held once still refuses a second holder, on record or not; `--add` is refused for it | 0223: resolution and composition tests with two holders on record and a third machine, with two claimants and nothing on record, and on `mesh-store`; a unit test that only `mesh-dns-resolver` is replicated, surviving the store's rows; store tests keeping several holders once each, a handover leaving one, and unassigning one taking only its row. | | A retired seat leaves the set once nothing claims it, from the compiled set and the store's table both | 0220: the closed-set test's count, and the store migration that deletes the row. | +| A node seat names only literal absolute paths; every path has one seat naming it outright, and stepping back does not cycle | 0287: a unit test over the compiled set; registration tests refusing a module's seat naming a path at mesh scope, with `${`, or already named by another seat. | +| A holder declares every path its seat names, and only a claimant declares one | 0287: `CanHold` tests at registration and handover (declared as a resource, as a rendered file, not at all); a registration test refusing `/etc/hosts` from a module not claiming `node-hostname`; a composition test refusing a generated resource on a seat's path. | +| The uplink's step-back comes from the seat | 0287: the composition tests for the uplink and the resolver on one machine pass with the case in the collision check removed. | | 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. | From 906d702b8ddf9ea8f2cc4dcb40257d9f6bbc30e2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 21:53:53 +0200 Subject: [PATCH 2/2] Narrow ADR 0287's claimant rule to any seat naming the path, and correct two facts the review found --- ...e-and-a-nodes-hosts-file-is-one-modules.md | 2 +- ...wns-and-a-claim-without-them-is-refused.md | 44 +++++++++++-------- 03-DESIGN/01-to-be/26-the-seats.md | 14 +++--- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md b/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md index 845f397a..2d1912b6 100644 --- a/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md +++ b/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md @@ -91,7 +91,7 @@ it; a module that once wrote a line there asks the mesh's resolver instead. > **The mechanism changed — 2026-10-10, by [ADR 0287](0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md).** > Decision 3 and 4 stand: `/etc/hosts` is the hosts seat's holder's, and no other module writes it. What moved -> is where that is stated and checked. The seat's definition now names `/etc/hosts` and `/etc/hostname`, a +> is where that is stated and checked. The seat's definition names `/etc/hosts` and `/etc/hostname` (built under issue 412), a > claim without them is refused, and a module that does not claim the seat and declares either is refused at > registration, for every seat that names a path, rather than by this record for one file. diff --git a/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md b/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md index cb750fc5..02cc2748 100644 --- a/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md +++ b/02-DECISIONS/0287-a-seat-names-the-paths-its-holder-owns-and-a-claim-without-them-is-refused.md @@ -18,8 +18,7 @@ The operator left the choices to the agent that graduated it ("I trust your judg ([ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)). Today the protocol is what the holder accepts, emits and serves. A seat's definition in the controller carries a name, a scope, what it delivers, -those three, what it receives, whether it is replicated, the seats it needs beside it, and the record that -made it a seat. The store's seat table carries a name, a scope, what it delivers and the record. **Neither +those three, what it receives, whether it is replicated, and the record that made it a seat. The store's seat table carries a name, a scope, what it delivers and the record. **Neither names a path.** **Who owns a machine-wide file has been decided one record at a time:** @@ -47,7 +46,7 @@ on 2026-10-03, *which module owns `/etc/resolv.conf`?*, had no place to be looke **What the catalogue declares, measured on 2026-10-10** (mesh-catalog main at `61dcdf0`, 125 manifests): -- 29 modules claim a node seat. +- 29 modules claim one of the mesh's own node seats, and two more claim a node seat they declare themselves. - Three paths that the holders declare are the role's on any machine, whatever program fills the role: `/etc/hosts` and `/etc/hostname` (declared by `hostname`, holder of `node-hostname`), and `/etc/resolv.conf` (rendered for `networkmanager` and `systemd-networkd`, holders of `node-uplink`, and for @@ -56,7 +55,9 @@ on 2026-10-03, *which module owns `/etc/resolv.conf`?*, had no place to be looke tools), which is access, not ownership ([ADR 0051](0051-shared-data-is-the-operators.md)). - Every other path a node seat's holder declares belongs to its program, not to the role: `/etc/fail2ban/jail.local`, `/etc/docker/daemon.json`, `/etc/pacman.conf`, `/etc/lemurs/config.toml`, - `/etc/NetworkManager/conf.d/50-mesh.conf`, or a path under the operator's home. + `/etc/NetworkManager/conf.d/50-mesh.conf`, or a path under the operator's home. Two are neither: the + holder of `node-secret-service` declares `/etc/pam.d/login` and `/etc/pam.d/passwd`, which are shared + files of other programs. They are not the role's on any machine either, so no seat names them. ## Considered Options @@ -75,11 +76,11 @@ on 2026-10-03, *which module owns `/etc/resolv.conf`?*, had no place to be looke made and the laptop runs. If `node-uplink` lost `/etc/resolv.conf`, a machine without a resolver of its own would have no owner of the file (against ADR 0223). If `node-resolver` lost it, ADR 0247 would be undone. -5. **A seat's paths as a column of the store's seat table.** Rejected. Whether a seat is replicated, what it - receives and which seats it needs are compiled with the set and never stored, because each is the mesh's - definition of the role and changes what every holder must satisfy - ([ADR 0220](0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)). A path - is the same kind of fact. A row edited without a release would make claims that were valid refused, with +5. **A seat's paths as a column of the store's seat table.** Rejected. Whether a seat is replicated and what + it receives are compiled with the set and never stored, as the seats a seat needed were while that field + existed ([ADR 0220](0220-what-a-machine-asks-needs-its-uplink-held-and-the-retired-resolver-pieces-go.md)), + because each is the mesh's definition of the role and changes what every holder must satisfy. A path is + the same kind of fact. A row edited without a release would make claims that were valid refused, with nothing in review. 6. **Check it only at the merge gate.** Rejected as the only place. The rule is a judgement over the whole catalogue, which is what registration judges (`CatalogueProblems`). The merge gate already runs @@ -110,20 +111,25 @@ a seat or a seat's need is. **3. A module holds the seat only if it declares every path the seat names.** It declares it as a resource or as a file the mesh renders for it, which is how the uplink and the resolver hold `/etc/resolv.conf`. A claim without them is refused by `CanHold`, the one judgement of whether a module can hold a seat (ADR 0131), -at registration and at handover, naming the seat and the missing paths. All four modules that hold one of +at registration and at handover, naming the seat and the missing paths. A seat a module declares for itself +is held to the same rule by the check that already judges what such a seat's holder serves +(`CatalogueProblems`). All four modules that hold one of these seats today declare what the table names, so the rule refuses nothing registered on 2026-10-10. -**4. Only a claimant declares a seat's path.** A module that does not claim the seat and declares a path the -seat names is refused at registration, naming the seat. The same refusal applies at composition to what the +**4. Only a claimant declares a seat's path.** A module that claims no seat naming a path, and declares that +path, is refused at registration, naming the seats that name it. A module claiming any one of them may declare +it, which is how both the uplink and the resolver declare `/etc/resolv.conf`. The same refusal applies at composition to what the controller generates (ADR 0222 §4), so the controller cannot write a seat's path either. Reading a seat's path is access, and stays allowed. **5. No two seats name one path, unless one steps back for the other by definition.** A seat may name a path -*stepping back for* another node seat: where that other seat is held on the machine, its holder owns the path -and this seat's holder does not declare it there. Every path has exactly one seat naming it without stepping -back, and the stepping back does not cycle. This makes the existing rule of ADR 0247 part of the definition, -in the place `checkResources` applies it today. A module declaring a seat that names a path another seat -already names is refused at registration, and the second declaration loses, as for a seat's name. +*stepping back for* another node seat: where that other seat is held on the machine, its holder owns the path, +and this seat's holder still declares it, but its declaration is not composed on that machine. Every path has +exactly one seat naming it without stepping back, and the stepping back does not cycle. This makes the +existing rule of ADR 0247 part of the definition, in the place `checkResources` applies it today. Only the +mesh's own seats step back, each by a record. A module declaring a seat that names a path another seat +already names, stepping back or not, is refused at registration, and the second declaration loses, as for a +seat's name. **6. The seat listing shows each seat's paths.** *Which module owns `/etc/resolv.conf` on a machine?* is answered by the controller's `seats` verb: the seat that names the path, and that seat's holder on the @@ -158,7 +164,7 @@ All in mesh-controller, built under issue 412: | Only a node seat names paths, absolute and without a placeholder | a unit test over the compiled set; a registration test refusing a module's mesh seat that names a path and a path with `${` | | Every path has one seat naming it without stepping back, and stepping back does not cycle | a unit test over the compiled set; a registration test refusing a module's seat that names `/etc/hosts` | | A claim without the seat's paths is refused, naming them | `CanHold` tests at registration and at handover, with a holder declaring the path as a resource, as a rendered file, and not at all | -| Only a claimant declares a seat's path | a registration test refusing a module that declares `/etc/hosts` without claiming `node-hostname`; a composition test refusing a generated resource on a seat's path | +| Only a claimant of a seat naming the path declares it | a registration test refusing a module that declares `/etc/hosts` without claiming `node-hostname`, and one accepting the uplink and the resolver both declaring `/etc/resolv.conf`; a composition test refusing a generated resource on a seat's path | | The step-back comes from the seat | the existing composition tests for the uplink and the resolver on one machine pass with the special case removed | | The listing shows the paths | the `seats` command test | | The catalogue as it is passes | `CatalogueProblems` over the catalogue's main gives no new problem; the merge gate runs it on every catalogue pull request | @@ -169,7 +175,7 @@ All in mesh-controller, built under issue 412: effort this graduates. - [26 — The seats](../03-DESIGN/01-to-be/26-the-seats.md), the design amended. - mesh-controller main at `c06a2cd`: `internal/catalogue/seats.go` (`Seat`, `CanHold`), - `internal/catalogue/resolve.go` (`checkResources`, `steppedBack`), + `internal/catalogue/resolve.go` (`checkResources`), `internal/catalogue/node_resolver.go` (`steppedBack`), `internal/catalogue/seats_declared.go` (`CatalogueProblems`), `cmd/mesh-controller/merge_gate.go`. - mesh-catalog main at `61dcdf0`: the manifests of `hostname`, `networkmanager`, `systemd-networkd` and `systemd-resolved`. diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 06543a12..5dc56c72 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -264,18 +264,22 @@ is compiled with the set and never stored, like what it needs, and changing it i **Holding the seat means declaring its paths.** A module that claims the seat declares every path it names, as a resource or as a file the mesh renders for it. `CanHold` refuses a claim without them, at registration -and at handover, naming the missing paths, beside the verbs it already requires. +and at handover, naming the missing paths, beside the verbs it already requires. A seat a module declares +for itself is held to the same rule by the registration check that judges what its holder serves. -**Only a claimant declares a seat's path.** Any other module declaring it is refused at registration, and +**Only a claimant declares a seat's path.** A module that claims no seat naming the path, and declares it, is +refused at registration, naming the seats that name it, and what the controller generates is refused at composition by the same rule, which is how the controller is held to writing no file a seat's holder owns. Reading the path is access and stays open to every module. **One seat per path, with a declared step-back.** Every path has exactly one seat naming it outright. A second seat may name it only *stepping back for* that seat: where the other seat is held on the machine, its -holder owns the path, and the stepping-back seat's holder does not declare it there. The uplink steps back +holder owns the path, and the stepping-back seat's holder still declares it, but its declaration is not +composed on that machine. The uplink steps back for a machine's own resolver this way ([ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md)), and the collision check reads that from the seat rather than from a case of its own. A module declaring a -seat that names a path another seat already names is refused at registration. +seat that names a path another seat already names is refused at registration: only the mesh's own seats +step back, each by a record. **The listing shows each seat's paths**, so the controller's `seats` verb answers who owns a path on a machine: the seat that names it, and that seat's holder there. @@ -339,7 +343,7 @@ checked as their tables say: | A replicated seat has every holder on record and composes on each; a seat held once still refuses a second holder, on record or not; `--add` is refused for it | 0223: resolution and composition tests with two holders on record and a third machine, with two claimants and nothing on record, and on `mesh-store`; a unit test that only `mesh-dns-resolver` is replicated, surviving the store's rows; store tests keeping several holders once each, a handover leaving one, and unassigning one taking only its row. | | A retired seat leaves the set once nothing claims it, from the compiled set and the store's table both | 0220: the closed-set test's count, and the store migration that deletes the row. | | A node seat names only literal absolute paths; every path has one seat naming it outright, and stepping back does not cycle | 0287: a unit test over the compiled set; registration tests refusing a module's seat naming a path at mesh scope, with `${`, or already named by another seat. | -| A holder declares every path its seat names, and only a claimant declares one | 0287: `CanHold` tests at registration and handover (declared as a resource, as a rendered file, not at all); a registration test refusing `/etc/hosts` from a module not claiming `node-hostname`; a composition test refusing a generated resource on a seat's path. | +| A holder declares every path its seat names, and only a claimant declares one | 0287: `CanHold` tests at registration and handover (declared as a resource, as a rendered file, not at all); a registration test refusing `/etc/hosts` from a module not claiming `node-hostname`, and one accepting the uplink and the resolver both declaring `/etc/resolv.conf`; a composition test refusing a generated resource on a seat's path. | | The uplink's step-back comes from the seat | 0287: the composition tests for the uplink and the resolver on one machine pass with the case in the collision check removed. | | 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. |