Narrow ADR 0287's claimant rule to any seat naming the path, and correct two facts the review found
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

This commit is contained in:
jochen
2026-10-10 21:57:24 +02:00
parent 6b49ed613c
commit 906d702b8d
3 changed files with 35 additions and 25 deletions
@@ -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.
@@ -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`.
+9 -5
View File
@@ -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. |