Files
felhom.eu/documentation/runbooks/publish-train-rules.md
T
admin f181efd6a7
gates / gates (push) Successful in 18s
hub v0.112.0: a floor carries a declared MinAgent past the golden (R-472)
Operator ruling 2026-09-13. Above the vouched golden, a floor saved with a
declared MinAgent is served under the same agent comparison; an undeclared
one is still held beyond the golden. The declaration is stored beside each
floor as FLOOR=MINAGENT so it never carries to a later floor. Both floor
forms require min_agent above the golden (flash floor_needs_min_agent,
nothing stored). The Hosts page and the API log name the source.

Vouch path and R-120 gate untouched. Scenarios A-E tested; red-proofs A
and C in documentation/audits/rulings-r472-r475-2026-09-13/.
2026-09-13 16:47:11 +02:00

110 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Publish-train rules — coupled controller/agent releases
Standing gates for every artifact publish train (agent binary + controller golden + Day-0 manifest +
floor). Each rule cites the incident that created it. The box-level backstop (rule 4) exists because
rules 1–3 are operator discipline and discipline fails; the rules stay the primary control.
## 1. Manifest before floor (the GL-1 rule, restated)
Publish and vouch the artifacts in the Day-0 manifest **before** any floor movement. A floor that
points at an unvouched (or unpublished) version bricks self-updates: boxes are told to move to a
version they cannot verify. (GL-1 supply-chain arc; see the go-live package records.)
**Since hub v0.112.0 (operator ruling 2026-09-13, R-472): the manifest leads the floor inside the
golden; above it, the release's own declared MinAgent does.** A controller image is pulled by tag from
the registry, so a floor above the golden does not need a golden to be delivered — it needs to know
which agent the release requires. The operator declares that with the floor, in the `min_agent` field
next to it, read from the release's controller `CHANGELOG.md` header (`**MinAgent: X.Y.Z**`, pinned by
`controller/scripts/minagent_header_gate.py`).
- **At or below the vouched golden:** the manifest's MinAgent governs, exactly as before. A declared
value is recorded and not used.
- **Above the golden, declared:** the hub compares each box's agent against the declared MinAgent
(rule 3's comparison, same hold text). Hub log: `managed floor SERVED … from declared`. The Hosts
page marks the box `floor: declared MinAgent`.
- **Above the golden, undeclared:** still **HELD beyond the golden**, unchanged (R-216). Both floor
forms refuse to save such a floor (`floor_needs_min_agent`), so the hold is now a sentence at save
time instead of a silent fleet-wide stop.
- A declaration belongs to the exact floor it was saved with (`FLOOR=MINAGENT`). Moving the floor
without a new declaration never carries the old one forward.
## 2. The manifest screen carries the LIVE DB floor — save the floor field LAST
The hub's Day-0 manifest UI also persists the global floor as a DB row
(`hub_settings.min_controller_version` — `hub/internal/store/store.go`,
`GetGlobalMinControllerVersion`). **The DB row overrides the env floor and acts immediately on the
next report cycle** — it is not staged by the manifest vouch.
**Incident:** publish train 0.81/0.113 (2026-07-11,
`documentation/pilot/RUNBOOK-publish-0.81-0.113-2026-07-11.md`): the operator saved the manifest
screen with the floor field filled; the floor acted at once and controller 0.113.0 reached Peti's
box ~9 minutes **before** agent 0.81.0 — the exact forbidden skew, benign only because the box had
zero NAS shares.
**Rule:** fill the floor field **last**, in a separate save, only after rule 3's fleet check passes.
## 3. MinAgent fleet gate — HUB-ENFORCED PER-BOX since hub v0.45.0
A controller release that depends on coupled agent behavior declares `MinAgent: X.Y.Z` in its
CHANGELOG entry header line (felhom-controller convention, since v0.114.0; retroactively, 0.113.0's
effective MinAgent was 0.81.0 for the NAS add). The floor may not effectively push a box past a
controller whose MinAgent that box's agent does not yet meet.
**Hub-enforced per-box since hub v0.45.0 (the manual fleet check is retired):** the operator sets
the golden's **`MinAgent`** in the Day-0 artifact manifest (Configuration → Day-0 artifacts →
"Min agent"; blank = uncoupled release, no gating). At report-ACK time the hub compares each box's
reported `hosts.agent_version` against that MinAgent
(`store.ResolveManagedFloor`, `hub/internal/store/store.go`): agent ≥ MinAgent → the controller
floor is served; agent below MinAgent **or unknown** → the floor is **HELD** (the ACK omits the
directive) and the box is flagged on the Hosts dashboard (`floor held: agent <v> < MinAgent <w>`) —
a held box is visible, never silently stale. The operator still SETS MinAgent at manifest time; the
hub does the per-box gating. This mechanises the "agent BEFORE controller floor" ordering that rule
2's incident violated by hand.
Effective-floor visibility (same v0.45.0): the floor card shows the resolved value + its source
(DB `hub_settings` vs env `DEFAULT_MIN_CONTROLLER_VERSION`, both raw values when they differ), and
the floor save is behind a type-to-confirm dialog stating the live below-floor blast radius — so
rule 2's "the floor acts immediately" is impossible to miss.
## 4. Box-level backstop: the controller's capability gate
Since controller v0.114.0, a coupled feature entry point probes the agent's capability
(`controller/internal/agentapi/features.go` — route probe: 2xx ⇒ supported, 404 ⇒ older agent,
transport/5xx ⇒ indeterminate, never "too old") and refuses up front with an honest Hungarian
message instead of failing mid-pipeline. This turns a violated ordering into a graceful refusal —
it does **not** license sloppy trains: rules 1–3 remain the primary control.
Convention for new coupled features: add a row to the `featureProbes` table AND the `featureMinAgent`
table + a `Supports` gate call at the feature's entry point, and declare MinAgent per rule 3. Since
controller v0.115.0 + agent v0.82.0 the agent reports its version in the `X-Felhom-Agent-Version`
response header, so `Supports` decides by version comparison when the version is known and only
falls back to the route probe for header-less (≤0.81) agents — capability detection is now explicit,
not probe-inferred.
## 5. Every ISO build asserts golden ≥ the managed floor (the R-71 gate)
**Incident:** `DIAG-f10-demo-hp-offsite-2026-07-23.md` / R-71. A box installed from an ISO whose
golden controller is BELOW the hub's managed floor boots below the floor, so the day-0 managed
update fires within minutes of first boot — racing the offsite apply-bridge in exactly the window
that burned demo-hp's one-time offsite credential (2 days unprotected). The gap is invisible at
build time unless something checks it.
**Rule:** a build where the managed floor exceeds golden must **fail loudly, at build time**, not
ship. `build-felhom-iso.sh` carries the assertion itself (`assert_golden_ge_floor`, gated on
`FELHOM_ASSERT_GOLDEN` = the hub's `artifact_golden_version` and `FELHOM_ASSERT_FLOOR` = its
`min_controller_version`); it prints both versions and `die`s on `golden < floor`. Resolve the two
values operator-side before the build and pass them in — e.g. from the hub DB
(`hub_settings.artifact_golden_version` / `min_controller_version`) or the operator artifacts UI —
so the gate is enforced, not skipped (an unset input warns LOUDLY and does not silently pass). The
fix for a tripped gate is never to lower the floor: **republish golden ≥ floor and vouch it**
(rule 1), then rebuild. This gate composes with rule 1 — the manifest still leads the floor; this
one stops an ISO from carrying a golden the floor has already outrun.
> **Dated note, 2026-08-18 — the current pair is golden `0.216.0` / floor `0.216.0`.** Equal, so the
> R-71 gate passes. Recorded so the next build has numbers to hand, and deliberately **not** as a
> replacement for the instruction above: **resolve both LIVE from the hub before a build.** A
> snapshot that looks authoritative is exactly how `build-golden.sh`'s hand-bumped `CONTROLLER_IMAGE`
> default rotted twice. Treat these as a sanity check on what you read, never as a substitute for
> reading it. (Floor raised to 0.216.0 by the operator on 2026-08-18 in a separate save after the
> vouch, per rule 2 — see R-343, which records that the raise moved `demo-felhom` nine seconds later.)