Files
felhom.eu/documentation/audits/night-burndown-2026-10-06/design-R-105.md
T

5.1 KiB

R-105 — the two "empty DR records" — design proposal (burn-down night 2026-10-06, no code)

Baselines read: felhom.eu 8e2dc204 (hub v0.140.0 live), felhom-agent 74b5eae. Architecture: 05-hub-architecture.md §9 and §11 (the "slim DR record"), 06-offsite-connectivity.md §3.5 (the escrow directive).

1. The problem, and what was measured

The row says two hub-held records are {} on every box: hosts.dr_record_json and host_escrow.directive_json (the third, dr_recipe…drives, was fixed 2026-07-28 and is not re-measured tonight).

Tonight's reading is in SOURCE, not in the database — reading a copy of the hub database was refused by the session's permission check, so no value was read. Source proves more than a reading would: for these two fields the empty value is the only value the shipped code can produce. The hub's host pages (read through the operator UI) show "DR Recipe present / Key Escrow present" on demo-hp, demo-felhom and Tester 1, and "none / none" on Tester 2.

2. What the code does today (read in source)

  • hosts.dr_record_json has no writer and no reader. Created by hub v0.7.0 (7c0c7545) with default '{}' (hub/internal/store/store.go:360); scanned into Host.DRRecordJSON (store.go:2795, :2867) and read by nothing else in the hub, the agent or the controller (repo-wide grep). The 05 §9 "slim DR record" was never built.
  • host_escrow.directive_json is written only by a by-hand flag. The agent sends a directive only when the operator runs --selftest=escrow-create -directive <file> (felhom-agent/cmd/felhom-agent/main.go:201, :2767-2771, :3132-3135). The production ceremony (the customer's escrow wizard) runs ONE fixed argv with no -directive (felhom-agent/configs/felhom-agent.sudoers:284). When that upload carries an identity blob, the hub stores the missing directive as '{}' (hub/internal/api/handler.go:1293-1305, store.go:3612) — so every wizard escrow writes {}, and also overwrites the one directive made by hand on 2026-07-04 (06 §3.5).
  • Nothing reads the directive's fields. The hub serves it only from /re-enroll and /restore-directive (hub/internal/api/dr.go:101, :155); no agent or controller code calls either route (grep).
  • The DR path that IS built does not need them. The agent's restore plan reads the desired-state restore_directive plus the DR recipe (felhom-agent/internal/dr/plan.go:45, :104); the recipe carries the PBS repo id and namespace (internal/hub/dr_recipe.go DRPBSCoord), the hub holds the endpoint's PBS fingerprint (hub/internal/tenantsync/client.go:136), and the wrapped key is host_escrow.blob.

So the row's two thirds are not a fault in a running path. They are a design that was half-built and then bypassed, and two architecture documents still describe it as if it existed.

3. Options

A. Retire both, and correct the documents. Drop the DRRecordJSON scan field; stop storing the directive (the column stays, read as {}); 05 §9/§11 and 06 §3.5 say where each fact really lives (recipe, tenantsync, escrow blob). Cost: ~45 min, hub only, no box change. Can go wrong: if a future re-enroll client wants the directive, it must be rebuilt — the routes stay and serve {}.

B. Populate the directive from the ceremony. The agent fills {pbs repo id, namespace, endpoint fingerprint} in the wizard ceremony. Cost: agent + sudoers argv change (the argv is pinned byte-for-byte) + a bundle delivery; ~2 h and a release. Can go wrong: a second copy of facts the recipe already carries, which can disagree with it — the R-106 namespace defect was exactly such a disagreement.

C. Do nothing more; close R-105 with this evidence. Cost: nothing. The two documents keep describing a record that does not exist, which is how this row was filed in the first place.

4. The pick — PROPOSAL for the operator, not a decision

Option A. One source per fact; the recipe is reported every cycle and was fixed to match the backup (R-106). It removes a design promise in 05 §9, so it needs the operator's word (a design decision is not a defect). Until then the row can move to WAITING-ON-OPERATOR: there is no data at risk — the empty fields have no reader.

5. First slice and its proof

  • Red test first: a hub test that the escrow PUT with an identity blob and no directive leaves directive_json unchanged from a hand-made value (today it overwrites with {} — fails); then decide by the option.
  • A test pinning "no reader": an AST/grep test in the hub that DRRecordJSON is not referenced outside the store (so a new reader cannot appear without this design being revisited).
  • Live proof: none needed on a box (hub-only); after deploy, the host page still shows "DR Recipe present".

6. Open questions for the operator

  1. Retire the "slim DR record" of 05 §9 (Option A)? If you do nothing: the fields stay empty and unread, the documents stay wrong, nothing breaks.
  2. Tester 2's host page shows no DR recipe and no key escrow. Expected for a box that never did the escrow wizard — is that the case? If you do nothing: a Tester 2 host loss has no hub-held recovery material.