diff --git a/CONTEXT.md b/CONTEXT.md index f0bd894c..3c5ee48f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -16,6 +16,14 @@ > and holds nothing of its own; this file does hold its own content, namely the standing rulings below. +> **2026-10-25 (evening, attended) — D50 = A; installer 1.36.1 published.** (`09` §3 decision 262.) The registry keeps the +> newest 5 agent versions + the MinAgent floor + the day-0 agent; the agent CI asserts exactly these +> (`retention-policy.json` `pinned`, copied from hub state by the vouching session — CI holds no hub secret). The operator +> also pruned controller, golden and hub images by hand; every artifact still named (manifest agent/bundle/golden, the +> floor agent, tags v0.158.1+, controller 0.343.0/0.342.0/0.331.0, samba 1.2.0, the ISO) downloads +> (`audits/night-2026-10-25/part1-artifacts.txt`). Installer 1.36.1 (`installer-v1.36.1` → `79ee3e33`, served sha256 +> `e18cdd17…`): the preflight words 401/404/409 again (R-1284). + > **2026-10-25 (attended release run) — D49 = A, D44/D45 = all picks, D46 = yes; released: hub 0.170.0, agent 0.165.0, > controller 0.343.0, catalog `ebbf1f4`** (`09` §3 decisions 258–261; `REPORT-release-2026-10-26.md`). **D49 = A:** a > restore whose app does not start keeps both copies and never pours back by itself (`07` §8.2e). **Fix A** (controller diff --git a/STATUS.md b/STATUS.md index 9582dc1f..e0d58768 100644 --- a/STATUS.md +++ b/STATUS.md @@ -49,12 +49,10 @@ Newest first. CC's pick is in the „Pick" column. Each row also has its full te **Day, 2026-10-23 — your do list.** 1. Print the break-glass sheet (`documentation/runbooks/break-glass-sheet.md`, the part from „The sheet”, one A4), fill it by hand, keep it outside the home (D32; R-923, R-924). If not: after a DooPlex loss no off-site copy opens. -2. On the hub's Hosts page: Discard appliance 49, and delete host Tester4-f967d4 (the BYO test host, removed 2026-10-25). If not: dead entries alarm. +2. On the hub's Hosts page: Discard appliance 49. If not: a dead entry alarms. 3. Give the homelab session R-1241, R-1242, the lists at the end of `documentation/audits/plex2shell-felhom-impact-2026-10-23/README.md` and `documentation/audits/dooplex-alarms-2026-10-24/README.md`, and an upload alarm on DooPlex's `eno1` (~100 Mbit/s line; the flood peaked at 905). If not: the next hacked app on DooPlex can read the hub's backups. **The rest** - System poster needs a refresh: the new 01:40 „release copy” arrow from DooPlex to ep0 (D33); the hub-database label already says every 6 hours. If not: the drawing misses one copy. -- Tell CC what removed four old agent packages from Gitea on 2026-10-25 (a clean-up rule?): the agent's CI is red until the gate and the rule agree (R-1290). -- Publish installer 1.36.1 (tag + webpage sync): a wrong passphrase then reads „passphrase REJECTED", not a doubled code (R-1284). - Rotate the reused homelab logins and decide the Gitea repo's visibility (R-925, P1). Since 2026-10-09. - Two DooPlex leftovers: `leftovers/2026-10-14/first-post/` (keep or delete); the `deny`-list edit in homelab-manifests' `.claude/settings.json` (commit or drop). Since 2026-10-14. - Before the first paying customer: Tandoor's permission (R-789); a lawyer for the licence list and the terms (R-802, R-813); SparkyFitness by e-mail (R-784). diff --git a/documentation/architecture/09-update-architecture.md b/documentation/architecture/09-update-architecture.md index 61e18054..e1daaa89 100644 --- a/documentation/architecture/09-update-architecture.md +++ b/documentation/architecture/09-update-architecture.md @@ -1099,6 +1099,16 @@ its length, and both fixes cost something the household would notice — operato `plex: security fix CVE-2026-96652,CVE-2026-96654,CVE-2026-96655,CVE-2026-96656 taken` and ended `done` in 30.1 s after the update's second-drive copy and undo copy. Released as controller 0.341.0, global floor 11:24 UTC. +### 2026-10-25 (evening, attended) — D50: which agent packages the registry keeps + +262. **The registry keeps the newest 5 released agent versions, plus the hub's MinAgent floor, plus the version the hub's + day-0 manifest names; CI asserts exactly these (D50 = A).** *Operator ruling 2026-10-25 evening.* Options: A) this + — cost: a box on a version outside the set cannot fetch its own binary again (none is, on the day); B) the newest 10, + never pruned by hand — cost: today's four are gone, so CI stays red until ten new versions exist. Why A: it is what + the operator prunes to, and the two pinned versions are the only old ones anything still asks for. Built: agent + `scripts/retention-policy.json` (`generic_versions_kept` 5, `pinned`) + `check-published-versions.py`; the pins are + copies of hub state, updated by the session that records a vouch or a new MinAgent (R-1290). + ### 2026-10-25 (attended release run) — D49: a restore that does not start keeps both copies; D44–D46: the review picks 258. **A restore whose app does not start keeps the restored data AND the old copy, tells the operator, and never puts the diff --git a/documentation/audits/night-2026-10-25/part1-artifacts.txt b/documentation/audits/night-2026-10-25/part1-artifacts.txt new file mode 100644 index 00000000..8743a904 --- /dev/null +++ b/documentation/audits/night-2026-10-25/part1-artifacts.txt @@ -0,0 +1,14 @@ +# Part 1 — what a box or the installer can still ask for (read only), 2026-10-25 ~19:00Z +# The operator pruned old controller, agent and golden packages by hand on 2026-10-25 (about the last 5; 10 for the controller). +hub manifest (hub.db + WAL copy, read only, deleted after): agent 0.165.0 e9d04a47…, bundle 8dea1f34…, golden 0.331.0 3881de62…, MinAgent 0.158.1, global floor 0.343.0 +registry now: agent 0.158.1, 0.161.0–0.165.0, 0.163.0-step1 | golden 0.311.0, 0.312.0, 0.313.0, 0.318.0, 0.329.0, 0.331.0 | controller 0.331.0, 0.341.0, 0.342.0, 0.343.0 (+ rc) | hub 0.165.0–0.170.0 | samba 1.0.0–1.2.0 +anonymous downloads (HTTP, bytes, sha256 prefix): + felhom-agent/0.165.0/felhom-agent 200 16286780 e9d04a47ee85222c = manifest + felhom-agent/0.165.0/felhom-config-bundle.json 200 370108 8dea1f34d77ba40a = manifest + felhom-agent/0.158.1/felhom-agent 200 14919180 20264df64ef646a8 (the MinAgent floor) + felhom-agent/0.158.1/felhom-config-bundle.json 200 345462 c23c3d355fe70a6f + felhom-golden/0.331.0/golden.tar.zst 200 637197198 3881de62b0986ccb = manifest +agent tags v0.158.1 … v0.165.0 (10): raw/tag//configs/felhom-os-apply → 200 each +controller images (docker manifest inspect from guest 9202, no login): 0.343.0 OK, 0.342.0 OK (rollback), 0.331.0 OK (the golden's), felhom-samba:1.2.0 OK (the pinned one); control 0.340.0 MISSING (pruned) — the check can fail +ISO iso.felhom.eu felhom-installer-1.30.0-pve9.2-1.iso 200 (1705324544 B); its manifest names only host-install-url https://felhom.eu/scripts/felhom-host-install.sh (fetched at run time — the ISO embeds no installer) → 200 +VERDICT: nothing that is still named is missing. diff --git a/documentation/audits/night-2026-10-25/part2-installer-messages.txt b/documentation/audits/night-2026-10-25/part2-installer-messages.txt new file mode 100644 index 00000000..034ca69d --- /dev/null +++ b/documentation/audits/night-2026-10-25/part2-installer-messages.txt @@ -0,0 +1,11 @@ +SCRIPT_VERSION="1.36.1" # the SINGLE version source (F-1): -h and the run banners follow it. +e18cdd17823911e7d87da12fcb8d48b6827f1706a84b3168114fe19fe30aa1f1 +== a customer id that does not exist +[ERROR] customer 'no-such-customer-cc' not found in hub (404) — create it in the hub first +[ERROR] PRE-FLIGHT FAIL (exit 1) — fix the finding above and re-run +== Tester4 with a wrong passphrase +[ERROR] passphrase REJECTED (401) for customer 'Tester4' +[ERROR] PRE-FLIGHT FAIL (exit 1) — fix the finding above and re-run +[ERROR] the hub answered 409 on the config preflight (GET /config): this customer already has a live box, which uses its own key. + HU: a hub 409-cel válaszolt (the config preflight (GET /config)): ennek az ügyfélnek már van élő gépe, amely a saját kulcsát használja. +[ERROR] PRE-FLIGHT FAIL (exit 1) — fix the finding above and re-run diff --git a/documentation/audits/night-2026-10-25/r1261-qbittorrent-api-measured.txt b/documentation/audits/night-2026-10-25/r1261-qbittorrent-api-measured.txt new file mode 100644 index 00000000..f3e51331 --- /dev/null +++ b/documentation/audits/night-2026-10-25/r1261-qbittorrent-api-measured.txt @@ -0,0 +1,16 @@ +0. fresh: + mode=0 uploadLimit=0 prefs={'up_limit': 0, 'alt_up_limit': 10240, 'scheduler_enabled': False} +1. setUploadLimit 2500000 (normal mode): + mode=0 uploadLimit=2499584 prefs={'up_limit': 2499584, 'alt_up_limit': 10240, 'scheduler_enabled': False} +2. setPreferences alt_up_limit 3072000: + mode=0 uploadLimit=2499584 prefs={'up_limit': 2499584, 'alt_up_limit': 3072000, 'scheduler_enabled': False} +3. toggleSpeedLimitsMode (to alt): + mode=1 uploadLimit=3072000 prefs={'up_limit': 2499584, 'alt_up_limit': 3072000, 'scheduler_enabled': False} +4. setUploadLimit 1024 in ALT mode: + mode=1 uploadLimit=1024 prefs={'up_limit': 2499584, 'alt_up_limit': 1024, 'scheduler_enabled': False} +5. setPreferences up_limit 5120000 in ALT mode: + mode=1 uploadLimit=1024 prefs={'up_limit': 5120000, 'alt_up_limit': 1024, 'scheduler_enabled': False} +6. toggle back to normal: + mode=0 uploadLimit=5120000 prefs={'up_limit': 5120000, 'alt_up_limit': 1024, 'scheduler_enabled': False} +7. setPreferences up_limit 0: + mode=0 uploadLimit=0 prefs={'up_limit': 0, 'alt_up_limit': 1024, 'scheduler_enabled': False} diff --git a/documentation/audits/night-2026-10-25/redproof-d50.txt b/documentation/audits/night-2026-10-25/redproof-d50.txt new file mode 100644 index 00000000..3a8dc8a8 --- /dev/null +++ b/documentation/audits/night-2026-10-25/redproof-d50.txt @@ -0,0 +1,4 @@ +# D50 decoys against the OLD gate (check-published-versions.py at 6b6ba0c), 2026-10-25 +FAIL: published: GENUINE (D50): a pinned version older than the window is asserted and downloads: rc=0 expected 0; missing ['verifying: 0.8.0'] +FAIL: published: FACT (D50): a pinned version older than the window whose package 404s: rc=0 expected 1 - LIVE HOLE; missing ['FAIL v0.8.0', 'binary NOT downloadable'] +FAIL: published: FACT (D50): a pin that names a version with no tag: rc=0 expected 1 - LIVE HOLE; missing ['FAIL pinned day0_agent=0.150.9', 'no git tag'] diff --git a/documentation/audits/night-2026-10-25/redproof-r1261.txt b/documentation/audits/night-2026-10-25/redproof-r1261.txt new file mode 100644 index 00000000..af0d8586 --- /dev/null +++ b/documentation/audits/night-2026-10-25/redproof-r1261.txt @@ -0,0 +1,22 @@ +# R-1261 red-proofs: each fix switched off in turn (controller working tree, 2026-10-25) +## 1. hold only the first running app +=== RUN TestR1261_EverySeederIsHeldAndEveryLimitComesBack + linehold_r1261_test.go:90: during the test qbittorrent={Bytes:1024 Alt:false Enabled:false KBps:0} transmission={Bytes:0 Alt:false Enabled:true KBps:500} — every seeder must be at the test value +--- FAIL: TestR1261_EverySeederIsHeldAndEveryLimitComesBack (0.00s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/upwatch 0.007s +## 2. no installed check +=== RUN TestR1261_ARecordOfAnAppRemovedWhileHeldIsDropped + linehold_r1261_test.go:121: the record of a removed app must be dropped, got &{App:qbittorrent Prev:{Bytes:2499584 Alt:false Enabled:false KBps:0} Held:{Bytes:1024 Alt:false Enabled:false KBps:0} At:} +--- FAIL: TestR1261_ARecordOfAnAppRemovedWhileHeldIsDropped (0.00s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/upwatch 0.007s +## 3. the release reads the current mode's slot +=== RUN TestR1261_ASpeedModeSwitchDuringTheTestNeverLeavesTheTestValue +=== RUN TestR1261_ASpeedModeSwitchDuringTheTestNeverLeavesTheTestValue/hold_in_alt_mode=false + linehold_r1261_test.go:168: after the test up_limit=1024 alt_up_limit=3072000, want 2499584 and 3072000 — never the 1 KiB/s test value +=== RUN TestR1261_ASpeedModeSwitchDuringTheTestNeverLeavesTheTestValue/hold_in_alt_mode=true + linehold_r1261_test.go:168: after the test up_limit=2499584 alt_up_limit=1024, want 2499584 and 3072000 — never the 1 KiB/s test value +--- FAIL: TestR1261_ASpeedModeSwitchDuringTheTestNeverLeavesTheTestValue (0.01s) +FAIL +FAIL gitea.dooplex.hu/admin/felhom-controller/internal/upwatch 0.012s diff --git a/documentation/audits/night-2026-10-25/rig/deploy-torrents.txt b/documentation/audits/night-2026-10-25/rig/deploy-torrents.txt new file mode 100644 index 00000000..05ced333 --- /dev/null +++ b/documentation/audits/night-2026-10-25/rig/deploy-torrents.txt @@ -0,0 +1,8 @@ +20:43:20 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'QBT_ADMIN_PASSWORD'] +20:43:20 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +20:43:35 [1] deployed, controller state=running, pinned={'qbittorrent': 'qbittorrentofficial/qbittorrent-nox:5.2.4-1'} +qbittorrent deployed True running +20:43:35 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'TR_ADMIN_PASSWORD'] +20:43:35 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} +20:43:55 [1] deployed, controller state=running, pinned={'transmission': 'lscr.io/linuxserver/transmission:version-4.1.3-r0'} +transmission deployed True running diff --git a/documentation/audits/night-2026-10-25/rig/drive-init.txt b/documentation/audits/night-2026-10-25/rig/drive-init.txt new file mode 100644 index 00000000..6ba148c3 --- /dev/null +++ b/documentation/audits/night-2026-10-25/rig/drive-init.txt @@ -0,0 +1,2 @@ +init 200 {'data': {'phase': 'formatting', 'started': True}, 'ok': True} +status {'data': {'device': '/dev/sdb', 'durable_id': '', 'error': '', 'opsign': '', 'phase': 'done', 'reason': '', 'started_at': '2026-10-25T19:42:47.597592227Z', 'updated_at': '2026-10-25T19:42:49.836540017Z', 'where': '/mnt/felhom-drives/adat'}, 'ok': True} diff --git a/documentation/audits/night-2026-10-25/rig/hold-watch.txt b/documentation/audits/night-2026-10-25/rig/hold-watch.txt new file mode 100644 index 00000000..e69de29b diff --git a/documentation/audits/night-2026-10-25/rig/install-console.txt b/documentation/audits/night-2026-10-25/rig/install-console.txt new file mode 100644 index 00000000..a72cb307 --- /dev/null +++ b/documentation/audits/night-2026-10-25/rig/install-console.txt @@ -0,0 +1,163 @@ + +[INFO] felhom-host-install v1.36.1 — mode=byo customer=Tester4 vmid=9201 + +[STEP] 1/8 pre-flight +[INFO] pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve) +[INFO] node: pf-test (explicit) +[INFO] agent config: /etc/felhom-agent/agent.json +[INFO] agent: not installed yet — will be fetched + installed in step 5/8 +[INFO] local-lvm free: ~10 GiB +[WARN] local-lvm free ~10 GiB is below the recommended 120 GiB — the install continues; proceed only if you sized the grows deliberately +[INFO] free RAM: ~4383 MiB +[INFO] existing guests on this host: 0 (pct+qm) +[INFO] archive storage 'local' present +[INFO] acl storages all present or pre-positioned: local local-lvm +[INFO] dnsmasq: not present before Felhom — recorded; uninstall will remove it again if we install it +[INFO] host DNS (:53): free +[INFO] hub reachable (https://hub.felhom.eu) +[OK] customer 'Tester4' exists + passphrase valid +[INFO] golden: none local — will fetch + verify from Gitea in step 7/8 +pf-test + +[STEP] BYO disclosure — installing Felhom on a host the operator does NOT own will create: + pveum: roles FelhomAgentBase/FelhomAgentGuest/FelhomAgentStore + user felhom-agent@pve + token felhom-agent@pve!agent + + scoped ACL grants (Base@/, Guest@/pool/felhom, Store@[local local-lvm]) + the 'felhom' pool + agent: service user felhom-agent + /usr/local/bin/felhom-agent (+ .bak-* backups; A/B slots via the guarded self-update) + + /etc/systemd/system/felhom-agent.service (+ felhom-agent-rollback.service + start-limit drop-in) + + /etc/sudoers.d/felhom-agent + state dir /var/lib/felhom-agent + config /etc/felhom-agent/agent.json (0600) + host: /usr/local/sbin/felhom-mkfs-guarded + /usr/local/sbin/felhom-selfupdate-guarded + + /usr/local/sbin/felhom-pbs-apply (PBS-DR apply wrapper — DR capability is baked + on every install; ACTIVATION stays a hub flag, off = zero effect on this host) + + felhom-mgmt-watchdog service+timer+script + /etc/tmpfiles.d/felhom-privsep.conf + + /usr/local/sbin/felhom-priv-apply + the guest-hook snippet under /var/lib/vz/snippets/ + (both from the agent's config bundle) + + the 'sudo' and 'age' packages if absent + install state dir /var/lib/felhom-install + wg: an OUTBOUND WireGuard tunnel to the Felhom hub (wg_tunnel.enabled=true — base + infrastructure like the cloudflared tunnel; hands-free peer registration; the + operator can disable it per-customer hub-side on request) + guest: the provisioned Felhom LXC (vmid 9201, capped 3 cores / 4096 MiB) + its volumes + + the golden vzdump imported onto storage 'local' + reuse: a golden already on 'local' is used only if its sha256 or its controller version + matches the hub's vouched golden; otherwise the vouched one is fetched (a --golden you name is refused) + update: operator-signed self-update authority: felhom-op-1 + NOT touched in byo mode: root@pam (no break-glass), host DNS (:53), OOB sshd. + +Type this host's short hostname (pf-test) to acknowledge the byo install: [OK] byo install acknowledged for host pf-test +[OK] pre-flight passed +[STEP] 2/8 Proxmox API token +[OK] token minted (secret captured, not logged) +[OK] scoped ACL applied (Base@/, Guest@/pool/felhom + /vms/990000..990009, Store@[local local-lvm]) +[SKIP] old broad role FelhomAgent already absent +[STEP] 3/8 compute volume grows +[INFO] auto-computed from a 10 GiB thin pool (16 §7: 50/100/130 GiB) +[INFO] grows: rootfs +0G (->32G), data +26G (->50G, ONE volume) +[STEP] 4/8 host enrollment (POST /host-enroll) +[OK] host MINTED (first enroll) +[INFO] host_id: Tester4-84bbe7 (api_key captured, not logged) +[SKIP] break-glass (step 4b) SKIPPED (byo: the host owner manages break-glass; root@pam untouched, nothing vaulted) +[STEP] 5/8 agent install (fetch + verify + install) +[SKIP] apt repo alignment skipped (byo — the owner manages repos) +[WARN] no git credential in controller.yaml — fetching artifacts ANONYMOUSLY (they are world-readable; sha256 verification unchanged) +[INFO] manifest: agent v0.165.0 (sha e9d04a47ee85222c…), golden v0.331.0 +[INFO] fetching agent binary v0.165.0 from Gitea … +[OK] verified sha256 e9d04a47ee85222c… matches the hub manifest +[OK] installed /usr/local/bin/felhom-agent (felhom-agent 0.165.0) +[INFO] installing the 'sudo' package (required for the non-root agent model) … +[OK] sudo installed (Sudo version 1.9.16p2) +[INFO] installing the 'age' package (escrow ceremony identity-wrap dependency) … +[OK] age installed (1.2.1) +[OK] created service user felhom-agent +[OK] added felhom-agent to systemd-journal (unprivileged journal read for NAS verify) +[OK] verified sha256 8dea1f34d77ba40a… matches the hub manifest + os-apply: BUNDLE /etc/ssh/sshd_config.d/10-felhom-root-key-only.conf skipped: a BYO host keeps its own settings (D43) + os-apply: BUNDLE START agent=0.165.0 sha=8dea1f34d77ba40a authority=installer files=32 write=26 same=1 kept=0 skipped=5 + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-mkfs-guarded (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-selfupdate-guarded (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-pbs-apply (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-backup-target-apply (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-vzdump-partial-clean (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-thin-check (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-internal-place (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-crash-guard (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-priv-apply (new) + os-apply: BUNDLE WROTE /var/lib/vz/snippets/felhom-guest-hook.sh (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-shared-parent.sh (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-shared-parent.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-crash-guard.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-crash-guard-check.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-crash-guard-check.timer (new) + os-apply: BUNDLE WROTE /etc/felhom/crash-guard.conf (new) + os-apply: BUNDLE WROTE /etc/grub.d/01_felhom_oneshot (new) + os-apply: BUNDLE WROTE /etc/grub.d/42_felhom_oneshot (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-agent.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-agent-rollback.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-agent.service.d/felhom-agent-limits.conf (new) + os-apply: BUNDLE WROTE /usr/local/sbin/felhom-mgmt-watchdog (new) + os-apply: BUNDLE WROTE /etc/tmpfiles.d/felhom-privsep.conf (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-mgmt-watchdog.service (new) + os-apply: BUNDLE WROTE /etc/systemd/system/felhom-mgmt-watchdog.timer (new) + os-apply: BUNDLE WROTE /etc/sudoers.d/felhom-agent (new) + os-apply: BUNDLE DONE agent=0.165.0 written=26 same=1 self-check=ok signers-created=False +[OK] installed every root-owned file from config bundle v0.165.0 (sha 8dea1f34d77ba40a…; checked, previous copies kept) +[SKIP] OOB (felhom-sshd) off (byo, or appliance --no-oob) — the bundle skipped its files +[STEP] 5b/8 internal data place (Belső tárhely) +[SKIP] byo: the host owner's thin pool is not ours to divide — no internal place +[STEP] 6/8 agent config + service +[WARN] backup target: DEGRADED — no eligible second drive, so the whole-system backup stays on the SYSTEM drive. +[WARN] It protects against file corruption but NOT against a disk failure. Attach a second drive and assign it in the dashboard. +[INFO] node=pf-test local_api=192.168.0.150:8443 tls_fp=65:D5:C9:E7:A0:4C… +[OK] wrote /etc/felhom-agent/agent.json (0600 felhom-agent) +[OK] byo config asserts passed (lan_resolver.enable / oob.enabled off; deployment_mode=byo; wg_tunnel intentionally ON — base infrastructure) +[OK] agent --selftest (read-only) passed +[OK] felhom-agent service active (non-root felhom-agent reads the config OK) +[OK] wrote /etc/felhom/os-trust.json + operator-signers (felhom-op-1) — the root-owned slow-lane anchors +[STEP] 7/8 golden archive +[WARN] no git credential in controller.yaml — fetching artifacts ANONYMOUSLY (they are world-readable; sha256 verification unchanged) +[INFO] fetching golden v0.331.0 from Gitea → /var/lib/vz/dump/vzdump-lxc-9100-2026_10_25-20_25_22.tar.zst +[OK] verified sha256 3881de62b0986ccb… matches the hub manifest +[OK] golden imported + verified: local:backup/vzdump-lxc-9100-2026_10_25-20_25_22.tar.zst +[STEP] 8/8 provision guest 9201 +[SKIP] pool felhom already exists +=== felhom-agent 0.165.0 selftest=provision (vmid=9201 customer=Tester4 hostname=tester4) === + --- front half: bring-up (provision) local:backup/vzdump-lxc-9100-2026_10_25-20_25_22.tar.zst → vmid 9201 --- +time=2026-10-25T20:26:42.227+01:00 level=INFO msg="bring-up: pool membership re-asserted" vmid=9201 pool=felhom + [OK] front half: vmid 9201 up (boot+running) in 1m9s; MAC=BC:24:11:09:4B:78 + --- back half: mint per-guest token + populate bootstrap config mount --- +time=2026-10-25T20:26:46.965+01:00 level=INFO msg="provision: back-half complete" vmid=9201 mount=mp9 guest_path=/etc/felhom-bootstrap endpoint=192.168.0.150:8443 + [OK] back half: bootstrap mount mp9 → /etc/felhom-bootstrap on vmid 9201 (host dir /var/lib/felhom-agent/guests/9201/bootstrap) + local-api endpoint 192.168.0.150:8443 · leaf fp 2096006172416bcd1bfe974a512763dcd920be6f0c4127f9e0de83ebe83810a9 · token: minted (not printed) +=== selftest=provision OK — guest 9201 provisioned + bootstrap-mounted (KEPT) === + next: reboot the guest → the golden's baked controller-bootstrap unit deploys the controller, + which PULLS its controller.yaml from the hub (retrieval passphrase) and merges in this local_api. +[OK] provision completed +[INFO] rebooting guest 9201 so the baked controller-bootstrap unit picks up the mount +[STEP] verify +[OK] pct status: running +[OK] onboot: 1 + mp0: local-lvm:vm-9201-disk-1,mp=/var/lib/felhom,backup=1,size=50G + mp8: /mnt/felhom-drives,mp=/mnt/felhom-drives + rootfs: local-lvm:vm-9201-disk-0,size=32G +[OK] pool: guest 9201 is a member of felhom +[OK] acl: FelhomAgentBase@/ present (user+token) +[OK] acl: FelhomAgentGuest@/pool/felhom present (user+token) +[OK] acl: FelhomAgentStore@/storage/local present (user+token) +[OK] acl: FelhomAgentStore@/storage/local-lvm present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990000 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990001 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990002 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990003 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990004 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990005 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990006 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990007 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990008 present (user+token) +[OK] acl: FelhomAgentGuest@/vms/990009 present (user+token) +[OK] authz signers: 2 (operator-signed self-update armed) +[OK] controller: Up 6 seconds (healthy) (after ~0s) +[INFO] controller image: gitea.dooplex.hu/admin/felhom-controller:0.343.0 +[WARN] cloudflared not visible yet +[INFO] (confirm in the hub UI that host Tester4-84bbe7 reports guest 9201) + +[OK] Day-0 provision SUCCESS — vmid=9201 host_id=Tester4-84bbe7 customer=Tester4 golden=local:backup/vzdump-lxc-9100-2026_10_25-20_25_22.tar.zst +INSTALL_RC=0 +Connection to 192.168.0.150 closed. diff --git a/documentation/audits/night-2026-10-25/tools/mktorrent.py b/documentation/audits/night-2026-10-25/tools/mktorrent.py new file mode 100644 index 00000000..7bd9783b --- /dev/null +++ b/documentation/audits/night-2026-10-25/tools/mktorrent.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""mktorrent.py — a single-file, PRIVATE (no DHT/PEX) v1 torrent, 4 MiB pieces. +The night's R-1261 rig only; stdlib only.""" +import hashlib, os, sys + +def benc(x): + if isinstance(x, int): + return b"i%de" % x + if isinstance(x, bytes): + return b"%d:%s" % (len(x), x) + if isinstance(x, str): + return benc(x.encode()) + if isinstance(x, list): + return b"l" + b"".join(benc(i) for i in x) + b"e" + if isinstance(x, dict): + return b"d" + b"".join(benc(k) + benc(v) for k, v in sorted(x.items())) + b"e" + raise TypeError(type(x)) + +path, announce, out = sys.argv[1:4] +plen = 4 << 20 +pieces = [] +with open(path, "rb") as f: + while True: + b = f.read(plen) + if not b: + break + pieces.append(hashlib.sha1(b).digest()) +info = {"name": os.path.basename(path), "length": os.path.getsize(path), "piece length": plen, + "pieces": b"".join(pieces), "private": 1} +open(out, "wb").write(benc({"announce": announce, "info": info, "created by": "felhom CC rig 2026-10-25"})) +print(out, hashlib.sha1(benc(info)).hexdigest()) diff --git a/documentation/audits/night-2026-10-25/tools/rigw.py b/documentation/audits/night-2026-10-25/tools/rigw.py new file mode 100644 index 00000000..d5a1fba6 --- /dev/null +++ b/documentation/audits/night-2026-10-25/tools/rigw.py @@ -0,0 +1,32 @@ +"""rigw.py — the box walk (catalog-sweep-2026-10-24/tools/box_walk.py) pointed at the night's rig: nested VM 9372 on +demo-hp (Proxmox host 192.168.0.150, BYO install, customer Tester4), guest 9201 at 192.168.0.138, domain tester4.test. +Import this instead of box_walk; it sets the target before box_walk reads it, then points the ssh at the rig host.""" +import os, sys +os.environ.setdefault("TARGET", "sweep") +os.environ.setdefault("BASE", "https://192.168.0.138") +os.environ.setdefault("DOMAIN", "tester4.test") +sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../catalog-sweep-2026-10-24/tools")) +import box_walk as w # noqa: E402 +w._T["ssh"] = "root@192.168.0.150" +w._T["hp"] = "rig-9372" + + +def _login(): + """box_walk.login, but the token is read from /settings: on this fresh box „/" redirects to /launcher, which + carries no csrf meta (measured 2026-10-25).""" + import re + pw = open(f"{w.SC}/.ctlpw").read().strip() + w.sh(["curl", "-sk", "-D", f"{w.SC}/hdr{os.getpid()}.txt", "-o", "/dev/null", "-H", w.HOSTHDR, + "-X", "POST", "--data-urlencode", f"password={pw}", f"{w.BASE}/login"]) + m = re.search(r"felhom_session=[A-Za-z0-9._-]+", open(f"{w.SC}/hdr{os.getpid()}.txt").read(), re.I) + if not m: + sys.exit("login failed: no session cookie") + open(f"{w.SC}/sess{os.getpid()}.txt", "w").write(m.group(0)) + r = w.sh(["curl", "-sk", "-H", w.HOSTHDR, "-H", f"Cookie: {m.group(0)}", f"{w.BASE}/settings"]) + c = re.search(r' {(ip, port): last_seen} + +def benc(d): + out = b"d" + for k in sorted(d): + v = d[k] + out += b"%d:%s" % (len(k), k.encode()) + out += (b"i%de" % v) if isinstance(v, int) else (b"%d:%s" % (len(v), v)) + return out + b"e" + +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + u = urllib.parse.urlsplit(self.path) + q = urllib.parse.parse_qs(u.query, keep_blank_values=True, encoding="latin-1") + ih = q.get("info_hash", [""])[0].encode("latin-1") + port = int(q.get("port", ["0"])[0] or 0) + ip = self.client_address[0] + now = time.time() + peers = PEERS.setdefault(ih, {}) + if q.get("event", [""])[0] == "stopped": + peers.pop((ip, port), None) + else: + peers[(ip, port)] = now + others = [p for p, t in peers.items() if p != (ip, port) and now - t < 600] + compact = b"".join(socket.inet_aton(a) + struct.pack(">H", pt) for a, pt in others) + body = benc({"interval": 30, "peers": compact}) + self.send_response(200) + self.send_header("Content-Type", "text/plain") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + sys.stdout.write("%s announce %s:%d -> %d peer(s)\n" % (time.strftime("%H:%M:%S"), ip, port, len(others))) + sys.stdout.flush() + + def log_message(self, *a): + pass + +http.server.ThreadingHTTPServer(("0.0.0.0", int(sys.argv[1]) if len(sys.argv) > 1 else 6969), H).serve_forever() diff --git a/documentation/backlog/CLOSED-ITEMS.md b/documentation/backlog/CLOSED-ITEMS.md index 810fec69..aeb117d4 100644 --- a/documentation/backlog/CLOSED-ITEMS.md +++ b/documentation/backlog/CLOSED-ITEMS.md @@ -26,6 +26,13 @@ --- +## 2026-10-25 (evening) — installer 1.36.1 published; D50 (full text of the rows: `git show b8411926:documentation/backlog/OPEN-ITEMS.md`) + +| Row | What | Closed | Evidence | +|---|---|---|---| +| **R-1284** | **The installer's preflight never words a refusal: `http_code` printed the status twice („401401"), so the 401 („passphrase REJECTED"), 404 („customer not found") and 409 (R-1167's live-box sentence) branches never matched — every refusal read „unexpected hub status 401401".** (P3) | CLOSED 2026-10-25 evening — **installer 1.36.1 published on the operator's yes:** tag `installer-v1.36.1` → `79ee3e33` (CI 2336 ✓), both git-sync refs in `manifests/webpage.yaml` (`b8411926`), ArgoCD sync of `Deployment/felhom-webpage` alone; `https://felhom.eu/scripts/felhom-host-install.sh` serves `SCRIPT_VERSION="1.36.1"`, sha256 `e18cdd17…` = the tag's file. Seen live on a throwaway Proxmox VM (demo-hp VM 9372): a wrong customer id → „customer … not found in hub (404)", a wrong passphrase → „passphrase REJECTED (401)", the right passphrase on a customer with a live box → „the hub answered 409 … already has a live box" — **R-1167's sentence, which could not show before 1.36.1, now shows.** | `audits/night-2026-10-25/` | +| **R-1290** | **The agent's CI is red since 2026-10-25 ~18:00Z: the registry no longer serves agent 0.159.0, 0.159.1, 0.159.2 and 0.160.0, while the `published` gate asserts the newest 10 released versions are downloadable (`felhom-agent/scripts/retention-policy.json`, „what the registry keeps").** (P3) | CLOSED 2026-10-25 evening — **D50 = A** (operator; `09` §3 decision 262): agent `feaaa0e` (CI 2339 ✓) — `retention-policy.json` keeps the newest 5 + `pinned` (MinAgent 0.158.1, day-0 0.165.0); `check-published-versions.py` asserts exactly those (a pin with no tag fails); three decoys, two of them LIVE HOLES on the old gate. Every artifact still named downloads (`audits/night-2026-10-25/part1-artifacts.txt`). | `audits/night-2026-10-25/` | + ## 2026-10-25 — the attended release run (full text of the rows: R-1265, R-1266 `git show 9647f1d1:documentation/backlog/OPEN-ITEMS.md`; the release closures `git show 36ef8dcd:documentation/backlog/OPEN-ITEMS.md`) | Row | What | Closed | Evidence | diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index 3af1f44d..21dd06b5 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -113,7 +113,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 — 8 rows (P3 6, P4 2) +## Install & onboarding — 7 rows (P3 5, P4 2) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -123,7 +123,6 @@ stopping line that lies. | **R-1273** | Install & onboarding | P3 | **An install that fails on the image registry's rate limit tells the household only „exit code 1”.** Catalog sweep 2026-10-24, code-server 16:01:37: `lscr.io … toomanyrequests` → the product cleaned up right (R-649) but the event and the card say „Alkalmazás telepítése nem sikerült: Code-Server — exit code 1”; nothing says the download server is busy and a later try works. The same install went through later (`audits/catalog-sweep-2026-10-24/code-server/`). | OPEN | — | Recognise `toomanyrequests` (and a pull timeout) in the deploy error and say „the app's download server is busy — try again in an hour”; a test with the stderr line | CC | | **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-1284** | Install & onboarding | P3 | **The installer's preflight never words a refusal: `http_code` printed the status twice („401401"), so the 401 („passphrase REJECTED"), 404 („customer not found") and 409 (R-1167's live-box sentence) branches never matched — every refusal read „unexpected hub status 401401".** Seen live 2026-10-25 on the BYO test VM (attended release run) with installer 1.36.0: a wrong id read `404404`, a wrong passphrase `401401`. R-1167 was CLOSED 2026-10-20 on a publish whose 409 sentence could not show. With `-f`, curl prints `-w` and exits 22 on a 4xx, and the fallback curl printed the code again (line 1833 already worked around it by keeping the last 3 characters). | OPEN — **fixed on main, NOT published:** installer 1.36.1 (`http_code`: one curl without `-f`, never failing); `test_http_code_prints_one_status_on_a_refusal`, seen failing on the old helper (`audits/release-2026-10-26/redproof/installer-http-code-doubled.txt`). | the publish is a DooPlex change (tag + webpage git-sync pins + the webpage sync) — the operator's | publish installer 1.36.1 (cut `installer-v1.36.1`, bump both `--ref` in `manifests/webpage.yaml`, sync the webpage alone); then a preflight with a wrong passphrase reads „passphrase REJECTED (401)"; then close | operator | ## Apps & catalog — 16 rows (P3 6, P4 10) @@ -353,7 +352,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 — 33 rows (P3 8, P4 25) +## Process & tooling — 32 rows (P3 7, P4 25) | ID | Category | Sev | What | State | Blocked on | Next action | Owner | |---|---|---|---|---|---|---|---| @@ -389,7 +388,6 @@ stopping line that lies. | **R-1236** | Process & tooling | P3 | **Gitea (the code and the registry) is OOM-killed at its 1 GiB limit — 4 restarts in 17 days, the last at 06:41:10 UTC on 2026-10-23, six seconds after the global floor moved three boxes to a new controller image.** Read 2026-10-23 (`kubectl get pod`: `lastState.terminated.reason OOMKilled`, exit 137; kernel: „git invoked oom-killer”; limit `memory: 1Gi`, request 256Mi; 620 Mi in use at rest). During a restart a box update, an agent package fetch, a CI clone or a push fails. Not changed: DooPlex's configuration is the operator's. | OPEN | the operator's yes (a DooPlex change) | Operator: raise the limit (e.g. 2 GiB; DooPlex has the RAM) — CC's pick; or keep it and accept a restart in release hours | operator | | **R-1264** | Process & tooling | P4 | **Skills tooling (review round 2, DOC-B U7/S22):** the grandfathered skill has no size cap — felhom-build-deploy is 240 lines while R-394 recorded 179 (`scripts/check_skills.py:45-47,98-101`); a NEW skill folder (`skills/felhom-web-design`) is not live until `install_skills.py` runs again (not run: no DooPlex change in this unattended run). | OPEN | — | cap the grandfathered entry at its current size; run install_skills.py in an attended session | CC | | **R-1277** | Process & tooling | P4 | **The catalog's box fixtures drifted from the apps they test** — fixed only in the sweep's copy (`audits/catalog-sweep-2026-10-24/tools/upgrade_fixtures_box.py`, marked SWEEP COPY): OpenGist 1.15 signs up at `/-/register`; Calibre-Web's admin is the generated ADMIN_USER; Mealie's after_install replaces the factory password (the seed raced it); Papra 26.6 needs an Origin header; Rallly 4.15 closed password sign-up (no fixture possible by password); Vaultwarden 1.37.4 answers 404 on `POST /identity/accounts/register` (the fixture was written on 1.36; the admin invite works, the invited sign-up has no route — the newer clients' register/finish flow);the read-back through a family gate needs a fresh cookie after the setup gate opens (Grimmory stayed inconclusive). New fixtures for Homebox, Immich, Komga, Jellyfin, Ghost, Radarr, Sonarr in `tools/sweep_fixtures.py`; `box_walk.py` gained a `sweep` target. | OPEN | — | Port them into `app-catalog-felhom.eu/scripts/` with the bench tests | CC | -| **R-1290** | Process & tooling | P3 | **The agent's CI is red since 2026-10-25 ~18:00Z: the registry no longer serves agent 0.159.0, 0.159.1, 0.159.2 and 0.160.0, while the `published` gate asserts the newest 10 released versions are downloadable (`felhom-agent/scripts/retention-policy.json`, „what the registry keeps").** Measured 2026-10-25 (attended release run): CI job 2326 (`4697f8a`, after the 0.165.0 upload) green; job 2334 (`6b6ba0c`, docs only) red on those four (HTTP 404); the registry lists 7 versions — 0.158.1, 0.161.0–0.165.0 and a test build `0.163.0-step1`. Our scripts delete only the version they re-upload (`publish-agent.sh:77`), so the prune is outside the repos (a Gitea package clean-up rule or a hand prune on DooPlex — not CC's to change). No box needs those versions (all run 0.165.0; MinAgent 0.158.1 is served), but every agent push now mails a CI failure and hides a real one. | OPEN | the operator (DooPlex's Gitea) | the operator: name what pruned on 2026-10-25 and its keep rule; then CC sets `generic_versions_kept` to that rule (or bounds the gate on the hub's MinAgent, R-287's follow-up) in one commit, and CI is green again by hash | operator |