controller v0.178.0 — R-88 Part 2: only a positive 'never' fires the valve

MinAgent: 0.105.0. scheduledRunAllowed fired on any nil age; it now requires a
licence from valveLicensed, which grants it for AgeStateAbsent and for a LEGACY
agent, and refuses it for AgeStateUnknown. An unreadable storage no longer
masquerades as a first-ever backup and no longer quiesces apps outside the window.

A missing wire field means legacy, not unknown — deliberately. Treating it as
unknown would stop the valve firing on un-upgraded boxes and starve genuinely new
ones. Degrade logged once; unrecognised future values also map to legacy.

Caught in passing: TieredBackend is satisfied by a RUNTIME assertion, so the
signature change compiled and vetted clean while quiesceBackend silently stopped
satisfying it — which would have degraded every box to the single-tier path with
no error. Added a compile-time witness.

Also corrects the notifier comment that claimed operator-only came from a missing
customerMessages entry; enforcement is hub-side operatorOnlyEvents (hub 0.79.0).
This commit is contained in:
2026-07-27 18:08:56 +02:00
parent ba8bf9cd75
commit 86ea482fc1
12 changed files with 390 additions and 43 deletions
@@ -33,9 +33,10 @@ type agedBackend struct {
ages map[string]*int64
}
func (a *agedBackend) DueFor(ctx context.Context, target string) (bool, *int64, error) {
due, _, err := a.tierBackend.DueFor(ctx, target)
return due, a.ages[target], err
func (a *agedBackend) DueFor(ctx context.Context, target string) (bool, *int64, string, error) {
due, _, _, err := a.tierBackend.DueFor(ctx, target)
// A real age implies a KNOWN state — that is what the agent would send.
return due, a.ages[target], string(AgeStateKnown), err
}
// SCENARIO D — a genuinely never-backed-up box still gets its first backup, outside the window.
@@ -114,7 +115,7 @@ func TestContract_SafetyValveBoundary(t *testing.T) {
{"past cadence+24h → the valve fires", h(49), true},
}
for _, c := range cases {
if got := scheduledRunAllowed(outside, window, c.age, cadence24); got != c.want {
if got := scheduledRunAllowed(outside, window, c.age, true, cadence24); got != c.want {
t.Errorf("CONTRACT VIOLATED: %s → scheduledRunAllowed = %v, want %v", c.name, got, c.want)
}
}