# CLOSED-ITEMS — finished work, compressed > **What this is.** Every register row that reached a terminal state, compressed to its title, the > version it shipped in, its evidence paths, and any sentence that states a RULE rather than a > narrative. **Nothing was deleted:** each entry names the commit that holds its full original text, > and `git show :documentation/backlog/OPEN-ITEMS.md` returns it verbatim. > > **Why it exists (operator ruling, 2026-08-22).** `OPEN-ITEMS.md` had grown to 672 KB across 286 > entries, over half of it finished work, with one single entry at 16 KB. A file that cannot be read > is a file that cannot be checked — and this project has already paid for that twice: a record > nobody could find because it sat inside an entry about something else, and a finding rediscovered > because nobody could see it. The register now holds **open work only**, so its size tracks the work > rather than the project's age. > > **A sibling rather than the bottom of the register**, deliberately: appending to the same file keeps > the byte count and the scroll, which is the thing being fixed. > > **Load-bearing reasoning was NOT compressed away.** Where a closed row states a rule, a fence or a > deliberate refusal, that sentence is carried here verbatim under **Reasoning kept**. Rules that > outlive their work item also live in their proper homes — `workspace-CLAUDE.md` standing rules, > `felhom.eu/CLAUDE.md`, `CONTEXT.md`, and the architecture folder — and this file is not their > primary record. > > **This file is not the register.** Nothing here is open. `OPEN-ITEMS.md` remains the single source > of truth for open work; `scripts/one_register_gate.py` enforces that against `ROADMAP.md`. --- ## 2026-10-05 (late afternoon) — the hub's own safety, boxes left behind, the agent's root grants (hub v0.135.0, controller v0.296.0, agent v0.146.1, golden 0.296.0; CC decisions 119–124) The full text of every row below: `git show 9bb45eaa:documentation/backlog/OPEN-ITEMS.md` (R-880 was opened and closed in this session). | Row | What | Closed | Evidence | |---|---|---|---| | **R-135** | **A cookie-less POST skipped the hub's CSRF gate, so a browser with cached Basic credentials could be made to POST cross-site.** hub v0.135.0: without a session a state change needs Basic credentials AND the header `X-Felhom-Operator` (decision 120); the gate sits before the route switch. 39 paths through RequireAuth→ServeHTTP; red-proof: the old shape lets all 39 through. Live: Basic + no header → 403 (also with `Origin: evil`, also on an unknown path); with the header → passes; header without credentials → 401. **Reasoning kept: a browser cannot add a custom header cross-site without a CORS preflight, which the hub never answers.** | CLOSED 2026-10-05 — FIXED hub v0.135.0 | `audits/hub-safety-2026-10-05/partA/`; `web/r135_csrf_test.go` | | **R-133** | **Every box's break-glass console password was plaintext in hub.db.** hub v0.135.0: sealed with the off-site seal and key (decision 121); legacy rows sealed at start-up — live: 4 rows sealed, 0 left plain; the demo-hp reveal still returned a password that minted a PVE ticket (HTTP 200; a wrong one 401); a wrong key → 500, nothing in the body or the log, no event. **Reasoning kept: the running hub still holds the key — this closes the database-copy route only; a database backup without `OFFSITE_SECRET_KEY` cannot open the console passwords (R-173).** | CLOSED 2026-10-05 — FIXED hub v0.135.0 | `audits/hub-safety-2026-10-05/partB/`; `store/r133_recovery_seal_test.go`, `web/r133_reveal_wrongkey_test.go` | | **R-604** | **A per-customer controller floor silently kept a box out of every global raise (demo-hp missed four).** hub v0.135.0: a global raise logs one line per customer whose own LOWER floor wins and sends ONE operator mail naming them (`floor_raise_skipped`); a per-customer floor records when it was set; the System page's "Version floors" table lists every per-customer floor with its age and which ones the global cannot move. 2 red-proofs. Live: the table shows the three per-customer floors (age "unknown" — set before v0.135.0). The mail was not exercised live (it needs a global raise below an override). | CLOSED 2026-10-05 — FIXED hub v0.135.0 | `audits/hub-safety-2026-10-05/partD/`; `web/r604_floor_held_back_test.go` | | **R-530** | **Nothing listed which boxes still run an old agent (agents update only by a per-box signed job).** hub v0.135.0: the System page's Agent cell (box → vouched, how far, since when; red after the wait) and `agent_behind` after 7 days (decision 119). Live: Tester 2 reads `0.142.0 → 0.146.1`. Signing stays per box (the 2026-09-16 ruling: CC may sign until the first paying customer). | CLOSED 2026-10-05 — FIXED hub v0.135.0 | `audits/hub-safety-2026-10-05/partD/`; `osupdates/r530_agent_alarm_test.go` | | **R-508** | **Customer tester-1 had no registered e-mail, and the page did not say so.** The address has been set since 2026-09-14 (the connect mails reach it — Gmail-read 2026-10-05); hub v0.135.0 adds the page warning: a configured customer with no box and no e-mail shows a red line (three branches tested, red-proof). | CLOSED 2026-10-05 — FIXED hub v0.135.0 | `audits/hub-safety-2026-10-05/partG/`; `web/r508_no_email_banner_test.go` | | **R-509** | **A box installed for an existing customer never got the connect e-mail.** Fixed in hub v0.114.0; the owed real-mail proof: three mails from the automatic "host delete" trigger, each within 1 s of the hub's own send line (2026-09-16 12:22:59 and 18:17:46, 2026-09-30 07:23:03 UTC), read through the Gmail connector (metadata only). The "e-mail set" trigger shares the send core and is unit-proven. | CLOSED 2026-10-05 — VERIFIED | `audits/hub-safety-2026-10-05/partG/r509-real-mails.txt` | | **R-880** | **An installed `felhom-os-apply` refuses a bundle naming a path it does not know (R16), so a release whose bundle ADDS a path cannot reach any box on an older bundle** (found 2026-10-05 before delivering v0.146.1, which adds four). Fixed by a step: `felhom-agent/scripts/build-step-bundle.py` — the box's current bundle with ONLY `felhom-os-apply` replaced (same paths), published as `0.146.1-step1`; then the release's bundle. Tests `StepBundle` (the R16 refusal reproduced; the step accepted; exactly one file changed). Live: demo-hp, demo-felhom and Tester 1 each took step1 (`written=1 same=20`) then 0.146.1 (`written=3 same=22`), self-check ok. **Reasoning kept: every future bundle that adds a path needs this step (decision 124); the step package stays published while any box may still be on the old bundle (Tester 2).** | CLOSED 2026-10-05 — FIXED (tooling, agent e4b5cf9) | `audits/hub-safety-2026-10-05/part{F,H}/`; memory `bundle-adding-a-path-needs-step-bundle` | --- ## 2026-10-05 (afternoon) — a box that is not always on: the catch-up, the banner, the alarms; the OS update repairs itself after a power cut (controller v0.295.0, agent v0.145.0, hub v0.134.0, golden 0.295.0; rulings 109–111, CC decisions 112–118) | Row | What | Closed | Evidence | |---|---|---|---| | **R-871** | **No architecture covered a box that is not always on, and a missed night was never made up.** Decision 109 (option A) built: controller v0.295.0 `internal/nightchain` — a ledger of when each backup leg ran to its end; on a start or a host resume ONE catch-up 15 min later, backup legs only, never the update leg; a late daily timer after a suspend is skipped; the whole-guest backup and the catch-up wait for each other; decision 110's banner. Design `07` §6.1.1. Live: 9202 (dump made 15 min after the start; after a crash mid-wait, all three legs at the next start), demo-felhom (dump 15 min after the start; the household's timeline line reached the hub); banner served on 9202 and closed by its real route. 13 red-proofs. **Reasoning kept: the ledger records that a leg RAN, and is never read as evidence that a backup EXISTS.** Full text: `git show 1b0678fa:documentation/backlog/OPEN-ITEMS.md`. | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/partA/`, `partB/` | | **R-873** | **A household whose box is off every night was mailed "cannot be reached" every night.** hub v0.134.0: at most once per 7 days to the household (persisted), the operator every edge, the recovery mail stays paired (decision 116). Proven by test through the real dispatcher (red-proof); no live occurrence in the session (Tester 2 stayed off). | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/partC/r873-red-proof.txt` | | **R-874** | **A restore-test never ran on a box with short power-on sessions.** agent v0.145.0: first due-check 30 min after start (decision 117). Live on demo-felhom: start 07:38:46 UTC → `restore-test first evaluation after start (R-874)` at 08:08:46 → a due tier restored and passed in 29 s. | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/partC/r874-*` | | **R-875** | **A kept report's reason said "the agent stopped mid-pass" for a hub-away pass.** agent v0.145.0: "sent late — kept on the box until the hub could take it". Test + red-proof. | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/partC/r875-red-proof.txt` | | **R-876** | **After a power cut mid-update every later pass failed until a person ran `dpkg --configure -a`.** agent v0.145.0: dpkg's state = `--audit` AND the update journal in one call; repair on either; belt: repair + retry once when apt says "interrupted" (decision 118). Live (operator's go): crash at 07:56:03 UTC mid-unpack → back by itself → next pass `REPAIR configured=0 journal=1` → `DONE rc=0 upgraded=12`, no person, no mail; package list identical. **Reasoning kept: a check that reads one of two places dpkg keeps its state is a check that misses the other.** | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/partD/` | | **R-877** | **The Tester 1 VM on demo-hp had no start-on-boot: the morning's demo-hp crash (06:14 UTC) left it off for 1 h 17 min, unnoticed** (the night-fixes report called every box healthy). Found 07:31 UTC; `qm set 341 --onboot 1`, started; the afternoon crash then brought it back by itself. Filed and closed in the same commit. | CLOSED 2026-10-05 — FIXED | `audits/catchup-2026-10-05/tester1/vm341-was-stopped.txt` | ## 2026-10-05 (day) — the night's fixes: off-site clean-up guard, first-install image race, R8 download, a killed pass's report (controller v0.294.0, agent v0.144.0 + v0.144.1, golden 0.294.0; rulings 100–103, CC decisions 104–108) | Row | What | Closed | Evidence | |---|---|---|---| | **R-867** | **The off-site clean-up guard refused honest 7-day retention and mailed an error every window.** Controller v0.294.0: the guard's "young" line is keep-daily CALENDAR days, built from the same constants as the policy (decision 104); every other refusal kept. Tests run restic 0.14.0's policy itself (proven identical to the binary over 92 snapshots), 5 red-proofs. Live by hand: demo-felhom window 5 16 → 14, demo-hp window 6 145 → 127 — exactly the predicted snapshots; hub rows `pruned`, no event, no mail, key files clean. **Reasoning kept: a line that can sit inside the keep window is a line that refuses the honest case — derive it from the policy, never pick an age.** Full text: `git show 7221ee5c:documentation/backlog/OPEN-ITEMS.md`. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partA/` | | **R-95** | **The box could delete its own off-site history.** Append-only key since 2026-10-03 (decisions 68–69); the last open item — a clean-up window that actually removes snapshots — was observed 2026-10-05 on both demo boxes (R-867's live windows, opened by the operator's one-shot grant; the dated check's four conditions all hold). The unattended weekly window is the same code path and is due ~2026-10-11/12; it is not separately re-checked (the DUE-CHECKS entry is removed with this row). Residual: R-822 (an add-only attacker steering older keeps). Full text: `git show 7221ee5c:documentation/backlog/OPEN-ITEMS.md`. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partA/`; `audits/offsite-lock-build-2026-10-03/` | | **R-863** | **A new box's first app install could fail: the one-time image clean-up deleted the image compose had just pulled.** Controller v0.294.0: every compose command that can pull holds a shared lock (`dockerexec.BeginImageWork`); a clean-up pass takes it exclusively without waiting and otherwise does not run; the one-time pass is retried every 2 min and writes its marker only after a pass that ran (decision 105). Live on 9202: the clean-up fired at minute 3 (05:31:36 UTC) inside BookStack's 35.5 s install, skipped itself, BookStack installed first time; the retry at 05:33:36 ran and kept everything; teardown through the product. The update path was already guarded (`Updating`); restore/undo were exposed in principle and now hold the same lock. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partB/` | | **R-864** | **A failed compose logged the head of stderr (pull progress) and cut the reason.** Controller v0.294.0 `tailStr` (rune-safe), pinned with the night's real first line. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partB/red-proofs.txt` | | **R-869** | **The move-aside log line printed an empty destination.** Controller v0.294.0: assigned before the log line; test + red-proof. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partD/r869-red-proof.txt` | | **R-865** | **R8 measured every download as 0 B (`--print-uris` with `-s` prints no URIs).** Agent v0.144.0: no `-s`; the fake answers like real apt (verbatim 9202 output), 2 tests, red-proof. Live: the installed wrapper's `download_bytes` on demo-hp read **12 802 456 B** for 13 pending upgrades (0 before), nothing installed. A live R8 REFUSAL line was not produced: it needs < 500 MB free on demo-hp's guest, i.e. 28.5 GB written into a thin pool with 18.7 GB free — it would have stopped every guest. | CLOSED 2026-10-05 — FIXED | `audits/night-fixes-2026-10-05/partC/` | | **R-866** | **The debug OS pass could not run with the hub away.** Agent v0.144.0: the daemon saves the hub's block; the selftest falls back to it and says `block=SAVED(