hub v0.112.0: a floor carries a declared MinAgent past the golden (R-472)
gates / gates (push) Successful in 18s

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/.
This commit is contained in:
2026-09-13 16:47:11 +02:00
parent 5ef0f52bcd
commit f181efd6a7
17 changed files with 599 additions and 27 deletions
@@ -117,6 +117,19 @@ reconciles only on change and reports which generation it has converged to).
**Geo is *not* in the agent's desired state** — it's customer→hub→Cloudflare (§7); the agent never
touches WAF.
**The controller floor and its agent requirement (hub v0.112.0, R-472).** The managed controller floor
rides the report ACK. `store.ResolveManagedFloor` decides per box whether to serve it, from three
inputs: the floor in force (per-customer override, else global), the vouched Day-0 manifest (golden
version + MinAgent), and a **declared MinAgent** stored beside the floor
(`hub_settings.min_controller_version_declared_min_agent` for the global floor,
`customer_configs.min_controller_declared_min_agent` for an override, each as `FLOOR=MINAGENT` so it
binds only to the floor it was saved with). Inside the golden the manifest's MinAgent governs. Above
the golden the declared one does; with no declaration the floor is **held beyond the golden**, as since
R-216. Either way the box's reported agent must meet the chosen MinAgent, else the floor is held with
`agent <v> < MinAgent <w>`. The decision carries `MinAgentSource` (`manifest` / `declared`), shown on
the Hosts page and logged once per change as `managed floor SERVED`. The rules the operator follows:
`runbooks/publish-train-rules.md` rule 1.
## 6. Authorization — signed-op queue + editing flow
Implements Part 4's gate on the hub side. The hub holds **no signing key**.
@@ -0,0 +1,13 @@
MUTATION in internal/store/store.go:
- if beyondGolden && d.DeclaredMinAgent != "" {
d.MinAgent, d.MinAgentSource = d.DeclaredMinAgent, MinAgentSourceDeclared
}
+ _ = MinAgentSourceDeclared
=== RUN TestDeclaredMinAgent_A_AboveGoldenServedWhenAgentMeetsIt
declared_floor_test.go:33: a declared floor above the golden must be SERVED: {Floor: Held:true AgentVersion:0.129.0 RequestedFloor:0.239.0 MinAgent:0.129.0 GoldenVersion:0.236.0 HeldBeyondGolden:true DeclaredMinAgent:0.129.0 MinAgentSource:manifest} (reason "held: floor 0.239.0 is ABOVE the vouched golden 0.236.0, so its agent requirement is unknown — vouch a golden carrying the floor's controller (publish-train rule 1)")
--- FAIL: TestDeclaredMinAgent_A_AboveGoldenServedWhenAgentMeetsIt (0.02s)
FAIL
FAIL gitea.dooplex.hu/admin/felhom-hub/internal/store 0.027s
FAIL
exit=1
@@ -0,0 +1,11 @@
MUTATION in internal/store/store.go:
- if beyondGolden && d.MinAgentSource != MinAgentSourceDeclared {
+ if false && beyondGolden && d.MinAgentSource != MinAgentSourceDeclared {
=== RUN TestDeclaredMinAgent_C_UndeclaredAboveGoldenIsHeldExactlyAsBefore
declared_floor_test.go:64: an UNDECLARED floor above the golden must stay held beyond the golden: {Floor:0.239.0 Held:false AgentVersion:0.130.0 RequestedFloor:0.239.0 MinAgent:0.129.0 GoldenVersion:0.236.0 HeldBeyondGolden:false DeclaredMinAgent: MinAgentSource:manifest}
--- FAIL: TestDeclaredMinAgent_C_UndeclaredAboveGoldenIsHeldExactlyAsBefore (0.02s)
FAIL
FAIL gitea.dooplex.hu/admin/felhom-hub/internal/store 0.031s
FAIL
exit=1
+15 -3
View File
@@ -98,6 +98,18 @@ the manifest vouches the target FIRST; any MinAgent must be satisfied fleet-wide
boxes below it automatically, and flags them); **save the floor LAST** — the DB row acts immediately
on every box below it, on their next report.
**Raise the floor to a release with no golden (hub v0.112.0+, R-472).** Between bakes this is the
normal route — do not hand-deploy.
1. Build and push the controller image (above). Do not install it on any box.
2. Read the MinAgent from the release's header: `grep -m1 -A3 '^## v<VER>' CHANGELOG.md` shows
`**MinAgent: X.Y.Z**`. `controller_gates.py --fast` refuses a release whose newest header lacks it.
3. Hub → Configuration → Managed updates: type `<VER>` in the floor field **and** that value in
`min_agent`. Save. Without `min_agent` the form refuses with *"This floor is above the vouched
golden — declare its MinAgent"* and stores nothing.
4. Verify: `sudo kubectl -n felhom-system logs deploy/hub | grep 'managed floor SERVED'` shows
`from "declared"`, and each box logs `SetFloor: floor "…" → "<VER>"` within about a minute.
## 4. Golden image (fresh Day-0 installs)
The golden is a pre-baked controller-era guest image built in the **drill VM** on 180
@@ -211,9 +223,9 @@ The full 0.188.0 run, with the observables: `documentation/audits/tester-gate-go
because `golden_currency_gate.py` trips on every release by design and the only honest ways past it
were a bake or a declared `--no-verify` (thirteen of those by 2026-09-01 — R-404/R-417). **The
ruling:** bake on a cadence. The ruling assumed every release would still raise the FLOOR (§4.1 step
5) and reach both demo boxes in ~20 s, with only the golden moving to a cadence. **⚠ CORRECTED THE SAME DAY (R-472): between bakes the floor does NOT carry a release — the hub holds any floor above the vouched golden (publish-train rule 1), so releases between bakes reach the demo boxes only by hand-deploy.** A
release between bakes is hand-deployed per the `felhom-build-deploy` skill, and the floor is left at
the vouched golden.
5) and reach both demo boxes in ~20 s, with only the golden moving to a cadence. **⚠ CORRECTED THE SAME DAY (R-472): the hub held any floor above the vouched golden (publish-train rule 1), so releases between bakes reached the demo boxes only by hand-deploy.** **RESOLVED in hub v0.112.0:** a floor above the golden is served when it carries a
declared MinAgent (§3 "Raise the floor to a release with no golden"). A release between bakes rides
the floor again; only an undeclared floor is still held.
**The cadence, and it is a step in a routine, not a memory:**
@@ -10,6 +10,24 @@ Publish and vouch the artifacts in the Day-0 manifest **before** any floor movem
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