From 818458521337731566be05e36a88f3749891bcf6 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 23:19:36 +0200 Subject: [PATCH] ADR 0199: a module that answers names declares its zone, and a node's hosts file is one module's The per-node resolvers 0194 retires held two kinds of names that are neither nodes nor routes: the lab's scenario machines and an operator's own lines. A zone a module declares is forwarded by the mesh's resolver to that module; /etc/hosts is held per node through node-hosts-file, the operator's lines in its kept region. Research 023 parks seats that define what their holder owns. --- .../00-overview.md | 37 +++++ ...e-and-a-nodes-hosts-file-is-one-modules.md | 127 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/08-connectivity.md | 13 ++ 03-DESIGN/01-to-be/26-the-seats.md | 2 + 5 files changed, 180 insertions(+) create mode 100644 01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md create mode 100644 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md 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 new file mode 100644 index 0000000..7bc80c1 --- /dev/null +++ b/01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md @@ -0,0 +1,37 @@ +--- +status: active +initiated: 2026-10-03 +touches: [the seats, the seat protocol, the controller's ownership check, 03-DESIGN/01-to-be/26-the-seats.md] +--- + +# 023 — A seat protocol that defines what its holder owns + +## What is investigated + +**A seat is a definition — a protocol — and a module occupies it by implementing that protocol.** +Today the protocol is what the holder accepts, emits and serves (ADR 0118, 0129, 0132): its verbs, as MCP +tool definitions. This asks whether the protocol should also name the **files and directories the +holder owns**, so that occupying the seat means owning them: `node-resolver-config` owns +`/etc/resolv.conf`, `node-hosts-file` owns `/etc/hosts`, the intrusion prevention owns its jail file. + +The direction is the protocol's, not the holder's: the seat states what any holder must own; a module +that wants the seat must declare those paths among its resources, or the controller refuses the claim +as not implementing the seat. Two seats may not name one path. + +## Why + +Who owns a singular file is today answered by reading every manifest, and enforced only after the fact, +when two modules on one machine both declare the same path. The question *which module owns +`/etc/resolv.conf`?* came up on 2026-10-03 with no place to look it up. A seat that names the path answers +it from the seat table, before any module is written, and makes "implements the seat" checkable. + +## What it touches + +- The seat definition and its table (ADR 0122) — a new part of the protocol. +- The controller's ownership check (`checkResources`), which already refuses two modules owning one path. +- Every node seat that is really about a file: `node-resolver-config`, `node-hosts-file` + ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)), + `node-intrusion-prevention`, `node-packet-filter`. + +Raised by the operator during the resolver work of ADRs 0194–0199 and parked there so that work was not +widened by it. 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 new file mode 100644 index 0000000..e4679ff --- /dev/null +++ b/02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md @@ -0,0 +1,127 @@ +--- +topic: the tiers +status: accepted +date: 2026-10-03 +deciders: jochen +reconstructed: false +extends: 0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md +--- + +# 199. A module that answers names declares its zone, and a node's hosts file is one module's + +## Context + +**[ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md) and +[ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) leave +one resolver holding the nodes' internal domains, and retire the resolver every node ran.** Two kinds +of names lived in those per-node resolvers that are neither a node nor a route, and both were found on +the workstation on 2026-10-03: + +- **Names a module answers.** The lab raises scenario machines and gives them addresses from its + scenario files — the anchor's stand-in at a documentation address, the home server's on the LAN — + and the workstation resolved `.incus` through two wildcard lines in a drop-in file its + resolver read. The lines were written by hand; the addresses are the lab's, known only while a + scenario runs. +- **The operator's own names, unrelated to the mesh.** Twelve ` ` lines for a + client's development hosts, kept in `/etc/hosts` and again in `/etc/hosts.local`, which the per-node + resolver read as additional hosts. + +**A manifest never names an address, a node or a domain** ([ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md)). +So the lab cannot list `.incus →
` in its definition, and the operator's twelve lines +are not any module's to define. + +## Considered Options + +**For a module's names:** + +1. **The manifest lists its records.** Refused by ADR 0112: the addresses are the lab's runtime facts + and the scenario's choice. +2. **The module reports its records at runtime to the mesh's resolver**, which writes them into its + configuration. It works, and it makes the resolver hold every module's runtime state and decide, + per call, whether the caller may write the name it sent — authorisation for a write, on the one + server every node depends on. +3. **The module declares the zone it answers and the listen that answers it; the mesh's resolver + forwards that zone there.** The definition names a zone (from a setting) and one of its own listens, + which ADR 0112 allows; the address and the port are the mesh's facts. The records stay where they + are known — in the module, at runtime. Chosen. + +**For the operator's names:** + +1. **Records the controller holds, served by the mesh's resolver.** They are not the mesh's: a client's + development hosts on one machine are nothing any other node should resolve, and the controller would + become the keeper of a workstation's private notes. +2. **A node-scoped module owns `/etc/hosts`, and the operator's lines live in its kept region** + ([ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md)), changed + through that module's tools on that machine. Chosen. + +## Decision + +**1. A module that answers names declares a zone.** Its definition names the zone — a single label or a +dotted name, from a setting, never a domain the mesh knows — and the listen that answers DNS for it. +The controller refuses two modules in the mesh declaring one zone, and a zone that is the mesh's suffix, +under it, or one of a node's public domains: a module may not shadow names the mesh or the public DNS +answers. + +**2. The mesh's resolver forwards each zone to the module that declared it.** The controller hands the +holder of `mesh-dns-resolver` every declared zone with the private address of the node its module runs +on and the port that listen is published on; the holder places one forwarding rule per zone into its +configuration and answers nothing in that zone itself. What names exist in the zone, and their +addresses, are the module's — answered by its own long-running code +([ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md)), +from its own state, as they change. Whether an answered address is reachable from the asking node is +the module's matter, not the resolver's. + +**3. A node's `/etc/hosts` is held by one module, through a node seat, `node-hosts-file`.** The seat is +the definition: its holder owns `/etc/hosts`, and implements three verbs — MCP tool definitions served +as `/node-hosts-file.` ([ADR 0195](0195-the-meshs-tools-are-found-by-address-not-announced-whole.md)): +**`entries`** (the file's lines, the module's and the operator's, each marked whose), **`add`** (one +address and its names, into the operator's region) and **`remove`** (one name or address from it). The +module writes the machine's own lines — loopback and the machine's name — and keeps a region for the +operator, which survives every push and is given back when the module goes. Its tools change that +region on that machine, escalating as the packet filter's do +([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md) §4). **The +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. + +## Consequences + +- **The lab's names follow its scenarios.** A scenario raised is resolvable from every node at once; a + scenario torn down is gone, with no line left behind in any file. +- **The mesh's resolver holds no module's state.** It holds the nodes' domains and a table of who + answers which zone, both composed by the controller; nothing writes to it at runtime. +- **A module answering a zone needs a DNS answerer of its own** — a long-running bundle, or a resolver + it runs. The lab gains one. +- **The operator's names reach the machine's own programs, not its containers.** A container does not + read the machine's `/etc/hosts`. For names unrelated to the mesh that is the right boundary; a name a + container needs belongs in a zone. +- **Taking `/etc/hosts` keeps what is there.** The first time the module writes the file, every line + that is not the machine's own goes into the operator's region, so a workstation's twelve lines survive + the take — the same adoption [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) gives + every shared file. + +**How each is checked:** + +- **Zones:** the controller's catalogue tests refuse a second module declaring a zone, a zone under the + mesh suffix, and a zone equal to a node's public domain. +- **Forwarding:** on the holder, the resolver's configuration carries one forwarding rule per declared + zone, at the declaring node's private address and published port; asking any node's resolver for a + name in the lab's zone while a scenario runs returns the scenario's address. +- **The hosts file:** a push leaves the operator's region byte for byte; `add` followed by `entries` + shows the line as the operator's; unassigning the module gives the region back. + +## References + +- [ADR 0194](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md), + [ADR 0196](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) — + the one resolver and how nodes ask it. +- [ADR 0112](0112-a-module-definition-names-no-node-mesh-or-path.md) — why a definition names no address. +- [ADR 0174](0174-a-node-varies-a-module-through-settings-and-kept-regions-never-an-edit.md), + [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md) — kept regions and shared files. +- [ADR 0198](0198-a-modules-long-running-code-is-launched-by-the-node-runtime-and-reaches-the-bus-through-it.md) — + where a zone's answerer runs. +- [Connectivity §2](../03-DESIGN/01-to-be/08-connectivity.md) and [the seats](../03-DESIGN/01-to-be/26-the-seats.md), + amended alongside. +- [Research 023](../01-RESEARCH/023-a-seat-protocol-that-defines-what-its-holder-owns/00-overview.md) — + the general form of decision 3's "the holder owns `/etc/hosts`". diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index c645d64..8e0dab9 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -225,6 +225,7 @@ python3 00-META/checks/index.py fail if stale - **0191** — [The mesh's resolver holds only the mesh's own names; a public name resolves publicly](0191-the-meshs-resolver-holds-only-the-meshs-own-names.md) - **0194** — [The mesh has one resolver, and every node asks it for the mesh's names](0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md) - **0196** — [A node asks the mesh's resolver first, and a public one only when it is silent](0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md) +- **0199** — [A module that answers names declares its zone, and a node's hosts file is one module's](0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md) ### What runs on them, and how it gets there diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 98de8d2..bcfa3c9 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -10,6 +10,7 @@ code: - mesh-host internal/apply (the service that reflects a rule set) updated: 2026-10-03 decisions: + - 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md - 02-DECISIONS/0196-a-node-asks-the-meshs-resolver-first-and-a-public-one-only-when-it-is-silent.md - 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md - 02-DECISIONS/0191-the-meshs-resolver-holds-only-the-meshs-own-names.md @@ -377,6 +378,16 @@ member's resolver answers a LAN; a router pointing at one is moved first. *Check `/etc/resolv.conf` naming `mesh-resolver` then a public resolver, by no node but the holder answering DNS on any address, and by the router's DHCP DNS option naming the router.* +**Names that are neither a node nor a route.** A module that answers names declares a zone (a +setting) and the listen that answers it; the controller hands the `mesh-dns-resolver` holder every +zone with its module's node address and published port, and the holder forwards that zone there and +answers nothing in it itself — the lab answers `.incus` for its running scenarios this way. +An operator's own names, unrelated to the mesh, live in `/etc/hosts`'s kept region, held per node by +the `node-hosts-file` seat's holder and changed through its tools; the controller holds none of them +([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)). +*Checked by the holder's configuration carrying one forwarding rule per declared zone, and by a push +leaving the hosts file's operator region byte for byte.* + *What follows describes the per-node resolver this replaces — how it was built and why the roles were split. The split stands; the serving role's scope is what moved.* @@ -1021,6 +1032,8 @@ The list is worth having in one place, because it is most of the argument: - **One resolver ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)).** Not built: every node still runs `node-dns-resolver`. The migration's four steps are in the record, in order. + Nor are zones or the hosts file's holder ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)): the + workstation moves to the one resolver only once both exist, its lab and operator names depending on them. - ~~**What happens when the hub is down.**~~ **Resolved** by [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), together with `06`'s diff --git a/03-DESIGN/01-to-be/26-the-seats.md b/03-DESIGN/01-to-be/26-the-seats.md index 648e48f..4946b92 100644 --- a/03-DESIGN/01-to-be/26-the-seats.md +++ b/03-DESIGN/01-to-be/26-the-seats.md @@ -12,6 +12,7 @@ code: - mesh-catalog modules/gitea/module.json updated: 2026-10-03 decisions: + - 02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md - 02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md - 02-DECISIONS/0161-what-deserves-a-seat.md - 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md @@ -129,6 +130,7 @@ convention, which later seats departed from. | `mesh-build-machine` | `the-build-machine` | node | — | a builder | | `mesh-resolver` | — | mesh | — | the mesh's one resolver, holding every node's internal domain ([ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md)) | | ~~`mesh-dns-port`~~ | `the-dns-port` | node | — | retired by [ADR 0194](../../02-DECISIONS/0194-the-mesh-has-one-resolver-and-every-node-asks-it-for-the-meshs-names.md): the local resolver became the mesh's one | +| `node-hosts-file` | — | node | — | owns `/etc/hosts`: the machine's own lines and the operator's kept region, changed through its verbs `entries`, `add`, `remove` ([ADR 0199](../../02-DECISIONS/0199-a-module-that-answers-names-declares-its-zone-and-a-nodes-hosts-file-is-one-modules.md)) | | `mesh-intrusion-prevention` | `the-intrusion-prevention` | node | — | an intrusion-prevention service | | `mesh-packet-filter` | `the-packet-filter` | node | — | the packet filter | | `mesh-private-network` | `the-private-network` | node | — | the private network the mesh runs over |