ADR 0043 — what a declaration is

Stage 2 could not start without it. Three constraints already bound the shape
and between them they decide most of it.

JSON, because the standard library carries it and carries no YAML, and a YAML
declaration would put a third-party parser inside the one binary whose whole
argument is that it needs nothing — to gain authoring comfort in a document
generated by a machine and read by a machine.

An ordered list, because ordering is a DECISION. A host deriving order from
declared dependencies 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.

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 a declaration and report success, which is 04-ISSUES/003 with the
declaration on the other side of the wire.

Complete for what the host OWNS, and only that. It removes what it previously
applied and is no longer declared, which it knows from the store rather than by
inference, and never removes what it did not create — a converger that treats
'not declared' as 'must not exist' deletes what the mesh never put there.

Two consequences arriving earlier than the build order suggested: the store is
load-bearing at stage 2, because nothing can be removed without knowing what was
applied. And a closed address space bounds the first vocabulary to what needs no
network, because a scenario has no route to a package repository.
This commit is contained in:
2026-08-26 02:04:41 +02:00
parent 23232e019a
commit bea052753e
2 changed files with 136 additions and 9 deletions
@@ -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.
+23 -9
View File
@@ -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`;