Files
felhom.eu/documentation/audits/day-2026-10-08/design-R-304.md
T
admin 5beedcce1a
gates / gates (push) Successful in 4m13s
R-304 option C (hub half): recovery_older_package allowlisted and operator-only; design + row updated
Unreleased; ships with tomorrow's hub release.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-08 10:09:20 +02:00

5.3 KiB

R-304 — the household's old recovery code and the retained packages: a one-page design (2026-10-08)

Status: option C's first slice BUILT 2026-10-08 afternoon on the operator's ruling (09:04, 09 §3 decision 183) — see the end. Ships with the next controller + hub releases. Customer data and promises are the operator's: they are the two questions at the end.

Where it stands (read in source today, not from the row)

  • Retention works and the material opens the old store (drill 2026-08-12, audits/DRILL-retained-key-2026-08-12.md).
  • R-311 shipped (hub v0.103.0, agent v0.129.0, controller v0.214.0): after the current package refuses a code, the agent fetches the retained packages (GET /hosts/<id>/escrow/retained, self-scoped, cap 16) and tries up to 6. If one opens, the screen says „the code is correct, it opens an earlier package; contact support" (HTTP 422).
  • R-312 is DECIDED (2026-08-13): no in-product route from the recovery screen to a set-aside store — „re-evaluate on a real customer request". So retention is an operator-only capability today, by decision.
  • What was still false until today: the agent answered „the code did not open the sealed bundle" (400) also when it had NOT tried every earlier package — the hub withheld rows (no key material, over the cap), a package was malformed, the 6-try cap stopped the loop, or the retained list could not be read. Fixed on main today (agent 424 older_unchecked, controller RecoveryOlderUnchecked, Hungarian + English: „we do not know whether your code is wrong … contact support"). Ships with tomorrow's releases. The fix is in the agent and the controller, not the hub: the hub never sees the code (zero-knowledge, 07 §2), so it cannot check a row; it already reports what it withheld (unopenable_count, truncated_count), and the agent now counts those.

„Which package?" — the question is smaller than it looked

Each escrow ceremony seals with a NEW recovery code (the household is shown it once). A code opens only the package it sealed. So when a household holds several old codes, each code selects its own package — no list, no choice screen. The agent already tries newest-superseded first. The only real limits are the two caps (16 served, 6 tried), which today's fix turns from a silent „wrong code" into an honest „not all checked".

Options for really serving the old copy

What Costs Customer data / promise
A Keep it operator-only (R-312). The screen's „contact support" is the route. Nothing more. The operator needs SQLite, age and a shell (the drill's §4) — slow, error-prone, undocumented as a runbook. No change.
B In-product: when a retained package opens and carries a repository password, the screen offers a READ-ONLY browse of the old store (list + download), never a restore into place. New surface: the old store's location (moved aside / orphaned, R-241), a second repository password in memory, a second browse path. ~2 sessions + a drill. Changes a promise (the household can reach old history alone). Reverses R-312 — operator only.
C Operator-assisted, first slice: when the agent answers 422 (opens retained) or 424 (not all checked), the controller sends ONE operator event naming the box and the package date; plus a runbook „open a retained package for a household" (the drill's §4 written down, the household types the code on its own box). Small: one event type (operator-only), one runbook. ~½ session. No new promise; makes the existing „contact support" true in practice.

Pick: C now; B only on R-312's own trigger (a real customer asks). C makes the sentence the screen already says — „contact support" — something the operator can act on the same day, without reversing a decision.

First slice of C: controller: on RecoveryCodeOpensRetained / RecoveryOlderUnchecked, send recovery_retained_needed (operator-only, warning, once per box per day) with the package date and the class — never the code. Hub: allowlist + operatorOnlyEvents in the same commit. Docs: runbooks/RUNBOOK-open-retained-package.md from the drill's §4.

Two questions for the operator

  1. May the product keep promising, in the capability map and the countdown banner, that old backups „stay recoverable"? Today that is true only with your hands. If you do nothing: the promise stays worded as it is, and the honest route is „contact support" (A/C).
  2. Do you want C's first slice built (an operator mail when a household's code opens — or may open — an old package)? If you do nothing: nothing is built; you learn of such a household only when they write to you.

Built 2026-10-08 (afternoon) — option C, first slice

  • Controller (a40729a): on a 422 (opens an older package) or a 424 (may open one), recovery_older_package (warning) to the hub — the class and the package's date, never the code; at most once per day per box (<data>/recovery-older-mail.day). TestR304_OlderPackageMail_*.
  • Hub (same day): the type is allowlisted and operator-only. TestR304_RecoveryOlderPackageIsAllowlistedAndOperatorOnly.
  • Not built: the runbook „open a retained package for a household" (the drill's §4 written down) — next slice.
  • Question 1 (the promise wording) was not answered: the wording stays.