Refuse a declaration for the other mode, or older than the mesh's last, and preview before applying (hq issue 104) #23

Merged
jschoubben merged 2 commits from fix/reconcile-refuses-stale into main 2026-09-23 21:38:33 +00:00
Owner

Fixes hq issue 104: mesh-host reconcile on an adopted control-node applied the converged genesis bundle, loaded the base filter and closed the machine for ~45 minutes. The host knew the node was adopted, the declaration said converged, nothing compared the two, nothing was printed before acting.

Refusals, at the point of application

  • The state records the node's mode (mode in state.json) — from every declaration the mesh sends, and at genesis from --adopted. reconcile, apply FILE and the reconcile loop refuse a declaration for the other mode: "this node is adopted; the declaration says converged — a converged declaration is not applied to an adopted node; converge on the controller is the act that changes it, and it sends the declaration that does" (and the mirror, naming adopt). Only a declaration the link delivers, signed, changes the recorded mode — that is how the flip arrives (inv.Converge then sendNodes), so it still works and nothing else can do it.
  • Genesis marks the bundle consumed (genesis in state.json: digest of the bytes applied, rewritten). reconcile holds a node the mesh has spoken to against declared.json, verified, never the bundle; refuses with the reason when it cannot verify it; and refuses the carried bytes when they are not what genesis applied.
  • apply FILE: the genesis bundle over a later mesh declaration is refused as older, naming both digests; any other file that is not what the mesh last said is refused too, saying that a declaration carries no sequence and no issued-at so the host cannot tell older from newer.

Preview: both commands print the plan — remove / forget / hold / create / update / check / run / disable / enable, in apply order, an action named with its command and verify — before touching anything; --dry-run prints it and stops (--json --dry-run gives it as JSON; --json apply output is now {plan, report}).

Tests (each fails to build against main): converged-on-adopted and adopted-on-converged refused with the message; the mesh's own flip not refused; older / not-last / not-consumed refused naming both; dry-run changes nothing and lists the action; reconcile after a controller declaration chooses declared.json (and refuses unproven, never the bundle); genesis records digest and mode; plan verbs. go build, go vet, go test ./... green; gofmt -l reports only the pre-existing internal/bootstrap/control_test.go, untouched here.

Not merged; awaiting review.

Fixes hq issue 104: `mesh-host reconcile` on an adopted control-node applied the converged genesis bundle, loaded the base filter and closed the machine for ~45 minutes. The host knew the node was adopted, the declaration said converged, nothing compared the two, nothing was printed before acting. **Refusals, at the point of application** - The state records the node's mode (`mode` in state.json) — from every declaration the mesh sends, and at genesis from `--adopted`. `reconcile`, `apply FILE` and the reconcile loop refuse a declaration for the other mode: *"this node is adopted; the declaration says converged — a converged declaration is not applied to an adopted node; `converge` on the controller is the act that changes it, and it sends the declaration that does"* (and the mirror, naming `adopt`). Only a declaration the link delivers, signed, changes the recorded mode — that is how the flip arrives (`inv.Converge` then `sendNodes`), so it still works and nothing else can do it. - Genesis marks the bundle consumed (`genesis` in state.json: digest of the bytes applied, rewritten). `reconcile` holds a node the mesh has spoken to against `declared.json`, verified, never the bundle; refuses with the reason when it cannot verify it; and refuses the carried bytes when they are not what genesis applied. - `apply FILE`: the genesis bundle over a later mesh declaration is refused as older, naming both digests; any other file that is not what the mesh last said is refused too, saying that a declaration carries no sequence and no issued-at so the host cannot tell older from newer. **Preview**: both commands print the plan — remove / forget / hold / create / update / check / run / disable / enable, in apply order, an action named with its command and verify — before touching anything; `--dry-run` prints it and stops (`--json --dry-run` gives it as JSON; `--json` apply output is now `{plan, report}`). Tests (each fails to build against main): converged-on-adopted and adopted-on-converged refused with the message; the mesh's own flip not refused; older / not-last / not-consumed refused naming both; dry-run changes nothing and lists the action; reconcile after a controller declaration chooses declared.json (and refuses unproven, never the bundle); genesis records digest and mode; plan verbs. `go build`, `go vet`, `go test ./...` green; `gofmt -l` reports only the pre-existing `internal/bootstrap/control_test.go`, untouched here. Not merged; awaiting review.
jschoubben added 1 commit 2026-09-23 21:15:48 +00:00
An operator ran `mesh-host reconcile` on an adopted control-node with twelve
modules assigned. It applied the bundle the host carries — the genesis
declaration, foundation only, converged: recreated the store, failed on the
broker's held port, wrote the converged base filter and started its service,
and stopped at the first failing action. The filter closed the machine for
forty-five minutes. The host reported the node adopted in every report, the
declaration said converged, and nothing compared the two; nothing was printed
before acting (hq issue 104).

The host now records the node's mode — from every declaration the mesh sends,
and at genesis from what the operator said — and refuses, at the point of
application, a declaration that says the other mode, naming both and the act
that changes it. Only a declaration the link delivers, signed, changes the
mode: that is how `converge` and `adopt` arrive, so the flip still works and
nothing else can do it. Genesis marks the bundle consumed, with the digest of
what it applied, so `reconcile` holds a node the mesh has spoken to against
what the mesh last said and never the bundle, and refuses the carried bytes
when they are not what genesis applied. A file is refused when it is not what
the mesh last said: a declaration carries no sequence and no issued-at, so the
host cannot tell older from newer, and says so. Both commands print what they
would change — a hold, a removal, an action named as one — before touching
anything, and --dry-run is that list and nothing more.
jschoubben added 1 commit 2026-09-23 21:35:51 +00:00
Review of the fix for hq issue 104 found three faults in it. A file applied
on an enrolled node — the mesh's own last declaration included — is applied
as the bundle is, so its resources are recorded as the machine's own and
what the mesh declared reads as undeclared: the plan removed the foundation.
`apply FILE` is for a machine the mesh has not spoken to, and is now refused
saying so whenever declared.json exists. The plan looked at what is held
before what the declaration says is taken, so the one cutover ADR 0100 says
must be previewed read as a hold; it now decides in holdOnAdopted's order,
models a step run inside a held container, and a test holds the plan's
sequence to the apply's outcomes. Genesis wrote the mode on every run, so a
re-run after `converge` left the state saying adopted while the kept,
signed declaration said converged, and the reconcile loop refused every five
minutes with no delivery coming to end it: genesis now writes the mode only
when none is recorded, and where the state and the verified kept declaration
disagree, the kept declaration wins and the repair is said.

Also: a file lock beside the state, taken by the link service, the host's
own commands and the installer alike, so a `reconcile` run by hand no
longer races the loop's save — chosen over refusing while a named service is
active, which would miss a `mesh-host run` started by hand; `--json
--dry-run` emits {plan} like an apply emits {plan, report}; the README's
duplicate flag line; and the bundle refusal is about the digest, not a claim
the carried bytes can never match what genesis applied.
jschoubben merged commit 977df39e0d into main 2026-09-23 21:38:33 +00:00
jschoubben deleted branch fix/reconcile-refuses-stale 2026-09-23 21:38:33 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-host#23