diff --git a/02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md b/02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md new file mode 100644 index 0000000..934c4c1 --- /dev/null +++ b/02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md @@ -0,0 +1,113 @@ +--- +status: accepted +date: 2026-08-26 +deciders: jochen +reconstructed: false +extends: 0037-the-host-applies-it-does-not-decide.md +--- + +# 43. A declaration is an ordered list of resources the host owns + +## Context + +[`05-the-node-host.md`](../03-DESIGN/01-to-be/05-the-node-host.md) leaves *what a declaration +is* open and calls it the first thing to settle in build. Stage 2 — applying with no mesh +present — cannot start without it. + +Three constraints already bind it, and between them they decide most of the shape: + +- **Data, not instructions**, with a finite, versioned vocabulary and anything outside it + refused rather than interpreted ([ADR 0039](0039-the-link-is-the-security-boundary.md)). +- **The host applies; it does not decide** ([ADR 0037](0037-the-host-applies-it-does-not-decide.md)). +- **The host depends on nothing** ([ADR 0041](0041-the-host-depends-on-nothing.md)). + +## Decision + +### JSON, because the host has no dependencies to spend + +Go's standard library carries `encoding/json` and no YAML. A YAML declaration would put a +third-party parser inside the one binary whose entire argument is that it needs nothing — to +gain authoring comfort in a document that is, in the ordinary case, generated by a machine and +read by a machine. + +The mesh's *authoring* formats stay YAML. What crosses the link is JSON. + +### An ordered list, because ordering is a decision + +A declaration states the order its resources are applied in. The host does not sort, does not +resolve dependencies, and does not decide what must come before what. + +This follows from [ADR 0037](0037-the-host-applies-it-does-not-decide.md) more strictly than it +first appears. A host that derived ordering from declared dependencies would be **deciding**, +and it would be deciding the thing most likely to differ between what the control plane +intended and what the machine does. The control plane knows what depends on what; it says so by +saying when. + +Consequence accepted: the control plane must order correctly, and a mis-ordered declaration +fails at the step that needed something not yet there — which is at least the *right* failure, +naming the resource rather than a mystery. + +### Every resource has a stable identity + +Not a position, not a hash of its content: a name the control plane keeps stable across +declarations. It is what lets the store say *this is the same resource I applied last time*, +which is what makes convergence possible at all. + +### Unknown is refused, never skipped + +An unknown declaration version, an unknown resource type, or an unknown field is a **refusal of +the whole declaration**. Not a warning, not a skip, not best-effort. + +A host that skipped what it did not understand would apply most of a declaration and report +success — a node that looks configured and is not, which is +[04-ISSUES/003](../04-ISSUES/003-firewall-scope-is-read-by-no-code/00-report.md) with the +declaration on the other side of the wire. Refusing whole also means an older host cannot be +handed a newer vocabulary and quietly do half of it. + +### Complete for what the host owns, and only that + +*Desired state* invites the question of removal, and the honest answer needs a boundary. + +**The host removes what it previously applied and is no longer declared.** It knows what it +applied because it recorded it (`store`), so this is a fact it holds rather than an inference. + +**The host never removes anything it did not create.** A machine has things on it that the mesh +did not put there, and a converger that treats *not declared* as *must not exist* deletes them. +The rule that prevents production data loss elsewhere in this repository is the same one: +[ADR 0018](0018-the-mesh-creates-no-symlinks.md) exists because a tool did something to a path +it did not own. + +So: authoritative over its own footprint, inert everywhere else. + +### Addressed, and checked when it can be + +A declaration names who it is for. A host that has an identity refuses one addressed elsewhere. +A host that has no identity yet — the first node, applying the bundle it carries — has nothing +to check against and applies it. + +## Consequences + +- **Ordering is now a control-plane responsibility**, and getting it wrong is a class of bug + that will appear. It is the correct place for it: the alternative puts a dependency solver in + tier 0 and a decision in the wrong tier. +- **The vocabulary is a security artefact.** Every type added widens what a compromised control + plane can express, so additions are reviewed as such rather than as features. +- **Removal is bounded but not free.** A resource dropped from a declaration is deleted on the + next apply, so removing a line is an act with an effect — which is the point, and is worth + saying out loud because it does not look like one. +- **The store becomes load-bearing at stage 2**, earlier than the build order suggests. Nothing + can be removed without knowing what was applied, so the record of applied resources arrives + with the first apply rather than with the link. +- **A closed address space bounds what the first types can be.** A scenario has no route to a + package repository, so a declaration whose resources must be fetched cannot be applied in the + lab at all. The first vocabulary is therefore what needs no network — files, directories, + service state — and packages and containers wait on *where `place:` gets its artifacts from*, + which is open in + [`02-scenario-declaration.md`](../03-DESIGN/01-to-be/02-scenario-declaration.md). + +## References + +- [ADR 0037](0037-the-host-applies-it-does-not-decide.md) — why ordering is not the host's. +- [ADR 0039](0039-the-link-is-the-security-boundary.md) — bounded by form. +- [ADR 0041](0041-the-host-depends-on-nothing.md) — why JSON. +- [ADR 0008](0008-a-failed-step-fails-the-job.md) — why a refusal is whole. 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 db149f3..df3ed70 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -11,6 +11,7 @@ decisions: - 02-DECISIONS/0039-the-link-is-the-security-boundary.md - 02-DECISIONS/0008-a-failed-step-fails-the-job.md - 02-DECISIONS/0041-the-host-depends-on-nothing.md + - 02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md --- # The node host @@ -132,15 +133,23 @@ the mesh, and the full peer set arrives derived. ## What a declaration is -**Open, and the first thing to settle in build.** The shape is constrained but not chosen: +Settled by [ADR 0043](../../02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md). -- It is data, not instructions — the host's vocabulary is finite, versioned and auditable, and - anything outside it is refused rather than best-effort interpreted. -- It is per-node and complete: what this machine should be, not a delta against what it was. - A delta requires the sender to know what the receiver holds, which is the coupling the store - exists to remove. -- Every addition to the vocabulary widens what a compromised control plane can express, so it - is a security artefact and additions are reviewed as such. +**JSON**, because the host has no dependencies to spend and the standard library carries no +YAML. **An ordered list of typed resources**, each with a stable identity — the order is stated +rather than derived, because deriving it would be the host deciding the thing most likely to +differ from what the control plane intended. + +**Unknown is refused, never skipped.** An unknown version, type or field refuses the whole +declaration. A host that skipped what it did not understand would apply most of it and report +success. + +**Complete for what the host owns, and only that.** It removes what it previously applied and +is no longer declared — a fact it holds, from the store, rather than an inference — and never +removes anything it did not create. + +**Addressed.** A host with an identity refuses a declaration addressed elsewhere; a host +without one, applying the bundle it carries, has nothing to check against. ## Build order @@ -154,6 +163,12 @@ what it is. No control plane, no declarations, no network. Verifiable immediatel the first node's path, and it is the claim the skeleton's Move 1 rests on and has never proved: that one host can raise the substrate alone. +The first vocabulary is bounded by something the lab makes unavoidable: **a scenario is a +closed address space**, so a resource that must be fetched cannot be applied there at all. So +stage 2 begins with what needs no network — files, directories, service state — and the types +that need artifacts wait on where those come from, which is open in +[`02-scenario-declaration.md`](02-scenario-declaration.md). + **3 — link and store.** The node connects, receives declarations, and holds what it applied. **4 — enrolment.** The one genuinely new mechanism in @@ -182,7 +197,6 @@ Each decision above owes a test: ## Open -- **What a declaration is.** Above; the first thing to settle. - **Whether one host can raise the substrate alone.** Move 1 assumes it. Stage 2 tests it, and if it is false the tier boundary moves. - **What the host carries versus what it finds.** It manages `wg`, `nft`, `pacman`, `docker`;