From b0a74b23fd778373a1764b6197422c0f83c291a1 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 17:45:48 +0200 Subject: [PATCH 01/11] ADR 0162: a merge produces a tiered plan the mesh keeps; dependencies are one relation; design 30; issues 184, 186 --- ...e-produces-a-tiered-plan-the-mesh-keeps.md | 92 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../30-the-mesh-updates-itself-on-a-push.md | 17 +++- .../00-report.md | 10 +- .../00-report.md | 7 ++ 5 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md new file mode 100644 index 0000000..7da271b --- /dev/null +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -0,0 +1,92 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0157-a-build-narrates-on-the-bus.md +--- + +# 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue + +## Context + +A merge on the forge reaches the controller as an event, and the controller asks the build +machine for what that merge changed. Until today that meant the modules whose recorded source is +that repository; since this afternoon it also means everything standing on what moved +([issue 186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md)). +Both are done inside the handler that received the event: it asks one build, waits for it, asks the +next, and returns when the last is done. Three things followed from that shape on 2026-10-01: + +- The controller hears nothing else for the length of the work — twenty-five minutes for the runtime + image and its forty-three dependents ([issue 184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md)). +- A controller replaced mid-merge loses the rest of the merge: the redelivered announcement reads as + history, and the dependents are asked by hand. +- Nothing is deployed between builds. A merge that changes the build machine and something the build + machine builds asks for both in order, but the second is built by whichever build machine is running + — the old one, unless somebody pushed in between. The order the dependents are sorted in exists for + the artifacts; it says nothing about what must be *running*. + +And the knowledge the order is computed from is scattered: a manifest's `build.on`, the artifacts a +build was made against, the repositories a build read, and the fact that every source-built module is +built by the build machine, each read by a different function in the merge handler. + +## Decision + +**1. A module's dependencies are one relation in the catalogue.** `depends-on` edges, each with the +kind of dependency on it: `stands-on` (the module's artifact is built on the other's), `packages` (the +module's build reads the other's repository), `built-by` (the module is built by the holder of the +build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query +of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside +the controller needs it — and nothing else computes an edge. + +**2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge +changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers: +tier 0 depends on nothing else in the set, tier 1 only on tier 0, and so on. The plan — the merge it +answers, the tiers, and each module's state — is written to the store before any build is asked. The +handler asks tier 0 and returns. Every build's outcome, taken in by the same handler that takes every +outcome in, advances the plan it belongs to; a controller replaced mid-plan resumes it from the store. + +**3. A tier is done when it is built, and when what the next tier needs from it is running.** A +module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next +tier is asked only once every module in this tier is built and every rolled-out module of this tier +that a later tier is `built-by` or `packages` has been applied by the machines running it — the +machines' reports say so. A module whose policy says *record* is built and not waited for. So a merge +touching the build machine and the controller builds the build machine, waits until it is the build +machine that is running, and only then asks for the controller's build. + +**4. A plan is read where the mesh is read.** `status` lists every open plan: the merge, the tier it +is at of how many, what it is waiting for and since when; `builds` lists the asked beside the built. +A plan that has waited past a bound is named red there, which is the first fact of +[issue 187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md)'s list. + +## Consequences + +- The merge handler returns in milliseconds; the receive loop is never held by a build again. Issue + 184's remaining cause — a handler that waits for its own work — is removed rather than worked + around; the bus's heartbeats stop being dropped under a merge. +- A controller roll in the middle of a plan costs nothing: the plan is in the store and the asks are + in the queue (mesh-controller 194). +- A release across repositories is a plan whose edges cross repositories; the order a person kept in + a work-order file is the order the tiers give. Issue 186's third fault is answered by the plan, + not by a separate release record. +- The explicit `build --on ` stays as the way to ask for the same plan by hand. +- A module's `build.on` remains the one place a manifest states a dependency the store cannot see. + +## How this is checked + +| Rule | Checked by | +|---|---| +| Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call | +| A merge's set is sorted into tiers, each depending only on earlier ones | a unit test on the tiering: the runtime, a module on it, a plugin on that, and an unrelated module left out | +| The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome | +| An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after | +| A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone | +| `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan | +| Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked | + +## References + +- [ADR 0157](0157-a-build-narrates-on-the-bus.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) +- [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) +- Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index da05750..52fb397 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -175,6 +175,7 @@ python3 00-META/checks/index.py fail if stale - **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) - **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) - **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md) +- **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) ### Its tiers, from the bottom up diff --git a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md index 949fd41..cc86e06 100644 --- a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md +++ b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md @@ -2,8 +2,10 @@ layer: to-be status: proposed code: [] -updated: 2026-09-27 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md + - 02-DECISIONS/0157-a-build-narrates-on-the-bus.md - 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md - 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md --- @@ -114,6 +116,19 @@ automate the freeze. (this is how the uplink managers and the re-registrations above were done). Only image-bearing modules need the build machine, which narrows what the deadlock above can block. +## What a merge does now (2026-10-01) + +Revision, [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md). The +trigger exists: the forge announces a merge on the bus and the controller acts on it (ADR 0157 made +the build narrate; this makes the merge a plan). A module's dependencies are one relation in the +catalogue — `depends-on` edges of four kinds: stands-on, packages, built-by, declared. A merge takes +what changed and everything reachable from it along those edges, sorts the set into tiers, writes the +plan to the store, asks the first tier and returns. Each outcome advances the plan; a tier whose +rolled-out modules a later tier is built by waits until the machines report them applied; a +controller replaced mid-plan resumes from the store. `status` lists open plans and names one that +has waited too long. The transition discipline for breaking changes in the list above is still +unwritten, and still the next thing. + ## Why now, and why not yet **Why it matters:** self-update is the difference between a mesh a person maintains by typing diff --git a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md index a01c796..c6bd562 100644 --- a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md +++ b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-10-01 -located-in: [mesh-controller internal/link/serve.go (act handles one message at a time; sourceMoved waits for every build the merge asks)] +located-in: [mesh-controller cmd/mesh-controller/upgrades.go (the merge handler builds inside the receive loop)] fixed-by: amended-design: [] --- @@ -50,3 +50,9 @@ busy should say so where `status` is read. and a build outcome for another module arrive together, and the outcome is recorded before the build finishes; live, the controller's log during the next catalogue merge shows registrations interleaved with the merge's own. + +## Decided, 2026-10-01 + +[ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): a merge +produces a tiered plan the store keeps; the handler asks the first tier and returns; outcomes advance +the plan; a controller replaced mid-plan resumes it. The loop is never held by a build again. diff --git a/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md b/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md index 501a1fb..91e01f1 100644 --- a/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md +++ b/04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md @@ -65,3 +65,10 @@ this report asks for. *How this would be checked:* a builder restarted between an ask and its build still builds it; a merge of a dependent repository before its prerequisite is held and named; `builds` lists asked, running and built. + +## Decided, 2026-10-01 + +The third fault is answered by [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): +a release across repositories is a plan whose dependency edges cross repositories, sorted into +tiers and deployed tier by tier, read in `status`. The order a person kept is the order the tiers +give. From b0de26730158c18c0a337a8c5bd011c2317a3b4f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 17:46:07 +0200 Subject: [PATCH 02/11] ADR 0162: the link to 0157 by its name --- .../0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md | 4 ++-- 03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md index 7da271b..7bd7e3c 100644 --- a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -4,7 +4,7 @@ status: accepted date: 2026-10-01 deciders: jochen reconstructed: false -extends: 02-DECISIONS/0157-a-build-narrates-on-the-bus.md +extends: 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md --- # 162. A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue @@ -87,6 +87,6 @@ A plan that has waited past a bound is named red there, which is the first fact ## References -- [ADR 0157](0157-a-build-narrates-on-the-bus.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) +- [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) - [Design 30 — The mesh updates itself on a push](../03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md) - Issues [184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), [186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md), [187](../04-ISSUES/187-the-mesh-tells-nobody-when-it-stops-working/00-report.md) diff --git a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md index cc86e06..1ea8dd8 100644 --- a/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md +++ b/03-DESIGN/01-to-be/30-the-mesh-updates-itself-on-a-push.md @@ -5,7 +5,7 @@ code: [] updated: 2026-10-01 decisions: - 02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md - - 02-DECISIONS/0157-a-build-narrates-on-the-bus.md + - 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md - 02-DECISIONS/0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md - 02-DECISIONS/0120-a-roster-fact-carries-its-format-as-a-template.md --- From 82496536cd0d2337fa94ce2757f51185550fc1c4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 17:53:57 +0200 Subject: [PATCH 03/11] ADR 0162: the three kinds of dependency, where the edges come from, and the one real cycle --- ...e-produces-a-tiered-plan-the-mesh-keeps.md | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md index 7bd7e3c..cb5c1b6 100644 --- a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -38,7 +38,25 @@ kind of dependency on it: `stands-on` (the module's artifact is built on the oth module's build reads the other's repository), `built-by` (the module is built by the holder of the build-machine seat), and `declared` (a manifest's `build.on`). The relation is answered by one query of the catalogue — the controller's inventory today, the catalogue seat's tool when something outside -the controller needs it — and nothing else computes an edge. +the controller needs it — and nothing else computes an edge. The edges are derived from facts +recorded at two moments and written by nobody: registration records the manifest (`declared`, and +`built-by` for anything with a source), a build's take-in records what the image was built on and +which repositories it read (`stands-on`, `packages`). A module's first build places it by its +declared edges alone; from its second it is placed by what was true. + +**The kinds are three dependencies, not one.** A *code* dependency — B packages A's source — means +B is rebuilt whenever A changes, in the same tier: B's build needs nothing of A's first. A *build* +dependency — B stands on A's artifact, or declares it — means B is rebuilt after A is *built*, the +next tier, and nothing need be deployed in between. A *runtime* dependency — B is built by A — means +B is rebuilt only after A is built *and running*, the next tier with a gate on the machines' reports. +One cycle is real and resolved by the kinds themselves: the runtime image is built by the build +machine, and the build machine stands on the runtime image; the image comes first, built by the +build machine that is running, which is the only one there could be — a `built-by` edge never orders +a module after a build machine that stands on it. A provision is not a dependency of this relation: +a consumer binds to its provider through what the push renders, and a change to the provider's image +changes nothing in the consumer's; a consumer whose build does read a provider's source declares it. +"A was deployed, so restart B" is the push's domain — B is replaced when what it reads changed — and +not the plan's. **2. A merge produces a plan, and the plan is a record.** The controller takes the modules the merge changed and everything reachable from them along `depends-on` edges, and sorts that set into tiers: @@ -50,8 +68,8 @@ outcome in, advances the plan it belongs to; a controller replaced mid-plan resu **3. A tier is done when it is built, and when what the next tier needs from it is running.** A module whose roll-out policy says *roll out* is sent to its machines when it moves, as today. The next tier is asked only once every module in this tier is built and every rolled-out module of this tier -that a later tier is `built-by` or `packages` has been applied by the machines running it — the -machines' reports say so. A module whose policy says *record* is built and not waited for. So a merge +that a later tier is `built-by` has been applied by the machines running it — the machines' reports +say so. A module whose policy says *record* is built and not waited for. So a merge touching the build machine and the controller builds the build machine, waits until it is the build machine that is running, and only then asks for the controller's build. @@ -78,7 +96,8 @@ A plan that has waited past a bound is named red there, which is the first fact | Rule | Checked by | |---|---| | Dependencies are one relation, each edge with its kind | an inventory test over a fixture catalogue: a runtime image, a module on it, a module packaging the controller's source, and the build machine; the four kinds come back from one call | -| A merge's set is sorted into tiers, each depending only on earlier ones | a unit test on the tiering: the runtime, a module on it, a plugin on that, and an unrelated module left out | +| A merge's set is sorted into tiers along the three kinds: a code dependency in the same tier, a build dependency after its base is built, a runtime dependency after the build machine; the build machine's own base first | a unit test on the tiering over the mesh's real shape: the runtime image, the build machine on it, modules built by it, a plugin declared on one, the proxy packaging the controller, an unrelated module left out; a cycle is one last tier and said | +| Only a runtime dependency gates on deployment, and only for a module that rolls out | the same test's gate cases | | The plan is written before any build is asked, and the handler returns | a controller test: a merge announcement produces a plan row with its tiers and one asked build per tier-0 module, and the handler is back before any outcome | | An outcome advances its plan; a complete tier asks the next; a tier with a rolled-out base waits for the machines' reports | store-backed tests over a two-tier plan: the first outcome marks built; the tier's roll-out gate holds until the report; the next tier is asked after | | A controller restarted mid-plan resumes it | a test that opens a plan, drops the handler, and advances from the store alone | From 187442ec7bcee29b21194d4e4ef253f3bd0ad9f4 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 18:10:57 +0200 Subject: [PATCH 04/11] Issue 188: a refusal inside on-the-network drops a machine silently --- .../00-report.md | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md diff --git a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md new file mode 100644 index 0000000..7efa488 --- /dev/null +++ b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md @@ -0,0 +1,48 @@ +--- +status: located +opened: 2026-10-01 +located-in: [mesh-controller cmd/mesh-controller/network.go (onTheNetwork resolves every machine unchecked and skips one that refuses, saying nothing)] +fixed-by: +amended-design: [] +--- + +# 188 — A refusal inside "who is on the network" drops a machine silently, and every symptom points elsewhere + +## What was observed + +At 15:28Z on 2026-10-01 the controller rolled to a build that refuses a machine with two modules +answering one provision and no pin naming which (mesh-controller 195). The control node had two +issuers of `acme-ca`. From that moment every plan of the control node failed with *step-ca has a +content that says `${machine:at}`, and this machine says mesh-range or name*; `seats` listed every +seat as unheld; the build machine refused the builder's and the proxy's builds with *no clone base +for that seat — nothing holds it*; and the roll-out of the next controller was refused with the +`${machine:at}` words. Not one of those names the cause. It was found by running the previous image +as a one-shot beside the current one and reading the difference, forty minutes later. + +## Why this is here + +`onTheNetwork` decides which machines have an address by resolving each one, unchecked, and +*skipping* any whose resolution errs. A machine skipped there has no `at`, so its own plan fails on +the first placeholder that needs one, in another module's words; everything held on it reads as +unheld; everything built from it cannot be built. The design lets one refusal become four unrelated +symptoms and no sentence about the refusal itself. It is the same shape as +[issue 187](../187-the-mesh-tells-nobody-when-it-stops-working/00-report.md): a fault that is swallowed +where it happens and discovered where it hurts. + +## What a fix needs + +- A machine whose resolution refuses is said, by `onTheNetwork`'s caller or in `status`: *the + control node does not resolve: more than one module provides acme-ca; pin one* — the resolver's + own words, which exist and were dropped. +- A refusal that a release introduces for a machine already converged — a new rule the stored + state does not meet — must not be silent at the roll either; the controller's prepare or first + resolution after a roll should name every machine it now refuses. + +*How this would be checked:* a controller test where one machine's unchecked resolution refuses: +`status` names the machine and the refusal, and the other machines keep their addresses. + +## Resolved in the live mesh, 2026-10-01 + +By hand: `pin novox acme-ca novox step-ca`, the provider the previous controller had in fact bound +the proxy to (read from its plan), after which the control node resolved, every seat read as held +and the roll-out proceeded. The design fault above stands. From a92e4bf121ee48a7553ce1bdc9ef952018ad44ce Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 18:14:37 +0200 Subject: [PATCH 05/11] Issue 188: what the live fault was and how it was resolved --- .../00-report.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md index 7efa488..d85abc9 100644 --- a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md +++ b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md @@ -43,6 +43,11 @@ where it happens and discovered where it hurts. ## Resolved in the live mesh, 2026-10-01 -By hand: `pin novox acme-ca novox step-ca`, the provider the previous controller had in fact bound -the proxy to (read from its plan), after which the control node resolved, every seat read as held -and the roll-out proceeded. The design fault above stands. +The refusal itself was a fault of mesh-controller 195, already corrected on main by its author's +hotfix (196) when the control node was found refusing; the running controller was the one build in +between. Found by running the previous image and main's image as one-shots beside the running one +and reading which resolved. Resolved by pushing the control node from a one-shot of main's image, as +the recipe for a controller that cannot roll itself says. A pin naming the proxy's issuer +(`step-ca`, the one the previous plan had bound) was made first and kept; it changes nothing. The +design fault above stands and is fixed by mesh-controller PR `fix/a-machine-not-on-the-network-is-said`: +the dropped machine and the resolver's words are said where the drop happens. From 606fbb7add85be232db9e68a36785b97510d9d46 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 18:22:19 +0200 Subject: [PATCH 06/11] Issue 188: the second fault beneath the first, and its fix --- .../00-report.md | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md index d85abc9..1f6cc1d 100644 --- a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md +++ b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md @@ -1,7 +1,7 @@ --- status: located opened: 2026-10-01 -located-in: [mesh-controller cmd/mesh-controller/network.go (onTheNetwork resolves every machine unchecked and skips one that refuses, saying nothing)] +located-in: [mesh-controller cmd/mesh-controller/plan.go (theRestOfTheMesh resolves every other machine without its pins and skips one that refuses, saying nothing), mesh-controller cmd/mesh-controller/network.go (onTheNetwork, the same)] fixed-by: amended-design: [] --- @@ -43,11 +43,13 @@ where it happens and discovered where it hurts. ## Resolved in the live mesh, 2026-10-01 -The refusal itself was a fault of mesh-controller 195, already corrected on main by its author's -hotfix (196) when the control node was found refusing; the running controller was the one build in -between. Found by running the previous image and main's image as one-shots beside the running one -and reading which resolved. Resolved by pushing the control node from a one-shot of main's image, as -the recipe for a controller that cannot roll itself says. A pin naming the proxy's issuer -(`step-ca`, the one the previous plan had bound) was made first and kept; it changes nothing. The -design fault above stands and is fixed by mesh-controller PR `fix/a-machine-not-on-the-network-is-said`: -the dropped machine and the resolver's words are said where the drop happens. +Two faults, one on top of the other. The refusal was mesh-controller 195's new rule — two modules +answering one provision on one machine need a pin — which its author hotfixed for the first pass +(196). The second pass of "the rest of the mesh" resolves every machine *without its pins*, so the +control node, pinned or not, was refused there and vanished: every seat it holds read as unheld, +the builder's and the proxy's builds were refused for want of the git seat's clone base, the +roll-out of the next controller was refused, and the first tiered plan failed at its first tier. +Found by a diagnostic build counting what each machine yielded. Fixed by mesh-controller PR +`fix/a-machine-not-on-the-network-is-said`: each machine is resolved with its own pins, and a machine +left out is named with the resolver's words in both places. The pin itself (`step-ca`, the issuer +the proxy already had) was made by hand and stands. From a2c9fbb665deb7a487d65f7f9e1b125029feff46 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 20:54:10 +0200 Subject: [PATCH 07/11] Issues 184 and 188 resolved: the merge handler returns at once; a machine is resolved with its pins and a dropped one is said --- .../00-report.md | 15 +++++++++++++-- .../00-report.md | 12 ++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md index c6bd562..d54f706 100644 --- a/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md +++ b/04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-10-01 located-in: [mesh-controller cmd/mesh-controller/upgrades.go (the merge handler builds inside the receive loop)] -fixed-by: +fixed-by: mesh-controller PR 197 (ADR 0162: the merge handler writes a plan, asks the first tier and returns; outcomes and a ticker advance it) and PR 199 amended-design: [] --- @@ -56,3 +56,14 @@ interleaved with the merge's own. [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md): a merge produces a tiered plan the store keeps; the handler asks the first tier and returns; outcomes advance the plan; a controller replaced mid-plan resumes it. The loop is never held by a build again. + +## Resolved, 2026-10-01 evening + +Since the controller holding ADR 0162's plan rolled, a merge announcement is handled in +milliseconds: the plan is written, the first tier asked, the loop free. The builds the merge +implies are asked from the store's record, tier by tier, so a controller replaced mid-plan resumes +it rather than losing it. The first live merge under it (a controller change) is the proof the +decision's table asks for; its tiers are read with `plans`. + +*How it is checked:* the plan tests in mesh-controller; live, `plans` after a merge and the loop's +log taking reports in while the plan builds. diff --git a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md index 1f6cc1d..80fa610 100644 --- a/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md +++ b/04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-10-01 located-in: [mesh-controller cmd/mesh-controller/plan.go (theRestOfTheMesh resolves every other machine without its pins and skips one that refuses, saying nothing), mesh-controller cmd/mesh-controller/network.go (onTheNetwork, the same)] -fixed-by: +fixed-by: mesh-controller PR 199 (each machine resolved with its own pins; a dropped machine said in both passes), rolled 2026-10-01 evening; PR 200 keeps the per-machine view quiet amended-design: [] --- @@ -53,3 +53,11 @@ Found by a diagnostic build counting what each machine yielded. Fixed by mesh-co `fix/a-machine-not-on-the-network-is-said`: each machine is resolved with its own pins, and a machine left out is named with the resolver's words in both places. The pin itself (`step-ca`, the issuer the proxy already had) was made by hand and stands. + +## Resolved, 2026-10-01 evening + +The controller holding the fix was rolled onto the control node by the operator and a colleague +(the running one could not roll itself); after it every seat read as held again, a build asked +through the git seat worked, all four machines resolved and pushed. The line naming a dropped +machine spoke once too often — in the per-machine view, where the others are resolved without the +planned machine's offers and may fail by design — and is quiet there since mesh-controller PR 200. From 50e4d9c2a79bde727b6c25875987e7472fe57196 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 21:00:10 +0200 Subject: [PATCH 08/11] ADR 0162: built, and proven live by the first tiered plan --- ...rge-produces-a-tiered-plan-the-mesh-keeps.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md index cb5c1b6..04a58da 100644 --- a/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md +++ b/02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md @@ -104,6 +104,23 @@ A plan that has waited past a bound is named red there, which is the first fact | `status` lists open plans and names one that waits past the bound | the status JSON test with a fixture plan | | Live | a catalogue merge touching a base and a dependent: the plan's tiers in `status`, the base rolled before the dependent is asked | +## Built and proven live, 2026-10-01 + +> **Progressive insight — 2026-10-01.** The decision stands; these are the facts of its building. + +Built in mesh-controller 197 (the relation, the plan record, the driver, `status`), 198 (`plans`), +199 (a `built-by` edge orders and gates but never widens — the first live plan had taken the whole +catalogue along for a controller change; `plans stop`), 200. The first merge handled by the finished +machinery, at 18:56Z, was a controller change and produced the plan this record describes: tier 0 +the build machine; tier 1 the controller and the proxy that packages its source. The handler +returned at once; the build machine was built, rolled, and the plan read *tier 0 built; waiting for +builder on novox to be applied* until the machine reported; then tier 1 was asked, both built, and +the plan read done — three minutes, read through the console with `plans`, the receive loop taking +reports throughout. What the day between decision and proof taught is in issues +[184](../04-ISSUES/184-a-merge-announcement-blocks-the-controllers-receive-loop/00-report.md), +[186](../04-ISSUES/186-a-release-across-repositories-is-an-order-in-a-persons-head/00-report.md) and +[188](../04-ISSUES/188-a-refusal-inside-on-the-network-drops-a-machine-silently/00-report.md). + ## References - [ADR 0157](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md), [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) From 2904c359b83e225f2eabe6d6d318281fae26de28 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 21:12:36 +0200 Subject: [PATCH 09/11] =?UTF-8?q?ADR=200163:=20taking=20a=20module=20over?= =?UTF-8?q?=20is=20a=20comparison=20=E2=80=94=20what=20it=20compares,=20re?= =?UTF-8?q?fuses=20and=20carries;=20designs=2005=20and=2009;=20group=206's?= =?UTF-8?q?=20issues=20located,=20093=20resolved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...63-taking-a-module-over-is-a-comparison.md | 138 ++++++++++++++++++ 02-DECISIONS/README.md | 1 + 03-DESIGN/01-to-be/05-the-node-host.md | 12 +- 03-DESIGN/01-to-be/09-the-node-lifecycle.md | 14 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 11 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 9 +- .../00-report.md | 7 +- 14 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md diff --git a/02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md b/02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md new file mode 100644 index 0000000..e8023af --- /dev/null +++ b/02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md @@ -0,0 +1,138 @@ +--- +topic: the mesh +status: accepted +date: 2026-10-01 +deciders: jochen +reconstructed: false +extends: 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md +--- + +# 163. Taking a module over is a comparison: what it compares, what it refuses, and what it carries + +## Context + +On an adopted machine the mesh holds what it finds until the module is taken, and taking is the +cutover ([ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md)). The whole-node flip +is previewed and confirmed by digest; the per-module cutover, the step that actually replaces a +running service, previews nothing. `take` names the held things the next push will replace and +where each original is kept. It does not say how the module's version of each differs from what +runs. Ten issues from the first migrations are the same omission seen from ten sides: + +- a port narrowed from everywhere to the private network, unannounced ([086](../04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md)); +- a configuration file replaced whole, dropping the one line that was the installation's own ([098](../04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md)); +- an image pin that had aged into a downgrade, discovered by three minutes of outage ([099](../04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md)); +- a secret minted for a service that already had one, with no way to carry the existing value in because it was a required secret and not the module's own ([100](../04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md)); +- a container moved onto the module's own network, out of reach of the neighbour that called it by name ([101](../04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md)); +- a resource whose target changed, leaving the old container running with no record naming it ([097](../04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md)); +- a volume path that changed without the running container noticing, because the host does not compare that field ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md)); +- a build that deployed at once because the module's policy said so, racing a data move ([126](../04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md)); +- a setting accepted where it was set and refusing the whole machine where it was read ([096](../04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md)); +- a module that could not take over what genesis raised, because the two differed in name, network, data and image ([090](../04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md)); +- a successor that could not stand beside its predecessor at all, answered by [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md)'s adapter ([093](../04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md)). + +What the host records of a found thing is enough to compare from: a file's original, kept, with +its digest, mode and owner; a container's id and whether it ran; whether anything changed it +since. What it does not yet record is what a comparison needs most: the found container's image +and when that image was made, the networks it is on and who else is on them, what it mounts, what +it publishes. And the controller's rule that a machine is told everything or nothing turns one +impossible statement into a machine nobody can talk to. + +## Decision + +**1. A take is previewed, and the preview is a comparison.** For every held thing the module would +replace, `take` puts what runs beside what the module declares and says the difference: + +- a **container**: its image against the module's, with each image's creation date so older and + newer have a meaning; its name; its networks, and the other containers on each found network + that is not the module's; its published ports and the reach of each, found firewall and guard + included; its mounts against the module's volumes and paths; +- a **file**: the kept original against the declared content, as a difference, not two digests; +- a **secret** the module takes that the mesh minted and nobody accepted, when the service's data + was found — a service that already runs already has a value; +- the module's **settings** on that machine, composed against its definition. + +`take` without `--yes` prints the comparison and stops; `take --yes ` cuts over exactly +what was previewed, the way the flip is confirmed, and a preview whose account of the machine is +older than the flip allows is refused the same way. The host supplies the facts in its report of +what it holds: the found container's image and its creation date, its networks and their members, +its mounts and published ports. + +**2. Three differences refuse by default, each overridden by naming it.** An image **older** than +the one running, by creation date — `--downgrade`, said once and recorded. A declared file that +**differs** from the kept original — `--replace `, or the module declares the file partially +and writes into it ([ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md)), which is +the right answer wherever the file is the service's own and the format allows it. A **minted, +unaccepted secret** for a service whose data was found — accept the value first, or `--mint +` to say the service shall take a new one. Two differences are said and not refused: a port +whose reach **narrows**, and a found network whose other members may reach the container **by +name**, each member named; both are the operator's to weigh, and the words are there to weigh them. + +**3. A secret the mesh would mint may be accepted instead, own or required.** `secret accept` +reaches a module's required secrets, not only its own: the value is a fact about the machine, and +the mesh's job at a take is to learn it. The accepted value is sealed to the module as a minted one +would be, and the provider that would have minted it is told it has one. Whether one accepted +value should reach every consumer of a provider at once is [issue 165](../04-ISSUES/165-one-accepted-value-must-be-accepted-once-per-consumer/00-report.md)'s +question and the next group's. + +**4. A taken container may keep a found network, for a while, by a setting.** A per-machine +setting names a found network the module's container also joins, so a neighbour that resolves it +by name keeps resolving it. It is migration scaffolding in the sense of +[ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md): assigned only on an +adopted machine, reported while it stands, removed when the neighbours are taken, and the preview +names it. Taking a group of modules at once is not decided here; the setting makes the order free. + +**5. The host compares every field it writes, and removes what it can no longer name.** A +container is current when every field the host would write agrees with the one running — volumes +and paths included; a field the host cannot compare recreates rather than passes. The host's +record keeps a resource's former targets: a container or file the host **wrote** under a name or +path the declaration no longer names is removed on the next apply and said; what was **found** is +never removed, as ADR 0100 says. And the host answers the question nothing answered on +2026-09-23: its report lists what runs on the machine that the mesh neither wrote nor holds — +containers and listeners — as *strays*, so a thing left behind is seen the day it is left. + +**6. A setting is judged where it is stored, and an impossible one costs a module, not a machine.** +Storing a setting composes it against the module's current definition and refuses with the node, +module, layer and key when it cannot work. A definition that later moves under a stored setting +makes composition leave *that module* out of the machine's declaration — its held things kept, its +containers untouched — and say the statement by name; the machine is still told everything else. +A machine is told everything or nothing about what it *is* told; what it is not told is said. + +**7. What genesis raises, it raises as the module that succeeds it declares** — name, network, +data directory and image — so the module adopts it by the found rule that already exists, and a +module meant to succeed a bootstrap service that it cannot adopt is a fault of genesis, found by a +test that raises and then assigns. **`build` says when a policy will act on its result**, so a +person choreographing a data move knows which module will not wait; under +[ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) the roll-out is the plan's, and +the plan says it too. + +## Consequences + +- `take` becomes the per-module twin of the flip: preview, digest, confirm. The flip's own preview + gains the same comparisons for every module it takes. +- The host's report of what it holds grows by the found container's image and creation date, + networks and members, mounts and published ports; its store keeps former targets and strays. +- Issues 086, 098, 099, 100, 101 close on rule 1 and 2; 097 and 126 on rule 5; 096 on rule 6; + 090 on rule 7; 093 is closed by ADR 0104's adapter, which runs. +- Nothing here changes what an adopted machine keeps or when: found stays held, held is never + removed, the original is kept before anything is written. + +## How this is checked + +| Rule | Checked by | +|---|---| +| The host reports a found container's image and creation date, networks and their members, mounts and published ports | host unit tests over a fake runtime; the adoption bed's report | +| `take` without `--yes` previews every held thing's difference and changes nothing; `--yes` with the digest cuts over; a stale account is refused | controller tests over a fixture report: a differing file, an older image, a narrowed port, a shared network, a minted secret | +| An older image, a differing file and a minted secret for found data refuse without their override | the same tests | +| A found network kept by a setting is joined, reported and named in the preview | a host test and a controller resolution test | +| `secret accept` takes a required secret | an inventory test; the provider is told | +| Every container field is compared; a former target the host wrote is removed and said; what was found is not | host tests: a volume path change recreates; a renamed container's predecessor is removed; a found one under the old name is kept | +| Strays are reported | a host test over a fake runtime with a container nobody declared | +| A setting that cannot compose is refused where stored, naming node, module, layer, key; a definition moving under one leaves that module out and says so | controller tests | +| Genesis raises the forge as its module declares it | a genesis test that raises, assigns, and finds the module holding rather than raising a second | +| Live | the next cutover on an adopted machine: `take` shows the comparison, refuses the downgrade if there is one, and the service keeps its configuration and its secret | + +## References + +- [ADR 0100](0100-a-node-in-use-is-adopted-before-it-is-converged.md), [ADR 0102](0102-the-mesh-writes-into-a-shared-file-never-over-it.md), [ADR 0103](0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md), [ADR 0104](0104-a-provision-may-be-answered-by-an-adapter-to-the-predecessor.md), [ADR 0162](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) +- [Design 05 — The node host](../03-DESIGN/01-to-be/05-the-node-host.md), [Design 09 — The node lifecycle](../03-DESIGN/01-to-be/09-the-node-lifecycle.md) +- Issues 086, 090, 093, 096, 097, 098, 099, 100, 101, 126 diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 52fb397..ff61db8 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -176,6 +176,7 @@ python3 00-META/checks/index.py fail if stale - **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md) - **0161** — [What deserves a seat: a role of a module is a seat, a singular fact about machines is a placement with a capacity of one, and a holder's software is the machine's](0161-what-deserves-a-seat.md) - **0162** — [A merge produces a tiered plan the mesh keeps, and a module's dependencies are one relation in the catalogue](0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) +- **0163** — [Taking a module over is a comparison: what it compares, what it refuses, and what it carries](0163-taking-a-module-over-is-a-comparison.md) ### Its tiers, from the bottom up 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 18238b0..c01ca0a 100644 --- a/03-DESIGN/01-to-be/05-the-node-host.md +++ b/03-DESIGN/01-to-be/05-the-node-host.md @@ -2,8 +2,9 @@ layer: to-be status: in-progress code: [mesh-host] -updated: 2026-09-29 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md - 02-DECISIONS/0141-the-host-delivers-its-own-successor.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0102-the-mesh-writes-into-a-shared-file-never-over-it.md @@ -153,6 +154,15 @@ checked:* unit tests hold the host to keeping a found file and container, conver taken, never removing a held file and reporting one that changed; the adoption bed asserts a found file byte for byte unchanged until its module is taken. +**What the host says of a found container, and what it removes** — revision, 2026-10-01 +([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). Its report of a held +container carries the image and the image's creation date, the networks it is on and the other +containers on each, its mounts and its published ports — the facts a take compares. The host compares +every field it writes before calling a container current, volumes and paths included; its record keeps +a resource's former targets, removes a container or file it wrote under a name the declaration no +longer names, never removes what was found, and reports what runs on the machine that it neither +wrote nor holds. *How it is checked:* ADR 0163's table. + **Found reaches every kind that can touch what the machine has** ([ADR 0103](../../02-DECISIONS/0103-what-an-adopted-node-holds-and-what-its-guard-refuses.md)). For a module not yet taken, a directory present with no record keeps its mode and owner, a unit present with no record keeps its state and boot setting, a diff --git a/03-DESIGN/01-to-be/09-the-node-lifecycle.md b/03-DESIGN/01-to-be/09-the-node-lifecycle.md index a7f3810..00fec44 100644 --- a/03-DESIGN/01-to-be/09-the-node-lifecycle.md +++ b/03-DESIGN/01-to-be/09-the-node-lifecycle.md @@ -8,8 +8,9 @@ code: - mesh-host packaging/nox-mesh-host-network.sh - mesh-controller internal/token - mesh-controller internal/inventory/nodes.go -updated: 2026-09-23 +updated: 2026-10-01 decisions: + - 02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md - 02-DECISIONS/0100-a-node-in-use-is-adopted-before-it-is-converged.md - 02-DECISIONS/0004-a-node-and-how-it-joins.md - 02-DECISIONS/0005-the-node-host.md @@ -311,6 +312,17 @@ found firewall again and converges the openings through it; what was taken stays a predecessor leaves one and asserts nothing that serves changes until a module is taken or the node is converged, and that the flip closes exactly what the preview said. +**Taking a module is previewed, and the preview is a comparison** — revision, 2026-10-01 +([ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md)). For every held thing a +module would replace, `take` puts what runs beside what the module declares: a container's image and +its age, name, networks and their other members, published ports and their reach, mounts; a file's +kept original against the declared content, as a difference; a secret the mesh minted for a service +that already has one; the module's settings composed against its definition. An older image, a +differing file and a minted secret for found data refuse unless named; a narrowed port and a shared +network are said. `take --yes ` cuts over what was previewed, as the flip does. A taken +container may keep a found network by a per-machine setting while its neighbours are not yet taken. +*How it is checked:* ADR 0163's table. + A candidate machine is not empty. It has a package manager, probably a container runtime, configuration somebody chose. [ADR 0005](../../02-DECISIONS/0005-the-node-host.md) says the host never touches what it did not create — adoption is the deliberate act of taking diff --git a/04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md b/04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md index d9c55a4..96c0f40 100644 --- a/04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md +++ b/04-ISSUES/086-taking-a-module-narrows-a-port-without-saying-so/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-22 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -35,3 +35,8 @@ it changes before it changes it, and for taking a module this one does not. included, and ask for the same kind of confirmation as the flip? - Or should taking refuse while a port of the module is reachable more widely than the module declares, until the operator either changes the module's exposure or confirms the narrowing? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the preview names a narrowing. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md b/04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md index 84879cc..2bbf181 100644 --- a/04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md +++ b/04-ISSUES/090-the-forge-module-does-not-take-over-the-forge-genesis-raised/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-22 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -48,3 +48,8 @@ network, or it is not a takeover. directory — so the module adopts it by the rule that already exists? - Should something refuse to call a module the successor of a bootstrap service it cannot adopt? - Is the forge's own address better resolved than set, which is [issue 088](../088-the-forges-own-address-names-a-port-it-may-not-have/00-report.md)? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 7: genesis raises as the module declares. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md b/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md index 9ea59e8..c8a2e3e 100644 --- a/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md +++ b/04-ISSUES/093-the-successor-proxy-cannot-serve-what-the-predecessor-still-serves/00-report.md @@ -1,8 +1,8 @@ --- -status: located +status: resolved opened: 2026-09-22 located-in: [mesh-catalog, mesh-controller internal/catalogue] -fixed-by: +fixed-by: ADR 0104 — the route adapter module (mesh-catalog modules/route-adapter) writes each migrated route into the predecessor's proxy; it runs on the home server's migration amended-design: --- @@ -71,3 +71,10 @@ answered by an **adapter** that writes into the predecessor's own configuration. the predecessor's proxy keeps serving every name and keeps its certificates, while each migrated module's name is pointed at the mesh's container. The proxy is the last cutover again, and by then every route is one the mesh contributed. + +## Resolved, 2026-10-01 + +The adapter ADR 0104 decided exists and runs: `route-adapter` provides `route` on an adopted +machine by writing each migrated module's route where the predecessor's proxy reads it, and the +proxy itself is the last cutover. [ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md) +records the rest of what a take compares. diff --git a/04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md b/04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md index 7d3ac29..dcc65ab 100644 --- a/04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md +++ b/04-ISSUES/096-a-setting-that-cannot-work-is-stored-and-stops-the-node/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -58,3 +58,8 @@ knowing the code. an operator to undo it without reading the source? - Is there anything a node must never be pushed without, such that sending a partial declaration is worse than sending none? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 6: judged where stored; an impossible statement costs a module. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md b/04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md index 99cb502..4486ced 100644 --- a/04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md +++ b/04-ISSUES/097-a-resource-that-changes-target-leaves-the-old-one-behind/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -75,3 +75,8 @@ found, and so would be kept for ever on purpose. module unassigned between the two declarations? - What reports this? Nothing on the machine currently answers "what is running here that the mesh did not ask for", which is the question that would have found this in seconds. + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rule 5: former targets are removed and strays reported. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md b/04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md index 84032bd..23a597e 100644 --- a/04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md +++ b/04-ISSUES/098-taking-a-module-replaces-a-configuration-nobody-compared/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -63,3 +63,8 @@ written. substitutes settings into content today. - Is the kept original enough of an answer, given nothing restores it and nothing points at it when the service starts behaving differently? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the difference is shown and a differing file refuses. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md b/04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md index 88a2b09..25671b9 100644 --- a/04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md +++ b/04-ISSUES/099-a-modules-image-pin-ages-into-a-downgrade/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -60,3 +60,8 @@ expected rate. nothing answers the first. - Is a digest pin the right thing for a module that takes over an existing service at all, or should a cutover be able to say *keep what is running* and record what that was? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 2: the images are compared by age and a downgrade refuses. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md b/04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md index 9df2872..fb2e94d 100644 --- a/04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md +++ b/04-ISSUES/100-a-minted-secret-cannot-be-the-one-the-service-already-uses/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -65,3 +65,8 @@ the module can only be installed fresh. Should it, so the dangerous case can be refused rather than discovered? - What is the reverse path: the mesh has minted one, the service ignored it, and the working value is still on the machine. Nothing reconciles those. + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 2 and 3: a minted secret for found data refuses; secret accept reaches required secrets. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md b/04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md index 6b8da96..fa0c521 100644 --- a/04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md +++ b/04-ISSUES/101-taking-a-service-reached-by-container-name-cuts-its-neighbours-off/00-report.md @@ -1,7 +1,7 @@ --- -status: open +status: located opened: 2026-09-23 -located-in: [] +located-in: [mesh-controller cmd/mesh-controller/adoption.go (take previews nothing), mesh-host internal/apply (the comparison and the record)] fixed-by: amended-design: --- @@ -61,3 +61,8 @@ exercise. learn to take a group atomically? Nothing takes more than one module at a time today. - Does the same hole exist for anything else the predecessor's runtime resolves and the mesh's does not — a network alias, a `depends_on`, a name in a shared `/etc/hosts`? + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 1 and 4: the neighbours are named; a found network may be kept by a setting. Building follows, +host first, then the controller's `take`. diff --git a/04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md b/04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md index 5480c3d..c3678c7 100644 --- a/04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md +++ b/04-ISSUES/126-a-volume-path-is-not-in-the-spec-comparison/00-report.md @@ -1,5 +1,5 @@ --- -status: open +status: located opened: 2026-09-26 located-in: [mesh-host internal/apply] --- @@ -45,3 +45,8 @@ Instant renames both ways broke the circular dependency (forge needed for builds builds needed for the push, push needed for the forge): data back to the old path, old-spec forge started, artifacts rebuilt, data renamed forward, push. Nothing lost; the install-page junk was discarded twice. + +## Decided, 2026-10-01 + +[ADR 0163](../../02-DECISIONS/0163-taking-a-module-over-is-a-comparison.md), rules 5 and 7: every field compared; build says the policy. Building follows, +host first, then the controller's `take`. From 48ca2fb41b0c68f1bfca90945dad61c1f21c9f5d Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 21:54:44 +0200 Subject: [PATCH 10/11] Issue 189: a rebuild from the same commit is not a move, so a packaging module's new image never rolls out --- .../00-report.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md diff --git a/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md b/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md new file mode 100644 index 0000000..f2537ce --- /dev/null +++ b/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md @@ -0,0 +1,35 @@ +--- +status: located +opened: 2026-10-01 +located-in: [mesh-controller internal/inventory/catalogue.go (RegisterModule records the source commit; a moved event follows a commit that changed, not an artifact that did), mesh-controller cmd/mesh-controller/upgrades.go (the roll-out follows the moved event)] +fixed-by: +amended-design: [] +--- + +# 189 — A rebuild from the same commit is not a move, so a packaging module's new image never rolls out + +## What was observed + +The build machine's definition packages the controller's source. A controller merge rebuilds it, and +the rebuilt image carries the new controller; its own source commit in the catalogue is unchanged. +Registering that build therefore moves nothing the catalogue announces: no *moved* event, no +roll-out, although the module's upgrade policy says roll out and the artifact is new. On 2026-10-01 +at 19:45Z the first tiered plan under [ADR 0162](../../02-DECISIONS/0162-a-merge-produces-a-tiered-plan-the-mesh-keeps.md) +built the build machine and waited for the machine running it to apply the new build — a wait +that nothing would end, because nothing had sent it. A push by hand opened the gate. + +## Why this is here + +A version is what a module *runs*, and that is the artifact. The source commit is how the mesh +knows the artifact's provenance, not what makes it new: a build that reads another repository, or +pulls a base image, produces a different artifact from the same commit. The roll-out followed the +commit, so every module that packages another's source, and every dependent rebuilt because its +base moved, is rebuilt and then left behind on every machine until somebody pushes. The plan now +sends what it waits for (mesh-controller PR `fix/a-plan-sends-what-it-waits-for`), which covers the +gate; the general rule — a new artifact for a module with a roll-out policy is sent, moved commit or +not — is the decision this report asks for, and the catalogue's *moved* event should say what moved: +the artifact. + +*How this would be checked:* a controller test registering a build of an unchanged commit with a +new artifact digest for a module whose policy rolls out: the machines are sent; `status` shows no +machine behind afterwards. From 6f26f97fdbf5c5a677a815a10b6044be79832520 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 22:25:31 +0200 Subject: [PATCH 11/11] Issue 189: the plan's half is built; the moved word stays for a decision --- .../00-report.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md b/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md index f2537ce..868a3d9 100644 --- a/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md +++ b/04-ISSUES/189-a-rebuild-from-the-same-commit-is-not-a-move/00-report.md @@ -33,3 +33,11 @@ the artifact. *How this would be checked:* a controller test registering a build of an unchanged commit with a new artifact digest for a module whose policy rolls out: the machines are sent; `status` shows no machine behind afterwards. + +## Built in part, 2026-10-01 + +mesh-controller 203 and 204: a plan sends the machines of every module in a built tier whose policy +rolls out, once, moved commit or not, and waits for the ones a later tier is built by. After a merge +nothing is left for a hand to push, except what a *record* policy leaves by design. What remains for +a decision is the catalogue's own word: *moved* should follow the artifact, so a module rebuilt +outside any plan — by `build` by hand — rolls out the same way.