The approval is the checkpoint, and what a declaration is #10
@@ -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 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 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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -11,6 +11,7 @@ decisions:
|
|||||||
- 02-DECISIONS/0039-the-link-is-the-security-boundary.md
|
- 02-DECISIONS/0039-the-link-is-the-security-boundary.md
|
||||||
- 02-DECISIONS/0008-a-failed-step-fails-the-job.md
|
- 02-DECISIONS/0008-a-failed-step-fails-the-job.md
|
||||||
- 02-DECISIONS/0041-the-host-depends-on-nothing.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
|
# The node host
|
||||||
@@ -132,15 +133,23 @@ the mesh, and the full peer set arrives derived.
|
|||||||
|
|
||||||
## What a declaration is
|
## 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
|
**JSON**, because the host has no dependencies to spend and the standard library carries no
|
||||||
anything outside it is refused rather than best-effort interpreted.
|
YAML. **An ordered list of typed resources**, each with a stable identity — the order is stated
|
||||||
- It is per-node and complete: what this machine should be, not a delta against what it was.
|
rather than derived, because deriving it would be the host deciding the thing most likely to
|
||||||
A delta requires the sender to know what the receiver holds, which is the coupling the store
|
differ from what the control plane intended.
|
||||||
exists to remove.
|
|
||||||
- Every addition to the vocabulary widens what a compromised control plane can express, so it
|
**Unknown is refused, never skipped.** An unknown version, type or field refuses the whole
|
||||||
is a security artefact and additions are reviewed as such.
|
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
|
## 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:
|
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.
|
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.
|
**3 — link and store.** The node connects, receives declarations, and holds what it applied.
|
||||||
|
|
||||||
**4 — enrolment.** The one genuinely new mechanism in
|
**4 — enrolment.** The one genuinely new mechanism in
|
||||||
@@ -182,7 +197,6 @@ Each decision above owes a test:
|
|||||||
|
|
||||||
## Open
|
## 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
|
- **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.
|
if it is false the tier boundary moves.
|
||||||
- **What the host carries versus what it finds.** It manages `wg`, `nft`, `pacman`, `docker`;
|
- **What the host carries versus what it finds.** It manages `wg`, `nft`, `pacman`, `docker`;
|
||||||
|
|||||||
Reference in New Issue
Block a user