ADR 0287: a seat names the paths its holder owns, so a file's owner is read from the seat (issue 412, research 023)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
+5
@@ -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.**
|
||||
|
||||
+176
@@ -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.
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user