diff --git a/hub/CHANGELOG.md b/hub/CHANGELOG.md index 8e49e1a8..9867185d 100644 --- a/hub/CHANGELOG.md +++ b/hub/CHANGELOG.md @@ -12,6 +12,12 @@ `TestD30_TheAlarmClearsWhenTheNewKeyShakesHands` (red-proved). The stale text says an agent with D30 re-keys by itself 15 minutes after it sees the sign, plus its next desired-state fetch; the `wg_tunnel_ok` log line is INFO; the source comment no longer says the ok event is mailed (it is stored, not mailed — review 2026-10-22 night). +- **R-1230 — a held box is not „stuck".** Measured on the test night (Tester4, reinstalled): a box that consumed its + one-time off-site password and now waits for the household's recovery code reports `offsite.state= + awaiting_recovery_key` and no offbox target; after one hour the delivery checker read it as „burned" — a false + `offsite_delivery_stuck` mail to the operator and a re-issue of the off-site login (a config bump that restarts the + box's controller). The checker now stands down for that declared state (the wait is the household's, it can take + days); an undeclared consumed shape alarms as before. `TestR1230_AHeldBoxIsNotStuck`, red-proved. - `scripts/wire_contract_gate.py`: `wireguard.last_handshake_age_s` left the unread list (the hub reads it since 0.166.0). ## v0.166.0 (unreleased — built 2026-10-22 night, NOT deployed; the deploy is the operator's yes) — the clock-ahead tunnel alarm (R-1204) diff --git a/hub/internal/monitor/offsite_delivery.go b/hub/internal/monitor/offsite_delivery.go index 1bd6bf1f..957aee56 100644 --- a/hub/internal/monitor/offsite_delivery.go +++ b/hub/internal/monitor/offsite_delivery.go @@ -102,6 +102,12 @@ func (c *OffsiteDeliveryChecker) Check() { declared := false if _, _, state, derr := c.store.LatestReportOffsiteDeclaration(cfg.CustomerID); derr != nil { c.logger.Printf("[WARN] offsite-delivery: %s: read declaration: %v (treating as undeclared)", cfg.CustomerID, derr) + } else if state == declaredAwaitingRecoveryKey { + // R-1230 (test night 2026-10-23, measured): a reinstalled box that HOLDS — its credential consumed, the + // household's recovery code not entered yet — reports no offbox target, so it read as „burned": a false + // operator mail and a needless re-issue of the off-site login (which restarts the box's controller). The + // box says what it waits for; that wait is the household's, and can take days. No event, no heal. + continue } else { declared = state == declaredNeedsCredential } @@ -137,6 +143,11 @@ const ( // are pinned together by TestDeclaredStateStringMatchesTheReconciler. const declaredNeedsCredential = "needs_credential" +// declaredAwaitingRecoveryKey mirrors the controller's backup.OffsiteStateAwaitingRecoveryKey (the box holds: the hub +// keeps a sealed package and only the household's recovery code opens it). Pinned by +// TestR1230_AHeldBoxIsNotStuck. +const declaredAwaitingRecoveryKey = "awaiting_recovery_key" + func shapeOf(status offsite.DeliveryStatus, declared bool) deliveryShape { if declared { return shapeDeclared diff --git a/hub/internal/monitor/offsite_delivery_test.go b/hub/internal/monitor/offsite_delivery_test.go index bbdd1fbd..1e729559 100644 --- a/hub/internal/monitor/offsite_delivery_test.go +++ b/hub/internal/monitor/offsite_delivery_test.go @@ -300,3 +300,30 @@ func TestDeliveryChecker_QuietShapes(t *testing.T) { t.Fatalf("disabled offsite must be skipped, got events=%v calls=%d", *h.events, h.reissuer.count()) } } + +// R-1230 (test night 2026-10-23, measured live on Tester4): a reinstalled box that waits for the household's recovery +// code reports `offsite.state=awaiting_recovery_key` and no offbox target. It must raise no „stuck" event and no +// re-issue — the wait is the household's. The control: the same consumed shape WITHOUT that declaration still alarms. +// RED-PROOF: with the awaiting_recovery_key skip removed, this fails (a stuck event and a re-issue) — seen failing +// 2026-10-23 night. +func TestR1230_AHeldBoxIsNotStuck(t *testing.T) { + h := newDTHarness(t, true) + h.burnedFixture(t, 5) + if err := h.st.SaveReport("c1", []byte(`{"health":{"status":"ok"},"offsite":{"enabled":false,"state":"awaiting_recovery_key","escrow_state":"pending"}}`)); err != nil { + t.Fatal(err) + } + h.checker.Check() + if n := len(h.savedEvents(t, "offsite_delivery_stuck")); n != 0 { + t.Fatalf("a box waiting for the household's recovery code is not stuck: %d stuck event(s)", n) + } + if h.reissuer.count() != 0 { + t.Fatalf("no re-issue for a held box, got %d", h.reissuer.count()) + } + if err := h.st.SaveReport("c1", []byte(dtReportNoOffsite)); err != nil { // control: no declaration → the old verdict + t.Fatal(err) + } + h.checker.Check() + if n := len(h.savedEvents(t, "offsite_delivery_stuck")); n != 1 { + t.Fatalf("control: an undeclared consumed shape must still alarm, got %d", n) + } +}