diff --git a/CONTEXT.md b/CONTEXT.md index ed6653f..040a9f5 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -17,6 +17,38 @@ ## Standing rulings +**S-28 — the escrow retention now covers the OFF-SITE data key, and customer-present recovery is the +accepted design, which makes that retention load-bearing (2026-08-04, R-198/R-197; hub v0.93.0).** + +**The contract changed, and this is the sentence to carry:** when an escrow blob supersedes another, +`demoteCurrentEscrowTx` retains **both** sealed keys — the K-escrow `blob` (PBS datastore key) **and** +`identity_blob` (the age-wrapped bundle carrying the restic **repository** password). Until v0.93.0 it +retained only the first, so the retention preserved the whole-guest key and destroyed the off-site data +key, and **the destroying act was the escrow ceremony a rebuilt box asks its customer to run.** + +*Why this is load-bearing rather than tidy:* the operator ruled on 2026-08-04 that **candidate (c) is +refused — no repository password is retained on the Proxmox host.** With no host-retained copy, the +**customer-present recovery path is the only way back from a rebuild**, and it runs entirely through +the retained identity blob. A session that touches escrow custody is touching the single copy. + +*Facts a future session needs before touching this area:* +- **The ordering is load-bearing and invisible from the routine.** `SaveHostDRBundle` writes + `identity_blob` **after** `SaveHostEscrow` returns (the escrow PUT, `api/handler.go`), so at demote + time `host_escrow` still holds the OLD identity blob. Invert that and the retained bytes become the + NEW blob filed under the OLD hash — recoverable-looking and wrong. Pinned by + `TestSaveHostEscrow_RetainsIdentityBlob`. +- **`demoteCurrentEscrowTx` is shared by re-escrow and host-delete.** Both have a test; fixing one + caller instead of the routine is a demonstrated red-proof, not a hypothetical. +- **Nothing was backfilled and nothing could be** — pre-v0.93.0 retained rows have no identity blob and + their source rows are already overwritten. Both demo boxes' pre-2026-08-04 repository passwords are + gone permanently, recovery code or not. +- **A retained key is not a recovery.** The hub's blob-serving endpoints have no client (R-199), the + injection seam has no form (R-200), and the path has never been exercised (R-201). v0.93.0 makes the + key survive; it does not hand it back. +- **`offsite_repo_key_changed`** (R-197, warning, operator-only, edge-triggered on a supersession where + both hashes are known and differ) is the evidential signal that a box's off-site data key moved. It + carries **no hash value**. `MarkEscrowStale` is **precautionary**, not evidential — see S-26(a). + **S-27 — a customer with NO machine ever bound is UNKNOWN, silently; one that was bound and went quiet still alarms (2026-08-04, R-195; hub v0.92.0).** Operator ruling, implemented as `store.HasEverBoundHost` (live `hosts` row OR `host_deletions` tombstone) consulted once at the top of diff --git a/REUSE.md b/REUSE.md index d37ae96..da5f4e9 100644 --- a/REUSE.md +++ b/REUSE.md @@ -78,6 +78,7 @@ | `(*Store).HasEverBoundHost` (v0.92.0, R-195) | hub/internal/store/store.go | `(customerID) (bool, error)` | Any verdict that must not fire for a customer with **no machine ever bound** — "was anything ever expected of this customer" | `hosts` row **OR** `host_deletions` tombstone. **NOT a liveness check and never a substitute for one:** a box that was bound and went silent returns `true` and must keep alarming — that is the case any change here breaks first (pinned by `TestCheckBackupDeadlines_BoundButNeverReported_StillAlarms`). Callers **fail OPEN** on its error: an unreadable binding must never SUPPRESS an alarm. Do **not** re-derive this from report presence — `store.GetCustomers()` (and therefore the staleness checker's `down` state) is a query over `reports`, so a never-reported customer has no state at all, which is exactly how the daily false alarm reached `david`. | | `(*Server).configFormData` (v0.49.0) | hub/internal/web/configs.go (~L430) | `(r, isNew, cfg, overrides, errMsg) configFormView` | The ONE view-model builder for the customer config form (standalone chrome + the customer page Edit tab) | `overrides=nil` → parses the STORED cfg.ConfigJSON; pass the SUBMITTED map on the update validation-error re-render or typed values reset (red-proofed). | | `config_form_body` sub-template (v0.49.0) | hub/internal/web/templates/config_form_body.html | `{{template "config_form_body" }}` | Rendering the config form on ANY surface (config_form.html chrome + customer Edit tab) | The floor/geo/danger cards on the Edit tab are SIBLINGS after `` — never nest a form inside it (breaks the offsite/PBS formaction sub-buttons). Includes the F5 in-flight `