From 23232e019a9bf0d9d8a75ddcc4a9e22afea163aa Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 01:08:34 +0200 Subject: [PATCH 1/3] =?UTF-8?q?ADR=200042=20=E2=80=94=20the=20approval=20i?= =?UTF-8?q?s=20the=20checkpoint,=20not=20the=20second=20pair=20of=20hands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §2 said "never merge your own", written for people. Applied to an agent it produced a contradiction that surfaced immediately: an agent asked to merge cannot merge, because it authored what it is being asked to merge. So every merge here was either performed by the thing that wrote it, or not performed. Rejected the literal reading, because an operator clicking merge dozens of times without reading is not a checkpoint — it is the SHAPE of one, which is worse, since the record then claims a review that did not happen. Rejected dropping the rule, because the failure it prevents is not one an agent is less prone to. So the rule names what the checkpoint actually is: a person deciding, not a person clicking. Work may be merged by whoever wrote it once a human has explicitly approved that merge. What "explicit" excludes is the half that can rot, so it is enumerated: a standing permission cited forever, an instruction to do the work read as approval to merge it, silence, and the author's own judgement that it is ready. This narrows rather than relaxes. The obligation moves from who performs the merge to whether a person decided — a higher bar in the case the old wording permits, where a reviewer merges someone else's work without reading it. Synced, and verified by reading the rule back out of the live page rather than by trusting the publish. --- 00-META/how-we-build.md | 2 +- .../0042-approval-is-the-checkpoint.md | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 02-DECISIONS/0042-approval-is-the-checkpoint.md 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..17628f0 --- /dev/null +++ b/02-DECISIONS/0042-approval-is-the-checkpoint.md @@ -0,0 +1,78 @@ +--- +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. **Name what the checkpoint actually is.** Chosen. + +## Decision + +**The checkpoint is a person deciding, not a person clicking.** + +An agent may merge its own work **when a human has explicitly approved that merge**. The +approval is the review; the merge is bookkeeping that follows it. + +Without an explicit approval, nothing changes: the agent does not merge, and +[§2](../00-META/how-we-build.md)'s *never open a pull request unprompted* continues to mean +that a permissions list is not a request. + +**What "explicit" excludes**, 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 agent'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. From bea052753e477f5733c02320779c3081983712ab Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 02:04:41 +0200 Subject: [PATCH 2/3] =?UTF-8?q?ADR=200043=20=E2=80=94=20what=20a=20declara?= =?UTF-8?q?tion=20is?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...n-is-an-ordered-list-of-owned-resources.md | 113 ++++++++++++++++++ 03-DESIGN/01-to-be/05-the-node-host.md | 32 +++-- 2 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 02-DECISIONS/0043-a-declaration-is-an-ordered-list-of-owned-resources.md 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`; From 00d5ba83765b00af364e851395c88b89f957515b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 26 Aug 2026 20:37:13 +0200 Subject: [PATCH 3/3] 0042 and 0043, as approved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0042 becomes the operator's own rule and stops there: every merge into the main branch is notified and approved. Notified means proposed and said out loud, not performed and mentioned; approved means a person says yes to THAT merge. Who performs it is not the thing worth constraining, which is what makes an agent merging its own work unremarkable — the checkpoint already happened. 0043 answers the question it did not: where the ordered list comes from. By hand today, in substrate.lock, because the first node has no control plane to derive anything from. Afterwards the control plane derives it from module assignments, resolved configuration, and what each module declares it needs — ordered by the dependency graph, which is research 011. So the record is complete on the consumer side and deliberately silent on the producer side, and that is a legitimate order to settle them in: the host must refuse what it does not understand whoever wrote it. One consequence that only appeared when the question was asked: if the graph turns out not to determine a total order, that is 011's problem and not the host's. The host is still handed a list and still applies it as given. Recorded because it is the seam where a future difficulty would otherwise try to migrate into tier 0. Both were marked accepted before they had been read. Approved now, so the field is true — which it was not when it was written. --- .../0042-approval-is-the-checkpoint.md | 19 +++++++------ ...n-is-an-ordered-list-of-owned-resources.md | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/02-DECISIONS/0042-approval-is-the-checkpoint.md b/02-DECISIONS/0042-approval-is-the-checkpoint.md index 17628f0..a4e25fa 100644 --- a/02-DECISIONS/0042-approval-is-the-checkpoint.md +++ b/02-DECISIONS/0042-approval-is-the-checkpoint.md @@ -28,25 +28,24 @@ approval."* 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. **Name what the checkpoint actually is.** Chosen. +3. **Require notification and approval, and stop there.** Chosen. Who performs the merge is + not the thing worth constraining. ## Decision -**The checkpoint is a person deciding, not a person clicking.** +**Every merge into the main branch is notified and approved.** Stated by the operator in +exactly those terms, and the whole of the rule. -An agent may merge its own work **when a human has explicitly approved that merge**. The -approval is the review; the merge is bookkeeping that follows it. +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. -Without an explicit approval, nothing changes: the agent does not merge, and -[§2](../00-META/how-we-build.md)'s *never open a pull request unprompted* continues to mean -that a permissions list is not a request. - -**What "explicit" excludes**, because this is the half that can rot: +**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 agent's own judgement that the work is ready. +- The author's own judgement that the work is ready. ## Consequences 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 index 934c4c1..d11120d 100644 --- 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 @@ -85,6 +85,33 @@ A declaration names who it is for. A host that has an identity refuses one addre 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