Day 2 2026-10-19: ep0 keep-weekly 2 (D18), passphrase measured (R-1159 READY, D19), hub 0.160.0 deployed, controller 0.333.0 floor, gofmt gates; register 167 -> 163 (closed 6, opened R-1165, R-1166)
gates / gates (push) Successful in 7m15s

This commit is contained in:
2026-10-19 12:20:37 +02:00
parent 73f3899266
commit 224fe41e06
28 changed files with 652 additions and 22 deletions
+10
View File
@@ -16,6 +16,16 @@
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
> **2026-10-19 (afternoon) — controller 0.333.0 is the floor (MinAgent 0.158.1, 12:16 local); hub 0.160.0 DEPLOYED
> (operator yes); ep0 prune `keep-weekly 2` (operator yes, D18).** ep0 change: `prune-demo-hp`/`prune-demo-felhom` only,
> after PBS's own `--dry-run true` (API `proxmox-backup-debug api create …/prune`; it creates no task) showed nothing
> removed; households with no prune job (tester-1, Tester-2) = R-1165. R-1155: FileBrowser 1.5.6 reads its token from the
> cookie, `?auth=`, Bearer AND the Basic-auth password (`extractToken`) — the forwardAuth refusal must read all four (the
> security review caught Basic). R-1159 measured: passphrase → `/config` key and `/host-enroll` existing host key → every
> off-site key route; D19 open (CC picks A: keys only while no host exists). R-1158: the reinstall's off-site re-issue only
> rotates the hub's own registrar login; kept. gofmt gates in hub and agent runners. demo-hp's .104 is taken by another
> device (R-1166, the router). Register 167 → 163. `REPORT-day2-2026-10-19.md`.
> **2026-10-19 (day) — controller 0.332.0 is the floor (MinAgent 0.158.1, 05:24 local); agent 0.160.0 released and on the
> three test hosts, NOT vouched; hub 0.159.0 DEPLOYED (operator yes in advance).** One sign-in for the file manager (R-1100,
> D16): dashboard pass → host-only `felhom_files` cookie → traefik forwardAuth `/__felhom_gate/files` sets the per-box
+120
View File
@@ -0,0 +1,120 @@
# REPORT — second day brief, 2026-10-19 (ep0 one copy per week; the passphrase measured; hub 0.160.0; controller 0.333.0; gofmt gates)
## In plain words
- **ep0 keeps one whole-box copy per week, for 2 weeks** (your „yes"). Before the change, PBS's own dry run showed it
removes nothing today. The first real run is 2026-10-20 03:30 UTC; CC reads it then (row R-1162 stays open until then).
- **The household passphrase:** alone it gets the box's keys, and with them every off-site key action. Two options are
in STATUS (D19). CC picks A: the passphrase gives keys only while the customer has no box record.
- **Controller 0.333.0 is the floor for every box** (12:16 local): a dashboard sign-out now also ends the file manager's
own 2-hour pass; the recovery-code page says the code becomes live after the two words.
- **Hub 0.160.0 is live** (your „yes"): a reinstall within 45 minutes mails you too; a repeated refused backup is counted
and the alarm repeats at most once a day; the old off-site re-issue on a reinstall is kept, with true words.
- **gofmt:** the hub and the agent are formatted, and both now have the controller's gate.
- **New for you:** another device keeps taking demo-hp's network address (R-1166) — fix it in the router.
## 1. ep0 (R-1162; `07` Plane-2 row and §6.1)
- **Before** (`audits/day-2026-10-19b/P1/ep0-settings-before.txt`): `prune-demo-hp` and `prune-demo-felhom` —
`keep-last 2`, 03:30, ns-scoped, `max-depth 0`; `prune-operator-hubdb` — keep-daily 14 + keep-weekly 8, 03:45.
`tester-1` (3 copies) and `Tester-2` (1) have **no job**. ep0's clock is UTC.
- **DooPlex's copy of ep0:** `prune-ep0-copy`, `keep-weekly 8`, 07:30 (decision 71) — already by time; not changed.
- **Dry run** (PBS's own: `proxmox-backup-debug api create /admin/datastore/felhom-offsite/prune --dry-run true …`, no
task created; cross-checked with an offline model of PBS's selection, `P1/prune_sim.py`; 8 snapshots before and after):
| Box | Snapshots | keep-last 2 removes | keep-weekly 2 keeps | keep-weekly 2 removes |
|---|---|---|---|---|
| demo-hp | 2 | none | 2026-10-08, 2026-10-15 | none |
| demo-felhom | 2 | none | 2026-10-06, 2026-10-13 | none |
| tester-1 (no job) | 3 | (2026-10-04) | 2026-10-11, 2026-10-18 | (2026-10-04) |
| Tester-2 (no job) | 1 | none | 2026-10-04 | none |
- **Answer:** „Yes, the two rules" (not the tester-1 job). **After** (07:49 UTC, `P1/ep0-settings-after.txt`): both jobs
`keep-weekly 2`, schedule 03:30 unchanged, nothing run by hand. Check for 2026-10-20: the 03:30 `prunejob` tasks OK with
`keep-weekly 2` and two snapshots per namespace (the next step of R-1162).
## 2. The household passphrase (R-1159)
Measured with the hub's real router (`P2/passphrase-reach.txt`; a made-up customer, a fake off-site service). A live
measurement on Tester4 was not possible: CC holds no Tester4 passphrase, and reading it from the hub database was
refused by CC's own safety check (correctly).
- Passphrase → `GET /config/{id}` 200 with `hub.api_key` (wrong passphrase 401); `POST /host-enroll` 200 with the
EXISTING host's key.
- Controller key → `register-key`, `confirm-key` (removes every other line, the box's own key included), `move-aside`,
`window-open`, `abandon-request` (the hub deletes a set-aside copy after 7 days unless cancelled), `abandon-cancel`,
`abandon-status`: all past auth (wrong key 401 each).
- Host key → escrow GET/PUT, recovery-credential PUT, wg, desired-state, jobs, host-report: all past auth.
- The passphrase is used only at install time (the installer, the controller's setup wizard).
- **Option A (CC's pick):** keys only while the customer has no host record (`/config` leaves out the key, `/host-enroll`
refuses once a host exists). Closes the chain at its source; a reinstall needs the operator's delete first — already the
rule since R-1154. **Option B:** a second factor on the destructive off-site routes. Every reinstall waits for the
operator, and the host key stays reachable by the passphrase, so B alone does not close it. No change in this task.
## 3. Controller 0.333.0 — R-1155, R-1161, the Install chip
- **R-1155:** first measured on demo-hp (0.332.0): the household's FileBrowser token opened FileBrowser after the
dashboard sign-out (200, cookie and Bearer). Fix: the forwardAuth answer refuses a request with no valid files cookie
that carries a non-admin FileBrowser token, everywhere FileBrowser reads one (cookie, `?auth=`, Bearer, Basic
password). **The commit's security review found Basic missing** in the first version (`c809dd3`); fixed in `a6fa3c0`
before any release. Red-proofs: without the check → 200 after the sign-out; without the Basic branch → 200; with every
token refused → admin's own token 302. Live (rc.2 demo-hp, 0.333.0 Tester 1): files cookie 200; token alone 401 in all
four forms, before and after the sign-out; FileBrowser's sign-in page 200.
- **R-1161:** new key `backups_escrow.ez_a_kod_az_ellenorzes` (hu + en); the old sentence only when the claim answer says
no confirm. Fixtures re-captured: `escrow_first`, `escrow_receremony` (reason: R-1151 made the old sentence untrue);
all others byte-identical. Live on demo-hp (`P6/06-escrow-page-text.txt`): new text shown, old hidden, switch present,
negative control 0.
- **The Chrome „Install" chip: NOT-A-FINDING.** FileBrowser 1.5.6 links `/public/static/site.webmanifest` on every page;
its only PWA setting is `disablePWAInstall` (already set). Not patched (`P3/install-chip.txt`).
## 4. Hub 0.160.0
- **R-1156:** a live re-enroll is a warning mailed to the operator, same instruction; a `--resume` mails too and says it
needs nothing. **R-1157:** further refusals in one episode counted (`refusals_in_episode`), alarm at most once a day,
total on the cleared record. **R-1158:** the reinstall's off-site re-issue changes nothing a box uses (only the hub's
own registrar reads that password; no box has received it since v0.127.0); kept because it repairs a broken stored
login; comments and the `offsite_reissued` text corrected (`05` §8). Each red-proved.
- **Answer:** „Yes, deploy now". Image `0.160.0` digest `sha256:e9a360bc00d00a15014e272c4d023fef118a8b043956f3fc928dd26c00b34230`
(clean clone at `a17db0c3`; the first push hit a DNS timeout, the retry pushed it). Manifest `5d3cb5c1`; only
Deployment/hub was OutOfSync and only it was synced; Synced/Healthy; running image digest = built digest; 0 `[ERROR]`
lines; floor read back from hub.db + WAL (0.332.0 then, 0.333.0 now); host and controller reports from demo-hp, Tester 1
and demo-felhom after the restart.
## 5. gofmt (R-1160)
Hub `d5e3c8b0` (22 files) and agent `1edfd8e` (14 files), formatting only, tests green before each. Gates: `hub-gofmt`
(`scripts/hub_gofmt_gate.py`, `repo_gates.py`) and `gofmt` (felhom-agent `scripts/gofmt_gate.py`, `agent_gates.py`), fast
set, decoys on scratch trees, red-proved. No agent release.
## 6. Controller release (decision 250)
Per-box floors 0.333.0 at 09:59:38 UTC → all three on 0.333.0 within about a minute (demo-hp from rc.2 at 10:13). Box
walk (`P6/`): 0 unhealthy, 0 allow-list refusals, catalog in sync; https-only 28 OK, 0 failures; „Fájlok ↗" with no second
login on demo-hp and Tester 1, sign-out asks again. **Global floor 0.333.0 / MinAgent 0.158.1 at 10:16:22 UTC**
(303 `floor_set`, read back with the WAL); per-box floors cleared.
## 7. Versions, commits, fixtures, rows
- **Controller 0.333.0** (`c809dd3`, `a6fa3c0`, REPORT `e1a2abf`), image digest
`sha256:f150a8d8cd329f5cde9e34249e34edc59b6b5db29db1e3200900f3869a0de11b`; rc.1/rc.2 on demo-hp only. Fixtures:
`escrow_first`, `escrow_receremony`.
- **Hub 0.160.0** (`d5e3c8b0`, `503c93fa`, `a17db0c3`, manifest `5d3cb5c1`).
- **Agent:** no release (`1edfd8e`, `0cbd500`). 0.160.0 stays the vouched one.
- **Rows: 167 before, 163 after; closed 6** (R-1155, R-1156, R-1157, R-1158, R-1160, R-1161), **opened 2** (R-1165,
R-1166); R-1159 → READY (D19), R-1162 → NARROWED (one run to read).
- **System poster:** text only — `felhom-system-poster.facts.md` and the matching text in `felhom-system-poster.html`
(„last 2" → „one per week, 2 weeks").
- **Teardown:** machine — rc images only on demo-hp 9201, now on the release; no test app. Host — none. Hub — no test
customer; the per-box floors cleared. ep0 — the two prune jobs changed on the operator's yes, nothing else.
## Observations
1. Two household namespaces on ep0 have no prune job; nothing creates one for a new household. FILED: R-1165.
2. Another device takes demo-hp's address 192.168.0.104 every few minutes; demo-hp also lost its link to ep0 for 8 minutes. FILED: R-1166.
3. A config download with the passphrase is a log line only, and the host key it yields can rewrite the box's escrow and recovery credential. FILED: R-1159.
4. The first R-1155 version missed FileBrowser's Basic-auth token source (the commit's security review). NOT-A-FINDING: fixed before any release in `a6fa3c0` and red-proved (the size rule).
5. Chrome offers „Install" for the file manager. NOT-A-FINDING: FileBrowser 1.5.6 has no setting that stops its manifest; the brief forbids patching it.
6. One of CC's probe scripts on DooPlex passed the household's FileBrowser token to curl on its command line (visible to local processes for about a second; never printed or saved). NOT-A-FINDING: CC's own tooling on DooPlex, not the product; every probe now passes tokens through 0600 header files, and the token expired two hours later.
7. Two copies of CC's hub-database snapshot ran into one folder at once and one read failed („malformed"). NOT-A-FINDING: CC's helper, a local copy only; the live hub and its database were untouched, and the next read was clean.
8. The first hub image push failed on a DNS timeout at DooPlex. NOT-A-FINDING: a short DNS outage (the website session saw it too); the build's own check found the tag absent and the retry pushed it.
9. FileBrowser's `admin` password login was not walked live. NOT-A-FINDING: CC holds no FileBrowser admin password; admin's token passing is unit-pinned and the sign-in page answers 200 live.
+4 -2
View File
File diff suppressed because one or more lines are too long
@@ -232,7 +232,9 @@ old box and re-issues NOTHING: **gone** (no controller wait connection open now,
inside its hold or BOTH the host report and the controller report older than `alerting.stale_threshold` or absent —
the `hostStatus` threshold and comparison) → the operator-only WARNING `host_reinstall_over_old_host`, mailed to the
operator: delete the old host (Hosts page) and run the installer again, or re-issue the claim code and the off-site
login from the customer page; **live** → the operator-only INFO `host_reenroll_while_live`, recorded, never mailed.
login from the customer page; **live** → the operator-only `host_reenroll_while_live` — since hub v0.160.0 (R-1156) a
WARNING mailed with the same instruction (a reinstall within 45 minutes of the old box's last report reads as live; an
installer `--resume` or a repair re-run mails too, and the text says those need nothing).
**Why not re-issue automatically** (a security review of the first draft, the same day): the passphrase is a
household-held secret typed into the installer, and both re-issues are operator SESSION-ONLY actions — not even an
operator token may run them (§17.5; `09` §3 decision 252, R-1055/R-1059: an off-site re-issue „stops its backup for a
@@ -240,6 +242,13 @@ while"). Re-issuing on the reuse path would give those powers to anyone holding
switched off or silent. The cost of the loud shape is two operator acts per reinstall that kept its old record. Both
event types are hub-origin only. `hub/internal/api/reenroll.go`, pinned by `hub/internal/api/r1154_reenroll_test.go`.
**[FACT, 2026-10-19 — R-1158, hub v0.160.0] What the MINT path's off-site re-issue still does.** It resets the Storage
Box sub-account password and stores the new one — a password only the hub itself uses (`offsitekeys.TargetFor`, the key
registrar's login). No box receives it since v0.127.0 (`consume-password` answers 410; `ConsumeOneTimeSecret` has no
production caller); the fresh box registers its own key with the controller API key. So it changes nothing a box uses.
It is kept because it hands the hub a fresh, working login at each clean reinstall, which repairs a broken stored one;
the `offsite_reissued` record now says the password is the hub's and the backups do not change.
## 9. DR model
The headline: the **old heavy infra-backup push retires** — not because the hub authors everything
@@ -384,7 +384,7 @@ never moved by an update or an undo, and the unit restore accepts it.
| **Tier-2** cross-drive | `<target nsRoot>/backups/secondary/<app>/` | **always** a full mirror of the unit (`tier2.go:368-369`) **plus** `mandatory + optional` file legs, v2 layout `hdd/<rel>` + `userdata/<rel>` | nightly at W+60m | mirror (rsync) | no |
| **Tier-3** restic offsite | Hetzner Storage Box over SFTP, one multi-path snapshot per app per run | the unit **plus** `mandatory` legs only; a separate `_shares` snapshot | nightly at W+105m | `--keep-daily 7 --keep-weekly 4 --keep-monthly 6` | **yes** (restic) |
| **Plane-2** whole-guest, local | `local:` → `/var/lib/vz/dump` on the host | rootfs + `mp0 /var/lib/felhom` (its `docker/` and `sys_drive/` folders, bound in the guest to `/var/lib/docker` and `/mnt/sys_drive` — §7.5.1; `mp1` no longer exists, corrected 2026-10-15) | **24 h** (`backup_cadence_seconds: 0`) | `local_backup_retention: 3` → `--prune-backups keep-daily=3`: one copy per day, 3 days (keep-last until agent 0.160.0, R-1149) | **no** |
| **Plane-2** whole-guest, offsite | `felhom-pbs:` → ep0 datastore `felhom-offsite`, per-customer namespace, over WireGuard | same contents | **7 days** (`604800`) | server-side prune on ep0, `keep-last 2` at `03:30` — **and the box asks for none** (R-191) | **yes** (per-customer key) |
| **Plane-2** whole-guest, offsite | `felhom-pbs:` → ep0 datastore `felhom-offsite`, per-customer namespace, over WireGuard | same contents | **7 days** (`604800`) | server-side prune on ep0, **`keep-weekly 2`** at `03:30` (one copy per ISO week, 2 weeks — R-1162, operator decision STATUS D18, 2026-10-19; `keep-last 2` before) — **and the box asks for none** (R-191) | **yes** (per-customer key) |
> **[FACT] Tier-3 has a fifth state the table above does not show: PAUSED (R-543, controller
> v0.245.0).** Off-site is ON by default from hub v0.116.0, and a run does not start until the
@@ -484,10 +484,16 @@ data drives** — 916 GB on demo-felhom, 938 GB on demo-hp.
> again pushes out older restore points. The agent now refuses (429, logged, kept across a restart, a CRITICAL degraded
> status for 24 h) a run beyond: the daily tier — **3 successful runs per rolling 24 h** (the night run, one „Mentés
> most", the next night: a press never costs the night, R-899); the weekly off-site tier — **one per 6 days**
> (max(cadence − 24 h, cadence / 2)). The question: how many runs may a guest make before the cap refuses? Options: no cap
> (max(cadence − 24 h, cadence / 2)). The hub turns that status into the operator-only `whole_guest_backup_refused` (hub v0.159.0, R-1150); since hub v0.160.0 a further refusal inside one degraded stretch (the agent's reason names a new time) is counted (`refusals_in_episode`) and the alarm repeats at most once a day (R-1157). The question: how many runs may a guest make before the cap refuses? Options: no cap
> (today, floodable); a per-tier count per rolling window (chosen: no ordinary day is refused); a time-bucketed prune
> instead (the durable shape, but a retention change — R-1149 for the local tier, R-973's design line for ep0). Limits:
> ep0's keep-last 2 is a count. **[DESIGN] The local tier keeps one copy per day (R-1149, operator decision D17,
> ep0's prune is time-bucketed since 2026-10-19: **[FACT] `prune-demo-hp` and `prune-demo-felhom` keep `keep-weekly 2`**
> (R-1162, operator decision STATUS D18, 2026-10-19 08:24; read back 07:49 UTC) — the newest copy of each of the two newest
> ISO weeks, so a flood inside a week replaces only that week's copy and the previous week's survives, however many runs
> get through. Before the change, PBS's own dry run and an offline model of its selection both showed the new rule
> removing nothing on either box (`audits/day-2026-10-19b/P1/`). DooPlex's copy of ep0 already prunes by time
> (`prune-ep0-copy`, `keep-weekly 8`, decision 71). **A household namespace gets no prune job by itself:** `tester-1` and
> `Tester-2` have none and keep every weekly copy (R-1165). **[DESIGN] The local tier keeps one copy per day (R-1149, operator decision D17,
> 2026-10-19; agent 0.160.0):** `keep-daily=3` instead of keep-last 3 — the same at most three archives, but extra runs on
> one day replace only that day's copy, so with the cap a day of flooding leaves the two earlier days' copies
> (`TestBackupCap_R1149_LocalFloodKeepsOlderDays`). Measured at the first new-rule prune on demo-hp (retention 1: the
@@ -88,7 +88,7 @@ Title: **"Where a household's data is copied"**
| **Tier 2 — cross-drive** | a mirror of Tier 1, plus the app's files | data drive 2, on the box (counted only on a different physical disk; in a VM made, not counted) | nightly **03:30** | mirror | not encrypted |
| **Tier 3 — off-site, per app** | Tier 1 + the app's must-keep files | **Hetzner Storage Box** (Germany), one folder per household | nightly **04:15** | 7 daily, 4 weekly, 6 monthly | the household's own restic password |
| **Whole guest — local** | the guest's system (not the data drives) | the box's own system disk | daily, in the window **04:30–08:30** | one copy per day, up to 3 days (fewer on a small disk) | not encrypted |
| **Whole guest — off-site** | the same | **ep0** (Hetzner, Germany), one namespace per household | weekly, same window | last 2 | the household's own key, escrowed with a recovery code |
| **Whole guest — off-site** | the same | **ep0** (Hetzner, Germany), one namespace per household | weekly, same window | one per week, 2 weeks | the household's own key, escrowed with a recovery code |
Footnote: "W = the household's backup window start, 02:30 by default. Tiers 1–3 follow at fixed offsets so they cannot run out of order. A missed night is caught up 15 minutes after the box starts."
@@ -99,7 +99,7 @@ Footnote: "W = the household's backup window start, 02:30 by default. Tiers 1–
- one namespace **per household** (whole-guest copies, each household's own key),
with a NEUTRAL note under it (not a red gap box): "By design: each household holds the key to its own copies; the operator cannot open them without their recovery code." — *fix (c), 2026-10-09: this is a property of the design, not a gap, and a red box said the opposite of what it means.*
- the **`operator`** namespace: two groups — **"hub database"** and **"DooPlex copy"** (code, password manager, secrets, signing keys).
Prune jobs on ep0: households keep the last 2; `operator` keeps 14 daily + 8 weekly.
Prune jobs on ep0: households keep one copy per week for 2 weeks (`keep-weekly 2`, R-1162, 2026-10-19 — a household without a job keeps all, R-1165); `operator` keeps 14 daily + 8 weekly.
Mark ep0 with a small shield: "**Protected** — holds the only off-premises copy of real household data."
- **Hetzner Storage Box** (grey): the shared box for the households' per-app restic copies (Tier 3), one sub-account per household, add-only keys.
- **Mail service** (grey, "Resend"): every mail the hub sends — to the operator and to households.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
== DooPlex PBS (read 2026-10-19)
[
{
"comment": "decision 71: keep the last 8 weekly copies per group (all namespaces); runs after the 05:00 pull",
"id": "prune-ep0-copy",
"keep-weekly": 8,
"schedule": "07:30",
"store": "ep0-copy"
}
]
[
{
"comment": "decision 70: nightly copy of ep0 felhom-offsite; never removes what ep0 removed",
"id": "ep0-felhom-offsite",
"remote": "ep0",
"remote-store": "felhom-offsite",
"remove-vanished": false,
"schedule": "05:00",
"store": "ep0-copy"
}
]
/mnt/5_hdd/backup/ep0-copy/ns/Tester-2/ct/9201/2026-10-04T16:31:13Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-felhom/ct/9201/2026-09-22T04:12:20Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-felhom/ct/9201/2026-09-29T04:16:43Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-felhom/ct/9201/2026-10-06T04:21:09Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-felhom/ct/9201/2026-10-13T04:22:51Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-hp/ct/9201/2026-09-24T20:06:25Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-hp/ct/9201/2026-10-01T20:15:29Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-hp/ct/9201/2026-10-08T20:34:27Z
/mnt/5_hdd/backup/ep0-copy/ns/demo-hp/ct/9201/2026-10-15T20:38:47Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-gitea/2026-10-10T22:22:52Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-gitea/2026-10-17T22:22:32Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-gitea/2026-10-18T22:22:01Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-hub/2026-10-11T00:30:55Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-hub/2026-10-18T00:31:48Z
/mnt/5_hdd/backup/ep0-copy/ns/operator/host/dooplex-hub/2026-10-19T00:32:07Z
/mnt/5_hdd/backup/ep0-copy/ns/tester-1/ct/9201/2026-10-04T19:57:18Z
/mnt/5_hdd/backup/ep0-copy/ns/tester-1/ct/9201/2026-10-11T20:02:13Z
/mnt/5_hdd/backup/ep0-copy/ns/tester-1/ct/9201/2026-10-18T20:04:51Z
@@ -0,0 +1,5 @@
box | job | snapshots today | keep-last 2 keeps / removes | keep-weekly 2 keeps / removes
Tester-2 | NO JOB | 1 | 2026-10-04 (w40) / none | 2026-10-04 (w40) / none
demo-felhom | prune-demo-felhom | 2 | 2026-10-13 (w42), 2026-10-06 (w41) / none | 2026-10-13 (w42), 2026-10-06 (w41) / none
demo-hp | prune-demo-hp | 2 | 2026-10-15 (w42), 2026-10-08 (w41) / none | 2026-10-15 (w42), 2026-10-08 (w41) / none
tester-1 | NO JOB | 3 | 2026-10-18 (w42), 2026-10-11 (w41) / 2026-10-04 (w40) | 2026-10-18 (w42), 2026-10-11 (w41) / 2026-10-04 (w40)
@@ -0,0 +1,31 @@
Mon Oct 19 07:49:11 UTC 2026
[
{
"comment": "R-1162 operator D1=A 2026-10-19: one copy per ISO week, 2 weeks, server-side (box tokens are write-only)",
"id": "prune-demo-hp",
"keep-weekly": 2,
"max-depth": 0,
"ns": "demo-hp",
"schedule": "03:30",
"store": "felhom-offsite"
},
{
"comment": "R-1162 operator D1=A 2026-10-19: one copy per ISO week, 2 weeks, server-side (box tokens are write-only)",
"id": "prune-demo-felhom",
"keep-weekly": 2,
"max-depth": 0,
"ns": "demo-felhom",
"schedule": "03:30",
"store": "felhom-offsite"
},
{
"comment": "R-173 hub DB copies; operator ns only",
"id": "prune-operator-hubdb",
"keep-daily": 14,
"keep-weekly": 8,
"max-depth": 0,
"ns": "operator",
"schedule": "03:45",
"store": "felhom-offsite"
}
]
@@ -0,0 +1,40 @@
felhom-hetzner
proxmox-backup-server 4.2.8-1 running version: 4.2.5
==prune-jobs
[
{
"comment": "R-173 hub DB copies; operator ns only",
"id": "prune-operator-hubdb",
"keep-daily": 14,
"keep-weekly": 8,
"max-depth": 0,
"ns": "operator",
"schedule": "03:45",
"store": "felhom-offsite"
},
{
"comment": "R-82 retention keep-last=2, server-side (box tokens are write-only)",
"id": "prune-demo-hp",
"keep-last": 2,
"max-depth": 0,
"ns": "demo-hp",
"schedule": "03:30",
"store": "felhom-offsite"
},
{
"comment": "R-82 retention keep-last=2, server-side (box tokens are write-only)",
"id": "prune-demo-felhom",
"keep-last": 2,
"max-depth": 0,
"ns": "demo-felhom",
"schedule": "03:30",
"store": "felhom-offsite"
}
]
==datastore
{
"gc-schedule": "sun 04:30",
"name": "felhom-offsite",
"path": "/mnt/pbs-datastore",
"verify-new": true
}
@@ -0,0 +1,8 @@
ns/Tester-2/ct/9201/2026-10-04T16:31:13Z
ns/demo-felhom/ct/9201/2026-10-06T04:21:09Z
ns/demo-felhom/ct/9201/2026-10-13T04:22:51Z
ns/demo-hp/ct/9201/2026-10-08T20:34:27Z
ns/demo-hp/ct/9201/2026-10-15T20:38:47Z
ns/tester-1/ct/9201/2026-10-04T19:57:18Z
ns/tester-1/ct/9201/2026-10-11T20:02:13Z
ns/tester-1/ct/9201/2026-10-18T20:04:51Z
@@ -0,0 +1,14 @@
== ns/demo-felhom
ns/demo-felhom/ct/9201/2026-10-06T04:21:09Z
ns/demo-felhom/ct/9201/2026-10-13T04:22:51Z
== ns/demo-hp
ns/demo-hp/ct/9201/2026-10-08T20:34:27Z
ns/demo-hp/ct/9201/2026-10-15T20:38:47Z
== ns/operator
== ns/tester-1
ns/tester-1/ct/9201/2026-10-04T19:57:18Z
ns/tester-1/ct/9201/2026-10-11T20:02:13Z
ns/tester-1/ct/9201/2026-10-18T20:04:51Z
== ns/Tester-2
ns/Tester-2/ct/9201/2026-10-04T16:31:13Z
== root
@@ -0,0 +1,24 @@
== demo-hp --keep-last 2 (PBS dry-run)
2026-10-08T20:34:27Z keep
2026-10-15T20:38:47Z keep
== demo-hp --keep-weekly 2 (PBS dry-run)
2026-10-08T20:34:27Z keep
2026-10-15T20:38:47Z keep
== demo-felhom --keep-last 2 (PBS dry-run)
2026-10-06T04:21:09Z keep
2026-10-13T04:22:51Z keep
== demo-felhom --keep-weekly 2 (PBS dry-run)
2026-10-06T04:21:09Z keep
2026-10-13T04:22:51Z keep
== tester-1 --keep-last 2 (PBS dry-run)
2026-10-04T19:57:18Z REMOVE
2026-10-11T20:02:13Z keep
2026-10-18T20:04:51Z keep
== tester-1 --keep-weekly 2 (PBS dry-run)
2026-10-04T19:57:18Z REMOVE
2026-10-11T20:02:13Z keep
2026-10-18T20:04:51Z keep
== Tester-2 --keep-last 2 (PBS dry-run)
2026-10-04T16:31:13Z keep
== Tester-2 --keep-weekly 2 (PBS dry-run)
2026-10-04T16:31:13Z keep
@@ -0,0 +1,32 @@
# Offline model of PBS's prune selection (pbs-datastore prune.rs compute_prune_info / mark_selections),
# run over ep0's snapshot list. Never touches ep0. ep0's clock is UTC (timedatectl), and PBS keys
# weekly buckets by local time "%G/%V" (ISO year/week).
import sys, datetime
snaps = {}
for line in open(sys.argv[1]):
line=line.strip()
if line.startswith('ns/') and '/ct/' in line:
ns = line.split('/')[1]; t = line.rsplit('/',1)[1]
snaps.setdefault(ns, []).append(t)
def sel_last(t): return t
def sel_week(t):
d = datetime.datetime.strptime(t, '%Y-%m-%dT%H:%M:%SZ'); y,w,_ = d.isocalendar(); return f"{y}/{w:02d}"
def compute(lst, opts):
lst = sorted(lst, reverse=True); mark = {}
for keep, sel in opts:
already = {sel(t) for t in lst if mark.get(t)=='keep'}; inc=set()
for t in lst:
if t in mark: continue
s = sel(t)
if s in already: continue
if s not in inc:
if len(inc) >= keep: break
inc.add(s); mark[t]='keep'
else: mark[t]='remove'
return {t: mark.get(t,'remove') for t in lst}
jobs = {'demo-hp':'prune-demo-hp','demo-felhom':'prune-demo-felhom'}
print("box | job | snapshots today | keep-last 2 keeps / removes | keep-weekly 2 keeps / removes")
for ns in sorted(snaps):
a = compute(snaps[ns], [(2, sel_last)]); b = compute(snaps[ns], [(2, sel_week)])
f = lambda m,k: ', '.join(f"{t[:10]} (w{sel_week(t)[5:]})" for t,v in m.items() if v==k) or 'none'
print(f"{ns} | {jobs.get(ns,'NO JOB')} | {len(snaps[ns])} | {f(a,'keep')} / {f(a,'remove')} | {f(b,'keep')} / {f(b,'remove')}")
@@ -0,0 +1,24 @@
=== RUN TestR1159_Measure
r1159_measure_test.go:85: GET /config/c1 (passphrase) -> 200; hub fields: [api_key enabled url]; carries the controller key: true
r1159_measure_test.go:86: GET /config/c1 (wrong passphrase) -> 401
r1159_measure_test.go:90: POST /host-enroll (passphrase) -> 200; returns the EXISTING host's key: true
r1159_measure_test.go:105: POST /offsite/register-key/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/confirm-key/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/move-aside/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/window-open/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/abandon-request/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/abandon-cancel/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:105: POST /offsite/abandon-status/c1 controller key -> 200, wrong key -> 401
r1159_measure_test.go:121: GET /hosts/h1/escrow host key -> 200, wrong key -> 401
r1159_measure_test.go:121: GET /hosts/h1/escrow/retained host key -> 200, wrong key -> 401
r1159_measure_test.go:121: PUT /hosts/h1/escrow host key -> 400, wrong key -> 401
r1159_measure_test.go:121: POST /hosts/h1/wg host key -> 400, wrong key -> 401
r1159_measure_test.go:121: POST /hosts/h1/pbs/consume-token host key -> 404, wrong key -> 401
r1159_measure_test.go:121: GET /hosts/h1/desired-state host key -> 200, wrong key -> 401
r1159_measure_test.go:121: GET /hosts/h1/jobs host key -> 200, wrong key -> 401
r1159_measure_test.go:121: PUT /hosts/h1/recovery-credential host key -> 400, wrong key -> 401
r1159_measure_test.go:121: POST /host-report host key -> 200, wrong key -> 401
r1159_measure_test.go:123: off-site service calls reached with the passphrase-derived key: RegisterKey, ConfirmKey, MoveAside, OpenWindowFor, RequestAbandon, CancelAbandon, AbandonStatusFor
--- PASS: TestR1159_Measure (0.04s)
PASS
ok gitea.dooplex.hu/admin/felhom-hub/internal/api 0.065s
@@ -0,0 +1,124 @@
package api
// R-1159 MEASUREMENT HARNESS (temporary, not committed): what a holder of the household's retrieval passphrase ALONE
// can reach through the real hub router. Field names and status codes only.
import (
"context"
"encoding/json"
"net/http"
"sort"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/offsitekeys"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
"gopkg.in/yaml.v3"
)
type r1159Tpl struct{}
func (r1159Tpl) Template() string { return "hub: {}\n" }
type r1159Keys struct{ calls []string }
func (f *r1159Keys) RegisterKey(_ context.Context, c, _ string) (offsitekeys.InstallResult, error) {
f.calls = append(f.calls, "RegisterKey")
return offsitekeys.InstallResult{Fingerprint: "SHA256:x"}, nil
}
func (f *r1159Keys) ConfirmKey(_ context.Context, c, _ string) (int, error) {
f.calls = append(f.calls, "ConfirmKey")
return 1, nil
}
func (f *r1159Keys) MoveAside(_ context.Context, c string) (string, error) {
f.calls = append(f.calls, "MoveAside")
return "repo.orphaned-x", nil
}
func (f *r1159Keys) OpenWindowFor(_ context.Context, c string, _ int) (offsitekeys.WindowGrant, error) {
f.calls = append(f.calls, "OpenWindowFor")
return offsitekeys.WindowGrant{}, nil
}
func (f *r1159Keys) CloseWindowFor(_ context.Context, c string, _ offsitekeys.WindowResult) error {
f.calls = append(f.calls, "CloseWindowFor")
return nil
}
func (f *r1159Keys) RequestAbandon(_ context.Context, c, _ string) (offsitekeys.AbandonStatus, error) {
f.calls = append(f.calls, "RequestAbandon")
return offsitekeys.AbandonStatus{State: "pending"}, nil
}
func (f *r1159Keys) CancelAbandon(c, _ string) (int, error) {
f.calls = append(f.calls, "CancelAbandon")
return 1, nil
}
func (f *r1159Keys) AbandonStatusFor(c string) (offsitekeys.AbandonStatus, error) {
f.calls = append(f.calls, "AbandonStatusFor")
return offsitekeys.AbandonStatus{State: "none"}, nil
}
func TestR1159_Measure(t *testing.T) {
h, st, _ := newTestHandler(t)
h.templateProvider = r1159Tpl{}
fk := &r1159Keys{}
h.SetOffsiteKeyService(fk)
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey-1159", RetrievalPassword: "owner-pass"}); err != nil {
t.Fatal(err)
}
if err := st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY-1159"}); err != nil {
t.Fatal(err)
}
// Step 1: the passphrase alone.
rr := r879Get(h, "/config/c1", "owner-pass")
var y map[string]any
_ = yaml.Unmarshal(rr.Body.Bytes(), &y)
var hubFields []string
if hm, ok := y["hub"].(map[string]any); ok {
for k := range hm {
hubFields = append(hubFields, k)
}
}
sort.Strings(hubFields)
key := ""
if hm, ok := y["hub"].(map[string]any); ok {
key, _ = hm["api_key"].(string)
}
t.Logf("GET /config/c1 (passphrase) -> %d; hub fields: %v; carries the controller key: %v", rr.Code, hubFields, key == "ckey-1159")
t.Logf("GET /config/c1 (wrong passphrase) -> %d", r879Get(h, "/config/c1", "nope").Code)
er := doEnroll(h, "c1", "owner-pass")
var e enrollResp
_ = json.Unmarshal(er.Body.Bytes(), &e)
t.Logf("POST /host-enroll (passphrase) -> %d; returns the EXISTING host's key: %v", er.Code, e.APIKey == "HKEY-1159")
// Step 2: the controller key taken from the config.
routes := []struct{ path, body string }{
{"/offsite/register-key/c1", `{"public_key":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl x"}`},
{"/offsite/confirm-key/c1", `{"fingerprint":"SHA256:x"}`},
{"/offsite/move-aside/c1", `{}`},
{"/offsite/window-open/c1", `{"count_before":10}`},
{"/offsite/abandon-request/c1", `{"path":"repo.orphaned-x"}`},
{"/offsite/abandon-cancel/c1", `{}`},
{"/offsite/abandon-status/c1", `{}`},
}
for _, r := range routes {
ok := do(h, http.MethodPost, r.path, key, r.body)
bad := do(h, http.MethodPost, r.path, "ckey-wrong", r.body)
t.Logf("POST %-30s controller key -> %d, wrong key -> %d", r.path, ok.Code, bad.Code)
}
hk := e.APIKey
for _, r := range []struct{ m, path, body string }{
{"GET", "/hosts/h1/escrow", ""},
{"GET", "/hosts/h1/escrow/retained", ""},
{"PUT", "/hosts/h1/escrow", `{}`},
{"POST", "/hosts/h1/wg", `{}`},
{"POST", "/hosts/h1/pbs/consume-token", `{}`},
{"GET", "/hosts/h1/desired-state", ""},
{"GET", "/hosts/h1/jobs", ""},
{"PUT", "/hosts/h1/recovery-credential", `{}`},
{"POST", "/host-report", validReportBody("h1")},
} {
ok := do(h, r.m, r.path, hk, r.body)
bad := do(h, r.m, r.path, "HKEY-wrong", r.body)
t.Logf("%-4s %-32s host key -> %d, wrong key -> %d", r.m, r.path, ok.Code, bad.Code)
}
t.Logf("off-site service calls reached with the passphrase-derived key: %s", strings.Join(fk.calls, ", "))
}
@@ -0,0 +1,12 @@
ARP watch for 192.168.0.104 (demo-hp vmbr0 7c:d3:0a:77:d9:76), from 09:58:07 UTC
09:58:07 mac=7c:d3:0a:77:d9:76 ssh=open
10:02:18 mac=b8:06:0d:60:45:58 ssh=shut
10:02:58 mac=7c:d3:0a:77:d9:76 ssh=open
10:03:38 mac=b8:06:0d:60:45:58 ssh=shut
10:08:59 mac=7c:d3:0a:77:d9:76 ssh=open
10:10:19 mac=b8:06:0d:60:45:58 ssh=shut
10:12:50 mac=7c:d3:0a:77:d9:76 ssh=open
10:17:10 mac=b8:06:0d:60:45:58 ssh=shut
10:17:50 mac=7c:d3:0a:77:d9:76 ssh=open
10:19:21 mac=b8:06:0d:60:45:58 ssh=shut
stopped by CC at 10:20:37 UTC (enough to show the flips)
@@ -0,0 +1,4 @@
FileBrowser 1.5.6-stable on demo-hp 9201, 2026-10-19
index.html links: <link rel="manifest" href="/public/static/site.webmanifest" crossorigin="use-credentials">
manifest: 200 application/manifest+json, display=standalone
PWA config keys in the binary: disablePWAInstall only (strings | grep -i pwa/manifest)
@@ -0,0 +1,10 @@
jwt alg: HS256 | claim names: ['Permissions', 'belongsTo', 'exp', 'iat', 'iss']
nested Permissions -> ['admin', 'api', 'create', 'delete', 'download', 'modify', 'realtime', 'share']
exp-iat/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/649cb06d-b4ca-49f2-9e2c-16c8bc36531f/scrat seconds: 7200
identity claims: {'belongsTo': 3}
signed in: cookie-token alone -> HTTP chpad/jwt_probe3.sh: line 23: T3: unbound variable
/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/649cb06d-b4ca-49f2-9e2c-16c8bc36531f/scrat401; bearer alone -> HTTP
dashboard sign-out: 302
after sign-out: coochpad/jwt_probe3.sh: line 23: T3: unbound variable
/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/649cb06d-b4ca-49f2-9e2c-16c8bc36531f/scratkie-token alone -> HTTP 401; bearer alone -> HTTP
chpad/jwt_probe3.sh: line 28: T3: unbound variable
@@ -0,0 +1,5 @@
with the files cookie (the household's normal path): HTTP 200
token alone, still signed in: cookie 401 · bearer 401 · basic 401 · ?auth= 401
dashboard sign-out: 302
token alone, after sign-out: cookie 401 · bearer 401 · basic 401 · ?auth= 401
FileBrowser's own sign-in page, no token: HTTP 200
@@ -0,0 +1,27 @@
=== demo-hp (enkisfelhom.hu) 10:15:18
containers: 24 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 10:13:21 updater.go:855: [INFO] [selfupdate] Post-update startup: update successful (0.333.0-rc.2 → 0.333.0)
allow-list refusals since start: 0
2026/10/19 10:13:21 sync.go:142: [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 12
public https://felhom.enkisfelhom.hu/: 302
public https://files.enkisfelhom.hu/: 200
=== demo-felhom (demo-felhom.eu) 10:15:20
containers: 9 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 09:59:39 [INFO] [selfupdate] Post-update startup: update successful (0.332.0 → 0.333.0)
allow-list refusals since start: 0
2026/10/19 09:59:40 [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 5
public https://felhom.demo-felhom.eu/: 302
public https://files.demo-felhom.eu/: 200
=== tester-1 (enkicsifelhom.hu) 10:15:22
containers: 10 running; not healthy/running: traefik Up 3 days;
unhealthy: 0
2026/10/19 09:59:48 [INFO] [selfupdate] Post-update startup: update successful (0.332.0 → 0.333.0)
allow-list refusals since start: 0
2026/10/19 09:59:49 [INFO] [sync] Initial sync: Sablonok naprakészek — nincs változás
installed apps (stacks with app.yaml): 3
public https://felhom.enkicsifelhom.hu/: 302
public https://files.enkicsifelhom.hu/: 200
@@ -0,0 +1,33 @@
control NEGATIVE (a stub answering 200 over plain http): convicted, as it must (200 -)
== tester-1 (enkicsifelhom.hu): 6 public name(s)
OK control POSITIVE http://felhom.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkicsifelhom.hu/r1033-check?q=1
OK http://files.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://files.enkicsifelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkicsifelhom.hu/r1033-check?q=1
OK http://paperless.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paperless.enkicsifelhom.hu/r1033-check?q=1
OK http://paste.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://paste.enkicsifelhom.hu/r1033-check?q=1
OK http://wiki.enkicsifelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkicsifelhom.hu/r1033-check?q=1
== demo-hp (enkisfelhom.hu): 14 public name(s)
OK http://arcade.enkisfelhom.hu/r1033-check?q=1 -> 301 https://arcade.enkisfelhom.hu/r1033-check?q=1
OK http://books.enkisfelhom.hu/r1033-check?q=1 -> 301 https://books.enkisfelhom.hu/r1033-check?q=1
OK http://docs.enkisfelhom.hu/r1033-check?q=1 -> 301 https://docs.enkisfelhom.hu/r1033-check?q=1
OK http://felhom.enkisfelhom.hu/r1033-check?q=1 -> 301 https://felhom.enkisfelhom.hu/r1033-check?q=1
OK http://files.enkisfelhom.hu/r1033-check?q=1 -> 301 https://files.enkisfelhom.hu/r1033-check?q=1
OK http://gist.enkisfelhom.hu/r1033-check?q=1 -> 301 https://gist.enkisfelhom.hu/r1033-check?q=1
OK http://ha.enkisfelhom.hu/r1033-check?q=1 -> 301 https://ha.enkisfelhom.hu/r1033-check?q=1
OK http://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1 -> 301 https://no-such-name-r1033.enkisfelhom.hu/r1033-check?q=1
OK http://papir.enkisfelhom.hu/r1033-check?q=1 -> 301 https://papir.enkisfelhom.hu/r1033-check?q=1
OK http://paste.enkisfelhom.hu/r1033-check?q=1 -> 301 https://paste.enkisfelhom.hu/r1033-check?q=1
OK http://pdf.enkisfelhom.hu/r1033-check?q=1 -> 301 https://pdf.enkisfelhom.hu/r1033-check?q=1
OK http://plex.enkisfelhom.hu/r1033-check?q=1 -> 301 https://plex.enkisfelhom.hu/r1033-check?q=1
OK http://travel.enkisfelhom.hu/r1033-check?q=1 -> 301 https://travel.enkisfelhom.hu/r1033-check?q=1
OK http://wiki.enkisfelhom.hu/r1033-check?q=1 -> 301 https://wiki.enkisfelhom.hu/r1033-check?q=1
== demo-felhom (demo-felhom.eu): 8 public name(s)
OK http://felhom.demo-felhom.eu/r1033-check?q=1 -> 301 https://felhom.demo-felhom.eu/r1033-check?q=1
OK http://files.demo-felhom.eu/r1033-check?q=1 -> 301 https://files.demo-felhom.eu/r1033-check?q=1
OK http://gist.demo-felhom.eu/r1033-check?q=1 -> 301 https://gist.demo-felhom.eu/r1033-check?q=1
OK http://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1 -> 301 https://no-such-name-r1033.demo-felhom.eu/r1033-check?q=1
OK http://prowlarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://prowlarr.demo-felhom.eu/r1033-check?q=1
OK http://radarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://radarr.demo-felhom.eu/r1033-check?q=1
OK http://sonarr.demo-felhom.eu/r1033-check?q=1 -> 301 https://sonarr.demo-felhom.eu/r1033-check?q=1
OK http://torrent.demo-felhom.eu/r1033-check?q=1 -> 301 https://torrent.demo-felhom.eu/r1033-check?q=1
== every public name redirects http -> https
@@ -0,0 +1,26 @@
== demo-hp
sec: cannot run /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/649cb06d-b4ca-49f2-9e2c-16c8bc36531f/scratchpad/sso_check.sh: Permission denied
== tester-1
sec: cannot run /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/649cb06d-b4ca-49f2-9e2c-16c8bc36531f/scratchpad/sso_check.sh: Permission denied
== tester-1 R-1155
with the files cookie (the household's normal path): HTTP 200
token alone, still signed in: cookie 401 · bearer 401 · basic 401 · ?auth= 401
dashboard sign-out: 302
token alone, after sign-out: cookie 401 · bearer 401 · basic 401 · ?auth= 401
FileBrowser's own sign-in page, no token: HTTP 200
== demo-hp
dashboard sign-in: 302
the Fajlok button goes to: https://files.enkisfelhom.hu/_felhom/sso
the pass answer goes to: https://files.enkisfelhom.hu/files/; files cookie set: 1
FileBrowser with the files cookie: felhom-household admin: False modify: True
FileBrowser without it, a fresh browser: HTTP 401
dashboard sign-out: 302
after the sign-out, the files cookie sends the browser to: https://felhom.enkisfelhom.hu/__files/open?to=%2Ffiles%2F
== tester-1
dashboard sign-in: 302
the Fajlok button goes to: https://files.enkicsifelhom.hu/_felhom/sso
the pass answer goes to: https://files.enkicsifelhom.hu/files/; files cookie set: 1
FileBrowser with the files cookie: felhom-household admin: False modify: True
FileBrowser without it, a fresh browser: HTTP 401
dashboard sign-out: 302
after the sign-out, the files cookie sends the browser to: https://felhom.enkicsifelhom.hu/__files/open?to=%2Ffiles%2F
@@ -0,0 +1,5 @@
page bytes: 53890
new text, shown by default (id=live-after-check, ASCII fragment 'lent a k'): 1
old text, hidden until the claim says no confirm (id=live-now display:none): 1
the switch in the page script: 1
negative control (a sentence that is not on the page): 0
@@ -0,0 +1,13 @@
## global floor 0.333.0 / MinAgent 0.158.1 — 10:16:22 UTC
HTTP/1.1 303 See Other
Location: /configuration?flash=floor_set
## clear per-box floors
tester-1: HTTP/1.1 303 See Other Location: /customers/tester-1?flash=floor_set
demo-hp: HTTP/1.1 303 See Other Location: /customers/demo-hp?flash=floor_set
demo-felhom: HTTP/1.1 303 See Other Location: /customers/demo-felhom?flash=floor_set
## read back (hub.db + WAL)
min_controller_version|0.333.0
min_controller_version_declared_min_agent|0.333.0=0.158.1
demo-felhom|
demo-hp|
tester-1|
+11
View File
@@ -26,6 +26,17 @@
---
## 2026-10-19 (second day brief) — ep0 one copy per week, the passphrase measured, hub 0.160.0, controller 0.333.0, gofmt gates (full text of each row: `git show 73f38992:documentation/backlog/OPEN-ITEMS.md`)
| Row | What | Closed | Evidence |
|---|---|---|---|
| **R-1155** | **FileBrowser's own 2-hour token for the one-sign-in user can outlive the dashboard session.** (P4) | CLOSED 2026-10-19 — **controller 0.333.0, the floor for every box.** Measured first on demo-hp: the household user's FileBrowser token (HS256; claims `belongsTo`, `Permissions`, `iat`, `exp` 2 h, `iss`) still opened FileBrowser (200) after the dashboard sign-out, as a cookie and as Bearer. Now the forwardAuth answer refuses a request with no valid files cookie that carries a non-admin FileBrowser token, in every place FileBrowser 1.5.6 reads one (cookie, `?auth=`, Bearer, Basic password); `admin`'s token passes. The commit's security review found Basic missing from the first version — fixed before release, red-proved. Live (rc.2 on demo-hp, 0.333.0 on Tester 1): the token alone 401 in all four forms before and after the sign-out; with the files cookie 200. | `audits/day-2026-10-19b/P3/r1155-live-rc2.txt`, `P6/05-files-one-sign-in.txt` |
| **R-1161** | **The escrow wizard still says „Ez mostantól az élő helyreállítási kód" („this is now the live recovery code") the moment the code is shown;** (P4) | CLOSED 2026-10-19 — **controller 0.333.0**: the reveal says the code becomes live after the two words come back and that closing the page keeps the old code; the old sentence shows only for an agent before 0.160.0 (no confirm). Fixtures `escrow_first`, `escrow_receremony` re-captured (R-1151 made the old text untrue); the rest byte-identical. Live on demo-hp: new text shown, old hidden, the switch present, negative control absent. | `audits/day-2026-10-19b/P6/06-escrow-page-text.txt` |
| **R-1156** | **A reinstall done within the stale threshold (45 min) of the old box's last report reads as „live":** (P4) | CLOSED 2026-10-19 — **hub 0.160.0 deployed** (the operator's yes, 11:2x local): `host_reenroll_while_live` is a WARNING mailed to the operator with the same instruction as the stale case (delete the old host, run the installer again); a `--resume` or repair re-run mails too and the text says it needs nothing. Red-proved (info severity → the test fails). Deploy: only Deployment/hub synced, Synced/Healthy, running digest = built digest, 0 ERROR lines, floor read back, host + controller reports from all three boxes after the restart. | `audits/day-2026-10-19b/P4/`, `05` §8 |
| **R-1157** | **While the cadence-cap flag stays degraded (the agent's 24 h hold), further refused backups raise no new alarm:** (P4) | CLOSED 2026-10-19 — **hub 0.160.0 deployed**: a changed agent reason while the cadence cap stays degraded is a further refusal — counted (`refusals_in_episode`, `first_refusal` in the details), the alarm repeats at most once a day, the cleared record carries the total. No agent change. Red-proved (without the branch a day-later refusal raises nothing). | `hub/internal/monitor/host_capability.go`, `07` §6.1 |
| **R-1158** | **The off-site re-issue on a clean-slate re-enroll may be obsolete:** (P4) | CLOSED 2026-10-19 — **measured from the code, kept, and its words corrected (hub 0.160.0).** The only reader of the Storage Box sub-account password is the hub's own key registrar (`offsitekeys.TargetFor`); `ConsumeOneTimeSecret` has no production caller and `consume-password` answers 410 since v0.127.0, so the leg changes nothing a box uses. Not proven useless: it hands the hub a fresh, working login at every clean reinstall (it repairs a broken stored one). Comments and the `offsite_reissued` record no longer say the controller takes the password (red-proved test). CC's memory note on reinstalls updated. | `05` §8 [FACT R-1158] |
| **R-1160** | **The hub and the agent have no gofmt gate, and both drift:** (P4) | CLOSED 2026-10-19 — **one formatting-only commit per repo** (hub `d5e3c8b0`, 22 files; agent `1edfd8e`, 14 files; tests green before each), then the controller's gofmt gate shape in both runners: `hub-gofmt` in `repo_gates.py`, `gofmt` in `agent_gates.py`, fast set, each with decoys (an unformatted file convicted by name, its formatted twin passes, CI without Go passes out loud, a dev machine without Go is INCONCLUSIVE); red-proved with the conviction switched off. No agent release (the next one carries it). | `scripts/hub_gofmt_gate.py`, felhom-agent `scripts/gofmt_gate.py` |
## 2026-10-19 — the website keeps syncing during an internet outage (`fd75003e`)
| Row | What | Closed | Evidence |
+9 -13
View File
@@ -112,7 +112,7 @@ match what the reader sees is how an instrument stops being believed (R-421). It
**nothing was proven on the day this line was drawn** and a stopping line that moves a status is a
stopping line that lies.
## Install & onboarding — 9 rows (P3 5, P4 4)
## Install & onboarding — 7 rows (P3 5, P4 2)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -123,8 +123,6 @@ stopping line that lies.
| **R-993** | Install & onboarding | P3 | **Operator: approve the minimum machine in `documentation/audits/sizing-2026-10-13/SIZING.md` §5** (the input R-991's hardware page needs). MEASURED 2026-10-13 on the N100 demo box (61 apps deployed and removed through the product, 15 min at rest each; heavy apps with one real action). Proposal: **minimum** x86-64, 4 cores (N100 class), **8 GB**, 256 GB SSD; **recommended 16 GB**, 512 GB SSD; out: ARM / Raspberry Pi, Synology-QNAP, routers, cloud VPS. Worked from the floor (2.5 GB for the whole box with no apps) + Immich, Jellyfin, Nextcloud, Home Assistant, Vaultwarden, Paperless-ngx at their peaks (5.0 GB) + 50 % headroom ≈ 12.3 GB. 8 GB runs that set without Immich; 16 GB runs all of it twice over. Data for the estimator: `sizing.json`. **2026-10-13 (estimator session) — the installer accepts a host BELOW this minimum:** `autosize_guest_caps` has no floor on the host, only on the guest; on a 4 GB box (MemTotal ≈ 3.5 GiB) the guest gets host − 1 GB ≈ 2.4 GB and Proxmox keeps ≈ 1 GB, where it used ≈ 2.0 GB on the N100 (SIZING.md §1). Whatever minimum is approved, nothing enforces it at install today. The three-verdict wording of §5 now matches the estimator page (`/gepigeny`, R-997). | **WAITING-ON-OPERATOR** — filed 2026-10-13 | the operator's decision | Operator: approve or change §5. If nothing is done: no hardware page; a buyer with a Raspberry Pi can still download the installer (R-991 stays open) | operator |
| **R-494** | Install & onboarding | P4 | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: operator-side automation; the operator creates the tunnel by hand per the day-0 runbook.** | — | — | CC |
| **R-504** | Install & onboarding | P4 | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** **Re-ranked 2026-10-03: P3→P4: households are sent to the website's download page; the bare address is cosmetic.** | — | — | operator |
| **R-1156** | Install & onboarding | P4 | **A reinstall done within the stale threshold (45 min) of the old box's last report reads as „live":** the hub writes only the info record `host_reenroll_while_live`, no warning mail, so the operator is not told to delete the old host and re-issue. The residual of R-1154's loud-only rule. Found 2026-10-19. | OPEN — filed 2026-10-19 | — | Decide: mail the live case too, or a shorter window for this check | CC |
| **R-1158** | Install & onboarding | P4 | **The off-site re-issue on a clean-slate re-enroll may be obsolete:** since hub v0.127.0 no box receives the provider password (a fresh box registers its key with the controller API key), so `reissueOnReenroll`'s off-site leg may now only reset a password nobody uses — and CC's memory note on reinstalls (2026-08-21) may be stale on this point. Read from code, not measured. Found 2026-10-19. | OPEN — filed 2026-10-19 | — | Measure on the next Tester4 install (after deleting its old host) | CC |
## Apps & catalog — 13 rows (P3 5, P4 8)
@@ -154,7 +152,7 @@ stopping line that lies.
| **R-683** | App updates | P3 | **[P3-LOW] Watch: after a power cut during an update's health check, the hold named an HOUR-OLD second-drive copy, not the one the update's own backup should have just made.** 2026-09-24 chaos round 3 (nextcloud, `backup_max_age: 1m`): no `backing-up` phase was seen and the hold named Tier 2 at 13:04 for an update pressed at 14:04; the pre-cut controller log was lost with the container (the runner now saves it at arm time — R-320). Round 11, the same action without a power cut, named a fresh 14:34 copy and logged the Tier-2 copy. The sentence was TRUE (it named the copy it offered); the question is why the update did not back up first. Not reproduced; watch the next power-cut drill. `audits/night-2026-09-24/E/round-03*.json`, `E/round-11-controller-pre.log` | **OPEN — P3; owner: CC (watch)** | — | — | CC |
| **R-785** | App updates | P3 | **[P3-LOW] SparkyFitness is pinned 11 releases and a major behind upstream (v0.17.3; upstream v1.7.3, v1.6.0 dated 2026-07-24).** READ 2026-10-01 (`audits/visitors-2026-10-01/C/bench/C1-previous-tag.txt`). **Needs:** an update walk 0.17 → 1.x through the ladder (bench + box), after R-784 is decided. | **OPEN — rank P3-LOW; owner: CC (after R-784)** | — | — | CC |
## Backup & restore — 28 rows (P2 3, P3 13, P4 12)
## Backup & restore — 28 rows (P2 3, P3 14, P4 11)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -184,8 +182,8 @@ stopping line that lies.
| **R-878** | Backup & restore | P4 | **A catch-up (R-871) runs the database-dump leg in the DAY, and that leg stops an app with a volume for its copy — the household may notice the stop, and a large volume makes it longer.** MEASURED 2026-10-05 on demo-felhom: the catch-up at 08:25:02 stopped opengist, copied 182.5 KB, started it again — about 1 s, then a few seconds of `health: starting`; the night does exactly the same, unseen. Nothing measured for a large volume. Fix direction (if it matters): skip the volume copy of a running app in a DAYTIME catch-up and leave it to the next night, or warn. `audits/catchup-2026-10-05/partA/live-demo-felhom.txt` | **READY — owner: CC** **2026-10-05 (burn-down night): NEEDS A LIVE MEASUREMENT** — a timed catch-up on 9202 for an app with a multi-GB volume, then the skip-or-warn choice. | — | measure a large volume first | CC |
| **R-32** | Backup & restore | P4 | **[P2-HIGH] RESET must purge the customer base dir; the orphan card must stay honest; unattributed bytes must be visible.** The rehearsal's S7 said in advance that an orphan card would BE a finding — and one appeared (16:58:14). Cause: RESET's `"hetzner":"ok"` leg destroys the sub-account, but **a Hetzner sub-account is an access-control object, not a data object** — its directory survives, so re-enabling offsite recreated an account over the previous lifecycle's ciphertext, encrypted under a key that same RESET had destroyed. **MIGRATED FROM `ROADMAP.md` 2026-08-22 (R-369) — originally filed 2026-07-21, size M, roadmap state `idea`.** Moved verbatim; nothing added or reinterpreted. The roadmap keeps its copy as history, marked moved. | **OPEN — migrated from ROADMAP 2026-08-22, rank unchanged** **2026-10-06 night: design written** (`audits/night-burndown-2026-10-06/design-R-32.md`) — pick A: purge the repo + set-aside copies through the sub-account's own password login BEFORE deleting it (no main-account credential); the „repo data destroyed" comment at `hub/internal/offsite/offsite.go:276-280` is false for the shared tier; part (3) waits on a read-only measurement (`du` on port 23). Operator: agree to route A. **2026-10-07 07:58: `09` §3 decision 167 — option A yes, and delete the old RESET leftovers once.** **2026-10-07 (Part D step 2, read only):** the hub shows bx11 holding 3.3 GB of data while the four live customers account for ~0.6 GB → **~2.7 GB belongs to no live customer**; no live customer shows a set-aside folder, so the leftovers are in homes whose sub-account is gone, reachable only by the pool box's MAIN account. **The one-time clean-up is STOPPED:** no main-account login for bx11 is held on DooPlex (the API token in the credentials file sees no box). Needs: the main-account login, or the operator lists and deletes in the Hetzner console (`audits/day-2026-10-07/D/D2-listing-readonly.txt`). **2026-10-07: option A DELIVERED (hub 0.142.0):** RESET purges the repo and every `.orphaned-*` through the sub-account's own login before deleting it, and keeps the sub-account if the purge fails (red-proved, `audits/day-2026-10-07/D/`). Not yet seen live (no scratch RESET today). **The one-time clean-up of the ~2.7 GB of leftovers is STOPPED:** it needs the pool box's main-account login (see above). **2026-10-07 14:43 — `09` §3 decision 173: leave the ~2.7 GB of old leftovers for now; re-ranked P2 → P4.** Option A itself is delivered (hub 0.142.0). What stays open is ONLY the leftovers: they were computed (box total minus live customers), never listed, and deleting them needs the pool box's MAIN login, which reaches every customer's data. **Blocker: a safe main-account path.** | — | **Ruling from the run (three parts, deliberately separate):** (1) because RESET destroys custody, the ciphertext it leaves behind is unrecoverable **BY DESIGN** → RESET gains a **main-account purge of the customer base dir** (the existing operator ack already covers it); (2) the **move-aside guard STAYS** for reinstall-*without*-RESET — there custody survives and the card's "history recoverable" promise is true (R-26 depends on exactly that); (3) the operator **Restic tab shows per-customer directory bytes vs attributed snapshot bytes**, so dead data cannot hide. Measured on the pool box that night: **49 M attributed** (2 snapshots, 48.717 MiB) against **1.4 G + 3.0 M unattributed** across TWO `.orphaned-*` dirs. Evidence `restic-and-pool.txt` | CC |
| **R-1087** | Backup & restore | P4 | **Whole-box copies left on the OLD local target are never pruned after the target moves.** MEASURED 2026-10-15 (storage-identity build B space check): the N100's host root (`local`) holds three copies from 2026-07-28 (5.5–5.7 GB each, ≈ 16.8 GB); its agent's `local_backup_target` is now `felhom-backup` (the USB disk, retention 3), and retention prunes only the current target. On a small-root box such leftovers would starve the local copies (R-548). Evidence: `audits/storage-identity-buildB-2026-10-15/space/felhom-pve-9201.txt`. | OPEN | — | Decide whether the agent prunes its own `vzdump-lxc-<vmid>-*` files on a former target (after a newer copy exists on the new one), or the target move asks; until then, the N100's three July files can be removed by hand with the operator's word. | CC |
| **R-1161** | Backup & restore | P4 | **The escrow wizard still says „Ez mostantól az élő helyreállítási kód" („this is now the live recovery code") the moment the code is shown;** since R-1151 (agent 0.160.0, controller 0.332.0) the code becomes live only after the two words are typed back, and closing the page keeps the old one. The safe direction (it overstates), but it is now untrue until the confirm. A Hungarian + English copy change; not done in-session (the controller's one release of the session was out). Found 2026-10-19. | OPEN — filed 2026-10-19 | — | Next controller release: say the code becomes live after the check | CC |
| **R-1162** | Backup & restore | P3 | **The whole-guest off-site tier (ep0) prunes by count (keep-last 2), so a compromised controller can still push out the pre-compromise copy, slowly:** with R-973's cap one run per 6 days gets through, so a copy older than the compromise survives about 6 days and every extra attempt alarms. Time buckets on ep0's prune job (e.g. keep-weekly 2) would make it unfloodable; check that DooPlex's pull copy of ep0 is keep-weekly, not keep-last. The off-site half of R-973, carried over when R-973 closed. An ep0/hub prune-job change. | OPEN — carried from R-973 2026-10-19 | — | Operator decides the ep0 prune shape | operator + CC |
| **R-1162** | Backup & restore | P3 | **ep0's whole-guest off-site prune was a count (keep-last 2), so a compromised controller could push out the pre-compromise copy, slowly.** Operator decision (STATUS D18, the brief's D1 = A, 2026-10-19 08:24): one copy per ISO week, 2 weeks. Before: PBS's own dry run and an offline model of its selection both showed `keep-weekly 2` removing nothing on demo-hp (2026-10-08, 10-15 kept) or demo-felhom (10-06, 10-13 kept); nothing deleted by the dry runs (8 snapshots before and after). On the operator's „yes": `prune-demo-hp` and `prune-demo-felhom` changed to `keep-weekly 2` (schedule 03:30 unchanged), read back 07:49 UTC. DooPlex's copy of ep0 already keeps `keep-weekly 8` (decision 71) — unchanged. Households without a job: R-1165. | NARROWED 2026-10-19 — changed and read back; the first scheduled run is 2026-10-20 03:30 UTC | `audits/day-2026-10-19b/P1/` | Read the 2026-10-20 03:30 UTC prune task on ep0 (`proxmox-backup-manager task list`, `prunejob` for both namespaces: OK, `keep-weekly 2`, two snapshots each), then close | CC |
| **R-1165** | Backup & restore | P3 | **A household namespace on ep0 gets no prune job by itself, so its whole-guest off-site copies are never pruned.** Read 2026-10-19: ep0 has prune jobs only for `demo-hp`, `demo-felhom` (made by hand in July, R-82) and `operator`; `tester-1` (3 weekly copies, growing one a week) and `Tester-2` (1) have none; `felhom-tenantsync.sh` creates no job and the hub's provisioner neither. The install script's own comment warns that a missing job is unbounded growth. Close to R-89 (per-customer retention on the hub), but this is the gap today, at every new household. The operator chose not to add a tester-1 job by hand today (2026-10-19). | OPEN — filed 2026-10-19 | `audits/day-2026-10-19b/P1/ep0-settings-before.txt` | The hub (or tenantsync) creates `prune-<ns>` with `keep-weekly 2` when it provisions a household's namespace; meanwhile one job per existing household | CC + operator |
## Storage & devices — 9 rows (P3 3, P4 6)
@@ -201,7 +199,7 @@ stopping line that lies.
| **R-1077** | Storage & devices | P4 | **No USB enclosure that hides or fakes a disk's serial has been measured.** The storage-identity spike (2026-10-15) had none at hand and found none in earlier evidence; the design's `usb_bridge` unknown rule (`architecture/16-storage-physical-identity.md` §3) is therefore an assumption. Cheap single- and dual-bay bridges are known to report one bridge serial for any disk. Evidence: `audits/storage-identity-2026-10-15/README.md` case 4b. | OPEN | — | Buy or borrow one cheap USB enclosure (single and dual bay), run `tools/physdisk_probe.py` against it, and set the rule from what it reports. | Operator (a device) then CC |
| **R-1094** | Storage & devices | P4 | **The whole-disk format refusal tells the household to format the free part — which is refused while the other part is in use.** SEEN 2026-10-16 (controller 0.318.0): 409 „Ezen a lemezen egy használatban lévő meghajtó van, ezért a teljes lemez nem formázható. Csak a szabad részét formázhatod." — since agent 0.158.1 a part next to a mounted part can only be USED (the root wrapper trusts no „Felhom's own" claim). Evidence: `audits/storage-identity-buildB-2026-10-15/live/14-samedisk-refusal-offer.txt`. | READY | — | Reword `err.web.meghajto_hasznalatban_formazas` (hu + en): „… nem formázható. A szabad részét használatba veheted." Next controller release; the copy gates run. | CC |
## Security & access — 22 rows (P1 1, P2 2, P3 12, P4 7)
## Security & access — 21 rows (P1 1, P2 2, P3 12, P4 6)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -225,8 +223,7 @@ stopping line that lies.
| **R-1054** | Security & access | P3 | **The hub's operator/global API key is still one bearer that can do everything on `/api/v1`, including reading every box's break-glass root password (`GET /admin/hosts/{id}/recovery-credential`).** Hub v0.152.0 gave the web surface scoped tokens (`05` §17) but tokens are refused on `/api/v1` by design. Who uses the global key today is not listed in one place. Fix: list its consumers; move each operator use to a scoped token on the web side (or a token class on the API), and keep the global key only where a box path needs it — or retire it. | OPEN — filed 2026-10-15 by the token build | — | List the global key's consumers (pod env, scripts, runbooks) | CC |
| **R-1118** | Security & access | P3 | **CC's secrets moved off the plain `~/.config/credentials` into an encrypted store with a broker and guards; what is left is retiring the old file.** BUILT 2026-10-17 (store B, operator ruling 11:28): private repo `admin/cc-secrets` (`ARCHITECTURE.md`, the design verbatim in `docs/`), `sec` (no get), PreToolUse refusals + PostToolUse masking for Bash and Read in `~/.claude/settings.json` (every CC session on DooPlex), nightly transcript sweep with mail. 55 tests, 18 red-proofs, 36-row child-session proof in both launch modes, fresh-session proof (`audits/cc-creds-build-2026-10-17/`). 25 names in the store (23 imported, `RESEND_API` from CC's environment, a self-test value). `12-agent-tooling.md` §5 points at it. | NARROWED — built; the old file's retirement is R-1134 | the week with no reader (R-1134) | `sec doctor` stays all OK; R-1134 | CC |
| **R-1121** | Security & access | P3 | **`~/rotated-secrets-2026-10-09.txt` still exists on DooPlex** — 632 bytes, modified 2026-10-09 13:08 +0200, mode 600 (SPIKE Q6: `test -e` + `stat` only, not opened). Left by the R-925 rotation work. If any value in it is still valid, it is one more plaintext secrets file in the home folder, readable by every CC session. | WAITING-ON-OPERATOR | what the file holds — only the operator knows | Operator: confirm every value in it is revoked, then `shred -u` it; or move the live ones into the R-1118 store | operator |
| **R-1155** | Security & access | P4 | **FileBrowser's own 2-hour token for the one-sign-in user can outlive the dashboard session.** The controller clears it together with the files cookie whenever a stale files cookie arrives (sign-out, password change), but a browser that lost only the files cookie (or a token copied out of it) keeps FileBrowser's token until it expires, and with no files cookie the forwardAuth answer passes the request through. Closing it would need the box to read FileBrowser's token (a box-held `auth.key`) or a shorter FileBrowser token life. Found 2026-10-19 (R-1100 build, security review). | OPEN — filed 2026-10-19 | — | Decide whether a box-held FileBrowser key is worth it | CC |
| **R-1159** | Security & access | P3 | **The household's retrieval passphrase already yields the controller's hub API key, and that key alone registers and confirms off-site keys.** `GET /config/{id}` (passphrase-gated) returns `controller.yaml` with `hub.api_key`; `/offsite/register-key` and `/offsite/confirm-key` accept that key (`offsiteKeyAuth`). Pre-existing, and wider than R-1154 (whose fix keeps the passphrase away from the operator's re-issues). Read from code by the hub helper 2026-10-19, not measured. | OPEN — filed 2026-10-19 | — | Measure what a passphrase holder can do to the off-site tier; decide a second factor or a scope | CC + operator |
| **R-1159** | Security & access | P3 | **The household's retrieval passphrase alone reaches the box's keys and through them the whole off-site key surface — MEASURED 2026-10-19** (the real hub router, a made-up customer, a fake off-site service; status codes and field names only — Tester4's passphrase is not in CC's store and reading it from the hub DB was refused by CC's own safety check). `GET /config/{id}` with the passphrase → 200, `hub` fields `api_key`, `enabled`, `url`, the key the box's controller uses; wrong passphrase 401. `POST /host-enroll` with it → 200 and the EXISTING host's key. With the controller key (wrong key → 401 each): `register-key` (pins the holder's own key append-only), `confirm-key` (removes every other line — the box's own key too), `move-aside` (renames the repository), `window-open` (a 20-min deleting line for the CONFIRMED key, weekly), `abandon-request` (the hub DELETES a set-aside copy after 7 days unless cancelled), `abandon-cancel`, `abandon-status` — all reach the service. With the host key: `GET /hosts/<id>/escrow` (sealed blob, loud), `PUT /hosts/<id>/escrow` and `PUT …/recovery-credential` (past auth: 400 on an empty body), `wg`, `desired-state`, `jobs`, `host-report`. A config download is a log line only. The passphrase is used only at install time (the installer, the controller's setup wizard). **Option A (CC's pick): the passphrase serves keys only while the customer has no host record** — `GET /config` leaves out `hub.api_key` and `/host-enroll` refuses (409, „delete the old host first") once a host exists; a reinstall needs the operator's delete first, which is already the rule (R-1154). Cost: a hub change; a guest rebuilt on a live host needs the operator. Closes the whole chain at its source. **Option B: a second factor on the destructive off-site routes** (`confirm-key` that removes a line, `move-aside`, `abandon-request`, `window-open`) — the operator's approval or the host key. Cost: every reinstall's key rotation waits for the operator, and the host key is itself reachable by the passphrase today, so B alone does not close it. | READY — measured 2026-10-19, options in STATUS D19 | `audits/day-2026-10-19b/P2/passphrase-reach.txt` | Operator picks A or B | operator + CC |
## Box system & updates — 12 rows (P2 1, P3 7, P4 4)
@@ -246,7 +243,7 @@ stopping line that lies.
| **R-1061** | Box system & updates | P4 | **A per-customer floor reached Tester 1 only at its next 15-minute report, not in seconds.** Set 2026-10-15 12:55:51 UTC for tester-1, demo-hp and demo-felhom together (hub 0.153.0, CC's token): demo-hp and demo-felhom ran 0.314.0 within 13 s (the hub's wake); Tester 1 moved at 13:09:35, its regular report (`audits/gate-restore-2026-10-15/release/03-boxes-move.txt`). It had restarted at 12:54:29 onto rc.1 and logged „hub wait channel active". Why the wake did not reach it is not read (the wake line is DEBUG). No harm: the floor arrived. | **OPEN** — filed 2026-10-15 | — | Read Tester 1's DEBUG ring at the next floor change; compare with the hub's bump log | CC |
## Monitoring & notifications — 12 rows (P2 1, P3 7, P4 4)
## Monitoring & notifications — 11 rows (P2 1, P3 7, P4 3)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -261,7 +258,6 @@ stopping line that lies.
| **R-285** | Monitoring & notifications | P4 | **A planned, supervised reinstall pages the operator as if the machine had died — there is no notion of expected downtime anywhere.** During the 2026-08-09 rehearsal the hub sent, all `status: sent` to the operator channel: `host_stale` 08:58 UTC, `node_stale` 09:00, **`host_down` 09:28 (error)**, **`node_down` 09:30 (error)**, `host_leaf_changed` 09:31, `host_recovered` 09:31, `node_recovered` 09:34, `offsite_delivery_stuck` 09:34 — eight operator mails for work that was deliberate, attended and announced. **This is the OPPOSITE gap from the one R-281 filed:** the alarms are not missing, they are indiscriminate. `host_stale` at 30 min and `host_down` at 60 min (`monitor/host_staleness.go:22-23`, `downAfter = 2 * threshold`) cannot distinguish a wiped-on-purpose box from a dead one, and `host_leaf_changed` firing on a reinstall is correct-but-expected. **Note the interaction with the mute used on 2026-08-09 evening:** blocking a customer silences everything, so today the only two settings are *page me for planned work* and *tell me nothing at all*. **What is owed is a middle:** a maintenance window, or an operator-set expected-downtime flag, that suppresses staleness and leaf-change while leaving genuine faults audible | **READY (M) — NEW 2026-08-09** | — | The evidence is the operator's mailbox plus `events`/`notification_log` for 2026-08-09 | CC |
| **R-1005** | Monitoring & notifications | P4 | **A recovery mail says how long the box was „stale", not how long it was gone: after a 51-minute power cut demo-hp's `host_recovered` read „was stale for 8m" and `node_recovered` „23m".** An operator reading only the recovery mail under-reads the outage (`audits/night-power-2026-10-13/` R6). Same kind, the other way: the off-site mail of 2026-10-14 02:18 was titled „3 of 3 apps failed to back up (offsite run)" while 11 other apps were backed up in that run — only the 3 flagged apps were counted. | **READY** — filed 2026-10-14 | — | CC: add the time since the last report before the gap to both recovery messages, and count every app of the run in the off-site subject, with a test that asserts each text; next hub release | CC |
| **R-1049** | Monitoring & notifications | P3 | **A pulled drive mails the operator „unexpectedly disconnected” (error), and nothing ever mails that it came back.** MEASURED 2026-10-14 night (`audits/media-drive-events-2026-10-14/K8-hub-mails.txt`, the hub DB with its WAL from the running pod, and the mailbox as the second channel): two pulls → two `storage_disconnected` mails; the hub RECEIVED both `storage_reconnected` and both `health_recovered` (severity info) and logged no notification for any of them — they are not in `recoveredPairedDownTypes` (`hub/internal/notify/dispatcher.go`), so the severity gate drops them. The R-97a shape: told it broke, never told it healed — the operator cannot tell a returned drive from one still gone without opening the hub. **BUILT 2026-10-15 — hub v0.151.0 (`12552aca`): both pairs in `recoveredPairedDownTypes`, cooldowns ended by the next loss (`TestR1049_DriveBackMailsBothEdges`, red-proved). Not deployed: the operator's „yes” first (decision 162); the image build waits for it too.** | **WAITING-ON-OPERATOR** — hub v0.151.0 code pushed, not built, not deployed | the operator's „yes” to the hub deploy | On the „yes”: build + deploy hub v0.151.0; read a „came back” mail on the next pull | CC |
| **R-1157** | Monitoring & notifications | P4 | **While the cadence-cap flag stays degraded (the agent's 24 h hold), further refused backups raise no new alarm:** a guest refused every day reads as one long episode, one mail. The same before hub 0.159.0. Found by the hub helper 2026-10-19. | OPEN — filed 2026-10-19 | — | Decide whether a refusal count in the event detail or a daily repeat is wanted | CC |
## Hub & operator — 13 rows (P3 6, P4 7)
@@ -302,7 +298,7 @@ stopping line that lies.
| **R-990** | Business & legal | P4 | **Operator: read `documentation/audits/forum-research-2026-10-12/FELHOM.md` §5 and rule on each recommendation.** READ-ONLY research 2026-10-12: three years (#7943–#11092, 2,908 posts, 195 people, 82 beginner threads) of the public PROHARDVER! topic „Home server / házi szerver építése”, tagged and counted by person (`FINDINGS.md`, method and spot-check in `METHOD.md`; no username or post text in git). Ten ranked recommendations, none acted on; the top three: a „which machine works” page (8 of 33 beginners who name their hardware own only a Synology, a router disk or a Raspberry Pi — the installer cannot run there; the same gap is R-989's first recommendation from a different source, and the shipped-page half is R-991), „you do not choose an operating system — we do” (the biggest beginner question, 20 of 82 threads), and „bring your old office PC” as the cheap entry. Catalog gap (≥ 3 people): a DLNA server (7), an ad/DNS blocker (5), a camera recorder (5). | **WAITING-ON-OPERATOR** — filed 2026-10-12 | the operator's reading | Operator: rule on each of the ten (yes / no / later). If nothing is done: nothing changes; the research ages | operator |
| **R-1035** | Business & legal | P4 | **Did the „felhő tárhely” page earn anything? Two dated checks.** **Baseline (reported by the operator from Search Console, 2026-10-14, last 3 months):** 1 click, 48 impressions, CTR 2.1 %, average position **73.6**; „felhő tárhely” 19 impressions, „felhőtárhely” 5, „felhő alapú tárhely” 3. CC measured separately that the phrase „felhő tárhely” appeared **0 times** on the home, FAQ and backup pages — Google was showing the home page for those queries by accident. `/sajat-felho-tarhely` + `/en/own-cloud-storage` published **2026-10-14**; full baseline, price sources and the power provenance in `audits/seo-2026-10-14/BASELINE.md`. **What to measure, both times:** impressions and average position for „felhő tárhely” and „saját felhő tárhely”, plus the new page's own impressions, clicks and position. **A null result at +4 weeks means nothing on its own** — a new page on a site at position 73.6 takes weeks to months to move, and „felhő tárhely” alone is held by Microsoft, Google and the big list sites; the page aims at the two longer phrases. That is why there are two checks and not one, and why neither should be read as a verdict on the copy. **There is no Search Console connector in this project, so CC cannot read these numbers** — the operator reads them, CC records and compares. **Blocked until the operator asks for indexing** (Search Console → URL-ellenőrzés → the URL → „Indexelés kérése”): until that happens, or Google finds the page itself, both checks read zero for a reason that is not the page. **This is a slow channel and does not replace talking to people.** | **WATCHING** | operator: request indexing, then supply the Search Console read | 2026-11-11: first read (+4 weeks); 2026-12-09: second (+8 weeks), compared with the first | operator + CC |
## Process & tooling — 31 rows (P3 8, P4 23)
## Process & tooling — 31 rows (P3 9, P4 22)
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|---|---|---|---|---|---|---|---|
@@ -336,7 +332,7 @@ stopping line that lies.
| **R-1128** | Process & tooling | P4 | **The controller's decoy suite edits real tracked files (`handler_debug.go`, `update.go`, the locale bundles, templates) and plants `.go` files, with no lock** — a full run while another session edits the controller tree can overwrite that session's edits. felhom.eu's suite took an `flock` after the 2026-10-08 clobber. Found by the R-1116 helper 2026-10-17. | OPEN — filed 2026-10-17 | — | Run the suite in a scratch copy of the tree, or take the felhom.eu-style lock | CC |
| **R-1123** | Process & tooling | P4 | **`reuse_refs_check.py` treats every git directory beside the repo as a sibling repo**, including stray clones in the workspace (`felhom.eu-media`, `felhom.eu-sizing-push`, `felhom.eu-wt-sizing`), so a citation can resolve against a stale clone and pass. Found by the R-1116 helper 2026-10-17 (it also makes the agent suite 20 s here vs 5 s in CI). | OPEN — filed 2026-10-17 | — | Search only the four known repo names (or the main clones), with a decoy | CC |
| **R-1152** | Process & tooling | P4 | **felhom CI's 5-minute job timeout is inert** (act_runner 0.6.1 does not enforce a job-level `timeout-minutes`, R-1136), so a hung step runs up to the runner's 3 h. Move it to a step-level `timeout-minutes` on each workflow's gate step (e.g. 20 min for felhom.eu, which runs ~7 min), measured first: a step timeout fails the job AND the `if: failure()` alarm still runs. Needs a scratch repo (the admin token cannot create one — the operator, or a token with `write:user`). Found 2026-10-18. | OPEN — filed 2026-10-18 | — | Measure on a scratch repo, then the four workflows | CC |
| **R-1160** | Process & tooling | P4 | **The hub and the agent have no gofmt gate, and both drift:** about 20 hub files and 14 agent files are not gofmt-clean (`gofmt -l`, 2026-10-19); the controller has `gofmt_gate.py`. Not fixed in-session: a mechanical rewrite of files other sessions own. | OPEN — filed 2026-10-19 | — | One gofmt commit per repo in a quiet window, then the controller's gate in both runners | CC |
| **R-1166** | Process & tooling | P3 | **Another device takes demo-hp's address (192.168.0.104) every few minutes.** Measured 2026-10-19 from DooPlex: the LAN's ARP entry for .104 flipped between demo-hp's `7c:d3:0a:77:d9:76` and `b8:06:0d:60:45:58` eight times in 19 minutes (10:02–10:17 UTC); while the other MAC holds it, SSH and the Proxmox UI answer „connection refused" and ping still answers. 09:46–09:54 UTC demo-hp also lost its link to ep0 (`Can't connect to 10.77.0.1:8007`, 52 lines). demo-hp's address is static (`nodes.md`), so the other device most likely got .104 from the router's DHCP. | OPEN — filed 2026-10-19 | `audits/day-2026-10-19b/P3/demo-hp-arp-watch.txt` | Operator: find `b8:06:0d:60:45:58` in the router's DHCP list and reserve .104 for demo-hp (or move demo-hp out of the DHCP range) | operator |
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
One row per dated check. The R-number must have a row above. Dates are UTC.