R-195: a customer with no machine ever bound does not alarm (hub v0.92.0) + R-193/R-192 spike
gates / gates (push) Successful in 7s

Part 4 (ships): `david` — a prospective customer with hosts=0, host_deletions=0,
reports=0 — e-mailed an expected_dbdump_missed ERROR at 03:00 UTC three mornings
running. The existing down-skip could never cover it: it reads the staleness
checker's state, which is seeded from a query over the `reports` table, so a
customer that never reported has no state at all and GetState() returns "" rather
than "down". store.HasEverBoundHost (hosts row OR host_deletions tombstone) is
consulted once per customer at the top of the deadline loop. The discriminator is
"was a host EVER bound", never "has a report arrived" — a box installed and never
heard from is a real fault and keeps alarming. Fail-OPEN on a read error. Red-proof
observed: removing the guard fails with `got [expected_dbdump_missed]`, verbatim the
event david sent.

Parts 0-3 (spike, NO production code for R-193/R-192):
audits/SPIKE-offsite-credential-recovery-2026-08-04.md establishes that the one-shot
provider password is the RECOVERABLE secret and the restic repository password is the
irreplaceable one — and that a guest rebuild mints a fresh one, orphaning the previous
off-site history. Measured without touching a box, by comparing
host_escrow.restic_pw_sha256 against host_escrow_superseded: BOTH demo boxes changed
(demo-hp 15 snapshots / 40.9 MB, demo-felhom 36 snapshots / 1.14 GB). demo-felhom's
"lucky" 76-second recovery restored delivery and not the repository, silently, for 13h.
ReissueCredentials does NOT rotate the restic password (R-39's record and two hub
comments are wrong -> R-196); candidate (b) is not implementable against a
zero-knowledge escrow; candidate (a) already exists as F3 and is wired to the wrong
event. Ends in ranked options and an unanswered question for the operator.

R-195 SHIPPED; R-196 + R-197 filed; R-192 + R-193 updated, neither closed.
This commit is contained in:
2026-08-04 11:04:39 +02:00
parent f456835bbc
commit 7fff45d688
9 changed files with 815 additions and 6 deletions
+22
View File
@@ -2262,6 +2262,28 @@ func (s *Store) GetHostByCustomer(customerID string) (*Host, error) {
return h, err
}
// HasEverBoundHost reports whether a machine was EVER bound to this customer — a live row in
// `hosts` OR a tombstone in `host_deletions`. It answers "was anything ever expected of this
// customer", which is the question the deadline verdicts actually need (R-195).
//
// It is deliberately NOT "has a report arrived", and the distinction is the whole point: a box
// that was installed, bound, and then went silent IS bound, and its silence is a real fault that
// must keep alarming. Only a customer that never had a machine at all is UNKNOWN.
//
// `host_deletions` is included because a customer whose host was removed HAD one — the deadline
// caller reaches its down-skip for that shape, and this predicate must not quietly take over a
// judgement the staleness checker owns.
func (s *Store) HasEverBoundHost(customerID string) (bool, error) {
var n int
if err := s.db.QueryRow(
`SELECT EXISTS(SELECT 1 FROM hosts WHERE customer_id = ?)
OR EXISTS(SELECT 1 FROM host_deletions WHERE customer_id = ?)`,
customerID, customerID).Scan(&n); err != nil {
return false, err
}
return n != 0, nil
}
// ListHostsByCustomer returns the customer's hosts ordered by host_id (v0.47.0 — the
// customer page's Host tab is a LIST by design: 1 host today, N for a later HA cluster).
// Uses the idx_hosts_customer index.