## v0.161.0 — three fixes (R-921, R-1070, R-1010); two texts made true after 0.160.0 (2026-10-20) — RELEASED on the operator's yes, NOT VOUCHED Released 2026-10-20 (operator yes, the morning answers) with `scripts/release-agent.sh` from a clean clone at `5152123`: binary sha256 `ff4e6dc52dd70234913445ecdeb88d31dea8e394d77c48704d26ff19b999d3b5`, config bundle sha256 `485f36c957d83f772aa22cac24e5e73f44da5cf8b957b1fd513bec2bc1d12c47`, tag `v0.161.0`. Delivered by one signed `agent_update` job per box to demo-hp (06:38 UTC), Tester 1 (06:54) and demo-felhom (06:55): each `signed op COMPLETED`, `capabilities self-check ok=76 total=76`, `update committed`. **The vouch is the operator's.** Live on demo-hp: `host_busy` answers `{"busy": true, "what": "backup:local"}` during a „Mentés most" whole-guest backup and `{"busy": false}` before and after (read from inside 9201 with its own token; felhom.eu `audits/night2-2026-10-20/P1/`). R-1083 (a key for an existing guest) is NOT in this release: see the R-1083 row. No sudoers, wrapper or config-bundle change. - **R-921 — `GET /backup/status` says when the host is busy.** New field `host_busy` (`{busy, what}`): whether the host-wide heavy-operation gate is held right now (another guest's backup, the restore test, a night OS step, the stale-lock sweep, a power-off) — the state that makes a `POST /backup` answer 409. Served on every request; absent when the gate is not wired. Controller ≥ 0.335.0 reads it before it stops the household's apps for a tier, so a refused tier costs no stop (the measured case of 2026-10-08: the night OS step, all 15 apps stopped for a refusal). A read, never a reservation. No new route. Test `TestBackupStatus_R921_HostBusy`, red-proved (assignment removed → `free gate: host_busy = `). - **R-1070 — a box with no tunnel set up no longer fails its host OS update.** Docker 29 prints „error: no such object" in lower case, so the tunnel probe's case-sensitive match fell through to `unknown` („could not ask the guest"), and the host health rule failed every box without a cloudflared container (Tester4, 2026-10-15, `os_update_health_failed`). Now the match ignores case and a missing container is a new state, **`absent`** („no tunnel set up on this box"); the host rule reads it as not applicable only when the reading from before the update (Docker answered) also had no cloudflared container. A tunnel that is down, gone or unreadable still fails. The hub alarms only on `not_running`, so `absent` never mails. Tests `TestHostHealthVerdict_R1070_NoTunnelSetUp` (7 rows) and three `TestClassifyTunnel` rows (Docker 29's wording measured on demo-hp), red-proved both ways. - **R-1010 — the stale-lock janitor clears a half-removed `vzdump` snapshot.** When the delete fails ONLY with „Failed to find logical volume …_vzdump" (one disk's snapshot already gone — demo-hp 2026-10-14 after a backup ran out of space), it retries once with Proxmox's own `force=1` on the same API call (`DeleteSnapshotForce`; the same token privilege, no sudoers line, no root command — `pvesh usage … -command delete` lists `--force`). Any other error is returned unchanged; a failed forced delete is returned, never a second force. Tests `TestStaleLock_R1010_ForcesOnceOnlyWhenTheVolumeIsGone` (6 rows) and `TestDeleteSnapshotForce_R1010`, red-proved (single plain delete → `calls [plain], want [plain force]`). - **The security-scan gate (R-986), tooling only:** `agent_gates.py` registers the SHARED `felhom.eu/scripts/security_scan_gate.py` (FAST set: the hook and CI) — govulncheck (called vulnerabilities) and gitleaks on the pushed commits, **warn-first**. `.security-accepted` holds the first scan's 15 accepted hits (escrow and local-API test constants), each with a reason. CI fetches the pinned gitleaks (`FELHOM_SCAN_FETCH=1`). Decoys: the shared 8 plus the gate on this repo, in `test_gate_decoys.py`. ### Earlier on 2026-10-19 (texts only) - The ceremony-complete log line now says the package uploads at the claim **or at the typed-back confirm** (R-1151); it read „package uploads at the claim" for both orders (seen in demo-hp's journal, 2026-10-19 01:47 UTC). - The R-973 comments in `cmd/felhom-agent/main.go` and `internal/localapi/backup_cap.go` named the hub event `agent_capability_degraded`; since hub v0.159.0 (R-1150) a refused backup raises `whole_guest_backup_refused`. Found by the hub helper of the day brief 2026-10-19. - **gofmt, R-1160:** `1edfd8e` formats the 14 Go files that were not gofmt-clean (formatting only, `go test ./...` green). New gate `scripts/gofmt_gate.py` (the controller's R-454 shape) in `agent_gates.py`, fast set; its decoys in `scripts/test_gate_decoys.py` plant an unformatted file in a SCRATCH copy (convicted by name), the formatted twin (passes), and the no-Go modes (CI passes out loud, a dev machine is INCONCLUSIVE). Red-proved: with the conviction switched off the FACT case reports a LIVE HOLE. The next release carries it; no release for it. ## v0.160.0 — five fixes: an abandoned or un-typed-back escrow ceremony keeps the old code (R-978, R-1151); a whole-guest backup flood is capped (R-973) and the local tier keeps one point per day (R-1149); an empty partition is not „data" (R-1093) (2026-10-19) Released 2026-10-19 on the operator's yes (02:48 answers, D17 „one per day, 3 days"). sha256 `7ebb547f2b1ab93544ba828d3040351b60c4126327a5b85bbcb33f8f2b3dfca1`, config bundle `6b06090190ff609af1fe9842447ebf385fd52da4be382bc7ca708b558c8e202f`. The vouch is the operator's. - **R-1093 — an EMPTY partition no longer asks the typed „data will be erased" confirmation.** `lsblk` prints the parent disk's table type (PTTYPE) on every partition, so `SudoHostOps.InspectDevice` read a blank partition as „device has a partition table". Now lsblk's PTTYPE is set aside only when `blkid -p` on the partition itself proved it read the device and that it is a partition of a table of exactly that type (`PART_ENTRY_NUMBER` + `PART_ENTRY_SCHEME` equal to lsblk's PTTYPE). The partition's own signatures still come from `blkid -p`: a filesystem or a nested table inside it stays data-bearing; a blkid that printed nothing, a whole disk, or a scheme that differs keeps the old, safe reading. **No new command, no sudoers or config-bundle change** (the two probes are the same allow-listed argv). Test `TestInspect_R1093_PartitionVerdicts` (8 rows), red-proved: with the old `if pttype != ""` both empty-partition rows fail `DataBearing=true reason="device has a partition table"`. **Hardened after the same night's review (fail-closed on every path):** `PART_ENTRY_*` is read from the PARENT's table, so lsblk's PTTYPE is set aside only when blkid also **exited 0** — exit 2, an I/O error or a missing binary keep „device has a partition table"; and an `lsblk` that exits 0 with output that does not parse into a device (or lists none) now leaves the device UNPROBED („could not be reliably inspected") instead of „blank" — that hole predates R-1093. Six more rows in the same test, each red-proved (`blkidErr` check removed → „device is blank"; `&& parsed` removed → `Probed=true … „device is blank"`). - **R-978 — an abandoned second escrow ceremony no longer makes the customer's recovery code useless.** The root ceremony (the FELHOM_ESCROW argv, unchanged: `--upload --output=json`) used to upload the new package before the new code was claimed, and the hub overwrites the current package when the sealed secrets are unchanged — so a customer who started the ceremony again and walked away (or an agent restart in the 10-minute window) was left with a written-down code that opened nothing. Now json mode hands the package back (`CeremonyOutput.PendingUpload`, output **version 2**; text mode still uploads at once) and the daemon uploads it at `POST /escrow/ceremony/claim`, **before** the code is handed out (`EscrowCeremonyConfig.Upload` → `putEscrowBody`, the same PUT and per-host key). A failed upload answers 502 and keeps the code claimable; the TTL and a new start wait while the claim's upload is in flight; no uploader → 503; an output that uploaded by itself or carries no package fails the ceremony. Status `uploaded` now reads false until the claim. The controller is unchanged (it never read `uploaded`; its claim already assumed the package is on the hub when the claim returns). **No sudoers or config-bundle change.** Tests (localapi) `TestEscrowCeremony_R978_*` (abandon by TTL / by a new start / by a restart → the hub's package still opens with code 1; claim stores before handing out; upload failure; in-flight TTL; no uploader; no package) and (cmd) `TestEscrowOptsFromCLI_JSONModeDefersUpload`, `TestDeliverEscrowPackage_JSONModeSendsNothing`, `TestR978_CeremonyUploadIsWiredIntoTheDaemon`. Red-proved: upload-at-create restored → „code 1 no longer opens it (uploads=2)"; the in-flight TTL guard removed → „the TTL voided the code while its package was uploading"; `deferUpload: false` → „json mode must defer the upload"; the `Upload:` line removed → „has no Upload". **Residual:** the customer's typed-back check after the claim is browser-only, so „confirmed" here means „claimed" — a customer who opens the code and closes the page without writing it down has replaced the package. - **R-1151 — the new recovery code replaces the hub's package only after the household typed it back.** R-978 moved the upload to the claim; but the wizard's typed-back check was the browser's alone, so a household that opened the new code and closed the page without writing it down had already replaced the package their old code opens. Now a claim with `{"defer_upload": true}` (controller ≥ 0.332.0) hands out the code with two word positions (`verify_positions`, drawn by crypto/rand) and keeps the package; the new `POST /escrow/ceremony/confirm {"words": [w1, w2]}` checks the two words (constant time, case and spaces aside) and only then uploads it. Right → 200; wrong → 422, still waiting; one word → 400; nothing waiting → 404; the 30-minute window ran out → 410 and the job reads `unconfirmed_void` (the package dropped, the previous code still works); hub failure → 502, still waiting. Only the two words are kept for the check, never R; a new start, the window and a restart drop them with the package. Status gains `awaiting_confirm` and `confirm_expires_in_sec`. **An old controller (≤ 0.331.0) claims with `{}` and never confirms: it keeps the R-978 order (upload at the claim)**, so its household's ceremony still completes; a new controller on an old agent sees no `confirm_required` and needs no confirm. No sudoers or bundle change. Tests `TestEscrowCeremony_R1151_*` (shown but not typed back — by the window, a new start, a restart — code 1 still opens the hub's package; typing; the old controller's order; hub failure at the confirm) and `TestPickConfirmPositions`. Red-proved: the defer flag ignored → „a code that was shown but never typed back replaced the package: code 1 no longer opens it (uploads=2)" in all three; the word check skipped → „one wrong word: got 200 (uploads=1)". - **R-1149 — the local whole-guest tier keeps one restore point per day for 3 days** (operator decision D17, 2026-10-19). The primary (local) tier's per-run prune is now `--prune-backups keep-daily=N` (N = `local_backup_retention`, default 3) instead of `keep-last=N` — the same at most 3 archives, but a run of extra backups inside one day replaces only that day's point, so with the R-973 cap a flood cannot push out the earlier days' copies. Additional tiers keep their explicit `keep_last` (the weekly off-site „last 2" is two weeks). `config.BackupTier.PruneSpec()` is the one place the spec is built (runDaemon used to format keep-last itself); the „backup tier armed" log line names it. Tests `TestBackupTierPruneSpec_R1149_LocalKeepsOnePerDay` and the consequence `TestBackupCap_R1149_LocalFloodKeepsOlderDays` (the agent's real spec, a Proxmox-shaped prune model, three nights then a day-long flood → the two earlier nights survive). Red-proved: keep-last for the primary → „primary tier prune = "keep-last=3", want "keep-daily=3"" and „holds 0 copies made before the compromise". No config or sudoers change. - **R-973 — the local-API whole-guest backup route has a cadence cap.** Every successful run prunes its tier, so a compromised controller calling `POST /backup` in a loop pushed out every pre-compromise restore point (local keep-last 3; off-site via ep0's keep-last 2). Now (`internal/localapi/backup_cap.go`): a tier whose cadence is ≤ 24 h allows **3 successful runs per rolling 24 h** (the `07` §6 day: the night run, a „Mentés most" press, and the next night, which since R-899 still runs and can land inside the same 24 h — so a press never costs the night); a longer tier allows **1 per max(cadence − 24 h, cadence/2)** — 6 days for the 7-day off-site tier, which the controller only runs when due and the button never targets. Only successes count (a failed run prunes nothing). A refusal answers **429** with the next allowed time, is logged at WARN, is saved, and for 24 h raises the CRITICAL host-report status `localapi:backup-cadence-cap` — the hub's existing operator-only `agent_capability_degraded` event, **no hub change**. **The cap survives an agent restart:** `BackupSuccessState` (the R-894 file) now also keeps the newest 8 success times and the last refusal per tier and guest (additive JSON fields; a pre-R-973 file counts its one known success). Tests `TestBackupCap_*` (an off-site flood keeps a pre-compromise copy for 5+ days; a local flood runs 3 a day; the scheduled day is never refused; restart; failures; the operator signal) + `TestBackupCadenceCap`, `TestBackupSuccessState_R973_*`, `TestBackupCapStatus_*`, `TestR973_CapRefusalIsWiredIntoTheReport`. Red-proved: the cap disabled → „the off-site tier lost every pre-compromise copy: 10 runs"; the disk history ignored → „after a restart the 4th run in a day was allowed"; the wiring line removed → „never binds backupCapRefusal". **Limits, stated:** with keep-last 3 the local tier's three points can still be replaced within one day (3 per day is the schedule's own pace plus one press); the hub's event text reads „agent privileged capability degraded" (the type is generic). No sudoers or bundle change. - **CI workflow (R-1135, no code change).** `.gitea/workflows/gates.yml`, the alarm step: the mail's run link is now `https://gitea.dooplex.hu//actions/runs/`. It used `GITHUB_SERVER_URL`, which on this runner is the in-cluster address (`http://gitea.gitea-system.svc.cluster.local:3000`, the address the runner registers at), so nobody outside the cluster could open it. It also used the run NUMBER, and Gitea 1.26 reads `/actions/runs/` as a run id first: a number that is also an older run's id opened that other run (measured 2026-10-18: `felhom.eu/actions/runs/1116` opened run id 1116, run number 613). It now uses `GITHUB_RUN_ID` and prints the link (`ALARM-LINK …`) into the job log. Same change in all four repos' workflows. ## v0.159.2 — the orphan thin check runs only after a failed backup (R-1012 follow-up) (2026-10-18) **Released 2026-10-18 (committed 2026-10-17): sha256 `4d98868e922d6c10665a6c0da9323e8627ea9c589094e81d27d741e625d9d8f9`, bundle `477704ea2980948433ffc34bb59f008cc11410fd9683d386bfd3e2f085d55bc9`; the vouch is the operator's.** No `configs/` change since v0.159.1. The independent review of 2026-10-18 found nothing in it. v0.159.0/v0.159.1 ran the R-1012 orphan check before EVERY snapshot-mode whole-guest backup, and that check takes a short metadata snapshot of the live thin pool. Now `BackupRunner` runs it only when the snapshot step has failed (the vzdump start or its task) — the moment the answer is needed. The finding is still appended in plain words to the failed backup's error, exactly as before; a successful backup never runs the check and keeps a clean record; stop mode and an unwired runner are unchanged. The `thin-orphan-check` capability text says „after a failed whole-guest backup". Tests `TestBackup_SnapshotFailure_NamesTheOrphan` (now: exactly 1 call, after the failure) and `TestBackup_ThinCheck_SilentWhenNotApplicable` (now: 0 calls on a successful backup), red-proved against the v0.159.1 `runner.go`: failure legs „thin check ran 2 times, want 1", success leg `calls=1`. No root file, sudoers or bundle change. (Installer side, felhom.eu, same night: `felhom-host-install.sh --rotate-recovery-only`, R-1141.) **Proven live 2026-10-18 (demo-hp):** a green snapshot backup with 0 `felhom-thin-check` calls (the 0.159.1 night backup had 1). The failed leg stays covered by the fixture test. ## v0.159.1 — the signed decommission prunes the guest-bind record (found by v0.159.0's own live proof) (2026-10-17) **v0.159.0 is superseded — do not vouch it.** Everything of v0.159.0 below, plus: `signedjobs.DecommissionExecutor` drops the drive from every guest's bind record (`SetGuestBinds`, wired to the agent's `GuestBindStore`), as the self-serve decommission always did. Without it a drive decommissioned by an operator-signed job and then forgotten (R-1124) left a record the 20-second guest-bind re-assert warned about every tick („reconcile: skipping non-enrolled drive") — seen on demo-hp minutes after the R-1124 proof. Test `TestDecommissionExecutor_PrunesTheGuestBindRecord`, red-proved. Released: tag `v0.159.1`, sha256 `de8f6ad9…aff9`, bundle `2af3d27d…7cd6` (the same root files as v0.159.0). **Not vouched.** Delivery to a box on the v0.158.1 bundle: `agent_update 0.159.1` → `agent_config_update 0.159.0-step1` (sha `1d65edb3…d6cf`, published) → `agent_config_update 0.159.1` (the bundle adds `felhom-thin-check`, R16). ## v0.159.0 — forget a decommissioned drive that is gone (R-1124); name an orphan thin device in a backup failure (R-1012); Go 1.27.2 (R-972) (2026-10-17) **Superseded by v0.159.1 — do not vouch.** Task: „agent release, the catalog allow-list, break-glass rotation, off-site history check" (felhom.eu `REPORT-agent-allowlist-rotation-2026-10-17.md`). - **Go 1.27.2 + x/crypto v0.58.0** (R-972, `37b99d7`): the first release that carries them (v0.158.1 was built with go1.26.0). - **R-1124 — a decommissioned drive that is gone is forgotten.** `SudoHostOps.ForgetRetiredDrives` (agent start and the 20-second storage tick): for a Felhom drive unit whose intent is `decommissioned`, whose `/dev/disk/by-uuid` link is gone and which is not mounted, `systemctl disable` + the new root verb `felhom-priv-apply unit-forget ` + `daemon-reload`. The unit is gone, so a later boot no longer waits for the drive and the agent stops listing it (no „absent by UUID" line every 20 s). A decommissioned drive still plugged in KEEPS its unit and raw mount, so the one-click re-enroll (H3) still works. `unit-forget` re-checks as root: a felhom by-UUID drive unit (not a network unit), disabled, its `Where` not mounted, its by-UUID link absent — otherwise refused. Sudoers `FELHOM_MOUNT` gains the one verb. Tests `TestForgetRetiredDrives_*` and `configs/test_felhom_priv_apply.py` `UnitForget`, red-proved. **Live on demo-hp:** a loop-backed test drive enrolled through the local API, decommissioned by a signed job (kept its unit while present), unplugged → 8 s later the unit and its wants link were removed and `GET /disks` dropped it. - **R-1012 — an orphan thin device is NAMED in the backup failure, never removed.** New READ-ONLY root helper `felhom-thin-check ` (config bundle): LVM's thin ids before and after, the pool's ids through a metadata snapshot it reserves and releases (refuses — exit 3 — when someone else holds one; releases on every exit; sends no other pool message). `internal/thincheck`: an orphan = in the pool and in neither LVM list, on two reads 3 s apart; fails open. `BackupRunner.SetThinCheck`: before a snapshot-mode backup (a WARN when found) and again after a failure — the finding is appended in plain words to the error the controller mails to the operator, pointing at the new runbook `whole-guest-backup-orphan-thin.md`. Sudoers `FELHOM_THINCHECK`, capability `thin-orphan-check`. Tests `internal/thincheck`, `TestBackup_SnapshotFailure_NamesTheOrphan`, `configs/test_felhom_thin_check.py`, red-proved. **Live on demo-hp:** on a scratch thin pool (a loop file on `/mnt/hdd_1`, never `pve/data`) the helper listed a made orphan (device 7) and refused a snapshot held by someone else; on `pve/data` it read 1, 2 = LVM's and released; a normal whole-guest backup of 9201 ran the check first and finished green. - Released: tag `v0.159.0`, sha256 `3cfd0a25…edfed5`, bundle `40663c4f…6d681`; step bundle `0.159.0-step1`. ## CI only — a manual proof that the failure mail can be sent (R-1129, 2026-10-17) No agent release; no binary changes. `.gitea/workflows/gates.yml` gains a manual `workflow_dispatch` trigger with one input, `force_alarm` (default false), and one step that runs ONLY on a dispatch: it prints the length of `RESEND_API_KEY` (never the key) and, with `force_alarm=true`, fails on purpose so the existing alarm step sends its real mail; the mail's subject then starts „FORCED TEST, not a real failure". A push run is unchanged: the new step is skipped and the subject is the old one. Same change in all four repos' workflows. Purpose: prove the failure mail now goes out after the operator set the key (every failed run until today ended „ALARM FAILED: RESEND_API_KEY is empty"). ## scripts only — the decoy suite is a gate of the full run (R-1116, 2026-10-17) No agent release; no binary changes. `scripts/test_gate_decoys.py` is now the `gate-decoys` row of `agent_gates.py`: the full run (which CI runs on every push), not `--fast` — it takes ~20 s in the workspace (measured: 30 cases; ~5 s in a CI-shaped shallow clone, also green). It was in no runner, the shape that let the catalog's R-1115 fail unseen for a night. It plants only in scratch clones and asks a fake Gitea on 127.0.0.1; it does not call the runner (no recursion). Declared in its own `COVERS`; `test_agent_gates.py` pins it (registered once, full run only, named by `--fast`'s SKIPPED line). Red-proved: a reuse-refs FACT case flipped to expect rc 0 turned the full run red (`CONVICTED: gate-decoys`); restored, green. CLAUDE.md's `--fast` sentence names it. ## unreleased — Go 1.27.2 and golang.org/x/crypto v0.58.0 (R-972, 2026-10-16) **Not released, not deployed** — agent releases wait for the operator's vouch; the next vouched release carries this. `go.mod`: `go 1.27.2` (was 1.25.0; the shipped 0.158.1 binary was built with go1.26.0), x/crypto v0.58.0 (was v0.52.0), x/sys v0.49.0. `go build`, `go vet`, the full `go test ./...` and `agent_gates.py --fast` green on Go 1.27.2. `release-agent.sh` builds with the toolchain `go.mod` names (GOTOOLCHAIN=auto), so no script change. ## scripts only — `release-agent.sh` refuses when the registry cannot say the version is new (R-1098, 2026-10-16) No agent release; no binary changes. `scripts/release-agent.sh` already refused a version whose tag exists or whose package answers 200. It let everything else through — a registry it could not reach (curl prints `000`) or a 500 read as „new". Now only a clean `404` proves the version is new; anything else refuses with the code it got. New `scripts/test_release_push_once.sh` (the real script, a fake `curl` and `go` on PATH; published / absent / unreachable / server error / tag exists), red-proved: with the 404 rule removed, the unreachable and server-error cases fail. Same rule as the controller `build.sh` and, since today, felhom.eu `scripts/build-hub.sh`. ## v0.158.1 — security review of v0.158.0: the mkfs wrapper trusts no sibling claim; the internal-place helper fails closed (2026-10-16) **v0.158.0 is superseded — do not vouch it.** A security review of `912f7c1` found two defects; both fixed here, each test seen failing against the 0.158.0 script: - **`felhom-mkfs-guarded`**: 0.158.0 let a format through when a SIBLING part was mounted at /mnt/ AND under /mnt/felhom-drives („Felhom's own drive"). That corroboration is a mount the agent can make itself, so a compromised agent could unlock a host owner's disk — the exact case this root wrapper exists for. Removed: any mount of any node outside /mnt/felhom-drives refuses again (now read from the kernel's mount table for every node; an unlistable disk refuses). Consequence: the free part next to an in-use Felhom drive cannot be formatted — `CandidatePartition.Formattable` is false for every part of a disk with a mounted node, and the controller does not offer it (a part with a filesystem can still be USED). Pinned by configs/test_felhom_mkfs_guarded.py + partitions_test.go. - **`felhom-internal-place`**: a failed `lvs` listing read as „nothing promised" (a too-large place) and a `blkid` error read as „no signature" (the only gate before mkfs). Now: a failed listing, a listing without the pool, or any blkid exit other than 0/2 REFUSES. Three new tests, all failing against 0.158.0. - Released: tag `v0.158.1` (code `98f3cdd`), sha256 `20264df6…84b76a1`, bundle `c23c3d35…cb1c41b`. **Not vouched.** ## v0.158.0 — the internal data place; partitions as places; add_internal_place (2026-10-16) Design: felhom.eu `architecture/16-storage-physical-identity.md` §5, §6, §7, §9 (operator rulings D3–D5; the D3 sizes ruled 2026-10-16 after the space check, felhom.eu `audits/storage-identity-buildB-2026-10-15/SPACE-CHECK.md`). - **The internal data place („Belső tárhely")** (`internal/localapi/internalplace.go`): when the record `/var/lib/felhom-agent/internal-place.json` names a filesystem, the agent mounts it at `/mnt/belso` by fs-UUID like an enrolled drive (startup + every 20 s tick) and records it `enrolled`; `GET /disks` reports it with kind `internal`. It never takes `/mnt/belso` from a household drive, and eject/decommission/format of it are refused (device-mapper on the system disk). Its identity is the system disk's, so build A never counts a copy to `sys_drive` as a second copy. - **Root helper `configs/felhom-internal-place`** (new in the bundle): `plan|create ` — the ruled size rule (reserve max(16 GiB, 10 %) ≤ 48 GiB, made only when ≥ 20 GiB, nothing promised beyond pool − reserve), the only lvcreate/mkfs of a system-disk volume; formats only a `felhom-belso` with no signature. Run by the installer (1.33.0) and by the new signed op. - **Signed op `add_internal_place`** (old one-disk boxes, `16` §9): no parameters (the size is the rule's), destructive class (operator key). Runs the helper with `create 0`, writes the record, mounts. - **Partitions as places** (`internal/storage/partitions.go`): `GET /disks/candidates` lists each partition of a free disk as empty / filesystem / in_use. A data-bearing part's wipe id binds disk id + part number + fs-UUID (`byid:<…-partN>+fsuuid:`); both must name the same device at format time. - **`felhom-mkfs-guarded`** (REVERTED in v0.158.1): a SIBLING part that is Felhom's own drive no longer blocked formatting the free part next to it. First tests of the wrapper: `configs/test_felhom_mkfs_guarded.py`. - `configs/` changed (sudoers `FELHOM_INTERNALPLACE`, the helper, the wrapper, the bundle list) — **a config-bundle job is needed** with the agent update. **Not vouched** (the operator's act). - Released: tag `v0.158.0` (code `912f7c1`), sha256 `a3589d1d…2caba`, bundle `17066273…fb5c76`. **Superseded by v0.158.1 — do not vouch** (its mkfs wrapper trusted a sibling claim the agent can fake; its helper failed open). ## v0.157.0 — every storage place knows its physical disk(s); POST /disks/same-disk (2026-10-15) Design: felhom.eu `architecture/16-storage-physical-identity.md` §3, §4, §10 (operator rulings D1–D6, 2026-10-15). Spike: felhom.eu `audits/storage-identity-2026-10-15/`. - **Identity of a storage place** (`internal/storage/identity.go`): the physical disk(s) under it, from `/sys`, `/run/udev/data` and `/proc/mounts` only — partition → disk, device-mapper / LVM thin → slaves, loop → backing file → its disk. Keys: `eui:` (NVMe), `serial:::`, `wwn:` only without a serial and only with a vendor (OUI ≠ 000000). `proven`, or `unknown` with one reason: `vm` (systemd-detect-virt), `network`, `usb_bridge` (a placeholder serial — the rule is the design's, UNPROVEN on a real enclosure, R-1077), `weak_wwn`, `no_serial`. No block I/O. - **Reported:** `GET /disks` rows and the hub report's `storage_targets` gain `kind`, `physical_disks`, `identity`, `identity_reason` (additive; the golden and the hub's copy change together). A dir storage is judged only on its OWN mount — an absent drive is "not computed", never the root disk. - **`POST /disks/same-disk`** `{a, b}` (guest paths) → `{different, proven, reason, a, b}`. Maps `/mnt/felhom-drives/` to the host mount at exactly that path, and `/mnt/sys_drive`, `/var/lib/felhom`, `/var/lib/docker` to the guest's data volume's storage. Anything else, or an absent drive → `unmapped`. A shared disk key is "same, proven" even when the disk is unknown. Scoped like every `/disks` call. - Tests: fixture `/sys` + udev trees for every spike case (one disk LVM thin, two disks, two partitions, a loop on a file, NVMe eui, vendor-less WWN, a VM, a USB placeholder serial, a network share) and the same-disk call (equal, different, VM, NAS, absent drive, scope); red-proofed (the LVM "diamond" bug was seen failing first; the VM, WWN, own-mount and sys_drive rules broken on purpose). - `configs/` unchanged — no config-bundle job needed. **Not vouched** (the operator's act). ## Unreleased (2026-10-15, afternoon) — the shared rule file; no code change - `.claude/rules/unprompted-work.md` §3 item 6: **a controller release needs no golden** — the floor carries it after the three test boxes; agent releases still wait for the operator's vouch; the golden is weekly (operator ruling 2026-10-15 11:26, `09` §3 decision 250; the same morning's „bake first", decision 247, reversed). Same text in all five copies. ## Unreleased (2026-10-15) — the shared rule file; no code change - `.claude/rules/unprompted-work.md` §3 item 6: **bake first** — a controller release is released only when its golden is baked; bake → the operator vouches → the operator raises the floor; never ask for a floor for a version with no golden (operator ruling 2026-10-15, `09` §3 decision 247, superseding decision 7). Same text in all five copies. ## v0.156.1 — R-1008 by process liveness, not the clock; 0.156.0 superseded — do not vouch it (2026-10-14) Released by `scripts/release-agent.sh`: binary sha256 `83c7da27d457b63ba8a4cde5a8717b3e5f9db9f83a46e63f38902dc7471bffb9`, bundle `5a678def792c70ec1cbbd198c14542161fac91d11a69bd0ce23dd86469e96ec3` (tag `v0.156.1` = `7ae3081`). Step bundle `0.156.1-step1` sha256 `ac83ea41507616d4c1056cccfd35de1410ff01e0296f4e64ff925207a43a8490` (the 0.155.0 bundle with only `felhom-os-apply` replaced), published as package version `0.156.1-step1`. **Not vouched** (the operator's act). Delivery as 0.156.0: per box `agent_update 0.156.1` → `agent_config_update 0.156.1-step1` → `agent_config_update 0.156.1`. - **0.156.0 is superseded — never vouch it.** A background security review of the pushed commit flagged „fail-open state drift": 0.156.0 called a listed vzdump task dead when its wall-clock start was before the kernel's `btime`. `btime` is derived from the wall clock and moves when the clock is corrected after the boot (an NTP step after a power cut), so a backup started between the boot and the correction could look „from before the boot" — and the stale-lock pass would clear a LIVE backup's lock. - **Now:** `taskAlive` is Proxmox's own rule (`PVE::ProcFSTools::check_process_running`, read on demo-hp): the UPID's pid and pstart against `/proc//stat` — same start tick (field 22) and not a zombie. No clock is involved. Fails safe (running) when the UPID does not parse, the stat cannot be read, or this agent cannot see root's processes at all (PID 1 not visible: a `hidepid` /proc or `ProtectProc=` would hide every worker — demo-hp: `/proc` plain, `ProtectProc=default`). - Tests: worker gone, pid reused, zombie, live worker, a „clock step" (wall-clock start absurdly early, worker alive → still running), doubt, hidden /proc — each red-proofed; 19 guards in all (`power-cut-fixes-2026-10-14/redproof.txt`). - R-1006 (the cleanup) is unchanged; its „before this boot" rule still reads `btime`, but a drifted `btime` cannot remove a live partial: the Go side asks this liveness rule, and the root helper refuses while any vzdump runs or anything holds the file open. ## v0.156.0 — after a power cut: the dead half-written backup is removed (R-1006); a backup task from before the boot is not „in flight" (R-1008) (2026-10-14) Released by `scripts/release-agent.sh`: binary sha256 `8db2278838dcb733d5201fba84b75a829b9adebdf899c89dd7de0939ec10d51a`, bundle `a21605217966ebc953d55a35157a9eb0243ca542c50dbd94fc9fa67257f27369` (tag `v0.156.0` = `a2f3aee`). Step bundle `0.156.0-step1` sha256 `8be2bb8f3d54bff8e7140cf99714664df31adecf13aa721597323fb2b9d43db5` (the 0.155.0 bundle `795f2ab4…` with only `felhom-os-apply` replaced; `scripts/build-step-bundle.py`), published as package version `0.156.0-step1`. **Not vouched** (the operator's act). **Delivery: the agent binary AND the config bundle, and the bundle ADDS a path** (`/usr/local/sbin/felhom-vzdump-partial-clean`) — so per box: signed `agent_update 0.156.0` → `agent_config_update 0.156.0-step1` (the step bundle) → `agent_config_update 0.156.0`. Evidence: felhom.eu `documentation/audits/power-cut-fixes-2026-10-14/` (the spike C1–C4, the red-proofs). - **R-1008 — one in-flight answer.** `staleLockController.BackupRunning` (the ONE implementation all four callers reach: the start-up stale-lock pass, the guest-power watchdog, the household power-off guard, the controller supervisor) now answers through `vzdumpInFlight`: a vzdump task for the vmid that started at or after the host's boot. Proxmox keeps a dead task in `tasks?source=active` for ~30–60 s after a power cut (measured, C3) — the agent starts inside that window and called it „genuinely in-flight". A task from before the boot is dead; logged once (vmid, UPID, start, boot). The start time comes from the list or, if absent, the UPID; an unreadable boot time or start time FAILS SAFE (running), as before. The boot time is `poweroff.ProcBtime` (no third /proc/stat parser). - **R-1006 — the half-written backup is removed.** New `internal/vzpartial`: on a `dir` backup target it finds exactly `vzdump-(lxc|qemu)--YYYY_MM_DD-HH_MM_SS.(tar.dat|vma.dat|tmp)` of THAT guest (never a finished archive, a log, another guest, a symlink, a wrong kind), and — only when no vzdump of the guest is in flight (R-1008) and the entry is dead (modified before this boot, or untouched for 120 s) — asks the root helper to remove it. Runs at agent start (after the stale-lock pass, every owned guest × every backup tier) and before every backup's space preflight (`BackupRunner.SetBeforeBackup`). Any doubt leaves the entry and logs why. - **The root helper `configs/felhom-vzdump-partial-clean`** (config bundle; sudoers `FELHOM_VZPARTIAL`, one exact line: a storage id, a vmid, one half-written name) re-checks everything as root: a `dir` storage from `/etc/pve/storage.cfg`; the name of that vmid; not a symlink; directly in `/dump`; **no vzdump on the host at all** (a job for several guests carries no vmid in its task name); **no process holds the entry open** (C2: after a killed worker, `tar` and `zstd` kept writing into the `.tar.dat` for 4 min 40 s, none named „vzdump"); dead (before this boot, or quiet 120 s). - **Not changed, by evidence (C4):** the space preflight's sizing (R-1006 part 2) — on „No space left" vzdump removes its own partial on both its paths (rsync and tar); only a power cut leaves one. - **Tests:** `stalelock_inflight_test.go` (the helper, the production controller, three callers end to end, a structural pin that every `BackupRunning` call goes through `s.staleLock` and there is one implementation), `vzpartial_test.go` (removes; keeps in flight / unknown / non-dir / other names / recent / wrong kind / symlink / a helper refusal — each keep with its positive control), `r685_space_test.go` (the cleanup runs BEFORE the preflight), the helper's own 21 Python tests (`configs/test_felhom_vzdump_partial_clean.py`, run by the Go suite), 8 sudoers injection cases. **17 guards red-proofed** by mutation (`power-cut-fixes-2026-10-14/redproof.txt`). - **Fixed without a row:** `internal/localapi/guestbind.go` and `controllersupervisor_test.go` were not gofmt-clean on `main` (an import order and a struct alignment) — formatted. ## v0.155.0 — the household can turn the machine off: POST /host/poweroff (R-998); the escrow reply race fixed (2026-10-13) Released by `scripts/release-agent.sh`: binary sha256 `bb339e944eff4aa6d10ae09158e0943de2e5c3777f52bf993c796d57e46800c3`, bundle `795f2ab41661e993e3fc4ce65df4f11ce94280d94871ab2e5cf1993ae3fbff7d` (tag `v0.155.0`). **Not vouched** (the operator's act). **The bundle changes `felhom-agent.sudoers`** (no new path — no step bundle): delivery is a signed `agent_update` + `agent_config_update`. Design: felhom.eu `documentation/architecture/14-host-power-off.md`. - **`POST /host/poweroff`** (local API, `withGuest`-scoped like `/guest/reboot`) — the controller's „A gép kikapcsolása" (controller v0.310.0). Refused (409, a stable `reason`) in the first 10 min after boot; a second time in one boot (`internal/poweroff`, a file keyed by `/proc/stat` btime); while ANY other guest or VM runs on the host (read from `/proc`: `lxc-start -n N`, `kvm -id N` — demo-hp's scratch guest and Tester 1 VM block it); while the heavy-operation gate is held (backup, restore test, OS step, sweep, trim), a backup job, a controller swap, a drive format, a NAS verify or an escrow ceremony runs, or Proxmox runs a vzdump of the guest. Every check fails closed. - On acceptance: the record first; the heavy gate TAKEN and kept; a host report carrying `deliberate_off.requested_at` accepted by the hub (bounded 60 s, `Loop.SetReportedHook`); then before each step the jobs and other guests are checked again — a job waits (≤ 15 min), another guest cancels at once, a job still running after the wait cancels (gate released, box stays on); `pct shutdown --timeout 120`; `systemctl poweroff`. - **Sudoers:** `FELHOM_POWEROFF` = `/usr/bin/systemctl poweroff`; `pct ^shutdown [0-9]+ --timeout [0-9]+$` joins `FELHOM_GUESTHOOK`; nine injection cases (`--forceStop`, `poweroff --force`, `reboot`, `halt`, `kexec`…). **New `TestEveryAliasIsGranted`:** an alias missing from the grant line passed the coverage check before. - **Host report:** `deliberate_off` (omitempty; the cross-repo golden unchanged). Hub v0.149.0 holds its silence alarms for 14 days on it. - **Fixed without a row:** `go test -race` found a data race in the escrow ceremony start reply (it read `job.Phase` while the job goroutine wrote it); the reply fields are copied first. Red before, green 3/3 after. - Security review (three passes the same evening): other guests, a gate not held, fail-open unwired checks, jobs and guests starting after acceptance — each fixed and red-proofed (`hostpoweroff_test.go`, 11 tests). ## v0.154.0 — no OS leg after a household press; the after-boot kernel report carries the real ring (R-899); no „wrong code" when older packages were not checked (R-304); the last backup survives a restart in the host report; the Secure Boot meta-package leaves the Proxmox lane (2026-10-09) Released by `scripts/release-agent.sh`: binary sha256 `36a54ad20e896cd61d7b9944e2a521f4209c2b76940d8f1827af05b645463226`, bundle `93487989f50d4a3039a05cec0f91ec478d62489a414705e49403e9b1340d20e0` (tag `v0.154.0` = `db25b46`). **Delivery: the agent binary AND the config bundle** — the wrapper `felhom-os-apply` changed (2026-10-09). - **The hub's backup evidence survives a restart (2026-10-09, found in the kernel-night read-back).** The host report's `backups` list came only from memory, which is empty after an agent restart. On demo-felhom the night backup landed 02:40 UTC and the kernel step restarted the host at 02:44 — no report fell in between, two nights running — so the hub alarmed „host tier: newest backup is 48h old" at 03:00 after a good backup. The report now adds R-894's saved newest success per tier and guest (`backup-success-state.json`, ONE instance shared with the local API) where memory holds no success at or after it. Only successes are saved, so a failure is never hidden. `hub.KnownBackupReporter`, `BackupSuccessState.KnownBackupSuccesses`; tests `TestCollectBackups_SavedSuccessSurvivesARestart` (red-proved: the restart case reported nothing), `TestBackupSuccessState_KnownBackupSuccessesSurviveAReopen`, and `TestR894_LastKnownBackupsIsWiredIntoTheDaemon` now also asserts the collector gets the same instance (red-proved). - **`proxmox-secure-boot-support` is a boot-chain package (2026-10-09).** On demo-hp (Secure Boot on) its upgrade pulled `shim-signed-common`; in the ring-0 Proxmox plan that refused the whole step (R6) and skipped the night's kernel step, after the household had been told „tonight". It joins `HOST_SLOW_RE` with shim and GRUB (they stay out of every lane, as before). `test_ring0_pending_pve_leaves_the_secure_boot_meta_out` (red-proved). - **R-899 (operator ruling 2026-10-08, option A):** `POST /backup?trigger=manual` (a „Mentés most" press, sent by the controller from its next release) runs no OS leg after it: the leg belongs to the night, after the night's own copy. Before, a press ran the leg at once (in the day; on 2026-10-07 08:51 demo-hp skipped it only by the 20-hour rule). A request without the parameter (an older controller, the scheduled path) behaves as before. `internal/localapi/server.go`; `TestAfterPrimaryBackup` gained two sub-cases (red-proved: ignoring `trigger`, the leg ran once after a press). - **R-304 — „wrong code" only when every earlier package was tried.** `POST /escrow/recover-offsite-password` answered 400 („the recovery code did not open the sealed bundle") whenever the current package and the TRIED earlier packages refused the code — even when the hub withheld earlier packages (rows with no key material, rows over its serve cap), a served package was malformed, the 6-attempt cap stopped the loop, or the retained list could not be read at all. Now those cases answer **424** with `older_unchecked` (the count, -1 = unknown) and a sentence that does not call the code wrong. `escrow.ErrRetainedUnchecked` / `RetainedUncheckedError`; the retained fetcher's second value is now every withheld package (unopenable + truncated + malformed). Tests `TestR304_*` (real age crypto; red-proved: without the check, four cases returned the wrong-code error) and a 424 row in `TestRecoverOffsitePassword_EachSituationGetsItsOwnStatus`. An older controller maps the unknown 424 to its neutral „we do not know why" sentence. - **The „ring 1" label after a boot:** in the first second after a reboot the agent has not fetched the hub's block, and the after-boot kernel reports (`judging`, `good`, `revert`, `fell_back`…) said ring 1 on a ring-0 box (demo-felhom, 2026-10-08 night). Now they read the fetched block, else the block the daemon saved on disk before the reboot (R-866), else ring 1 as before. A label only — the hub's approval reads its own ring list. `kernelReportRing`, `TestKernelReportRing_BeforeFirstFetch` (red-proved: ring 1, want 0). ## v0.153.0 — ring 0 stages exactly the told kernel (R-898; `09` §3 decision 176) (2026-10-07) Released by `scripts/release-agent.sh`: binary sha256 `b204ebe6d65944f43b70dcfa4ae0dfb90da38c92e2ba6a38a1826e488eeb6630`, bundle `6db216275eb0dd39188d93481a2045998a69e8cb7838ad908a58d65d9a6a9b57` (tag `v0.153.0` = `2d1e5d0`). Delivered (binary only — the bundle files are unchanged from 0.152.0) to demo-hp, demo-felhom, Tester 1 on 2026-10-07 19:03. **Delivery: the agent binary only** — no root file changed (the wrapper is unchanged; its tests gained two cases). - `internal/osupdate/kernel.go`: ring 0's night kernel step stages EXACTLY the kernel the household was told about (select `listed`, `KernelSet(kver)` = the series meta-package and the signed image at the kernel's own version) instead of "whatever is pending tonight". Seen 2026-10-07: demo-felhom was told about 7.0.14-20 while its sources offered 7.0.14-22 by night — the old code staged `pending-kernel` and the wrapper refused it (R23), losing the night. A told version that is no longer installable is refused by the wrapper before any change (R7) and the hub tells the household again for the newer kernel (hub v0.143.1). Tests `TestKernel_Ring0ToldNightStagesThenReboots` (red-proved against the old select), `TestKernelSet`; wrapper `test_ring0_listed_installs_the_told_kernel_not_the_newest`, `test_ring0_told_kernel_gone_is_refused_before_any_change`. ## v0.152.0 — the kernel lane (R-836; `09` §3 decisions 164, 172; `11` §5.11) (2026-10-07) Released by `scripts/release-agent.sh`: binary sha256 `95ff42208e36ba49b6e2b09a97e81a6fa11562ecc8042f1ed18d378b8b1f88b1`, config bundle sha256 `f0c2cec374b711b3c131c955012b33fd0ad495337d049b7a743c3eef9e85c20b` (tag `v0.152.0` = `d03ab7f`). Step bundle `0.152.0-step1` sha256 `0b71d32b054cf3b7ade0234ffcbb0df159901f542cde540adaee411db466f48e` (the 0.151.0 bundle with only `felhom-os-apply` replaced; `scripts/build-step-bundle.py`), published as package version `0.152.0-step1`. **Delivery: agent binary, then the STEP bundle `0.152.0-step1`, then the bundle `0.152.0`** — the bundle ADDS two paths (the GRUB generators), and an installed `felhom-os-apply` refuses a path its own table lacks (R16, R-880). A new kernel boots ONCE; if it crashes the box comes back on the old kernel by itself; it becomes the default only after a healthy boot; a booted-but-unhealthy kernel is reverted ONCE by the agent with no person. Built on the spike's candidate 2 (`audits/kernel-spike-2026-10-07/`), with option C on the one-shot entry. - `configs/felhom-grub-oneshot.sh` → `/etc/grub.d/01_felhom_oneshot` (bundle): reads `felhom_next` from a GRUB env block on the ESP (`EFI/felhom/oneshot.env`), clears and saves it BEFORE the menu, and sets the default to that kernel's one-shot entry only when the name is an installed kernel. No vfat ESP → prints nothing. - `configs/felhom-grub-oneshot-entries.sh` → `/etc/grub.d/42_felhom_oneshot` (bundle): one entry per installed kernel, id `felhom-oneshot-`, the normal entry plus `softlockup_panic=1 hardlockup_panic=1 hung_task_panic=1 panic=10` (option C). Sorted after `10_linux`: never entry 0, never the default. - `configs/felhom-os-apply`: layer `kernel` (lane slow; an appliance; authority = a signed `os_kernel_step` or the root-owned ring-0 mark). Modes: `apply` STAGES (select `pending-kernel` or a signed `listed` set; `expect_kver` = the kernel the household was told about): pins the GRUB default to the RUNNING kernel in `/etc/default/grub.d/zz-felhom-kernel-default.cfg` and proves it from grub.cfg, installs, proves the default did not move and the one-shot entry exists, writes the flag; never reboots. `kernel-reboot` (a staged step only), `kernel-boot` (judging | fell_back | self_reverted | revert_failed), `kernel-good` (the new kernel becomes the default, proved), `kernel-revert` (ONE per step; refused when the default is not the old kernel), `kernel-cancel`, `kernel-status`. New refusals: R20 (the box cannot do a one-shot: not UEFI, no vfat ESP, a separate /boot, GRUB without fat/loadenv, the generators missing, a hand pin), R21 (the crash guard tripped or an unclean boot in its window), R22 (the phase does not allow the mode; never two steps within 20 h), R23 (not exactly one newer kernel, or not the one signed / told). State `/var/lib/felhom-kernel/state.json`. Facts carry `kernel_lane`; the next-boot kernel reads the flag and the grub.cfg default. Tests: `KernelLane` (27), red-proof `felhom.eu/documentation/audits/kernel-lane-2026-10-07/A/redproof.txt`. - `configs/felhom-crash-guard` unchanged; `KernelStepCannotLeaveTheBoxOff` (3 tests) pins that a step's planned reboot, one crash and one self-revert add ONE unclean boot (a panic before userspace adds none), so the box cannot stay off. - `internal/osupdate/kernel.go`: the night leg ends with the kernel step — after a healthy host step (and a healthy Proxmox step when one ran), trigger `night` only, on a night the hub's `os_update.kernel` block marks `tonight` (the household was mailed the day before — no mail, no step). Ring 0 stages + reboots; ring 1 reboots only a kernel a signed `os_kernel_step` staged (`KernelStepExecutor`: stage only, under the heavy-op gate). The hub hears `staged` BEFORE the reboot. At every start `KernelAfterBoot`: on the new kernel it JUDGES the boot — `KernelVerdict` = the host health rule (`11` §8.2) AND the box reached the hub (the `judging` report itself) — for 20 minutes (measured: everything healthy 68 s after the reboot on demo-felhom, 272 s on demo-hp; under the hub's 45-minute `host_stale` (`alerting.stale_threshold`)). Healthy → `kernel-good`, outcome `applied`; not healthy → outcome `health_failed`, then ONE `kernel-revert`. Tests: `TestKernel*` (13); red-proofs in the same file. - `internal/hub`: `WireOSUpdate.Kernel` {kver, tonight, notified_at}. `internal/reconcile`: `os_kernel_step` is destructive-class. `cmd/felhom-opsign`: the op is listed. ## v0.151.0 — the agent can no longer hand the guest any image; the Proxmox package lane; the other-key archives reported; the DR directive retired (R-861, R-812 A, R-366, R-105; `09` §3 163, 165, 168, 169) (2026-10-07) Released by `scripts/release-agent.sh`: binary sha256 `0464354f2cdf452a7c5d2a74d9191fe91415fcfa244154480d26d5b30e10b194` config bundle sha256 `bacd1d175a9392bc1755341d01a106abbd72aba48ab575e7a32dd819d8f5da4c` (tag `v0.151.0` = `dd7cdc0`). **Deliver the binary FIRST, then the bundle:** the bundle's sudoers removes the `tee` grant the 0.150.0 binary still uses. No path added (26 → 26), so no step bundle. ### Part of v0.151.0 — the agent can no longer hand the guest any image; felhom-op's pct lines are exact (R-861 (a) A1, (b) B2; `09` §3 decision 165) **Delivery order: agent binary FIRST, then the config bundle.** The new sudoers drops the agent's in-guest `tee` grant; an older binary still calls `tee`, so a bundle that lands before the binary would stop managed controller updates (and the old binary's capability probe would read `controllerswap-write` degraded). No bundle path is added (`felhom-priv-apply` and `/etc/sudoers.d/felhom-op` are already bundle files), so no step bundle. - `configs/felhom-priv-apply`: new verb `controller-image ` — reads the ref on stdin (≤ 256 bytes, ASCII, one optional trailing newline), requires `^gitea\.dooplex\.hu/admin/felhom-controller:[0-9]+\.[0-9]+\.[0-9]+$` (the agent's own `controllerImageRe`), then runs `pct exec -- tee /etc/felhom-controller-image` AS ROOT; refusal rule `I1` (rc 3), a bad vmid `A1` (rc 2); listed in `--self-check`. - `configs/felhom-agent.sudoers` `FELHOM_CONTROLLERSWAP`: `pct ^exec [0-9]+ -- tee /etc/felhom-controller-image$` REMOVED; `/usr/local/sbin/felhom-priv-apply ^controller-image [0-9]+$` added. - `internal/localapi`: `GuestExecutor.GuestExecStdin` replaced by `WriteControllerImage`; `GuestBinder.WriteControllerImage` pipes `ref\n` to the verb through the fenced runner; the swap's `writeImage` calls it. Capability `controllerswap-write` now probes the verb. - `configs/felhom-op.sudoers` (B2, hygiene): `pct start|stop|unlock [0-9]*` → `pct ^start [0-9]+$` etc. (the glob's `*` matched spaces: `pct stop 9201 --skiplock 1` passed). - Tests: `ControllerImage` (5, `configs/test_felhom_priv_apply.py`), `TestSudoersRefusesTheR861Injections` (+3 lines), `TestSudoersAllowsTheControllerImageVerb`, `TestFelhomOpSudoersPctIsExact`, `TestR861_WriteControllerImageUsesTheRootVerb`, `TestControllerSwap_WriteViaRootVerb_NoShell`. Red-proofs: `felhom.eu/documentation/audits/day-2026-10-07/C/`. - `README.md`: the controller-swap paragraph described the removed `tee` path — corrected. ### Part of v0.151.0 — the Proxmox package lane (R-812 option A, `09` §3 decision 163) **MinAgent impact: none** (a new layer; an older hub ignores the pve report). **The bundle carries the new `felhom-os-apply` — deliver it with the binary** (signed `agent_update`, then signed `agent_config_update`). - `configs/felhom-os-apply`: new layer `pve`, lane `slow` only — the host's Proxmox USERSPACE packages: origin `Proxmox Debian Repository` only (R2), never a kernel / boot / firmware / microcode name (R14, `HOST_SLOW_RE` — the kernel is R-836's lane), no removal (R4), no undo (R5), a new package only from `PVE_NEW_ALLOW` (`proxmox-firewall-data`, measured on demo-felhom; R6 otherwise), an appliance only (R12), authority = a signed `os_pve_step` or the root-owned ring-0 mark (R3). Select `pending-pve` (ring 0): installed Proxmox-origin packages with a pending upgrade. The report carries `pve_manager` (pveversion after the step). - `internal/pvegate` (new): the agent's own writes to /etc/pve wait while a pve step runs (pmxcfs restarts); the step waits for writes in flight (bounded, 2 min — then it fails and does not run). Wired at `proxmox.Client.doBody` (every non-GET) and `ExecRunner.RunStdin` (`WritesEtcPVE`: pct config verbs, pvesm, pveum, felhom-pbs-apply create/reconcile). - `internal/osupdate`: `LayerPVE`; the night leg runs the pve step in ring 0 after a healthy host step (an appliance; ring 1 never in the night leg); `PVEHealthVerdict` = the host rule + every running container keeps its id + pveversion reads the installed pve-manager; the pve report carries Proxmox userspace only (the hub's candidate set). `PVEStepExecutor` (signed `os_pve_step`, ring 1, under the heavy-op gate and the /etc/pve gate); `reconcile.ClassOSPVEStep` (destructive-class); `felhom-opsign -op os_pve_step` (params by `-params`). - Tests: wrapper `PVELane` (17; red first — the `pve-manager` plan was refused R12 on the old code), `pvegate` (5), `TestPVEGate_*` + `TestWritesEtcPVE`, `TestPVE_*`, `TestPVEHealthVerdict`, `TestPVEStepExecutor_*`. Red-proofs: `felhom.eu/documentation/audits/day-2026-10-07/B/`. ### Part of v0.151.0 - R-366 slice 2 (`09` §3 decision 168): the restore-test pick records, per tier, the archives it skipped as written with another key (count, oldest, newest — no key material) in a `ForeignKeyLedger`; the host report carries it as `foreign_key_archives.tiers` (the stanza absent until a tier was evaluated since start, `tiers: []` when none — no null on the wire, the report contract forbids it). The hub turns a change into one operator line. Tests `TestR366_PickRecordsArchivesWrittenWithAnotherKey`, `TestR366_EvaluatedWithNoneIsAnEmptyList` (red-proved, `felhom.eu/documentation/audits/day-2026-10-07/E/`). - R-105 option A (`09` §3 decision 169): the `--selftest=escrow-create -directive ` flag and the escrow upload's `directive` field are removed — nothing read the directive; the DR path reads the recipe, tenantsync and the escrow blob. The hub ignores a `directive` from an older agent. ## v0.150.0 — the Docker step proves the engine reports a memory kill; after a restart the agent remembers the last backup per tier; three more SMART counters on the wire (R-528, R-894, R-330; `09` §3 decisions 157, 161) (2026-10-07) Released by `scripts/release-agent.sh`: binary sha256 `a23d1c9085bc7fd4fc48fe0327f6504aa83e6331510fb4a3d23e042dddb26f9c` config bundle sha256 `88456b386d9b1027bd22861cac8c23df004bf9fd9f67644d6595bfca8c94498e` (tag `v0.150.0` = `3a72a48`). **The bundle carries the new `felhom-os-apply` (the memory-kill check) — deliver it with the binary:** signed `agent_update`, then signed `agent_config_update`. No path added (26 → 26), so no step bundle. ### Part of v0.150.0 (2026-10-06 night, later) — after a restart the agent remembers the last backup per tier (R-894); three more SMART counters on the wire (R-330) Ships with the memory-kill check below as v0.150.0, AFTER the 2026-10-07 night read-back. Nothing delivered tonight. - **The defect (measured 2026-10-05 on demo-hp):** the agent restarted at 04:57; at 06:25 the off-site storage answered *Can't connect*; the per-tier backup record is in memory only, so the due-check fell back to an EMPTY record and the 7-day tier (last copy 4 days old) read DUE; the controller asked and vzdump failed. - New `internal/backup/backup_state.go` `BackupSuccessState`: the newest SUCCESSFUL backup per tier and guest, on disk (`/backup-success-state.json`, atomic tmp+rename, 0600). Only successes are written; a corrupt file reads as nothing known. - `internal/localapi` `handleBackupDue`: when the tier's storage CANNOT be read, the saved copy stands in for the in-memory record. A fresh copy → not due („… (storage unreadable — age from the last success saved on disk)"); a copy older than the cadence → DUE; no copy → the old answer (DUE, age unknown). A storage that answers stays the ground truth: an archive absent there is due even when the file remembers one. - Wired in `buildLocalAPIServer` (`LastKnownBackups`); the local API's backup job saves each success. - Tests: `TestBackupDue_R894_*` (restart = a new server and a new state from the same file; fresh / old / none / storage answers / failed backup not saved), `TestBackupSuccessState_*`, `TestR894_LastKnownBackupsIsWiredIntoTheDaemon` (AST). Four red-proofs observed (`felhom.eu/documentation/audits/night-burndown-2026-10-06/s4/`). - **R-330 (disk health Phase 2, the wire only):** the SMART summary carries three more SATA raw counters — `reported_uncorrect` (187), `command_timeout` (188, carried as the vendor reports it; some pack several counters), `udma_crc_errors` (199). Pointer + omitempty: an attribute the drive does not report is OMITTED (unknown), never 0. No verdict reads them yet. Tests `TestParseSMART_R330_*` (two red-proofs, `felhom.eu/documentation/audits/night-burndown-2026-10-06/r330/`). ### Part of v0.150.0 (2026-10-06 night) — the Docker step proves the engine reports a memory kill (`09` §3 decision 157, R-528) To be released as v0.150.0 with its config bundle AFTER the 2026-10-07 night read-back (the night of 2026-10-06 runs v0.149.0 on purpose). - `configs/felhom-os-apply`: after a docker-layer APPLY (after `health_after`) the wrapper runs `oom_check()`: a throwaway container from the image the running controller uses (`--pull never`, `--network none`, no volume, label `felhom.oomcheck=1`, 64 MB cap) asks for one 200 MB block; „pass" only when `OOMKilled=true` AND the `oom` event; it waits 2 s and reads the events window to the guest's epoch + 1 (measured: a window closed in the same second missed the event); the container is always removed. Reported as `oom_check`; it never changes the step's outcome or health. A wrapper-only mode `oom-check` runs the check alone (no apt, no engine change), by hand as root. - `internal/osupdate`: `WrapperReport` and `Report` carry `oom_check` verbatim, on the normal pass and on the kept-copy path (R-868). - `configs/test_felhom_os_apply.py`: its `unittest.main()` sat in the middle of the file, so 11 tests (UnsentReport, SaveReportOnDisk, AgentDiesMidPass, CrashLeftTheJournal) never ran — moved to the end; all pass. - Tests: the OOMCheck class (pass, OOMKilled=false, no event, unreadable image, removal on an inspect error, not on other layers or in health mode, the events window after the settle wait, mode oom-check alone and its refusals); TestDocker_OOMCheckReachesTheHubUnchanged, TestR868_KeptCopyCarriesTheOOMCheck. 12 red-proofs in `felhom.eu/documentation/audits/readback-2026-10-07/F/`. ### Part of v0.150.0 (2026-10-06 evening) — the shared rule file (`09` §3 decision 152); no code change - `.claude/rules/unprompted-work.md` added, byte-identical to the copies in felhom.eu, felhom-controller, app-catalog-felhom.eu and the workspace root (checked with `diff` against the controller's copy and one md5 across all five). Its copies line names five copies. ### Part of v0.150.0 (2026-10-06 afternoon) — instruction files kept true (`09` §3 decision 150); no code change - `CLAUDE.md` „Gates — ONE entry point": the runner runs every gate in its `GATES` table (five: three shared, `published`, `release-complete`); `--fast` skips `published` (network). It said two gates and „all of them". - `CLAUDE.md`: the decoy gate and its audit are named with their `felhom.eu/` prefix (they do not exist in this repo). - `.claude/rules/health-checks.md` (comment): the health-check rule's copies live in felhom.eu `hub.md` and the controller's `gates.md`; it named felhom.eu `CLAUDE.md` „Code quality rules", which holds no such rule. ## v0.149.0 — a weekly disk trim of each customer guest, the crash-boot fact for the controller, the phantom WARN names its runbook (R-444, R-856, R-99; operator rulings `09` §3 139, 143, 140) (2026-10-06) Released by `scripts/release-agent.sh`: binary sha256 `6bcae9c2eb5d97e8285316583870059835793893299e291891a53a4ce505585f` config bundle sha256 `e182c82dcf4a67faa3bcb74dbe4ffa7b06e0b27dc8451cb7574d6339ce91ad66` (tag `v0.149.0` = `f277e61`). **The bundle carries the new sudoers rule for the trim (`FELHOM_FSTRIM`) — deliver it with the binary:** signed `agent_update`, then signed `agent_config_update`. - R-856 (`09` §3 decision 143): new local-API route `GET /host/crash-guard` — passes the host crash guard's last-boot record (present, last_boot_at, last_boot_unclean, tripped) from /var/lib/felhom-crash-guard/state.json to the controller, which waits ~15 min with app mails after a crash boot. Read-only, no Proxmox call, guest-token authed; a missing/unreadable/garbled file answers 200 present:false (never an error page). An older agent answers 404, which the controller reads as unknown (normal 90 s grace) — no controller MinAgent raise needed. - R-444 (`09` §3 decision 139): weekly guest disk trim. New sudoers alias FELHOM_FSTRIM with ONE exact rule `/usr/sbin/pct ^fstrim [0-9]+$` (rides the signed config bundle; decoys pinned by TestSudoersFstrimRuleIsExact) and capability guest-fstrim (non-critical). New internal/fstrim job: each owned RUNNING guest gets `pct fstrim ` once a week - due Wednesday from 10:00 host-local, starts only 10:00-20:59 (never the 01:00-06:59 night), holds the one-heavy-op gate so it never runs beside a backup or restore-test (busy -> deferred to the next hourly tick; a box that was off catches up at its next daytime hour); a failed trim WARNs and is retried at most 3 times that week; bytes parsed from `pct fstrim`'s "(N bytes) trimmed" lines; positive log `fstrim: guest N trimmed X GiB in Ys`; last result per guest persisted in /guest-disk-trim.json and reported as the new omitempty host-report stanza `guest_disk_trim`. Opt-out: agent.json "disk_trim": {"disable": true}. - R-99 (`09` §3 decision 140): the agent's WARN for a PBS archive below the 1 MiB plausibility floor now ends with the pointer to the sanctioned cleanup (`documentation/runbooks/pbs-phantom-cleanup.md`); detection only — nothing is deleted automatically. Dir-storage archives keep the old text. ## unreleased - R-426: scripts/test_gate_decoys.py (new) — the published gate judged against a fake Gitea (127.0.0.1, via GITEA_BASE; never the real registry): 11 cases; COVERS published — `felhom-agent/published` leaves the decoy-coverage EXEMPT list. - R-426: release-complete gate — 10 decoy cases (scratch clone + scratch bare origin + fake Gitea); COVERS release-complete — `felhom-agent/release-complete` leaves the decoy-coverage EXEMPT list. - R-426: the shared reuse-refs/instructions/observations gates get agent-side decoys (9 cases on a scratch clone of this repo); COVERS reuse-refs, instructions, observations — three `felhom-agent/*` entries leave the decoy-coverage EXEMPT list. - **Fixed without a row:** `configs/test_felhom_config_bundle.py` read the two ISO first-boot files that installer 1.32.0 now NAMES under KEPT (R-275) as files the installer writes — `go test ./internal/osupdate` was red on DooPlex from 21:25 to 01:55 (felhom.eu `85de3f9b`); they are listed with why. Test only; agent v0.148.0's code is unaffected. ## v0.148.0 — the host report names the running binary's sha; the format answer carries the new filesystem's UUID (burn-down night: R-349, R-25 agent halves) (2026-10-06) Released by `scripts/release-agent.sh`: binary sha256 `3e68a0870e0e2ce262cb4819294edddeb0a73e8c558a31611a20329a6d9ee283` config bundle sha256 `a6fa4f589d184b58c9911303bd087e300be1e75b3647e4302c9594df6989c4de` (tag `v0.148.0` = `861d32a`). Delivery order as for v0.147.0: signed `agent_update`, then signed `agent_config_update`. - **R-349:** the host report carries `agent_sha256`, the sha256 of the running agent binary (read once from `/proc/self/exe`; empty = unknown), so a hand-built binary under the vouched version name becomes visible. The hub comparison is a separate hub change. Test `TestCollect_AgentSHA256IsTheRunningBinary`; red-proved. - **R-25 (agent half):** `POST /disks/format` and `GET /disks/format/status` return `fs_uuid`, the new filesystem's UUID read back after mkfs only when the bound durable id still resolves to the formatted device and the superblock is the requested type (empty = not verified); `DeviceProbe` gains `FSUUID` from blkid. Tests `TestFormat_*FSUUID*`; three red-proofs. The controller half (mount that UUID) is a controller change. ## v0.147.0 — the recovery recipe spells the root namespace the way PBS does; a removed drive no longer shows the root disk's size; a rotated-out token stops at once; the dnsmasq check looks at the right package (burn-down round 2: R-124, R-118, R-269, R-317) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `642c4d196c48671c14ff653118303c5903af1670b7abb550edeaf73e701cd5b8`, config bundle sha256 `326527d0993c9a62df2f790c7700ca645cedbf0673dcfb6dc1768d8610b8007d` (tag `v0.147.0` = `f1b9b41`). Delivery order as for v0.146.1: signed `agent_update`, then signed `agent_config_update`. MinAgent impact: none (the controller needs nothing new from this agent). Config bundle content unchanged from v0.146.1. - **R-124 (operator ruling 2026-10-05: fix it):** the DR recipe's `pbs.namespace` for a box in PBS's ROOT namespace is now `""` — PBS's own spelling — beside `namespace_state: resolved`; it used to be the word `root`, which no namespace is named, so `--ns root` failed in a recovery. `hub.PBSRootNamespace`; `TestR124_RootNamespaceOnTheWireIsPBSSpelling` (red-proof: back to "root" → FAIL). Runbook: `felhom.eu runbooks/ep0-datastore-copy.md` step 2 says how to read it (and to treat a recorded `root` from older agents as empty). The hub stores the recipe raw; its fixture follows. - **R-118:** the local API's drive list reads a drive's capacity only while its DEVICE is present — with the device gone the bare mountpoint is a directory on the root filesystem, whose size was reported as the drive's. `TestDisks_UnionPath_AbsentDeviceReportsNoRootCapacity` (red-proof convicts). - **R-269:** the token store re-reads its shared file whenever it has grown, BEFORE answering — so a token rotated out by another process stops authorizing on its next use (it used to keep working until an unrelated miss). One `stat` per call. `TestTokenStore_RotatedOutTokenRejectedFirst` (red-proof convicts). - **R-317:** the LAN resolver decides whether to install `dnsmasq` by its service UNIT, not by `/usr/sbin/dnsmasq` (which the `dnsmasq-base` package also ships). Same `apt-get install` command; no sudoers change. `TestEnsureDnsmasq_*` (red-proof convicts). Red-proofs: `felhom.eu/documentation/audits/burndown2-2026-10-05/agent-red-proofs.txt`, `r124-red-proof.txt`. Also in this release (no binary effect; from burn-down round 1): - **R-291:** `scripts/retention-policy.json` names where its 10 comes from — the R-267 newest-10 prune of generic packages, established 2026-08-10 (R-287) — instead of „observed, no located ruling"; the non-existent `registry-retention.md` reader is dropped. `check-published-versions.py` still reads 10 (checked). - **R-348:** `internal/backup/store.go` no longer says backups are „unaffected" by a restart: the reported backup list reads 0 until the next backup runs; only the hub's verdict (7-day look-back) is unaffected. ## v0.146.1 — R-861 review fixes: the signed update flips a root-owned copy; no Wants=/continuations in mount units; the escrow read follows no symlink anywhere (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `badd6c9a2e40c8bfe856d2d1a203443b21b7eb92ecc35d6090ab44518d4d082a`, config bundle sha256 `42333e969028867ad8142335e6c1bc4040eec231de0d8d330c2d4b2cf7bc3442`. **Supersedes v0.146.0, which was released but never vouched or delivered to any box.** The same order applies: signed `agent_update` first, then the signed `agent_config_update`. **Delivery needs a STEP bundle (R-880, found while delivering).** An installed `felhom-os-apply` checks an incoming bundle's paths against its OWN table (R16), so every box on the v0.145.0 bundle REFUSES the v0.146.1 bundle (it adds 4 paths). `scripts/build-step-bundle.py` builds the transition: the box's current bundle with ONLY `felhom-os-apply` replaced (same paths — the old wrapper accepts it), published as bundle version `0.146.1-step1`; then the release's own bundle. Order on a box: `agent_update` 0.146.1 → `agent_config_update` 0.146.1-step1 → `agent_config_update` 0.146.1. Tests `StepBundle` (the R16 refusal reproduced; the step accepted; exactly one file changed). Tooling only — not in the binary or the bundle. A background security review of the v0.146.0 commit found three holes in the new code; each is fixed and red-proved (`felhom.eu/documentation/audits/hub-safety-2026-10-05/partF/red-proof.txt`, S1–S3): - **S1 — a race in the signed update.** `felhom-os-apply` hashed the agent's staged file and then let the A/B wrapper copy it BY PATH; the agent owns that directory and could swap the file in between. Now the root step reads the file ONCE (`read_staged_once`: O_NOFOLLOW, fstat, owner, size), hashes those bytes, writes them to a root-owned directory (`/var/lib/felhom-os-apply/agent-update/`) and hands ONLY that copy to `felhom-selfupdate-guarded apply`, which now refuses any other directory, a symlink, or a file not owned by root. Tests: `AgentUpdate` (+1), `SelfupdateWrapperConfinement`. - **S2 — an allowlist escape in `felhom-priv-apply`.** `[Unit]` accepted `Wants=`/`Requires=`/`Before=` naming any unit, so a mount unit could start e.g. `reboot.target`. `[Unit]` now holds only `Description` and `After=local-fs-pre.target` (what the renderers write), and any line ending in a backslash (a systemd continuation this parser would read differently) is refused. Tests `test_U2_wants_starts_another_unit`, `test_U2_continuation_line`. - **S3 — a path traversal in the escrow read.** `O_NOFOLLOW` guards only the last component; a symlinked DIRECTORY in the agent's own state dir still redirected the root read. `readStagedNoFollow` now walks the path from `/` with `openat(O_NOFOLLOW)` per component. Test `TestAttach_RefusesASymlinkedDirectory`. ## v0.146.0 — the agent's root grants narrowed: exact sudo patterns, a root content checker, fixed files from the bundle, the signed update checked as root (R-861) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `b860af465076041e07f35fed1b12d64ae2b2985d8995f0ce167418d39c2b00d5`, config bundle sha256 `161c737e523aa7910cf32ce41b83f989569bee55b8c5938e7211c92aef68548e`. **Order on a box: the signed `agent_update` FIRST (the old bundle still grants the old flip), then the signed `agent_config_update`.** Between the two (minutes) the new agent's checker calls are refused and retried; nothing is lost. After the bundle, an agent BELOW 0.146.0 cannot update itself on that box any more (the unsigned flip grant is gone) — deliver both together. Design: `felhom.eu/documentation/architecture/03-host-agent.md` §3.1 (new). Measured before the change (real sudo 1.9.16, a throwaway container): the v0.145.0 sudoers let **23 of 29** attack command lines through; v0.146.0 lets **0** through and still allows all **64** commands the agent's capability check uses. - **Exact patterns.** A sudoers `*` in the arguments also matches spaces: `pct set [0-9]* -onboot 1` matched `pct set 100 --dev0 /dev/sda -onboot 1` (a raw host disk for a guest), `mount --bind /mnt/*/felhom-data /mnt/felhom-drives/*` matched a `..` path onto `/etc/sudoers.d`, `nft add element … *` took a chained `; flush ruleset`. Every varying argument list is now a sudo regex (`^…$`): one value per slot, a fixed character set, no `..`, no extra argument. `TestSudoersRefusesTheR861Injections` (29 attacks) + `TestManifestCoveredBySudoers` (regex-aware now). - **`felhom-priv-apply`** (new root wrapper, in the bundle). A systemd mount/automount unit, a dnsmasq drop-in, the WireGuard config and the OOB sshd config + felhom-op key reach their root-read places only through it: fixed source, fixed destination, CONTENT checked against what the agent's renderers write (no `[Service]`, `Where=` only `/mnt/` or `/mnt/felhom-drives/` and equal to the unit name, no `bind`/`suid`; a network share must carry `nosuid,nodev`; no `dhcp-script=`; no `PostUp=`; the sshd config only the one template with its Port). Its 30 tests (`configs/test_felhom_priv_apply.py`) + Go contract tests feeding each renderer's real output (`internal/privapplytest`). Pre-flight: every live file on both demo boxes reads OK. - **NFS/SMB options gain `nosuid,nodev`** (a set-uid file on a server outside the box never acts on the host). - **Fixed files from the bundle.** The guest pre-start hook (`/var/lib/vz/snippets/felhom-guest-hook.sh`, run as root at every guest start) and the shared drive parent script + unit are bundle files now (byte-identical to the agent's constants, pinned). The agent no longer installs them from `/tmp`; it checks them (`guesthook.SnippetReady`, `ensureSharedParentBoot`) and only registers / enables. - **The signed update is checked as root.** `felhom-os-apply` mode `agent_update` verifies the operator signature (root-owned signers, this host, the window, the nonce), re-hashes the staged binary against the SIGNED sha, then runs the A/B flip; `felhom-selfupdate-guarded apply` is no longer in the agent's sudoers. 7 tests (`AgentUpdate`). - **The root escrow run reads no path from the agent's config.** As root it pins the PVE secret dir and the WireGuard state dir to their defaults, refuses a storage id that is a path, and reads its two staged files without following a symlink (`readStagedNoFollow`) — before, a symlink in the agent's own directory sealed any root file into the blob. - **Not narrowed here (named in `03` §3.1):** `FELHOM_CONTROLLERSWAP` stays guest-scoped (a compromised agent can run a chosen controller image in the guest — the household's data, not host root); `FELHOM_ESCROW` still hands the agent R by design (the agent relays the ceremony); the mkfs / pbs-apply / backup-target wrappers keep a coarse argument and their own checks. - Red-proofs F1–F9: `felhom.eu/documentation/audits/hub-safety-2026-10-05/partF/red-proof.txt` (F1's first run did NOT convict — the name rule masked it — and the test now uses the pair only the Where rule stops). ## v0.145.0 — the OS update repairs itself after a power cut; a short-session box gets restore-tested; "sent late" (R-876, R-874, R-875) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `894da35c7b9e1ac78885690b78352b634e6831e7d99b321573c75d878db8886e`, config bundle sha256 `78c00adce662d2d966b2ac50ebde46cde1ae225f0107c6a7c02b70ec8ce80c4f`. The wrapper changed: a box needs the signed `agent_update` AND the signed `agent_config_update`. - **R-876.** After a crash during an install, `dpkg --audit` can read clean while dpkg's update journal (`/var/lib/dpkg/updates/`) is not — and apt refuses every install until `dpkg --configure -a` (measured on demo-hp 2026-10-05: every later pass failed until a person typed it). The wrapper now reads `--audit` and the journal in ONE `sh -c` call (`DPKG_STATE_SCRIPT`) — a clean pass still costs one call (R-845's speed, pinned) — and repairs when either shows something; as a belt, when apt itself says "dpkg was interrupted", it repairs and retries the install ONCE. `REPAIR` now logs `journal=N`; a journal still not empty after the repair refuses (R13). Tests `CrashLeftTheJournal` (the measured shape, the speed, the belt); 3 red-proofs. - **R-874.** The restore-test's first due-check runs 30 minutes after the agent starts (`DefaultFirstEval`), then every interval; a box whose power-on sessions are shorter than the 6 h interval never evaluated. A crash-looping agent restarting faster than 30 minutes still never evaluates (the earned restraint, pinned). - **R-875.** A kept report's reason is neutral — "sent late — kept on the box until the hub could take it" — the copy cannot tell a killed agent from an absent hub. - Red-proofs: `felhom.eu/documentation/audits/catchup-2026-10-05/part{C,D}/`. ## v0.144.1 — a killed pass really keeps its report: the wrapper survives a dead reader; the agent looks again every 5 minutes (R-868, measured live) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `6ccd521d47e64999017e8eb5bc613d724543cfdc5ef9b13bcae9e3ea8c53b8f3`, config bundle sha256 `e89a9ddfb767e177f8874d56f3dcd3bfd47409d157ff830d362653333bf815e8`. The wrapper changed again: a box needs the signed `agent_update` AND the signed `agent_config_update`. - **Found live on demo-hp 2026-10-05 05:45 UTC with v0.144.0** (the night's A5 shape: kill -9 of the pass and the daemon while apt-get ran): apt finished all 13 packages, but the wrapper's next log line went to a stderr pipe no process read any more → `BrokenPipeError` → the wrapper died before it saved its report copy (journal: `PLAN upgrade=13`, then nothing; no copy; the hub got nothing). v0.144.0's mechanism was right and never reached. `Runner.log` and the final `OSAPPLY-REPORT` line now survive a dead reader (the journal still gets every line). Test `AgentDiesMidPass` drives the REAL `log()` into a pipe that breaks while apt-get runs. - **Also found live:** the restarted daemon looked for kept copies ~7 s before the orphaned wrapper wrote one. The daemon now looks at start and every 5 minutes (`Leg.SendUnsentLoop`); `TestR868_ACopyWrittenAfterTheStartIsSentByTheLoop`. - Red-proofs: `felhom.eu/documentation/audits/night-fixes-2026-10-05/partD/r868-brokenpipe-red-proof.txt`. - A second agent release in one session, against "one release per repo": recorded as `09` decision 108 (operator may reverse) — the alternative was to ship a fix proven not to work. ## v0.144.0 — R8 measures the real download; an OS pass reports even when its agent was killed; the debug pass runs with the hub away (R-865, R-868, R-866) (2026-10-05) Released by `scripts/release-agent.sh`: binary sha256 `f18093c3466749ec4cd47f83f97a401160a24bad1e704f1183051adad14db928`, config bundle `felhom-config-bundle.json` sha256 `6acf42fe46df5223384d767801cb2bf73238ba4dab82debb7811ca2f790591d8`. The wrapper `felhom-os-apply` changed, so a box needs BOTH the signed `agent_update` and the signed `agent_config_update`. - **R-865.** `download_bytes` runs `apt-get --print-uris` WITHOUT `-s`: with `-s` apt prints the simulation and no URI list, so R8 summed 0 B and only its 500 MB floor ever applied. `--print-uris` alone downloads nothing (measured on 9202: the archive cache and the versions unchanged). The test fake now answers like real apt (with `-s`: no URIs), and `test_R8_counts_the_real_download` / `test_download_bytes_never_simulates` pin it. - **R-868.** The wrapper writes every apply pass's report to `/report---apply.json` before it prints it (root writes into the agent's dir: the dir opened O_NOFOLLOW and checked to be the agent's own, the file created O_EXCL|O_NOFOLLOW, 0600, handed to the agent). The plan now carries `run_id`, `trigger`, `ring`, echoed in the report. The agent deletes the copy once the hub has the report; a copy left on disk (the agent was killed, or the hub was away) is sent at the agent's start and before every pass (`Leg.SendUnsent`), then deleted. A pass lock (flock on `pass.lock`, across the daemon and a selftest) keeps the sender off a pass that is still running. - **R-866.** The daemon saves the hub's newest os_update block (`os-update-block.json`); `--selftest=os-update` uses it when the hub cannot be reached and says so in its header (`block=SAVED(