diff --git a/01-RESEARCH/012-the-minimum-viable-node/00-overview.md b/01-RESEARCH/012-the-minimum-viable-node/00-overview.md index cb60041..6e580db 100644 --- a/01-RESEARCH/012-the-minimum-viable-node/00-overview.md +++ b/01-RESEARCH/012-the-minimum-viable-node/00-overview.md @@ -190,7 +190,7 @@ running the predecessor is **adopted** when the mesh comes up on it — the pred stopped, the machine's firewall and files kept in force, the mesh opening what it needs through them — and stays adopted while its modules migrate one at a time, until the operator **converges** it. Measured on the control-node, and weighed against the alternatives, in -[*migrating a node that is in use*](migrating-a-node-in-use.md); proposed as +[*migrating a node that is in use*](migrating-a-node-in-use.md); decided in [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md). ## Open questions @@ -204,7 +204,7 @@ it. Measured on the control-node, and weighed against the alternatives, in | Can a module say which of its settings are load-bearing? | The question that dissolves the conflict rule rather than choosing a side. A setting the module *requires* cannot be kept from the machine without producing something installed and broken; a setting it merely *prefers* should always yield. Until a module can say which is which, adoption is defaulting in the dark. Belongs with the graph. | | Does a `failed` line still let adoption complete? | *Flags inform, they do not block* was decided about conflicts, where the mesh chose and the machine works. A failure is *we could not*, which is different in kind — and treating them alike hides the worse one behind the commoner one. | | How is a flagged conflict reconciled, and by whom? | The briefing hands it to a session. What that session is empowered to change, and whether the resolution is recorded so the next adoption does not re-raise it, is undecided. | -| ~~How long is a machine adopted?~~ | **Proposed** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)) — for as long as it is being migrated: a mode per node, recorded, ended by an explicit and previewed flip. | +| ~~How long is a machine adopted?~~ | **Decided** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)) — for as long as it is being migrated: a mode per node, recorded, ended by an explicit and previewed flip. | | Where does the kept original live, and for how long? | Whether it is recorded in the node's state so adoption is visibly reversible, and whether it is returned when the mesh stops managing the thing. | | What shape is a briefing? | Structured enough to be acted on, prose enough to be read. It is the first thing a session on a new node sees, which makes it an interface rather than a log. | | Does owning a package mean owning its version? | Owning configuration and owning the package are different scopes. The second means the mesh decides which version is installed, and that decision then has to survive the machine's own package manager updating it. | diff --git a/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md b/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md index 8638fb4..21520b3 100644 --- a/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md +++ b/02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md @@ -1,6 +1,6 @@ --- topic: the mesh -status: proposed +status: accepted date: 2026-09-22 deciders: jochen reconstructed: false diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 5de935a..e928743 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -88,7 +88,7 @@ python3 00-META/checks/index.py fail if stale - **0083** — [One push leaves the mesh consistent](0083-one-push-leaves-the-mesh-consistent.md) - **0088** — [The foundation filters before anything listens](0088-the-foundation-filters-before-anything-listens.md) - **0090** — [A failure that repeats is said to be stuck](0090-a-failure-that-repeats-is-said-to-be-stuck.md) -- **0100** — [A node in use is adopted before it is converged](0100-a-node-in-use-is-adopted-before-it-is-converged.md) *(proposed)* +- **0100** — [A node in use is adopted before it is converged](0100-a-node-in-use-is-adopted-before-it-is-converged.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/05-the-node-host.md b/03-DESIGN/01-to-be/05-the-node-host.md index 7c0aecf..e77c3d4 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-host] -updated: 2026-09-21 +updated: 2026-09-22 decisions: + - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0019-how-this-repository-works.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0005-the-node-host.md @@ -130,6 +131,19 @@ the link; never asked downward. [`09-the-node-lifecycle.md`](09-the-node-lifecycle.md)**, in full and in one place. This document is the component; that one is what happens to it. +## What it finds, on an adopted node + +*2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* A declaration says whether the node is adopted. On an adopted node +the host does not replace a file it finds at a declared path: it records what was there — its +content, so the original is kept before anything is written — and holds the file as found. A +module's files are converged when that module is assigned, because assigning a module is +migrating it; the rest when the node is converged. The host's report says which files it is +holding rather than converging, so an adopted node never reads as converged. This is the +companion the host's ownership rule needed: *never touch what you did not create, unless adoption +made it yours — and while the node is adopted, keep it as you found it.* *How it is checked:* +unit tests hold the host to keeping a found file on an adopted node and replacing it on a +converged one, and the adoption bed asserts a found file byte for byte unchanged. + ## Where a declaration comes from One behaviour, two sources diff --git a/03-DESIGN/01-to-be/08-connectivity.md b/03-DESIGN/01-to-be/08-connectivity.md index 7598b7d..4ee6945 100644 --- a/03-DESIGN/01-to-be/08-connectivity.md +++ b/03-DESIGN/01-to-be/08-connectivity.md @@ -7,8 +7,9 @@ code: - mesh-controller internal/identity/authority.go - mesh-host internal/identity/serving.go - mesh-host internal/apply (the service that reflects a rule set) -updated: 2026-09-21 +updated: 2026-09-22 decisions: + - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0099-a-step-that-runs-once-names-what-it-reads.md - 02-DECISIONS/0098-a-fact-a-provider-makes-at-first-start-is-fetched-from-it.md - 02-DECISIONS/0005-the-node-host.md @@ -532,6 +533,20 @@ undeclared one does not — then the module is removed and the port closes with rule. *A rule set that is written but never loaded passes every check that reads the file, which is why the check reads packets.* +### On an adopted node + +*2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* **The firewall found on the machine stays in force**, and the mesh +loads no table of its own there — neither genesis's base ruleset nor the derived one. The kernel +runs every table at a hook and a drop in any is final, so a second, stricter table would close +every port the machine serves. What the mesh needs reachable — its foundation's ports and each +migrated module's `listens` — it opens *through the found firewall*, as rules marked as the mesh's, +and it removes only rules it marked. Converging the node replaces the found firewall with the +derived filter in one step, after previewing which open ports will close. The mesh must speak the +found firewall in its own terms; one kind is found on the machines measured, and a machine with +another is not covered until someone writes for it. *How it is checked:* the adoption bed asserts +the found firewall's rules differ only by the mesh's marked rules, that a second machine enrols +through them, and that after the flip the declared port is open and the undeclared one closed. + ## 5 — Certificates **Two authorities, kept separate on purpose.** diff --git a/03-DESIGN/01-to-be/09-the-node-lifecycle.md b/03-DESIGN/01-to-be/09-the-node-lifecycle.md index 3918924..31fc0d2 100644 --- a/03-DESIGN/01-to-be/09-the-node-lifecycle.md +++ b/03-DESIGN/01-to-be/09-the-node-lifecycle.md @@ -8,8 +8,9 @@ code: - mesh-host packaging/nox-mesh-host-network.sh - mesh-controller internal/token - mesh-controller internal/inventory/nodes.go -updated: 2026-08-31 +updated: 2026-09-22 decisions: + - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0005-the-node-host.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md @@ -282,8 +283,23 @@ runs. Building the start mechanism before deciding that would be building it for ## Adoption: what happens to what is already there -Adoption is not a state. It is what the **first apply** does when it is told to own something a -machine already has ([research 012](../../01-RESEARCH/012-the-minimum-viable-node/00-overview.md)). +**An enrolled node is adopted or converged, and the mesh records which** ([ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md)). +This was once said the other way — adoption as only what the first apply does — and a machine +being migrated from a mesh already running on it made the middle state last: while its services +move one at a time, what it already has must stay in force. So *adopted* is a second axis of an +enrolled node, not a step on the path above: a machine in use is enrolled adopted, and stays so +until its migration is done and the operator converges it. A machine that was empty is enrolled +converged, as before +([research 012](../../01-RESEARCH/012-the-minimum-viable-node/00-overview.md)). + +On an adopted node the firewall found there stays in force and the mesh opens what it needs +through it ([08-connectivity](08-connectivity.md)); a file found at a path the mesh declares is +kept until the module declaring it migrates ([05-the-node-host](05-the-node-host.md)). +**Converging is one act per node, previewed**: it lists every port the found firewall allows and +whether an assigned module declares it or it will close, then loads the mesh's own filter, retires +the found one and converges what was kept. *How it is checked:* a lab bed prepares a machine the +way a predecessor leaves one and asserts nothing on it changes until the flip, and that the flip +closes exactly what the preview said. A candidate machine is not empty. It has a package manager, probably a container runtime, configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) diff --git a/03-DESIGN/01-to-be/17-raising-a-mesh.md b/03-DESIGN/01-to-be/17-raising-a-mesh.md index 7ded49a..09c25c2 100644 --- a/03-DESIGN/01-to-be/17-raising-a-mesh.md +++ b/03-DESIGN/01-to-be/17-raising-a-mesh.md @@ -5,8 +5,9 @@ code: - mesh-host cmd/mesh-bootstrap - mesh-host internal/bootstrap - mesh-lab test/integration/whole-mesh-full.test.ts -updated: 2026-09-21 +updated: 2026-09-22 decisions: + - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0067-genesis-is-a-pivot.md - 02-DECISIONS/0069-a-module-is-a-repository-and-a-path.md - 02-DECISIONS/0006-the-substrate-and-the-control-plane.md @@ -97,6 +98,20 @@ the temporary one.** The pivot is complete: what raised the mesh is gone, and wh like any other. From here the mesh can build and roll out its own upgrades, including to the thing that runs it. +### Genesis on a machine in use + +*2026-09-22, [ADR 0100](../../02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md).* A control-node that is already running a predecessor mesh is raised +**adopted**. The predecessor's control on it is stopped first — the daemons that write its +configuration — and its services keep running. **Every port the foundation binds is an input to +genesis**, not a constant in the bundle or the catalogue, and genesis checks each is free before it +raises anything, refusing with the name of what holds it. On such a machine the store's and the +bus's usual ports were free and the registry's, the broker's management port and the private +network's port were held. Genesis adopted **does not load the base ruleset**: the machine's own +firewall already filters, and the mesh opens its foundation's ports through it +([08-connectivity](08-connectivity.md)). *How it is checked:* the adoption bed raises genesis +with the registry's port held and asserts the refusal names its holder, then with another port +given asserts the foundation comes up and the machine's service is still reachable. + ## After the pivot, and still part of installing Genesis ends with a mesh of one that runs, and that is not the same as a mesh that works. What it