diff --git a/00-META/how-we-build.md b/00-META/how-we-build.md index ddeed6b..4f1c98f 100644 --- a/00-META/how-we-build.md +++ b/00-META/how-we-build.md @@ -41,7 +41,7 @@ incident behind it is not written down, and the fix is to write it down, not to | **Never bypass the pipeline** | No manual database edit, no manual restart as a workaround. Fix the cause and deploy. A workaround that works is a workaround that is never removed, and the next person cannot tell the node from its declaration. | | **Never create a symlink** | A hand-made link caused production data loss through container volume resolution, and the judgement needed to make a safe exception is exactly the judgement unavailable at the moment it matters. **The mesh creates none at all** ([ADR 0018](../02-DECISIONS/0018-the-mesh-creates-no-symlinks.md), which supersedes [ADR 0011](../02-DECISIONS/0011-the-installer-owns-linking.md)). The links the installer still reconciles are a migration, not a permission. | | **Never push directly to the main branch** | Branch, push, review, merge. Every merge is a human checkpoint, without exception — **including in this repository**. A documentation repository is not a lower tier of care; a decision record lands the same way a service does. | -| **One change per pull request, and never merge your own** | Unrelated improvements bundled together cannot be reviewed or reverted separately. Self-merging removes the checkpoint that is the entire point. | +| **One change per pull request, and never merge unapproved work** | Unrelated improvements bundled together cannot be reviewed or reverted separately. And the checkpoint is **a person deciding, not a person clicking** — work may be merged by whoever wrote it once a human has explicitly approved *that merge*, and never on a standing permission, an instruction to do the work, silence, or the author's own judgement that it is ready. [ADR 0042](../02-DECISIONS/0042-approval-is-the-checkpoint.md) | | **Never open a pull request unprompted** | A permissions list saying it is allowed is not a request. | | **A failed step fails the job** | A sequence that continues past a failure does the next thing in the wrong place. Gate each step on the last. [ADR 0008](../02-DECISIONS/0008-a-failed-step-fails-the-job.md), and §5. | diff --git a/02-DECISIONS/0042-approval-is-the-checkpoint.md b/02-DECISIONS/0042-approval-is-the-checkpoint.md new file mode 100644 index 0000000..a4e25fa --- /dev/null +++ b/02-DECISIONS/0042-approval-is-the-checkpoint.md @@ -0,0 +1,77 @@ +--- +status: accepted +date: 2026-08-26 +deciders: jochen +reconstructed: false +--- + +# 42. The approval is the checkpoint, not the second pair of hands + +## Context + +[§2](../00-META/how-we-build.md) reads *"one change per pull request, and never merge your +own"*, and gives the reason: *"self-merging removes the checkpoint that is the entire point."* + +The rule was written for people. Applied literally to an agent it produces a contradiction +that surfaced immediately: an agent asked to merge cannot merge, because it authored what it +is being asked to merge. Every merge this repository has seen was therefore either performed +by the thing that wrote it, or not performed at all. + +Stated by the operator: *"you are allowed to self-merge, after I've given you explicit +approval."* + +## Considered options + +1. **Read it literally.** An agent never merges; the operator merges by hand every time. + Rejected: it makes the operator a mechanical step rather than a reviewer, and a step + performed dozens of times without reading is not a checkpoint — it is the *shape* of one, + which is worse, because the record then claims a review that did not happen. +2. **Drop the rule for agents.** Rejected: the rule is right, and the failure it prevents — + work merged with nobody having looked — is not one an agent is less prone to. +3. **Require notification and approval, and stop there.** Chosen. Who performs the merge is + not the thing worth constraining. + +## Decision + +**Every merge into the main branch is notified and approved.** Stated by the operator in +exactly those terms, and the whole of the rule. + +Notified: the merge is proposed and said out loud, not performed and mentioned. Approved: a +person says yes to *that merge*. Who then performs it does not matter, which is what makes an +agent merging its own work unremarkable — the checkpoint already happened. + +**What approval is not**, because this is the half that can rot: + +- A standing permission granted once and cited forever. +- An instruction to do the work, read as approval to merge it. +- Silence. +- The author's own judgement that the work is ready. + +## Consequences + +- **The record must show which it was.** A merge performed on approval and a merge performed + unilaterally look identical afterwards, and the difference is the whole rule. Until the forge + can record an approval, the merge commit says so — which is weaker than a recorded review and + is stated here rather than left to be discovered. +- **Branch protection would make this structural instead of textual.** Required approvals turn + the rule into something the forge enforces. It cannot be set through the API on the forge + version in use, so this remains a rule held by intention — the condition + [§5](../00-META/how-we-build.md) exists to name. +- **The one-change-per-pull-request half is untouched**, and was broken twice on 2026-08-26. + Both breaches are recorded in the pull requests themselves rather than in a conversation. +- **This narrows a rule rather than relaxing one.** The obligation moves from *who performs the + merge* to *whether a person decided*, which is a higher bar in the case the original wording + actually permits: a reviewer who merges someone else's work without reading it satisfies the + old rule and fails this one. + +## Sync + +Run and verified 2026-08-26. §2 in the enforced page now reads *never merge unapproved work*, +confirmed by reading the rule back out of the live page rather than by trusting the publish — +the previous sync reported success and changed nothing. + +## References + +- [`how-we-build.md`](../00-META/how-we-build.md) §2 — the rule, now carrying this. +- [ADR 0008](0008-a-failed-step-fails-the-job.md) — the standard a checkpoint is held to: a + step that reports success without doing anything is the fault, not the shortcut. 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..d11120d --- /dev/null +++ b/02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md @@ -0,0 +1,140 @@ +--- +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. + +## Where the list comes from + +This record specifies what the host **accepts**. What produces a declaration is deliberately +not settled here, and the reason is worth stating rather than leaving as an omission. + +**Today, and at stage 2: by hand.** `substrate.lock` is authored and pinned — a person writes +the resources and writes the order. That is the first node's path, where there is no control +plane to derive anything from. + +**Afterwards: the control plane derives it**, from three things it already holds — which +modules are assigned to this node, what those modules' configuration resolves to, and what each +module declares it needs. + +**And the order comes from the graph.** Each module expands to resources; the modules are +ordered by their declared dependencies on one another. That is +[research 011](../01-RESEARCH/011-the-module-graph/00-overview.md) — `requires`, `provides`, +`excludes` — and a declaration is the graph's output, flattened for one node. + +So this record is complete on the consumer side and silent on the producer side, because the +producer does not exist and its shape is what 011 is investigating. The consumer can be settled +first because the host must refuse what it does not understand whoever wrote it. + +**What this means for ordering.** [ADR 0037](0037-the-host-applies-it-does-not-decide.md) puts +the ordering decision in the control plane; 011 decides how the control plane makes it. If the +graph turns out not to determine a total order, that is 011's problem to solve and not the +host's — the host will still be handed a list, and will still apply it as given. + +## 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`;