CHANGELOG: hub v0.96.0 (R-204 item 4, hub half)
gates / gates (push) Successful in 7s

This commit is contained in:
2026-08-05 10:49:23 +02:00
parent fe1e81628d
commit b2462b8f36
+69
View File
@@ -1,3 +1,72 @@
## v0.96.0 — the hub answers a rebuilt box's request (2026-08-05, R-204 item 4 / R-193 / R-192)
The hub half of the last manual intervention the 2026-08-04 drill needed. The box declares
(controller v0.199.0); this answers.
### `internal/offsiteheal` — the sibling of `pbsdrheal`
Same shape, same restraint: **declared states only**, a debounce, **restage → escalate**, a distinct
audit event per remediation, and a healthy box is a pure no-op. It never blind-timer-reissues and
never re-runs a provisioning step.
**The debounce is TWO distinct reports, derived rather than chosen.** The controller reports every
~15 minutes, so two distinct declarations mean the state survived a full report cycle — a restart, a
slow first report or a transient config read all resolve well inside one. One report would act on a
blip; three would leave a stranded customer waiting ~45 minutes for the one thing they cannot obtain
any other way. The reconciler's own tick (5 min) is deliberately faster than the report cadence so it
adds no latency of its own; the debounce counts **fresh evidence, not ticks**.
**§8.4 — is there a deliberately-unhealed state, as `pbsdrheal` has in `verify_failed`?** Yes, and it
is excluded **by construction** rather than by a case in the switch — worth stating plainly, because
"there is nothing like that here" is usually wrong. The analogue is the **regressed** shape: a box
that had a working tier and lost its target while still holding its repository password. Re-arming a
credential would not help it. It cannot reach this reconciler at all, because the controller's
declaration predicate requires the repository password to be **absent**.
### Restage before mint — and the fact it rests on
`store.RestageOneTimeSecret` clears the consumed flag without touching the value, mirroring
`RestageHostPBSSecret`. **That the value survives a consume was established from this schema and from
`ConsumeOneTimeSecret` — which stamps `consumed_at` and nothing else — not inherited from the PBS
analogy.** The two secrets are different objects with different lifecycles, and assuming a shared
shape is how two earlier sessions confused the credentials. `TestRestageOneTimeSecret_ReArmsTheSameValue`
asserts the **same** value comes back, so a future "hardening" that cleared the column would fail
loudly instead of silently turning every rebuild into an external mint.
### `reportHasOffsite` tightened — the ninth comment-vs-code entry, caught before it shipped
Its comment asserted *"the report builder attaches `offsite` only when the box actually has an offbox
target configured, so presence == applied-on-the-box"*. Controller v0.199.0 breaks that premise
deliberately. Left as bare presence, this would have read a **request for help** as proof the tier was
**applied** — making `DeliveryStateFor` return `DeliveryApplied` for exactly the stranded boxes, and
suppressing their stuck event. It now requires `enabled:true`, which is **provably a no-op for every
report shape that existed before** (an attached object has always carried `enabled:true`), and the
equivalence is measured in `TestReportHasOffsite_EnabledOnly` rather than argued.
### R-192's guard half — CLOSED BY REPLACEMENT, not repaired
The delivery checker decided the shape by counting how many of the **oldest 500** reports after a
consume carried an offbox target. On demo-hp all 500 predated the rebuild, so it confidently reported
the *regressed* shape and declined to heal — for 108 reports, while the box sat stranded. A
declaration needs no window, no count and no inference, so it **outranks both inferred shapes**
(`shapeDeclared`), the stuck e-mail says what was declared instead of prescribing Re-issue, and
`maybeHeal` **stands down with a record** because `offsiteheal` owns the remediation. Two mechanisms
healing one customer would double-issue — and this one can only mint, so it would also skip the cheap
path.
### What is deliberately NOT automated
**No escrow ceremony is ever run or requested.** A credential is replaceable; the recovery code is
not, because only the customer holds it. **Credential automatic, key customer-present.**
**On security, weighed rather than skipped:** a re-issued credential is transport only. Anyone able to
forge the declaration already controls the box, and the box already holds its own installed access
key. The data stays sealed under the recovery code throughout.
### Rider — the pre-push hook refuses a clone outside the workspace
One assertion, identical in all four repos. See the controller's v0.199.0 entry.
## v0.95.0 — a re-issue no longer marks a healthy escrow stale (2026-08-05, R-196 / R-204 item 2)
`ReissueCredentials` called `MarkEscrowStale` on every re-issue that found an escrow row. The mark was