R-204 item 4 (box half): a rebuilt box DECLARES that it needs a credential (v0.199.0)

An absent off-site object has four meanings — never configured, mid-restart, a
transient config read failure, and rebuilt-and-stranded — and the hub cannot tell
them apart. The box can, from two local facts it holds with certainty, so it says
so instead of leaving the hub to deduce it from a silence (operator ruling).

The ACK's identity_blob_present is now recorded on EVERY ACK, before the gates
that used to discard it: on a box with no off-site target the auto-confirm returns
immediately, which is exactly a rebuilt box, so the one fact distinguishing it from
a box that never had off-site backups was thrown away every cycle.

The declaration needs BOTH halves — a fresh data area AND a hub-held recovery
package. Freshness alone is a box that never had off-site backups; dropping that
condition makes the whole fleet ask for credentials, which is what the Scenario B
test exists to catch.

The object carries enabled:false and zero sizes, which is what makes it inert to
the hub's existing fill and staleness checkers and to a pre-upgrade hub. A
configured box's JSON is byte-identical to v0.198.0's.
This commit is contained in:
2026-08-05 10:47:51 +02:00
parent 68f195676b
commit 1214bae0a2
7 changed files with 438 additions and 5 deletions
+63 -2
View File
@@ -1188,6 +1188,15 @@ func offboxAnchorAfterRun(prev, at string, runErr error) string {
type OffboxReportStatus struct {
Enabled bool `json:"enabled"`
EscrowState string `json:"escrow_state"`
// State (v0.199.0, R-204 item 4 / R-193) is a DECLARED condition — the box naming its own
// situation rather than the hub deducing it from a silence. Empty on every configured box, so a
// healthy report's JSON is byte-identical to v0.198.0's.
//
// WHY A DECLARATION AND NOT AN INFERENCE (the operator ruling, 2026-08-05). An ABSENT off-site
// object has FOUR meanings — never configured, mid-restart, a transient config read failure, and
// rebuilt-and-stranded — and the hub cannot tell them apart. The BOX can, from two local facts it
// holds with certainty. So it says so.
State string `json:"state,omitempty"`
LastRun string `json:"last_run,omitempty"` // RFC3339
LastStatus string `json:"last_status,omitempty"` // "ok" | "incomplete" (R-203) | "error" | "running"
// LastSuccess (R-100) is the last run that SUCCEEDED — the hub's staleness anchor. Absent on a
@@ -1199,11 +1208,63 @@ type OffboxReportStatus struct {
QuotaGB int `json:"quota_gb"`
}
// OffboxReportStatus returns the offsite summary for the hub report (nil = not configured; the hub's
// checker treats absence as "nothing to watch" — pre-v0.109 reports look the same).
// OffsiteStateNeedsCredential is the ONE declared state (v0.199.0, R-204 item 4 / R-193): this box
// has no off-site tier, holds no repository password, and the hub says it is keeping a sealed
// recovery package for it — i.e. it is a REBUILT box whose predecessor spent the one-time provider
// password, and it cannot configure its off-site tier without a credential it has no way to obtain.
// That was the last of the four manual interventions the 2026-08-04 drill needed.
const OffsiteStateNeedsCredential = "needs_credential"
// needsOffsiteCredential is the stranded-rebuild predicate. BOTH facts are required and neither is
// sufficient on its own — this is the whole correctness of the feature:
//
// - the data area is FRESH (no repository password on disk). Alone, this is simply a box that never
// had off-site backups, and declaring on it would make every un-configured box in the fleet ask
// for a credential.
// - the HUB holds a sealed recovery package (the ACK's identity_blob_present, cached in settings).
// Alone, this is a healthy box that has run its ceremony.
//
// Only together do they mean "this box HAD an off-site tier, and no longer has what it needs to use
// it". A target that exists but is DISABLED is not stranded either — the customer switched it off —
// so the caller only consults this when there is no enabled target, and a non-nil disabled target
// short-circuits to false here.
func (m *Manager) needsOffsiteCredential(t *settings.OffboxTarget) bool {
if t != nil {
return false // a target exists (merely disabled) — the customer's own choice, not a rebuild
}
if m.settings == nil || !m.settings.GetHubEscrowIdentityPresent() {
return false // the hub holds nothing for us: never had off-site backups
}
if _, ok := m.OffboxRepoPasswordHash(); ok {
return false // we still hold our repository password: not a fresh data area
}
return true
}
// OffboxReportStatus returns the offsite summary for the hub report.
//
// nil = nothing to say (not configured, and nothing to ask for) — the hub's checker treats absence as
// "nothing to watch"; pre-v0.109 reports look the same.
//
// v0.199.0: there is now ONE case where an UNCONFIGURED box still reports an object — the stranded
// rebuild, which DECLARES OffsiteStateNeedsCredential rather than leaving the hub to deduce it from a
// silence. An absent object has FOUR meanings (never configured / mid-restart / a transient config
// read failure / rebuilt-and-stranded) and the hub cannot tell them apart; the box can.
//
// WHY THE DECLARATION IS INERT TO EVERY EXISTING READER, established from their code rather than
// assumed: it carries Enabled=false and zero quota/size, and the hub's OffsiteChecker gates
// `isStale` on `!off.Enabled` (returns false) and `fillBand` on a zero quota/size (returns OK). So it
// raises no staleness and no fill alarm on a new hub OR an old one, and an unknown `state` string is
// ignored by encoding/json. The ONE reader that would have misread it is the store's
// `reportHasOffsite` ("presence == applied-on-the-box"), which hub v0.96.0 tightens to require
// enabled=true — provably a no-op for every report shape that exists today, because this function has
// never emitted a disabled object before.
func (m *Manager) OffboxReportStatus() *OffboxReportStatus {
t := m.settings.GetOffboxTarget()
if t == nil || !t.Enabled {
if m.needsOffsiteCredential(t) {
return &OffboxReportStatus{Enabled: false, State: OffsiteStateNeedsCredential}
}
return nil
}
return &OffboxReportStatus{