diff --git a/documentation/architecture/00-capability-map.md b/documentation/architecture/00-capability-map.md index dac9c762..b7bccdf4 100644 --- a/documentation/architecture/00-capability-map.md +++ b/documentation/architecture/00-capability-map.md @@ -103,7 +103,7 @@ | ID | Capability | Component | Status | Scope | Evidence | |---|---|---|---|---|---| | [B1](capability-map-detail/B.md#b1) | Deploy an app from the catalog (env config, memory guard, health-aware progress) | controller, catalog (65 apps, images pinned) | **PROVEN-LIVE** | **re-checked 2026-10-24** on controller 0.342.0 (after 6d90374 + 2fe6341): all 65 apps through the deploy request on a fresh box — 62 installed and answered, Wanderer installed but cannot run without DNS (R-739), 2 not offered by lifecycle; the memory guard did not fire (one app at a time) | `audits/catalog-sweep-2026-10-24/README.md` (2026-10-24); `tests/CAMPAIGN-2-2026-07-07.md` (2026-07-07); `audits/RERUN-p1p3-2026-07-06.md` (2026-07-06) | -| [B2](capability-map-detail/B.md#b2) | App lifecycle: start/stop/restart/update/logs/remove/redeploy | controller | **PROVEN-LIVE** | the actions; remove-with-data re-proven 2026-09-13; **re-check (2026-10-24):** mechanism changed after its evidence — 8ae9178 (removal deletes labelled volumes, R-1117) | `audits/R442-2026-09-13/` (2026-09-13); `tests/CAMPAIGN-2-2026-07-07.md` (2026-07-07) | +| [B2](capability-map-detail/B.md#b2) | App lifecycle: start/stop/restart/update/logs/remove/redeploy | controller | **PROVEN-LIVE** | the actions; remove-with-data re-proven 2026-09-13; **re-checked 2026-10-28 night on 9202** after 8ae9178 (removal deletes labelled volumes, R-1117): install, stop, start, restart, logs, the protected stacks refuse a stop (403), remove with data → its labelled volume and container gone (`audits/night-2026-10-28/part5-claims/lifecycle-walk.*`) | `audits/R442-2026-09-13/` (2026-09-13); `tests/CAMPAIGN-2-2026-07-07.md` (2026-07-07) | | [B3](capability-map-detail/B.md#b3) | Update is GUARDED: it refuses without a restorable backup, backs up first when the copy is stale, and HOLDS an app that does not come up — on ANY backup tier, and the release itself arrives by the managed floor | controller v0.237.0 + v0.238.0 + v0.238.1 + v0.239.0, hub v0.112.0 | **PROVEN-LIVE** | — | `audits/slice4-2026-09-13/` (2026-09-13); `audits/rulings-r472-r475-2026-09-13/` (2026-09-13) | | [B4](capability-map-detail/B.md#b4) | A failed update is UNDONE by the box itself — the previous version back with its data from seconds before the update; the app is held only if that undo fails too | controller v0.263.2 (the undo), v0.264.0 + hub v0.120.0 (the mail) | **PROVEN-LIVE** | scratch 9202 | `audits/undo-live-2026-09-23/README.md` (2026-09-23) | | [B5](capability-map-detail/B.md#b5) | The catalog holds only TESTED steps — an image move without a proven test record (bench + box, digests the registry still serves) is refused at push time | catalog `6db08a5` (test-record gate, `ladder.py`) | **PROVEN-LIVE** | written PROVEN: the catalog gate against the live registry | `audits/DRILL-night-2026-09-23.md` (2026-09-23) | @@ -195,8 +195,8 @@ | [E5](capability-map-detail/E.md#e5) | Files from Windows Explorer / Mac Finder (SMB server) | controller v0.145.0 + `felhom-samba:1.0.0` | **PROVEN-LIVE** | — | *(evidence not on file — not re-verifiable)*: the row cites `felhom-controller` REPORT.md (v0.144.0, outside `documentation/`), the design `controller/sharing.md` and the transport verdict `audits/SPIKE-lan-discovery-2026-07-18.md` — none is a run of the shipped server | | [E6](capability-map-detail/E.md#e6) | Media to TV via DLNA | — | **MISSING** | — | R-6; R-8 | | [E7](capability-map-detail/E.md#e7) | File access via browser | FileBrowser (infra app, auto-mount sync) | **IMPLEMENTED** | browse/download through FileBrowser in no doc | `audits/storage-lifecycle-acceptance-2026-06-15.md` (2026-06-15); R-75 | -| [E8](capability-map-detail/E.md#e8) | Indítópult (app launcher) — one-tap grid of the household's openable apps | controller v0.163.0 | **IMPLEMENTED** | live click-through pending | R-72 | -| [E9](capability-map-detail/E.md#e9) | Indítópult megosztás (vendég link) — capability URL `/s/` serves a standalone read-only guest launcher (no account, no admin session); optional per-share password; QR | controller v0.165.0 | **IMPLEMENTED** | — | R-15 | +| [E8](capability-map-detail/E.md#e8) | Indítópult (app launcher) — one-tap grid of the household's openable apps | controller v0.163.0 | **PROVEN-LIVE** (2026-10-28 night, 9202, endpoint-level — the household's page lists a tile per openable app, each linking to its public address) | `audits/night-2026-10-28/part5-claims/launcher-walk.*` | R-72 | +| [E9](capability-map-detail/E.md#e9) | Indítópult megosztás (vendég link) — capability URL `/s/` serves a standalone read-only guest launcher (no account, no admin session); optional per-share password; QR | controller v0.165.0 | **PROVEN-LIVE** (2026-10-28 night, 9202, endpoint-level: switched on, opened with NO session — the apps, no admin link; a wrong token 404; rotate → the old link 404, the new 200; switched off → 404; the box left as found) | `audits/night-2026-10-28/part5-claims/launcher-walk.*` | R-15 | | [E10](capability-map-detail/E.md#e10) | Forgot dashboard password → instant reset code | controller v0.123, hub | **PROVEN-LIVE** | — | `audits/DRILL-day0-take2-2026-07-12.md` (2026-07-12) | | [E11](capability-map-detail/E.md#e11) | The dashboard in English — the household picks its language; Hungarian unchanged | controller v0.247.0 (spike) → v0.250.0 (slice 1, R-556 CLOSED) | **PARTIAL** | every dashboard template live; other surfaces open | `audits/i18n-slice1-2026-09-17/C/live/` (2026-09-17); `audits/i18n-2026-09-17/live/` (2026-09-17) | | [E12](capability-map-detail/E.md#e12) | The box tells visitors apart — a stranger's wrong passwords lock only the stranger (dashboard, setup gate, apps that read the visitor from the right) | controller v0.286.1, catalog router resets | **PARTIAL** | — | `audits/visitors-2026-10-01/A/L2-demo-hp-real-tunnel.txt` (2026-10-01); R-779 | diff --git a/documentation/architecture/03-host-agent.md b/documentation/architecture/03-host-agent.md index 8c0ff16f..34c41b68 100644 --- a/documentation/architecture/03-host-agent.md +++ b/documentation/architecture/03-host-agent.md @@ -569,6 +569,16 @@ never opens, re-seals or logs it; the audit (`escrow_carried`) carries host ids hosts → no carry, `escrow_carry_refused` to the operator. The box then sees „identity present", its mint guard (R-241) holds, and it offers the recovery screen. Proven live 2026-10-21 (`audits/fixes-2026-10-21/P1-live/`: the right code opened the household's history, Nextcloud's 3 031 files came back byte-identical, the next off-site run appended). +**[DESIGN] 2026-10-28 night — WHICH retained row is carried (R-1255 H3; felhom.eu `1ee6d5a5`, rides the next hub +release):** the newest retained row that has an identity blob AND a repository-password hash; only when no row has a +hash, the newest row as before (a legacy package whose hash was never recorded may still seal the password — never fewer +ways back). Before, the newest row alone was taken: a newer package without the password was carried while the +household's real one stayed under the deleted host. **And the set-aside purge (R-1255 H4):** a set-aside is named +`.orphaned-` (unique per run; was the date, which a same-day abandonment freed for the next +set-aside), and a retained row is purged only when it was tagged with the deleted path no later than that path's +deletion (`repo_tagged_at`; a row tagged before the column existed uses `superseded_at`) — a reused path's later copy is +still stored, and its packages stay. Store tests `TestR1255_*`, red before the fix; no live proof (a hub deploy is not +tonight's). #### The separation principle (the rule that governs every posture) Reading customer data needs **BOTH** the encrypted chunks **AND** a usable key. **Zero-knowledge @@ -652,6 +662,15 @@ nothing waiting 404, hub failure 502 (still waiting). A controller that has not R-978 order. Proven on demo-hp 2026-10-19: ceremony 1 typed back → code 1 opens the hub's package, code 0 is a kept earlier one (422); ceremony 2 shown and abandoned → code 1 still opens it, code 2 opens nothing (`audits/day-2026-10-19/P1/escrow-*.txt`). +**[DESIGN] 2026-10-28 night — a lost or late answer (R-1254 H2; agent `5ddd124` + controller `e33cffa`, unreleased):** +the upload at the claim or the confirm can lose its answer AFTER the hub stored the package, and the household was then +told „the previous code still works" — false. Now, after a failed upload, the agent asks the hub which package it holds +(`GET /api/v1/hosts//escrow`, the identity blob compared byte for byte, ciphertext only, 15 s): THIS package → a +success; another package → 502, the old sentence (now true); no answer → **504**, a sentence that promises nothing about +either code, the wait goes on. The controller words 504 and its own failed call as „Nem tudjuk biztosan… Őrizd meg +mindkét kódot…" (`api.escrow.confirm_store_unknown`), and waits up to 60 s for the claim and the confirm (was the +client's 15 s — shorter than the agent's 30 s upload, so an abandoned claim could store a package whose code reached +nobody). Tests `TestR1254_*` in both repos, red first. Live proof with the agent release (the vouch). #### Custody across generations — what the hub retains when a blob is superseded (hub v0.60.0; **contract corrected hub v0.93.0, R-198**) diff --git a/documentation/architecture/08-alarm-ladder.md b/documentation/architecture/08-alarm-ladder.md index eba37c55..bf1456fb 100644 --- a/documentation/architecture/08-alarm-ladder.md +++ b/documentation/architecture/08-alarm-ladder.md @@ -251,6 +251,19 @@ today's engine, and its build (read the counter of every container; „probably before any code, by the brief's measure-first rule — `09` §3 decision 155 stands for the operator to confirm or drop. Cost measured for the record: one `docker exec … cat memory.events` across demo-hp's 21 containers takes 1.6 s wall (0.47 s user, 0.44 s sys); cloudflared has no `cat` and reads as unknown. `audits/design-build-2026-10-06/C/`. +**[DESIGN] 2026-10-28 night — the host's counter beside Docker's flag (R-528, as designed in the row 2026-10-26; agent +`6f933d6` + controller `eed23e7`, unreleased):** the agent serves `GET /guest/oom-kills` — the kernel's `oom_kill` of +each of the asking guest's containers, read on the host from `/sys/fs/cgroup/lxc//ns/system.slice/docker-.scope/memory.events` +(0444; the non-root agent reads it — no sudo); the 30-s scan asks once per pass (5 s cap; an agent without the route +404s and is asked again after 10 minutes; no agent = Docker's flag only). A running container whose CURRENT run counts a +kill (its scope is new at each start) raises the same `app_oom` (once per run), whatever Docker's flag says; the +event's details carry `source` (`docker`, `cgroup` or both) and the host's count is the run's kill count — so a +container with a false flag now also reaches the storm rung. **[FACT] live 2026-10-28 23:27 on Tester 1** (test builds +of both, put back afterwards): a throwaway Radicale capped at 128 MB, `tail /dev/zero` inside it → the hog killed +(exit 137), the container kept running, the host counter 0 → 1; the controller logged the OOM line naming Radicale, +„seen by docker,cgroup", and pushed ONE `app_oom` (none in the next two minutes); the operator's mailbox (a second +channel) held exactly one mail, details `"source":"docker,cgroup"`. Docker 29.8.2 reported this kill too, so the +cgroup-only path is proven by the tests, not live. `audits/night-2026-10-28/r528-oom/`. **The box STOPS a crash loop or an out-of-memory storm (decision 28 of `09` §3, R-667, controller v0.269.0 / hub v0.123.0, 2026-09-24).** Alarming was not enough: gokapi crash-looped for hours at 385 → 546 restarts diff --git a/documentation/architecture/capability-map-detail/E.md b/documentation/architecture/capability-map-detail/E.md index 1cdcd2dd..db5f30b9 100644 --- a/documentation/architecture/capability-map-detail/E.md +++ b/documentation/architecture/capability-map-detail/E.md @@ -142,7 +142,9 @@ **Evidence (as written):** New FIRST sidebar page `/launcher`: colored tiles (deterministic slug color or `.felhom.yml` `brand_color`) + white glyph/monogram, one per openable app (tile ⟺ „Megnyitás" — subdomain presence is the single criterion; controller excluded). Operational → `` to the public URL; stopped → greyed + state badge, no link. `/` stays the Vezérlőpult. Endpoint-level + render-test verified; `felhom-controller/REPORT.md` (2026-07-24) -**Gap / roadmap (as written):** Live operator click-through of a real tile → app pending (browser automation not available on DooPlex). Follow-up: curate `brand_color` for top catalog apps (R-72). Sharing the launcher outside the household is now the capability-URL guest link — see the row below +**Walked 2026-10-28 night (9202, endpoint-level — the page and links the browser receives):** `/launcher` answered 200 with one tile per openable app (`hike`, `paperless`, `tasks`), each a link to the app's public address; the controller has no tile. `audits/night-2026-10-28/part5-claims/launcher-walk.*`. + +**Gap / roadmap (as written):** Live operator click-through of a real tile → app pending (browser automation not available on DooPlex; the endpoint walk above stands in). Follow-up: curate `brand_color` for top catalog apps (R-72). Sharing the launcher outside the household is now the capability-URL guest link — see the row below --- @@ -157,7 +159,7 @@ **Status (as written):** **IMPLEMENTED** -**Evidence and gap (as written — this row's text carries a `|` outside code, so its last cells are kept together as they stood):** 160-bit `crypto/rand` token, constant-time match (empty stored = disabled = byte-identical to the mux default 404); guest headers `noindex`/`no-referrer`/`no-store`; optional SEPARATE bcrypt share password + its own per-IP attempt map; signed cookie `HMAC(token`\ | `passwordHash)` keyed with `session_secret` (rotate-token OR change-password invalidates all cookies); token redacted in logs (`/s/`). Groups A–G (14 tests) + 3 red-proofs; **§13 endpoint-level live validation on 9201 all-pass** (`felhom-controller/REPORT.md` 2026-07-24). **Design ruling: member accounts SUPERSEDED by this capability-URL model; per-member tile visibility parked under the SSO arc (R-15).** | Full operator browser click-through + a validation doc pending → then PROVEN-LIVE. Accepted residuals: link-preview crawlers fetch once (noindex prevents indexing); reverse-proxy/CF access logs hold the path (ops-tier); the modal link carries the request Host (LAN-IP admin ⇒ LAN-IP link) +**Evidence and gap (as written — this row's text carries a `|` outside code, so its last cells are kept together as they stood):** 160-bit `crypto/rand` token, constant-time match (empty stored = disabled = byte-identical to the mux default 404); guest headers `noindex`/`no-referrer`/`no-store`; optional SEPARATE bcrypt share password + its own per-IP attempt map; signed cookie `HMAC(token`\ | `passwordHash)` keyed with `session_secret` (rotate-token OR change-password invalidates all cookies); token redacted in logs (`/s/`). Groups A–G (14 tests) + 3 red-proofs; **§13 endpoint-level live validation on 9201 all-pass** (`felhom-controller/REPORT.md` 2026-07-24). **Design ruling: member accounts SUPERSEDED by this capability-URL model; per-member tile visibility parked under the SSO arc (R-15).** | **Walked 2026-10-28 night on 9202 (endpoint-level, `audits/night-2026-10-28/part5-claims/launcher-walk.*`):** switched on from the launcher (303 `flash.share.enabled`); the link opened with NO session → 200, the household's app tiles, no admin link; a wrong token → 404; rotate → the old link 404, the new one 200; switched off → 404 (it was off before the walk). NOT-A-FINDING: an unknown non-`/s/` route answers an anonymous visitor 302 to the login page, so a wrong token is told apart from an unknown ROUTE (404 vs 302) — what the design hides is a wrong token from a disabled one, which both answer 404. → PROVEN-LIVE. Accepted residuals: link-preview crawlers fetch once (noindex prevents indexing); reverse-proxy/CF access logs hold the path (ops-tier); the modal link carries the request Host (LAN-IP admin ⇒ LAN-IP link) --- diff --git a/documentation/architecture/where-felhom-stands.html b/documentation/architecture/where-felhom-stands.html index 0b45b806..3d66c019 100644 --- a/documentation/architecture/where-felhom-stands.html +++ b/documentation/architecture/where-felhom-stands.html @@ -6,14 +6,14 @@

Where Felhom stands

What we built, what happens when things go wrong, and what is still missing. Generated from where-felhom-stands.yaml, which cites the capability map, register row or evidence document behind every claim — and which is checked by scripts/check_stands.py.
-
Verified 2026-08-22 against felhom-agent 40d857b527, felhom-controller 2da259af38, hub 877fcd2a38. 15 claim(s) carry a recorded status move (8 re-checked in this pass) — each is marked on the page.
-
Walked (20)done end to end on real hardware, evidence on file
Built (15)shipped and tested, the real path never walked
Partial (16)some walked, some not — the note says which
Missing (4)does not exist
needs-hardwareonly a running box could settle it
+
Verified 2026-08-22 against felhom-agent 40d857b527, felhom-controller 2da259af38, hub 877fcd2a38. 16 claim(s) carry a recorded status move (8 re-checked in this pass) — each is marked on the page.
+
Walked (22)done end to end on real hardware, evidence on file
Built (13)shipped and tested, the real path never walked
Partial (16)some walked, some not — the note says which
Missing (4)does not exist
needs-hardwareonly a running box could settle it
The journey
Seven stages, left to right, as they happen to a person.
1  Getting a box
A blank machine installs itself from our own boot image and registers itself as unclaimed — proven on two different boardsTwo boards: N100 2026-07-18, HP t740 2026-07-21.capability-map Bare-metal Felhom ISO (blank hardware → zero-touch auto-ins… · evidence tests/VALIDATION-n100-rehearsal-2026-07-18.md
The installer is published rather than pushed: rolling it back is one actchanged 2026-08-09, was walkedGate 6 of hostinstall_gates asserts the manifest names an installer-v tag; ran green tonight.why it moved: gate 6 asserts the manifest names an installer tag, but no walk of a rollback is on filecapability-map The installer is PUBLISHED, not pushed · register R-110
Installing onto hardware the customer already owns — the path exists, the first real one has not happenedUPGRADED: a real --mode byo install completed on demo-hp 2026-08-09 (Day-0 provision SUCCESS, 3m49s). Still not a customer's own hardware, so not 'walked' — but 'has not happened' is now false.evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
A machine that ends up on the wrong network port explains itself on screen and finds its way back — proven in a virtual drill, never on metalMap says the same: PROVEN-LIVE (nested drill — nested != metal).capability-map Box survives a wrong-NIC install · evidence audits/SPIKE-firstboot-nic-sweep-2026-07-22.md
Reinstalling a machine we previously installed refuses, because our own removal leaves a name service holding the port our own installer checksWarning card. Confirmed live 2026-08-09; dnsmasq restarted by --uninstall seizes :53.register R-272 · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
2  Making it theirs
An emailed one-time code; the customer sets their own password and the operator never sees itExercised live 2026-08-09: reset code accepted at /claim, 302 + session.capability-map Customer claim: one-time emailed code → customer sets own p… · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
The customer can bind their own machine from a link, with no operator present — done once, for realattempts=0, locked=0, source customer_selfbind, 2026-07-18.capability-map Customer binds their own appliance (self-service) · evidence tests/VALIDATION-n100-rehearsal-2026-07-18.md
It has never been done by a person who is not the operatorMap records MISSING (as evidence). Still true after 2026-08-09.capability-map A customer (not the operator) performs a restore via UI alone
Each of the two secrets has one name on every surface, and the re-enrol mail points at a page a rebuilt machine showsShipped (controller v0.211.0, hub v0.104.0/v0.105.0); no customer has typed the passphrase yet. Operator ruling 2026-10-12 (S13). The old defect cost a real code on 2026-08-09.register R-282 · register R-295 · register R-323 · register R-327
-
3  Using it
Apps installed from a catalog of about 52, with a memory guard and health-aware progress53 templates listed on the rebuilt box 2026-08-09; deploy driven live.capability-map Deploy an app from the catalog (env config, memory guard, h… · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
Start, stop, restart, update, logs, remove — and the parts that must not be stopped cannot bechanged 2026-08-09, was walkedwhy it moved: no walk document cited by the map row or anywhere elsecapability-map App lifecycle: start/stop/restart/update/logs/remove/redeploy · register R-108
Reachable from anywhere through a tunnel, per-app addressesVerified tonight: the rebuilt box answered on its public URL from outside.capability-map Remote access via Cloudflare Tunnel + Traefik (per-app subd… · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
Reachable on the home network when the internet is downneeds-hardwareOnly an observation on a running box with WAN pulled could settle it. Boxes are off.capability-map LAN access when internet is down (lan_resolver)
Phone photos, documents with text recognition, files from Windows Explorer or a Maccapability-map Files from Windows Explorer / Mac Finder (SMB server) · evidence audits/SPIKE-lan-discovery-2026-07-18.md
A one-tap launcher, and a read-only guest link for visitorscapability-map Indítópult (app launcher) — one-tap grid of the household's…
Media to a TVMap: MISSING.capability-map Media to TV via DLNA
Separate accounts per household memberMap: MISSING.capability-map Multiple household users / per-person accounts
+
3  Using it
Apps installed from a catalog of about 52, with a memory guard and health-aware progress53 templates listed on the rebuilt box 2026-08-09; deploy driven live.capability-map Deploy an app from the catalog (env config, memory guard, h… · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
Start, stop, restart, update, logs, remove — and the parts that must not be stopped cannot bechanged 2026-08-09, was builtWalked 2026-10-28 night on scratch 9202: install, stop, start, restart, logs; traefik and cloudflared refuse a stop (403); remove with data leaves no volume and no container. The update half: the same day's guarded updates on 9202 (bench + box + undo walks).why it moved: a walk document is now cited (2026-10-28 night)capability-map App lifecycle: start/stop/restart/update/logs/remove/redeploy · register R-108 · evidence audits/night-2026-10-28/part5-claims/lifecycle-walk.log · evidence audits/dockerhub-bigsteps-2026-10-28
Reachable from anywhere through a tunnel, per-app addressesVerified tonight: the rebuilt box answered on its public URL from outside.capability-map Remote access via Cloudflare Tunnel + Traefik (per-app subd… · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
Reachable on the home network when the internet is downneeds-hardwareOnly an observation on a running box with WAN pulled could settle it. Boxes are off.capability-map LAN access when internet is down (lan_resolver)
Phone photos, documents with text recognition, files from Windows Explorer or a Maccapability-map Files from Windows Explorer / Mac Finder (SMB server) · evidence audits/SPIKE-lan-discovery-2026-07-18.md
A one-tap launcher, and a read-only guest link for visitorschanged 2026-08-09, was builtWalked 2026-10-28 night on scratch 9202 through the pages the browser receives: the launcher lists each openable app; the guest link opens with no login and no admin link, a wrong token is 404, rotate and switch-off kill the old link.why it moved: walked live 2026-10-28 night (map E8, E9 → PROVEN-LIVE)capability-map Indítópult (app launcher) — one-tap grid of the household's… · evidence audits/night-2026-10-28/part5-claims/launcher-walk.log
Media to a TVMap: MISSING.capability-map Media to TV via DLNA
Separate accounts per household memberMap: MISSING.capability-map Multiple household users / per-person accounts
4  Drives
A new drive is found, offered, formatted, mounted and enrolled — including on awkward older boot layoutsmoved twice: 2026-08-09 walked -> built, then 2026-08-10 back to walkedApplies to a NEW drive. Re-attaching an existing one after a reinstall is R-280 and fails.why it moved: receipt found 2026-08-10: a live throwaway drive on felhom-pve walked scan -> format -> mount -> PVE dir storage -> one-click Regisztralas, with the resulting storage.cfg entry and systemd mount unit recorded. The map already read PROVEN-LIVE; the dataset was behind it. Scope unchanged: this is a NEW drive. Re-attaching an existing one after a reinstall is R-280 and still fails.capability-map Drive wizard: scan/format/mount/enroll, incl. legacy-boot L… · register R-220 · evidence audits/SPIKE-raw-drive-enroll-2026-06-15.md
Moving data between drives, crash-safe; removing a drive safely; unplug detectedchanged 2026-08-09, was walkedwhy it moved: no walk document citedcapability-map Data migration between drives (all / per-app), crash-safe
A network drive can be browsed and hold bulk media, but may not hold an app's data — enforcedRefuseAsAppNamespace is the fail-closed predicate.capability-map Network storage (NAS) is browse + bulk-media only · register R-108 · evidence audits/R108-network-app-namespace-2026-07-30.md
After a reinstall the data drive cannot be re-attached through any dashboard routeWarning card. /api/disks/candidates returns empty; the restore page promises two clicks.register R-280 · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md
5  Backing up
App data on the machine, nightly database dumps, a copy on a second drivemoved twice: 2026-08-09 walked -> built, then 2026-08-10 back to walkedTrue for every app but one, and that one was found on 2026-08-21: paperless-ngx's 72-table PostgreSQL was dumped nightly and written into a folder named after an app that does not exist, so it never entered the recovery unit, the second-drive copy or the off-site copy. Fixed and proven live 2026-08-22 (R-355) — the dump is now in the app's own unit and in the off-site snapshot for the first time. A catalogue-wide sweep, itself proven able to convict a planted case, says 1 of 53 was affected.why it moved: receipt found 2026-08-10: an adversarial, destructive, unattended overnight campaign across both boxes and ep0, with A2 (one quiesce, two tiers) proven end to end. Map already read PROVEN-LIVE.capability-map Tier-2 secondary-drive copy: class-driven legs · register R-355 · evidence audits/CAMPAIGN-8-backup-restore-2026-07-27.md · evidence audits/DRILL-backup-truth-2026-08-21/evidence/phase4-paperl…
A whole-machine archive that lands off the guest's own disk — a single-drive machine is recorded as degraded rather than pretendingchanged 2026-08-09, was walkedwhy it moved: no walk document citedcapability-map Whole-guest backup lands OFF the guest's own physical device · register R-165
An encrypted off-site copy, sealed with a key the operator cannot readchanged 2026-08-09, was walkedThe COPY is real and the key is still unreadable to us — what failed is DAILY and UNBROKEN. The 2026-08-09 note said '18 snapshots, daily, unbroken'; the next snapshot after 2026-08-09 08:30 was 2026-08-21 22:17, put there by hand during the drill. Twelve days, no alarm. Two causes in series: the 2026-08-21 rebuild lost the off-box target (R-193's shape), and after the self-heal restored it EVERY per-app switch was still off, so the first run logged 'backup OK: 0 app(s) backed up, 14s'.why it moved: the claim is about a CONTINUING daily copy; a 12-day silent gap was found on 2026-08-21 and nothing reported itcapability-map Offsite (restic → Hetzner Storage Box) · register R-199 · register R-193 · evidence audits/REHEARSAL-byo-reinstall-2026-08-09.md · evidence audits/REPORT-DRILL-backup-truth-2026-08-21.md
The backups prove themselves: a restore is actually performed, unattended, on every tier, on both machineschanged 2026-08-09, was walkedSTILL GREY, and for a sharper reason than in August. The scheduler runs and the check works — it failed again on 2026-08-21, unprompted, and named the cause exactly: after the reinstall the box presents a different PBS key than its own older archives were sealed with, so those archives cannot be opened at all (R-366). A tier whose archives are orphaned is a different alarm from a tier whose test failed, and only the second is being said.proof decay: PROOF-DECAY RULE FIRED (first time it has). A receipt EXISTS - architecture/_recovery-inventory-2026-07-28.md carries live journal lines for scheduled restore-tests on both boxes and both tiers - but it is superseded by later observation: demo-hp logged restore_test_failed on 2026-08-05, and the box has since been wiped and reinstalled (2026-08-09). The claim is about a CONTINUING scheduled behaviour, so a 2026-07-28 observation cannot carry it. Stays grey until a scheduled restore-test is seen passing on the rebuilt box. THE CAPABILITY MAP STILL READS PROVEN-LIVE (2026-08-03) AND IS NOW THE THING OUT OF STEP.why it moved: no walk document cited, and the last recorded restore-test on demo-hp FAILED (2026-08-05)capability-map Restore-proof is UNATTENDED — the scheduler covers EVERY tier · register R-86 · register R-366 · evidence audits/REPORT-DRILL-backup-truth-2026-08-21.md
The customer is warned before a drive fills, per drive, in their own languagechanged 2026-08-09, was walkedThe warning is real and was SEEN firing on 2026-08-21 with the right Hungarian copy, naming the drive and the free space. What 'BEFORE' cannot survive is the cadence: the watcher runs once a day at 03:30 plus once at startup (R-363), so a filesystem that fills at 03:31 goes unannounced for ~24 h. Watched live: the 69 GB volume carrying all 40-class app data was filled to 99% and the watcher said nothing, while the backup reserve was already refusing an app per run and telling the hub about it.why it moved: a daily check cannot carry the word BEFORE; observed silent for the whole window a filesystem sat at 99%capability-map The customer is warned BEFORE a filesystem fills · register R-167 · register R-363 · evidence audits/SPIKE-r165-mp1-merge-2026-08-02.md · evidence audits/REPORT-DRILL-backup-truth-2026-08-21.md
A backup that covered nothing still calls itself successfulWarning card — and the drill found two more of it, both live. (1) An off-site run with no app selected logs 'backup OK: 0 app(s) backed up'; the card does say 'nincs kijelölt alkalmazás' beside the green tick, so this one is honest if you read past the tick. (2) A restore that placed nothing reported success: '0 fájl visszaállítva', ok=true. The second is the one that matters and it is R-353, still open. The volume half of it is fixed (R-354): the message now names what came back.register R-240 · register R-353 · register R-354 · evidence audits/DRILL-backup-truth-2026-08-21/evidence/phase3-experi…
6  When something goes wrong
The machine watches itself and repairs some faults without telling anyone it had tochanged 2026-08-09, was walkedR-264 records that the self-heal counters reach the hub and are decoded nowhere.why it moved: no walk document citedcapability-map Box survives an unattended app or guest-network failure · register R-264
Failures reach the operator by email, one mail per run, every failing app namedchanged 2026-08-09, was walkedCONFIRMED against source: backup_run_failures digest is allowlisted (api/handler.go:1837), operator-only (dispatcher.go:423), templated (templates.go:48); recovery_unit_capture_failed is record-only (dispatcher.go:376). NOTE: R-182's register row still describes the PRE-FIX behaviour — see R-289.why it moved: the digest is wired and source-verified, but no run of it has been observed deliveringcapability-map A failed per-app Tier-1 backup reaches the OPERATOR — EVERY… · register R-182
Failures reach the customerThe customer leg is built; today's log shows customer-channel rows skipped as operator_only.capability-map App crashes → customer notified (one event per transition, …
An already-paired box is still told to pair itselfWarning card.register R-214 · register R-235
diff --git a/documentation/architecture/where-felhom-stands.yaml b/documentation/architecture/where-felhom-stands.yaml index 329482bd..6d3b58e7 100644 --- a/documentation/architecture/where-felhom-stands.yaml +++ b/documentation/architecture/where-felhom-stands.yaml @@ -155,17 +155,20 @@ claims: band: journey stage: 3 title: "Start, stop, restart, update, logs, remove — and the parts that must not be stopped cannot be" - status: built + status: walked + note: "Walked 2026-10-28 night on scratch 9202: install, stop, start, restart, logs; traefik and cloudflared refuse a stop (403); remove with data leaves no volume and no container. The update half: the same day's guarded updates on 9202 (bench + box + undo walks)." sources: - capability-map: "App lifecycle: start/stop/restart/update/logs/remove/redeploy" - register: "R-108" + - evidence: "audits/night-2026-10-28/part5-claims/lifecycle-walk.log" + - evidence: "audits/dockerhub-bigsteps-2026-10-28" changed: - from: walked - reason: "no walk document cited by the map row or anywhere else" + from: built + reason: "a walk document is now cited (2026-10-28 night)" verified: - date: 2026-08-09 - verdict: downgraded - depth: register+map + date: 2026-10-28 + verdict: upgraded + depth: source-read - id: use.tunnel band: journey stage: 3 @@ -207,13 +210,18 @@ claims: band: journey stage: 3 title: "A one-tap launcher, and a read-only guest link for visitors" - status: built + status: walked + note: "Walked 2026-10-28 night on scratch 9202 through the pages the browser receives: the launcher lists each openable app; the guest link opens with no login and no admin link, a wrong token is 404, rotate and switch-off kill the old link." sources: - capability-map: "Indítópult (app launcher) — one-tap grid of the household's openable apps" + - evidence: "audits/night-2026-10-28/part5-claims/launcher-walk.log" + changed: + from: built + reason: "walked live 2026-10-28 night (map E8, E9 → PROVEN-LIVE)" verified: - date: 2026-08-09 - verdict: confirmed - depth: register+map + date: 2026-10-28 + verdict: upgraded + depth: source-read - id: use.dlna band: journey stage: 3 diff --git a/documentation/audits/night-2026-10-28/bench/bench-state-after-stop.txt b/documentation/audits/night-2026-10-28/bench/bench-state-after-stop.txt new file mode 100644 index 00000000..93dfd36a --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/bench-state-after-stop.txt @@ -0,0 +1,6 @@ +0 +status: stopped +VMID Status Lock Name +9201 running demo-hp +9202 running demo-hp-scratch +9401 stopped upgrade-harness diff --git a/documentation/audits/night-2026-10-28/bench/bench-state-at-start.txt b/documentation/audits/night-2026-10-28/bench/bench-state-at-start.txt new file mode 100644 index 00000000..3c649a66 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/bench-state-at-start.txt @@ -0,0 +1,11 @@ +Wed Oct 28 21:31:37 UTC 2026 +containers: +0 +20 + total used free shared buff/cache available +Mem: 10240 62 9929 0 248 10177 +Swap: 0 0 0 +6 +MemTotal: 10485760 kB +MemFree: 10167652 kB +MemAvailable: 10422146 kB diff --git a/documentation/audits/night-2026-10-28/bench/bench-state-before-stop.txt b/documentation/audits/night-2026-10-28/bench/bench-state-before-stop.txt new file mode 100644 index 00000000..f76dd3c3 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/bench-state-before-stop.txt @@ -0,0 +1,11 @@ +containers: 0 volumes: 20 networks: bridge entrytest_default host none traefik-public +C3 +MV-bookstack +MV-gitea +MV-grocy +MV-homepage +MV-jellyfin +MV-lubelogger +MV-nzbget +MV-prowlarr +MV-qbittorrent diff --git a/documentation/audits/night-2026-10-28/bench/meili_collect.sh b/documentation/audits/night-2026-10-28/bench/meili_collect.sh new file mode 100644 index 00000000..695753bc --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/meili_collect.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# meili_collect.sh [interval_s=10] [duration_s=420] — R-1338 memory collector, read-only. +# +# Runs INSIDE the guest that runs Docker (bench: `pct exec 9401 -- bash meili_collect.sh wanderer-search`; a box guest +# the same way through `pct push` + `pct exec`). Changes nothing: it only reads cgroup v2 files, /proc and `docker +# inspect/logs/exec cat`. Prints: +# lines starting with '#' — a header (guest RAM/swap/CPUs, what the CONTAINER sees in /proc/meminfo and nproc, its +# Meilisearch env keys MEILI_* with values of *KEY* redacted, its start-up log lines) and, +# at the end, the top anonymous mappings of every process in the container; the full +# memory.stat at the start and the end; +# CSV — one row per sample, every interval_s for duration_s. Byte values, no rounding. +# NOTE (measured 2026-10-28 night, bench): an allocator's MADV_FREE'd pages stay counted in `anon` (and RssAnon) but sit +# on the INACTIVE FILE LRU, so inactive_file can exceed file. pglazyfree/pglazyfreed (pages) show it; the kernel drops +# them without swap when the cgroup reaches memory.max (ev_max counts those reclaim passes). +# The cgroup is looked up again on every sample, so a recreated container is followed (its row says the new id). +set -u +C=${1:?usage: meili_collect.sh [interval_s] [duration_s]} +IV=${2:-10}; DUR=${3:-420} +cg_of() { local id; id=$(docker inspect -f '{{.Id}}' "$C" 2>/dev/null) || return 1 + for p in /sys/fs/cgroup/system.slice/docker-$id.scope /sys/fs/cgroup/docker/$id; do [ -d "$p" ] && { echo "$p"; return 0; }; done; return 1; } +st() { awk -v k="$2" '$1==k{print $2; f=1} END{if(!f) print ""}' "$1/memory.stat"; } +ev() { awk -v k="$2" '$1==k{print $2; f=1} END{if(!f) print ""}' "$1/memory.events"; } +rd() { [ -r "$1" ] && cat "$1" || echo ""; } +mi() { awk -v k="$1:" '$1==k{print $2*1024}' /proc/meminfo; } +# sum a /proc//status field (kB -> bytes) over every process in the cgroup +ps_sum() { local s=0 v; for p in $(cat "$1/cgroup.procs" 2>/dev/null); do v=$(awk -v k="$2:" '$1==k{print $2}' /proc/$p/status 2>/dev/null); s=$((s + ${v:-0})); done; echo $((s*1024)); } + +echo "# meili_collect $(date -u +%FT%TZ) container=$C interval=${IV}s duration=${DUR}s guest=$(hostname) kernel=$(uname -r)" +echo "# guest: nproc=$(nproc) MemTotal=$(mi MemTotal) MemAvailable=$(mi MemAvailable) SwapTotal=$(mi SwapTotal) SwapFree=$(mi SwapFree)" +echo "# container sees: nproc=$(docker exec "$C" nproc 2>/dev/null || echo ?) $(docker exec "$C" sh -c 'grep -E "^(MemTotal|MemAvailable|SwapTotal):" /proc/meminfo' 2>/dev/null | tr -s ' ' | tr '\n' ' ')" +echo "# container image: $(docker inspect -f '{{.Config.Image}}' "$C" 2>/dev/null) started=$(docker inspect -f '{{.State.StartedAt}}' "$C" 2>/dev/null)" +echo "# container env (MEILI_*; *KEY* values redacted): $(docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$C" 2>/dev/null | grep '^MEILI_' | sed -E 's/^(MEILI_[A-Z_]*KEY[A-Z_]*)=.*/\1=/' | tr '\n' ' ')" +cg=$(cg_of) && echo "# cgroup: $cg memory.max=$(rd $cg/memory.max) memory.swap.max=$(rd $cg/memory.swap.max) memory.high=$(rd $cg/memory.high)" +echo "# start-up log lines (first 60 lines of docker logs that name memory/indexing/thread/version/config):" +docker logs "$C" 2>&1 | head -200 | grep -iE 'memory|index|thread|version|config|env|commit|database|max' | head -60 | sed 's/^/# /' + +echo "ts,elapsed_s,container_id,memory_current,memory_peak,memory_max,swap_current,swap_peak,anon,file,file_mapped,active_file,inactive_file,active_anon,inactive_anon,shmem,kernel,slab,sock,pgmajfault,anon_thp,pglazyfree_pages,pglazyfreed_pages,ev_low,ev_high,ev_max,ev_oom,ev_oom_kill,proc_VmRSS,proc_RssAnon,proc_RssFile,proc_RssShmem,proc_VmSwap,proc_VmHWM,guest_MemAvailable,guest_SwapFree" +t0=$(date +%s); end=$((t0 + DUR)) +if cg=$(cg_of); then echo "# memory.stat at start: $(tr '\n' ' ' < $cg/memory.stat)"; fi +while :; do + now=$(date +%s) + if cg=$(cg_of); then + id=$(basename "$cg" | sed -E 's/^docker-//; s/\.scope$//' | cut -c1-12) + echo "$(date -u +%T),$((now-t0)),$id,$(rd $cg/memory.current),$(rd $cg/memory.peak),$(rd $cg/memory.max),$(rd $cg/memory.swap.current),$(rd $cg/memory.swap.peak),$(st $cg anon),$(st $cg file),$(st $cg file_mapped),$(st $cg active_file),$(st $cg inactive_file),$(st $cg active_anon),$(st $cg inactive_anon),$(st $cg shmem),$(st $cg kernel),$(st $cg slab),$(st $cg sock),$(st $cg pgmajfault),$(st $cg anon_thp),$(st $cg pglazyfree),$(st $cg pglazyfreed),$(ev $cg low),$(ev $cg high),$(ev $cg max),$(ev $cg oom),$(ev $cg oom_kill),$(ps_sum $cg VmRSS),$(ps_sum $cg RssAnon),$(ps_sum $cg RssFile),$(ps_sum $cg RssShmem),$(ps_sum $cg VmSwap),$(ps_sum $cg VmHWM),$(mi MemAvailable),$(mi SwapFree)" + else + echo "$(date -u +%T),$((now-t0)),NO-CGROUP,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,," + fi + [ "$now" -ge "$end" ] && break + sleep "$IV" +done +if cg=$(cg_of); then + echo "# memory.stat at end: $(tr '\n' ' ' < $cg/memory.stat)" + echo "# memory.events at end: $(tr '\n' ' ' < $cg/memory.events)" + echo "# top anonymous mappings per process at the end (kB: Rss Anonymous Swap; mapping):" + for p in $(cat "$cg/cgroup.procs"); do + echo "# pid $p $(tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null | cut -c1-80)" + awk '/^[0-9a-f]+-[0-9a-f]+ /{m=$0} /^Rss:/{r=$2} /^Anonymous:/{a=$2} /^Swap:/{s=$2; if (a>1024) printf "# %8d %8d %8d %s\n", r, a, s, m}' /proc/$p/smaps 2>/dev/null | sort -k3 -nr | head -12 + grep -E '^(Rss|Anonymous|Swap|Pss_Anon|Pss_File):' /proc/$p/smaps_rollup 2>/dev/null | tr -s ' ' | tr '\n' ' ' | sed 's/^/# rollup: /'; echo + done +fi diff --git a/documentation/audits/night-2026-10-28/bench/plex/catalog_gates_fast_plex.txt b/documentation/audits/night-2026-10-28/bench/plex/catalog_gates_fast_plex.txt new file mode 100644 index 00000000..a5983af1 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/catalog_gates_fast_plex.txt @@ -0,0 +1,170 @@ +catalog_gates — apps: plex [--fast] + --fast SKIPPED: image-resolvable, volume-persistence, gate-decoys — they need network and a container runtime and take minutes + per app (gate-decoys: ~3 minutes on its own), so they are NEVER in a hook. They + remain deliberate periodic runs: start + of a catalog campaign, before a publish train, or when a template's volumes:/image + changes. Run them with no --fast, on a scratch host. + +============================================================================== +== gate: image-pins (check-image-pins.py) +============================================================================== +image-pin gate OK — 65 templates, 0 unpinned images + +============================================================================== +== gate: engine-major (check-engine-major.py) +============================================================================== +engine-major gate — range origin/main..HEAD: 0 compose file(s) changed, 0 engine pin(s) compared +engine-major gate OK — no forbidden engine major, and no engine major bundled with another image move (rule: CLAUDE.md, amended by R-469 2026-09-21) + +============================================================================== +== gate: catalog-since (check-catalog-since.py) +============================================================================== +catalog-since gate — range origin/main..HEAD: 0 compose file(s) changed, 0 image move(s) dated +catalog-since gate OK — every image move in the range carries a catalog_since on or after its commit day + +============================================================================== +== gate: copy-i18n (check-copy-i18n.py plex) +============================================================================== +copy-i18n: matcher controls OK (positive „Jelentkezz be…" convicts, negative „Encrypted notes…" does not) +copy-i18n: retrieval-promise vocabulary matches the sibling clone +copy-i18n: 65 apps · 3391 Hungarian copy strings frozen · 3391 with English +copy-i18n: translated so far — actualbudget 52/52, adventurelog 53/53, audiobookshelf 54/54, bentopdf 47/47, bookstack 56/56, calcom 56/56, calibre-web 60/60, claper 52/52, code-server 52/52, crafty-controller 57/57, dawarich 54/54, docmost 51/51, emby 22/22, ghost 54/54, gitea 54/54, glance 43/43, gokapi 48/48, grafana 50/50, gramps-web 53/53, grimmory 57/57, grocy 54/54, home-assistant 50/50, homebox 50/50, homepage 42/42, immich 59/59, jellyfin 54/54, karakeep 52/52, kimai 53/53, komga 56/56, lubelogger 57/57, mealie 53/53, metube 51/51, n8n 45/45, navidrome 55/55, nextcloud 60/60, nzbget 55/55, onlyoffice 52/52, opengist 49/49, outline 53/53, paperless-ngx 66/66, papra 50/50, plant-it 48/48, plex 57/57, privatebin 45/45, prowlarr 44/44, qbittorrent 54/54, radarr 55/55, radicale 51/51, rallly 48/48, recipe-importer 52/52, romm 74/74, sabnzbd 60/60, seerr 50/50, sonarr 55/55, sparkyfitness 54/54, tandoor 49/49, termix 46/46, transmission 52/52, uptime-kuma 49/49, vaultwarden 56/56, vikunja 50/50, wanderer 50/50, wger 52/52, wishlist 47/47, zipline 52/52 +copy-i18n: OK + +============================================================================== +== gate: probe-matches-compose (check-probe-matches-compose.py plex) +============================================================================== +probe-matches-compose: 1 app(s), mode: full + +probe-matches-compose: OK — every probe dials the port (and, where it can fail, the path) that the app's own compose healthcheck dials + +============================================================================== +== gate: probe-measured (check-probe-measured.py plex) +============================================================================== +probe-measured: OK — 0 probes, each with a measured before/after + +============================================================================== +== gate: test-record (check-test-record.py plex) +============================================================================== +test-record gate — 1 template(s) read, 0 carry a ladder, 0 convicted + +============================================================================== +== gate: test-record-move (check-test-record-move.py) +============================================================================== +test-record-move gate — range origin/main..HEAD: 0 compose file(s) changed + +============================================================================== +== gate: onboarding (check-onboarding.py) +============================================================================== +onboarding gate — 63 checklist ids; 65 template dirs: 53 exempt (published before 2026-10-01), 12 new; 2 exempt app(s) with a record (shape-checked): sparkyfitness, wger + NOT CHECKED here (sibling repository absent — the pre-push hook on DooPlex checks them): 11 path(s) + https://github.com/nzbgetcom/nzbget + the app needs the internet for the one thing it exists to do (reach the household's Usenet provider), and for nothing else. No account with us, no token, no call home once the update check is off + NNTP, usually 563 with TLS, and OUTGOING ONLY + https://github.com/qbittorrent/qBittorrent + https://github.com/sabnzbd/sabnzbd + the app needs the internet for the one thing it exists to do (reach the household's Usenet provider), and for nothing else. No account with us, no token, no call home + NNTP, usually 563 with TLS, and OUTGOING ONLY + https://github.com/transmission/transmission + gunicorn with 2 workers since 2026-10-08 (`WGER_USE_GUNICORN=True`, `WEB_CONCURRENCY=2`, R-762): 2 × „Booting worker" in the log; login page, CSS and a photo 200; 10-min watch 0 oom_kill, 0 restarts + DJANGO_DEBUG=False` (R-762, 2026-10-06): `collectstatic` runs at start; the static files are served by `wger-files` (nginx) +onboarding gate OK + +============================================================================== +== gate: family-gate (check-family-gate.py) +============================================================================== +family-gate: newest baked golden 0.344.0 >= 0.287.0 +family-gate gate OK: 4 family-gated template(s) ['glance', 'grimmory', 'homepage', 'metube'] + +============================================================================== +== gate: mem-limit-sum (check-mem-limit-sum.py plex) +============================================================================== +mem-limit-sum gate OK: 1 template(s), every mem_limit is the sum of its compose limits + +============================================================================== +== gate: db-credentials (check-db-credentials.py plex) +============================================================================== +db-credentials gate OK: 1 template(s), 0 database password reference(s), each a `type: secret` field + +============================================================================== +== gate: data-key (check-data-key.py plex) +============================================================================== +data-key gate OK: 1 template(s), 0 data key(s), every flag agrees with the name rule and REGISTRY + +============================================================================== +== gate: unclean-boot-images (check-unclean-boot-images.py plex) +============================================================================== +unclean-boot-images gate OK: 0 after_unclean_boot block(s) on a proven image [] + +============================================================================== +== gate: guide-version (check-guide-version.py plex) +============================================================================== +guide-version: 1 app(s) · 1 guide(s) current · 0 may be STALE · 0 hidden (checked_version empty) · 0 without a guide +guide-version: advisory — never fails a push + +============================================================================== +== gate: login-kind (check-login-kind.py) +============================================================================== +check-login-kind gate OK — 8 login_kind key(s), each one of first_visit, email, none + +============================================================================== +== gate: image-volumes (check-image-volumes.py) +============================================================================== +check-image-volumes gate OK — 4 registered data path(s) mounted + +============================================================================== +== gate: redis-persistence (check-redis-persistence.py) +============================================================================== +check-redis-persistence gate OK — 7 Redis service(s), none append-only + +============================================================================== +== gate: privileges (check-privileges.py) +============================================================================== +allow-list copy: byte-identical to the controller's +check-privileges: 120 files judged, every class-setting on the allow-list + +============================================================================== +== gate: download-binds (check-download-binds.py) +============================================================================== +download-binds gate OK — 6 download folder(s), every one `excluded` or `optional` by a named ruling (metube): metube:media/metube=optional, nzbget:media/nzbget=excluded, qbittorrent:media/torrents=excluded, sabnzbd:media/usenet=excluded, transmission:media/transmission=excluded, transmission:media/transmission/watch=excluded + +============================================================================== +== gate: security-scan (check-security-scan.py) +============================================================================== +security-scan (R-986, WARN-FIRST — it lists, it never refuses): /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu + govulncheck: no Go module in this repo — nothing to scan + gitleaks 8.24.3 on -n 1 HEAD (the newest commit; nothing unpushed): 0 hit(s) +security-scan: 0 new hit(s), 0 accepted — warn-first, the push goes on + +============================================================================== +== gate: upstream (check-upstream.py) +============================================================================== +upstream gate OK: 97 pins, every one names its upstream (21 stated gaps) + +============================================================================== +== summary +============================================================================== + image-pins OK (exit 0) + engine-major OK (exit 0) + catalog-since OK (exit 0) + copy-i18n OK (exit 0) + probe-matches-compose OK (exit 0) + probe-measured OK (exit 0) + test-record OK (exit 0) + test-record-move OK (exit 0) + onboarding OK (exit 0) + family-gate OK (exit 0) + mem-limit-sum OK (exit 0) + db-credentials OK (exit 0) + data-key OK (exit 0) + unclean-boot-images OK (exit 0) + guide-version OK (exit 0) + login-kind OK (exit 0) + image-volumes OK (exit 0) + redis-persistence OK (exit 0) + privileges OK (exit 0) + download-binds OK (exit 0) + security-scan OK (exit 0) + upstream OK (exit 0) + +all catalog gates OK diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge-driver.txt b/documentation/audits/night-2026-10-28/bench/plex/edge-driver.txt new file mode 100644 index 00000000..bffae9d6 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge-driver.txt @@ -0,0 +1,4 @@ +/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/night/bench/tools/edge_run.py:21: DeprecationWarning: Python 3.14 will, by default, filter extracted tar archives and reject files or modify their metadata. Use the filter argument to control this behavior. + tarfile.open(fileobj=io.BytesIO(base64.b64decode(pull.stdout.strip()))).extractall(ev) +rc=0 +evidence -> /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/0b76c4d7-9b39-4e4e-869d-d0d2c6006331/scratchpad/night/bench/plex/edge diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/MV-plex.log b/documentation/audits/night-2026-10-28/bench/plex/edge/MV-plex.log new file mode 100644 index 00000000..4a256557 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/MV-plex.log @@ -0,0 +1,116 @@ +[21:36:24] scratch drive folders cleared before FROM (R-656): ['/srv/felhom-gate/hdd/userdata/media'] +[21:36:24] MV-plex: deploying plex at FROM {'plex': 'plexinc/pms-docker:1.41.4.9463-630c9f557'} +[21:36:56] FROM settled=True in 31.0s :: {"plex": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[21:37:24] plex: POST /library/sections felhom8b78be62 http=201 (server 1.41.4.9463-630c9f557, claimed=false) +[21:37:24] plex: seeded library section felhom8b78be62 key=1 uuid=1d0d206d-7a57-4da3-b384-81ca6c1ba2af +[21:37:24] plex: section felhom8b78be62 read back (server 1.41.4.9463-630c9f557), same uuid=True, folders=['/media'] +[21:37:24] C1 (seed reads back BEFORE): True +[21:37:25] MV-plex: swapping to TO {'plex': 'plexinc/pms-docker:1.43.5.11029-6bf8af3f0'} +[21:37:34] TO up -d rc=0 +[21:38:05] TO settled=True in 30.9s :: {"plex": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[21:38:05] migration lines observed: 1 +[21:38:05] plex: section felhom8b78be62 read back (server 1.43.5.11029-6bf8af3f0), same uuid=True, folders=['/media'] +[21:38:05] RESULT (seed reads back AFTER): True +[21:38:05] memory watch: 600s, 4 callers on 1 path(s) at 172.18.0.2:32400 +[21:38:21] + 15s plex=231M/2048M peak=251M kills=0 rs=0 reqs=300 +[21:38:36] + 30s plex=232M/2048M peak=251M kills=0 rs=0 reqs=604 +[21:38:51] + 45s plex=232M/2048M peak=251M kills=0 rs=0 reqs=904 +[21:39:06] + 60s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1204 +[21:39:21] + 76s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1504 +[21:39:36] + 91s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1804 +[21:39:51] + 106s plex=232M/2048M peak=251M kills=0 rs=0 reqs=2104 +[21:40:07] + 121s plex=233M/2048M peak=251M kills=0 rs=0 reqs=2404 +[21:40:22] + 136s plex=233M/2048M peak=251M kills=0 rs=0 reqs=2704 +[21:40:37] + 151s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3004 +[21:40:52] + 166s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3304 +[21:41:07] + 182s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3604 +[21:41:22] + 197s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3904 +[21:41:37] + 212s plex=235M/2048M peak=251M kills=0 rs=0 reqs=4204 +[21:41:52] + 227s plex=235M/2048M peak=251M kills=0 rs=0 reqs=4504 +[21:42:08] + 242s plex=234M/2048M peak=251M kills=0 rs=0 reqs=4801 +[21:42:23] + 257s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5101 +[21:42:38] + 272s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5405 +[21:42:53] + 287s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5705 +[21:43:08] + 302s plex=235M/2048M peak=251M kills=0 rs=0 reqs=6005 +[21:43:23] + 318s plex=235M/2048M peak=251M kills=0 rs=0 reqs=6305 +[21:43:38] + 333s plex=236M/2048M peak=251M kills=0 rs=0 reqs=6605 +[21:43:53] + 348s plex=236M/2048M peak=251M kills=0 rs=0 reqs=6905 +[21:44:09] + 363s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7205 +[21:44:24] + 378s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7505 +[21:44:39] + 393s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7805 +[21:44:54] + 408s plex=237M/2048M peak=251M kills=0 rs=0 reqs=8105 +[21:45:09] + 424s plex=238M/2048M peak=251M kills=0 rs=0 reqs=8405 +[21:45:24] + 439s plex=238M/2048M peak=251M kills=0 rs=0 reqs=8705 +[21:45:39] + 454s plex=237M/2048M peak=251M kills=0 rs=0 reqs=9002 +[21:45:54] + 469s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9302 +[21:46:10] + 484s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9602 +[21:46:25] + 499s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9902 +[21:46:40] + 514s plex=238M/2048M peak=251M kills=0 rs=0 reqs=10202 +[21:46:55] + 529s plex=239M/2048M peak=251M kills=0 rs=0 reqs=10502 +[21:47:10] + 545s plex=239M/2048M peak=251M kills=0 rs=0 reqs=10803 +[21:47:25] + 560s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11103 +[21:47:40] + 575s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11403 +[21:47:55] + 590s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11706 +[21:48:11] + 605s plex=240M/2048M peak=251M kills=0 rs=0 reqs=12006 +[21:48:11] memory watch: killed=False tight=[] requests=12006 codes={'401': 12006} +[21:48:11] MV-plex: ABORT — putting the FROM images back +[21:48:51] plex: section felhom8b78be62 read back (server 1.41.4.9463-630c9f557), same uuid=True, folders=['/media'] +[21:48:51] ABORT: app came back in 30.9s; data present=True +{ + "harness_version": 5, + "edge": "MV-plex", + "app": "plex", + "note": "night 2026-10-28 (R-1238): the catalog transition 6cc317d, older tag -> current pin", + "from": { + "plex": "plexinc/pms-docker:1.41.4.9463-630c9f557" + }, + "to": { + "plex": "plexinc/pms-docker:1.43.5.11029-6bf8af3f0" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": "\u001b[2Kplex | [fix-attrs.d] applying ownership & permissions fixes...", + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 605.4, + "requested_s": 600, + "requests": 12006, + "codes": { + "401": 12006 + }, + "first_kill": null, + "containers": { + "plex": { + "limit": 2147483648, + "peak": 264138752, + "peak_pct": 0.123, + "anon_peak_sampled": 230502400, + "anon_peak_pct": 0.107, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 30.9, + "measured_at": "2026-10-28T21:48:51Z", + "evidence": "evidence/MV-plex", + "scratch_cleared": [ + "/srv/felhom-gate/hdd/userdata/media" + ], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 746.7 +} diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/edge_run_inner.py b/documentation/audits/night-2026-10-28/bench/plex/edge/edge_run_inner.py new file mode 100644 index 00000000..fd53c887 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/edge_run_inner.py @@ -0,0 +1 @@ +import importlib.util,sys; sys.path.insert(0,'/opt/upg'); s=importlib.util.spec_from_file_location('ut','/opt/upg/upgrade-test.py'); ut=importlib.util.module_from_spec(s); s.loader.exec_module(ut); ut.EDGES['MV-plex']=dict(app='plex', note='night 2026-10-28 (R-1238): the catalog transition 6cc317d, older tag -> current pin', frm={'plex': 'plexinc/pms-docker:1.41.4.9463-630c9f557'}, to={'plex': 'plexinc/pms-docker:1.43.5.11029-6bf8af3f0'}); sys.exit(ut.main(['--soak', '600', 'MV-plex'])) diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/abort-states.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/abort-states.json new file mode 100644 index 00000000..db5e3df4 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/abort-states.json @@ -0,0 +1,8 @@ +{ + "plex": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/compose-final.log b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/compose-final.log new file mode 100644 index 00000000..e9e6b0e0 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/compose-final.log @@ -0,0 +1,17 @@ +plex | [s6-init] making user provided files available at /var/run/s6/etc...exited 0. +plex | [s6-init] ensuring user provided files have correct perms...exited 0. +plex | [fix-attrs.d] applying ownership & permissions fixes... +plex | [fix-attrs.d] done. +plex | [cont-init.d] executing container initialization scripts... +plex | [cont-init.d] 40-plex-first-run: executing... +plex | Plex Media Server first run setup complete +plex | [cont-init.d] 40-plex-first-run: exited 0. +plex | [cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +plex | [cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +plex | [cont-init.d] 50-plex-update: executing... +plex | [cont-init.d] 50-plex-update: exited 0. +plex | [cont-init.d] done. +plex | [services.d] starting services +plex | Starting Plex Media Server. +plex | [services.d] done. +plex | Critical: libusb_init failed diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/engine-state.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/engine-state.json new file mode 100644 index 00000000..ec747fa4 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/engine-state.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after-detail.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after-detail.json new file mode 100644 index 00000000..68db7a93 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after-detail.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media": {} +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after.json new file mode 100644 index 00000000..65fca25a --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-after.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before-detail.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before-detail.json new file mode 100644 index 00000000..68db7a93 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before-detail.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media": {} +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before.json new file mode 100644 index 00000000..65fca25a --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/files-before.json @@ -0,0 +1,3 @@ +{ + "/srv/felhom-gate/hdd/userdata/media": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/memory-samples.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/memory-samples.json new file mode 100644 index 00000000..2f75f942 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/memory-samples.json @@ -0,0 +1,762 @@ +[ + { + "t": 15.1, + "containers": { + "plex": { + "limit": 2147483648, + "current": 243261440, + "peak": 264138752, + "anon": 230408192, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 300 + }, + { + "t": 30.3, + "containers": { + "plex": { + "limit": 2147483648, + "current": 243642368, + "peak": 264138752, + "anon": 230379520, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 604 + }, + { + "t": 45.4, + "containers": { + "plex": { + "limit": 2147483648, + "current": 243908608, + "peak": 264138752, + "anon": 230400000, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 904 + }, + { + "t": 60.5, + "containers": { + "plex": { + "limit": 2147483648, + "current": 243908608, + "peak": 264138752, + "anon": 230395904, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1204 + }, + { + "t": 75.7, + "containers": { + "plex": { + "limit": 2147483648, + "current": 244121600, + "peak": 264138752, + "anon": 230502400, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1504 + }, + { + "t": 90.8, + "containers": { + "plex": { + "limit": 2147483648, + "current": 244199424, + "peak": 264138752, + "anon": 230420480, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 1804 + }, + { + "t": 105.9, + "containers": { + "plex": { + "limit": 2147483648, + "current": 244224000, + "peak": 264138752, + "anon": 230440960, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2104 + }, + { + "t": 121.0, + "containers": { + "plex": { + "limit": 2147483648, + "current": 244686848, + "peak": 264138752, + "anon": 230436864, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2404 + }, + { + "t": 136.2, + "containers": { + "plex": { + "limit": 2147483648, + "current": 244350976, + "peak": 264138752, + "anon": 230445056, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 2704 + }, + { + "t": 151.3, + "containers": { + "plex": { + "limit": 2147483648, + "current": 245436416, + "peak": 264138752, + "anon": 230416384, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3004 + }, + { + "t": 166.4, + "containers": { + "plex": { + "limit": 2147483648, + "current": 245751808, + "peak": 264138752, + "anon": 230445056, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3304 + }, + { + "t": 181.5, + "containers": { + "plex": { + "limit": 2147483648, + "current": 245563392, + "peak": 264138752, + "anon": 230445056, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3604 + }, + { + "t": 196.7, + "containers": { + "plex": { + "limit": 2147483648, + "current": 245907456, + "peak": 264138752, + "anon": 230408192, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 3904 + }, + { + "t": 211.8, + "containers": { + "plex": { + "limit": 2147483648, + "current": 246439936, + "peak": 264138752, + "anon": 230449152, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4204 + }, + { + "t": 226.9, + "containers": { + "plex": { + "limit": 2147483648, + "current": 246763520, + "peak": 264138752, + "anon": 230408192, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4504 + }, + { + "t": 242.0, + "containers": { + "plex": { + "limit": 2147483648, + "current": 245985280, + "peak": 264138752, + "anon": 230408192, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 4801 + }, + { + "t": 257.2, + "containers": { + "plex": { + "limit": 2147483648, + "current": 246435840, + "peak": 264138752, + "anon": 230408192, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5101 + }, + { + "t": 272.3, + "containers": { + "plex": { + "limit": 2147483648, + "current": 246939648, + "peak": 264138752, + "anon": 230109184, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5405 + }, + { + "t": 287.4, + "containers": { + "plex": { + "limit": 2147483648, + "current": 247377920, + "peak": 264138752, + "anon": 230322176, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 5705 + }, + { + "t": 302.5, + "containers": { + "plex": { + "limit": 2147483648, + "current": 247332864, + "peak": 264138752, + "anon": 230166528, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6005 + }, + { + "t": 317.7, + "containers": { + "plex": { + "limit": 2147483648, + "current": 247386112, + "peak": 264138752, + "anon": 230121472, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6305 + }, + { + "t": 332.8, + "containers": { + "plex": { + "limit": 2147483648, + "current": 247795712, + "peak": 264138752, + "anon": 230125568, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6605 + }, + { + "t": 347.9, + "containers": { + "plex": { + "limit": 2147483648, + "current": 247840768, + "peak": 264138752, + "anon": 230100992, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 6905 + }, + { + "t": 363.0, + "containers": { + "plex": { + "limit": 2147483648, + "current": 248016896, + "peak": 264138752, + "anon": 230100992, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7205 + }, + { + "t": 378.2, + "containers": { + "plex": { + "limit": 2147483648, + "current": 248377344, + "peak": 264138752, + "anon": 230191104, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7505 + }, + { + "t": 393.3, + "containers": { + "plex": { + "limit": 2147483648, + "current": 248057856, + "peak": 264138752, + "anon": 230182912, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 7805 + }, + { + "t": 408.4, + "containers": { + "plex": { + "limit": 2147483648, + "current": 248844288, + "peak": 264138752, + "anon": 230121472, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8105 + }, + { + "t": 423.5, + "containers": { + "plex": { + "limit": 2147483648, + "current": 249647104, + "peak": 264138752, + "anon": 230096896, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8405 + }, + { + "t": 438.7, + "containers": { + "plex": { + "limit": 2147483648, + "current": 249815040, + "peak": 264138752, + "anon": 230096896, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 8705 + }, + { + "t": 453.8, + "containers": { + "plex": { + "limit": 2147483648, + "current": 249139200, + "peak": 264138752, + "anon": 230191104, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9002 + }, + { + "t": 468.9, + "containers": { + "plex": { + "limit": 2147483648, + "current": 249733120, + "peak": 264138752, + "anon": 230182912, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9302 + }, + { + "t": 484.0, + "containers": { + "plex": { + "limit": 2147483648, + "current": 249655296, + "peak": 264138752, + "anon": 230125568, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9602 + }, + { + "t": 499.2, + "containers": { + "plex": { + "limit": 2147483648, + "current": 250421248, + "peak": 264138752, + "anon": 230100992, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 9902 + }, + { + "t": 514.3, + "containers": { + "plex": { + "limit": 2147483648, + "current": 250118144, + "peak": 264138752, + "anon": 230133760, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10202 + }, + { + "t": 529.4, + "containers": { + "plex": { + "limit": 2147483648, + "current": 251273216, + "peak": 264138752, + "anon": 230178816, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10502 + }, + { + "t": 544.6, + "containers": { + "plex": { + "limit": 2147483648, + "current": 250630144, + "peak": 264138752, + "anon": 230187008, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 10803 + }, + { + "t": 559.7, + "containers": { + "plex": { + "limit": 2147483648, + "current": 251371520, + "peak": 264138752, + "anon": 230182912, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11103 + }, + { + "t": 574.8, + "containers": { + "plex": { + "limit": 2147483648, + "current": 250826752, + "peak": 264138752, + "anon": 230084608, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11403 + }, + { + "t": 589.9, + "containers": { + "plex": { + "limit": 2147483648, + "current": 251547648, + "peak": 264138752, + "anon": 229978112, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 11706 + }, + { + "t": 605.1, + "containers": { + "plex": { + "limit": 2147483648, + "current": 251879424, + "peak": 264138752, + "anon": 230023168, + "swap": 0, + "swap_peak": 0, + "oom_kill": 0, + "restarts": 0, + "oomkilled_flag": false, + "status": "running", + "cgroup": true + } + }, + "requests": 12006 + } +] \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/migration-lines.txt b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/migration-lines.txt new file mode 100644 index 00000000..dd9b48dd --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/migration-lines.txt @@ -0,0 +1 @@ +plex | [fix-attrs.d] applying ownership & permissions fixes... \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/run.log b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/run.log new file mode 100644 index 00000000..87b8bb43 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/run.log @@ -0,0 +1,58 @@ +[21:36:24] scratch drive folders cleared before FROM (R-656): ['/srv/felhom-gate/hdd/userdata/media'] +[21:36:24] MV-plex: deploying plex at FROM {'plex': 'plexinc/pms-docker:1.41.4.9463-630c9f557'} +[21:36:56] FROM settled=True in 31.0s :: {"plex": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[21:37:24] plex: POST /library/sections felhom8b78be62 http=201 (server 1.41.4.9463-630c9f557, claimed=false) +[21:37:24] plex: seeded library section felhom8b78be62 key=1 uuid=1d0d206d-7a57-4da3-b384-81ca6c1ba2af +[21:37:24] plex: section felhom8b78be62 read back (server 1.41.4.9463-630c9f557), same uuid=True, folders=['/media'] +[21:37:24] C1 (seed reads back BEFORE): True +[21:37:25] MV-plex: swapping to TO {'plex': 'plexinc/pms-docker:1.43.5.11029-6bf8af3f0'} +[21:37:34] TO up -d rc=0 +[21:38:05] TO settled=True in 30.9s :: {"plex": {"status": "running", "health": "healthy", "restarts": 0, "exit": 0}} +[21:38:05] migration lines observed: 1 +[21:38:05] plex: section felhom8b78be62 read back (server 1.43.5.11029-6bf8af3f0), same uuid=True, folders=['/media'] +[21:38:05] RESULT (seed reads back AFTER): True +[21:38:05] memory watch: 600s, 4 callers on 1 path(s) at 172.18.0.2:32400 +[21:38:21] + 15s plex=231M/2048M peak=251M kills=0 rs=0 reqs=300 +[21:38:36] + 30s plex=232M/2048M peak=251M kills=0 rs=0 reqs=604 +[21:38:51] + 45s plex=232M/2048M peak=251M kills=0 rs=0 reqs=904 +[21:39:06] + 60s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1204 +[21:39:21] + 76s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1504 +[21:39:36] + 91s plex=232M/2048M peak=251M kills=0 rs=0 reqs=1804 +[21:39:51] + 106s plex=232M/2048M peak=251M kills=0 rs=0 reqs=2104 +[21:40:07] + 121s plex=233M/2048M peak=251M kills=0 rs=0 reqs=2404 +[21:40:22] + 136s plex=233M/2048M peak=251M kills=0 rs=0 reqs=2704 +[21:40:37] + 151s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3004 +[21:40:52] + 166s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3304 +[21:41:07] + 182s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3604 +[21:41:22] + 197s plex=234M/2048M peak=251M kills=0 rs=0 reqs=3904 +[21:41:37] + 212s plex=235M/2048M peak=251M kills=0 rs=0 reqs=4204 +[21:41:52] + 227s plex=235M/2048M peak=251M kills=0 rs=0 reqs=4504 +[21:42:08] + 242s plex=234M/2048M peak=251M kills=0 rs=0 reqs=4801 +[21:42:23] + 257s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5101 +[21:42:38] + 272s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5405 +[21:42:53] + 287s plex=235M/2048M peak=251M kills=0 rs=0 reqs=5705 +[21:43:08] + 302s plex=235M/2048M peak=251M kills=0 rs=0 reqs=6005 +[21:43:23] + 318s plex=235M/2048M peak=251M kills=0 rs=0 reqs=6305 +[21:43:38] + 333s plex=236M/2048M peak=251M kills=0 rs=0 reqs=6605 +[21:43:53] + 348s plex=236M/2048M peak=251M kills=0 rs=0 reqs=6905 +[21:44:09] + 363s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7205 +[21:44:24] + 378s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7505 +[21:44:39] + 393s plex=236M/2048M peak=251M kills=0 rs=0 reqs=7805 +[21:44:54] + 408s plex=237M/2048M peak=251M kills=0 rs=0 reqs=8105 +[21:45:09] + 424s plex=238M/2048M peak=251M kills=0 rs=0 reqs=8405 +[21:45:24] + 439s plex=238M/2048M peak=251M kills=0 rs=0 reqs=8705 +[21:45:39] + 454s plex=237M/2048M peak=251M kills=0 rs=0 reqs=9002 +[21:45:54] + 469s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9302 +[21:46:10] + 484s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9602 +[21:46:25] + 499s plex=238M/2048M peak=251M kills=0 rs=0 reqs=9902 +[21:46:40] + 514s plex=238M/2048M peak=251M kills=0 rs=0 reqs=10202 +[21:46:55] + 529s plex=239M/2048M peak=251M kills=0 rs=0 reqs=10502 +[21:47:10] + 545s plex=239M/2048M peak=251M kills=0 rs=0 reqs=10803 +[21:47:25] + 560s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11103 +[21:47:40] + 575s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11403 +[21:47:55] + 590s plex=239M/2048M peak=251M kills=0 rs=0 reqs=11706 +[21:48:11] + 605s plex=240M/2048M peak=251M kills=0 rs=0 reqs=12006 +[21:48:11] memory watch: killed=False tight=[] requests=12006 codes={'401': 12006} +[21:48:11] MV-plex: ABORT — putting the FROM images back +[21:48:51] plex: section felhom8b78be62 read back (server 1.41.4.9463-630c9f557), same uuid=True, folders=['/media'] +[21:48:51] ABORT: app came back in 30.9s; data present=True \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-full.log b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-full.log new file mode 100644 index 00000000..e9e6b0e0 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-full.log @@ -0,0 +1,17 @@ +plex | [s6-init] making user provided files available at /var/run/s6/etc...exited 0. +plex | [s6-init] ensuring user provided files have correct perms...exited 0. +plex | [fix-attrs.d] applying ownership & permissions fixes... +plex | [fix-attrs.d] done. +plex | [cont-init.d] executing container initialization scripts... +plex | [cont-init.d] 40-plex-first-run: executing... +plex | Plex Media Server first run setup complete +plex | [cont-init.d] 40-plex-first-run: exited 0. +plex | [cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +plex | [cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +plex | [cont-init.d] 50-plex-update: executing... +plex | [cont-init.d] 50-plex-update: exited 0. +plex | [cont-init.d] done. +plex | [services.d] starting services +plex | Starting Plex Media Server. +plex | [services.d] done. +plex | Critical: libusb_init failed diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-states.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-states.json new file mode 100644 index 00000000..db5e3df4 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/to-states.json @@ -0,0 +1,8 @@ +{ + "plex": { + "status": "running", + "health": "healthy", + "restarts": 0, + "exit": 0 + } +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/verdict.json b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/verdict.json new file mode 100644 index 00000000..f1285979 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/edge/evidence/MV-plex/verdict.json @@ -0,0 +1,58 @@ +{ + "harness_version": 5, + "edge": "MV-plex", + "app": "plex", + "note": "night 2026-10-28 (R-1238): the catalog transition 6cc317d, older tag -> current pin", + "from": { + "plex": "plexinc/pms-docker:1.41.4.9463-630c9f557" + }, + "to": { + "plex": "plexinc/pms-docker:1.43.5.11029-6bf8af3f0" + }, + "verdict": "proven", + "seed_read_before": true, + "seed_read_after": true, + "healthy_after": true, + "migration_observed": "\u001b[2Kplex | [fix-attrs.d] applying ownership & permissions fixes...", + "abort": "starts-and-serves", + "abort_detail": null, + "engine_state_after": null, + "memory": { + "soak_s": 605.4, + "requested_s": 600, + "requests": 12006, + "codes": { + "401": 12006 + }, + "first_kill": null, + "containers": { + "plex": { + "limit": 2147483648, + "peak": 264138752, + "peak_pct": 0.123, + "anon_peak_sampled": 230502400, + "anon_peak_pct": 0.107, + "swap_peak": 0, + "oom_kills": 0, + "restarts": 0, + "oomkilled_flag": false, + "measured": true + } + }, + "unmeasured": [], + "venue_swap_bytes": 0, + "load": "reached" + }, + "marks": [], + "bench_overrides": null, + "duration_s": 30.9, + "measured_at": "2026-10-28T21:48:51Z", + "evidence": "evidence/MV-plex", + "scratch_cleared": [ + "/srv/felhom-gate/hdd/userdata/media" + ], + "files_changed": [], + "files_changed_detail": [], + "files_ignored": [], + "total_s": 746.7 +} \ No newline at end of file diff --git a/documentation/audits/night-2026-10-28/bench/plex/spike1-claim-absent.txt b/documentation/audits/night-2026-10-28/bench/plex/spike1-claim-absent.txt new file mode 100644 index 00000000..3dc98395 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/spike1-claim-absent.txt @@ -0,0 +1,66 @@ +### pull +docker.io/plexinc/pms-docker:1.41.4.9463-630c9f557 +docker.io/plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +### FROM plexinc/pms-docker:1.41.4.9463-630c9f557 (PLEX_CLAIM absent) +container ip 172.20.0.2; caller = bench host via 172.20.0.2 dev br-2e509a86fc93 src 172.20.0.1 uid 0 +identity 200 after ~8s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"0.1.0","claimed":false,"machineIdentifier":"9342c4142cc9ee373ef97d5a064ff4514d7956d1","startState":"startingPlugins","version":"1.41.4.9463-630c9f557"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":0,"allowSync":false,"title1":"Plex Library"}} +[http 200] + +--- GET /myplex/account +{"MyPlex":{"username":"","mappingState":"mapped","mappingError":"","signInState":"none","publicAddress":"","publicPort":0,"privateAddress":"","privatePort":0,"subscriptionFeatures":"00077925-6031-401b-8679-f6617ed0cec6,007fb90d-2224-4d24-bd42-e87ffde13558,06d14b9e-2af8-4c2b-a4a1-ea9d5c515824,1417df52-986e-4e4b-8dcd-3997fbc5c976,22b27e12-472e-4383-92ea-2ec3976d8e72,24b4cf36-b296-4002-86b7-f1adb657e76a,2c3311e6-a303-4c24-b560-f498a24e08ac,2ea0e464-ea4f-4be2-97c1-ce6ed4b377dd,300231e0-69aa-4dce-97f4-52d8c00e3e8c,34ddfac9-3a76-459a-974d-591520b809dd,34e182bd-2f62-4678-a9e9-d13b3e25019d,359796ce-d614-427f-8165-19ff62ca053a,39dbdd84-8339-4736-96a1-0eb105cc2e08,3ae06d3a-a76b-435e-8cef-2d2008610ba2,3eb2789b-200c-4a15-91d2-dedfe560953c,3f6baa76-7488-479a-9e4f-49ff2c0d3711,4b522f91-ae89-4f62-af9c-76f44d8ef61c,547514ab-3284-46e5-af77-bbaff247e3fc,567033ef-ffee-44fb-8f90-f678077445f9,5b6190a9-77a4-477e-9fbc-c8118e35a4c1,5d819d02-5d04-4116-8eec-f49def4e2d6f,5e2a89ec-fb26-4234-b66e-14d37f35dff2,5f09fe50-f141-4440-8293-7a18a7a8daf0,64adaa4e-aa7e-457d-b385-51438216d7fe,65685ff8-4375-4e4c-a806-ec1f0b4a8b7f,68747f3a-ce13-46ce-9274-1e0544c9f500,6b85840c-d79d-40c2-8d8f-dfc0b7d26776,6c4d66d9-729d-49dc-b70d-ab2652abf15a,6d7be725-9a96-42c7-8af4-01e735138822,7b392594-6949-4736-9894-e57a9dfe4037,7f46bf17-fabf-4f96-99a2-cf374f6eed71,81c8d5fa-8d90-4833-aa10-a31a51310e2f,849433b0-ef60-4a71-9dd9-939bc01f5362,85ebfb7b-77fb-4afd-bb1a-2fe2fefdddbe,86da2200-58db-4d78-ba46-f146ba25906b,96cac76e-c5bc-4596-87eb +--- POST /library/sections?name=FelhomSpike&type=movie&agent=tv.plex.agents.movie&scanner=Plex%20Movie&language=en-US&location=%2Fmedia%2Fmovies +the server is still starting up. Please retry later + +[http 400] + +--- GET /library/sections +{"MediaContainer":{"size":0,"allowSync":false,"title1":"Plex Library"}} +[http 200] + +### negative control: no such section name +0 +### claimed? (Preferences.xml PlexOnlineToken present = claimed; value never printed) +0 +[fix-attrs.d] done. +[cont-init.d] executing container initialization scripts... +[cont-init.d] 40-plex-first-run: executing... +Creating pref shell +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +Critical: libusb_init failed +### SWAP to plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +identity 200 after ~6s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"1.2.3","claimed":false,"machineIdentifier":"9342c4142cc9ee373ef97d5a064ff4514d7956d1","startState":"startingPlugins","version":"1.43.5.11029-6bf8af3f0"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":0,"allowSync":false,"title1":"Plex Library"}} +[http 200] + +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +### teardown +0 diff --git a/documentation/audits/night-2026-10-28/bench/plex/spike2-claim-absent.txt b/documentation/audits/night-2026-10-28/bench/plex/spike2-claim-absent.txt new file mode 100644 index 00000000..2c50a86f --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/spike2-claim-absent.txt @@ -0,0 +1,63 @@ +### pull +docker.io/plexinc/pms-docker:1.41.4.9463-630c9f557 +docker.io/plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +### FROM plexinc/pms-docker:1.41.4.9463-630c9f557 (PLEX_CLAIM absent) +container ip 172.21.0.2; caller = bench host via 172.21.0.2 dev br-f7401aa38df9 src 172.21.0.1 uid 0 +identity 200 with no startState after ~52s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"0.1.0","claimed":false,"machineIdentifier":"a34886c6b3231d99de4a8083f3f7116d317e0201","version":"1.41.4.9463-630c9f557"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":0,"allowSync":false,"title1":"Plex Library"}} +[http 200] + +--- POST /library/sections?name=FelhomSpike&type=movie&agent=tv.plex.agents.movie&scanner=Plex%20Movie&language=en-US&location=%2Fmedia%2Fmovies +{"MediaContainer":{"size":1,"Directory":[{"art":"/:/resources/movie-fanart.jpg","composite":"/library/sections/1/composite/1793223238","thumb":"/:/resources/movie.png","key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"c433e532-2e23-40be-b0fb-a99ae0b0a264","updatedAt":1793223238,"createdAt":1793223238,"content":true,"directory":true,"contentChangedAt":8740763687856723809,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 201] + +--- GET /library/sections +{"MediaContainer":{"size":1,"allowSync":false,"title1":"Plex Library","Directory":[{"allowSync":false,"art":"/:/resources/movie-fanart.jpg","composite":"/library/sections/1/composite/1793223238","filters":true,"refreshing":false,"thumb":"/:/resources/movie.png","key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"c433e532-2e23-40be-b0fb-a99ae0b0a264","updatedAt":1793223238,"createdAt":1793223238,"scannedAt":1793223238,"content":true,"directory":true,"contentChangedAt":8740763687856723809,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 200] + +### negative control: no such section name +0 +### claimed? (Preferences.xml PlexOnlineToken present = claimed; value never printed) +0 +[fix-attrs.d] done. +[cont-init.d] executing container initialization scripts... +[cont-init.d] 40-plex-first-run: executing... +Creating pref shell +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +Critical: libusb_init failed +### SWAP to plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +identity 200 with no startState after ~14s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"1.2.3","claimed":false,"machineIdentifier":"a34886c6b3231d99de4a8083f3f7116d317e0201","version":"1.43.5.11029-6bf8af3f0"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":1,"allowSync":false,"title1":"Plex Library","Directory":[{"allowSync":false,"filters":true,"refreshing":false,"key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"c433e532-2e23-40be-b0fb-a99ae0b0a264","updatedAt":1793223238,"createdAt":1793223238,"scannedAt":1793223238,"content":true,"directory":true,"contentChangedAt":8740763687856723809,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 200] + +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +### teardown +0 diff --git a/documentation/audits/night-2026-10-28/bench/plex/spike3-claim-junk.txt b/documentation/audits/night-2026-10-28/bench/plex/spike3-claim-junk.txt new file mode 100644 index 00000000..a4d3b418 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/spike3-claim-junk.txt @@ -0,0 +1,66 @@ +### pull +docker.io/plexinc/pms-docker:1.41.4.9463-630c9f557 +docker.io/plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +### FROM plexinc/pms-docker:1.41.4.9463-630c9f557 (PLEX_CLAIM set-to-junkgjunkclaim0123456789abcd) +container ip 172.22.0.2; caller = bench host via 172.22.0.2 dev br-e260f5ae876f src 172.22.0.1 uid 0 +identity 200 with no startState after ~38s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"0.1.0","claimed":false,"machineIdentifier":"2fa52b70d045fe078574598f87383acf6152b6a9","version":"1.41.4.9463-630c9f557"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":0,"allowSync":false,"title1":"Plex Library"}} +[http 200] + +--- POST /library/sections?name=FelhomSpike&type=movie&agent=tv.plex.agents.movie&scanner=Plex%20Movie&language=en-US&location=%2Fmedia%2Fmovies +{"MediaContainer":{"size":1,"Directory":[{"art":"/:/resources/movie-fanart.jpg","composite":"/library/sections/1/composite/1793223303","thumb":"/:/resources/movie.png","key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"6811ca8e-d23e-4f6f-816e-b608f4e79b75","updatedAt":1793223303,"createdAt":1793223303,"content":true,"directory":true,"contentChangedAt":3531497414163235587,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 201] + +--- GET /library/sections +{"MediaContainer":{"size":1,"allowSync":false,"title1":"Plex Library","Directory":[{"allowSync":false,"art":"/:/resources/movie-fanart.jpg","composite":"/library/sections/1/composite/1793223303","filters":true,"refreshing":false,"thumb":"/:/resources/movie.png","key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"6811ca8e-d23e-4f6f-816e-b608f4e79b75","updatedAt":1793223303,"createdAt":1793223303,"scannedAt":1793223303,"content":true,"directory":true,"contentChangedAt":3531497414163235587,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 200] + +### negative control: no such section name +0 +### claimed? (Preferences.xml PlexOnlineToken present = claimed; value never printed) +0 +Attempting to obtain server token from claim token + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed + + 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 + 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 + 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +Critical: libusb_init failed +### SWAP to plexinc/pms-docker:1.43.5.11029-6bf8af3f0 +identity 200 with no startState after ~12s +--- GET /identity +{"MediaContainer":{"size":0,"apiVersion":"1.2.3","claimed":false,"machineIdentifier":"2fa52b70d045fe078574598f87383acf6152b6a9","version":"1.43.5.11029-6bf8af3f0"}} +[http 200] + +--- GET /library/sections +{"MediaContainer":{"size":1,"allowSync":false,"title1":"Plex Library","Directory":[{"allowSync":false,"filters":true,"refreshing":false,"key":"1","type":"movie","title":"FelhomSpike","agent":"tv.plex.agents.movie","scanner":"Plex Movie","language":"en-US","uuid":"6811ca8e-d23e-4f6f-816e-b608f4e79b75","updatedAt":1793223303,"createdAt":1793223303,"scannedAt":1793223303,"content":true,"directory":true,"contentChangedAt":3531497414163235587,"hidden":0,"Location":[{"id":1,"path":"/media/movies"}]}]}} +[http 200] + +Plex Media Server first run setup complete +[cont-init.d] 40-plex-first-run: exited 0. +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: executing... +[cont-init.d] 45-plex-hw-transcode-and-connected-tuner: exited 0. +[cont-init.d] 50-plex-update: executing... +[cont-init.d] 50-plex-update: exited 0. +[cont-init.d] done. +[services.d] starting services +Starting Plex Media Server. +[services.d] done. +### teardown +0 diff --git a/documentation/audits/night-2026-10-28/bench/plex/test_upgrade_bench.txt b/documentation/audits/night-2026-10-28/bench/plex/test_upgrade_bench.txt new file mode 100644 index 00000000..61b6eec0 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/test_upgrade_bench.txt @@ -0,0 +1,61 @@ +test_all_three_conditions_allow (__main__.BenchAdminSeedGuard.test_all_three_conditions_allow) ... ok +test_each_condition_alone_refuses (__main__.BenchAdminSeedGuard.test_each_condition_alone_refuses) ... ok +test_the_bench_seeds_through_the_admin_invite_and_never_shows_the_token (__main__.BenchAdminSeedGuard.test_the_bench_seeds_through_the_admin_invite_and_never_shows_the_token) ... /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/test_upgrade_bench.py:177: ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/.felhom-h-zt3ldp_e' mode='r' encoding='UTF-8'> + headers.append(open(extra[i + 1][1:]).read().strip()) +ResourceWarning: Enable tracemalloc to get the object allocation traceback +ok +test_the_bench_venue_names_itself (__main__.BenchAdminSeedGuard.test_the_bench_venue_names_itself) ... ok +test_the_box_walk_never_signs_in_as_admin (__main__.BenchAdminSeedGuard.test_the_box_walk_never_signs_in_as_admin) ... ok +test_a_household_shaped_box_is_never_seeded (__main__.BoxAdminSeedGuard.test_a_household_shaped_box_is_never_seeded) ... ok +test_a_refused_invite_is_inconclusive (__main__.BoxAdminSeedGuard.test_a_refused_invite_is_inconclusive) ... ok +test_all_conditions_allow (__main__.BoxAdminSeedGuard.test_all_conditions_allow) ... ok +test_each_condition_alone_refuses (__main__.BoxAdminSeedGuard.test_each_condition_alone_refuses) ... ok +test_only_a_drill_address_is_invited (__main__.BoxAdminSeedGuard.test_only_a_drill_address_is_invited) ... ok +test_the_test_box_seeds_through_the_invite_inside_the_box (__main__.BoxAdminSeedGuard.test_the_test_box_seeds_through_the_invite_inside_the_box) ... ok +test_the_tester_1_box_is_a_test_box_and_demo_hp_9201_is_not (__main__.BoxAdminSeedGuard.test_the_tester_1_box_is_a_test_box_and_demo_hp_9201_is_not) ... ok +test_an_unknown_target_is_refused (__main__.BoxWalkTargets.test_an_unknown_target_is_refused) ... ok +test_every_row_is_complete (__main__.BoxWalkTargets.test_every_row_is_complete) ... ok +test_hdd_path_is_the_drive_itself (__main__.BoxWalkTargets.test_hdd_path_is_the_drive_itself) +R-839: the deploy API refuses an HDD_PATH that is a folder inside a registered drive (409, measured ... ok +test_remove_looks_for_the_install_record_not_the_template_folder (__main__.BoxWalkTargets.test_remove_looks_for_the_install_record_not_the_template_folder) +/opt/docker/stacks/ is the catalog's template copy and stays after a clean remove; app.yaml is the ... ok +test_the_default_stays_scratch_9202 (__main__.BoxWalkTargets.test_the_default_stays_scratch_9202) ... ok +test_the_old_guest_switch_still_selects_demo_hp_9201 (__main__.BoxWalkTargets.test_the_old_guest_switch_still_selects_demo_hp_9201) ... ok +test_the_tester_1_box_is_reached_through_the_hp_box (__main__.BoxWalkTargets.test_the_tester_1_box_is_reached_through_the_hp_box) ... ok +test_names_changed_added_removed_and_nothing_else (__main__.ChangedFiles.test_names_changed_added_removed_and_nothing_else) ... ok +test_never_a_bare_root_never_outside (__main__.ClearScratch.test_never_a_bare_root_never_outside) ... /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/test_upgrade_bench.py:52: ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/bench-scratch-jw5or5l5/hdd/appdata/x/config/config.php' mode='w' encoding='UTF-8'> + open(p, "w").write("last run") +ResourceWarning: Enable tracemalloc to get the object allocation traceback +ok +test_the_apps_own_folders_are_cleared_and_said (__main__.ClearScratch.test_the_apps_own_folders_are_cleared_and_said) ... /mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/test_upgrade_bench.py:52: ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/bench-scratch-66bb1px8/hdd/appdata/nextcloud/config/config.php' mode='w' encoding='UTF-8'> + open(p, "w").write("last run") +ResourceWarning: Enable tracemalloc to get the object allocation traceback +/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts/test_upgrade_bench.py:52: ResourceWarning: unclosed file <_io.TextIOWrapper name='/tmp/bench-scratch-66bb1px8/hdd/appdata/immich/config/config.php' mode='w' encoding='UTF-8'> + open(p, "w").write("last run") +ResourceWarning: Enable tracemalloc to get the object allocation traceback +ok +test_nothing_new_is_refused (__main__.DefinitionEdge.test_nothing_new_is_refused) ... ok +test_the_edge_names_the_new_service (__main__.DefinitionEdge.test_the_edge_names_the_new_service) ... ok +test_the_to_render_reads_the_new_definition (__main__.DefinitionEdge.test_the_to_render_reads_the_new_definition) ... ok +test_answers_of_any_code_are_the_app_answering (__main__.LoadVerdict.test_answers_of_any_code_are_the_app_answering) ... ok +test_every_request_errored_is_inconclusive (__main__.LoadVerdict.test_every_request_errored_is_inconclusive) ... ok +test_under_half_is_inconclusive_and_none_is_inconclusive (__main__.LoadVerdict.test_under_half_is_inconclusive_and_none_is_inconclusive) ... ok +test_a_listed_name_that_grew_or_vanished_still_marks (__main__.MarkerIgnore.test_a_listed_name_that_grew_or_vanished_still_marks) ... ok +test_a_moved_tree_the_file_walk_cannot_name_keeps_the_mark (__main__.MarkerIgnore.test_a_moved_tree_the_file_walk_cannot_name_keeps_the_mark) ... ok +test_an_unlisted_changed_file_still_marks (__main__.MarkerIgnore.test_an_unlisted_changed_file_still_marks) ... ok +test_every_entry_has_a_reason (__main__.MarkerIgnore.test_every_entry_has_a_reason) ... ok +test_the_list_belongs_to_its_app (__main__.MarkerIgnore.test_the_list_belongs_to_its_app) ... ok +test_the_six_measured_markers_do_not_mark_and_each_carries_a_reason (__main__.MarkerIgnore.test_the_six_measured_markers_do_not_mark_and_each_carries_a_reason) ... ok +test_env_is_0600_and_shredded (__main__.SecretHygiene.test_env_is_0600_and_shredded) ... ok +test_evidence_files_are_redacted (__main__.SecretHygiene.test_evidence_files_are_redacted) ... ok +test_redact_longest_first (__main__.SecretHygiene.test_redact_longest_first) ... ok +test_secret_values_are_the_generated_fields_and_the_seed_password (__main__.SecretHygiene.test_secret_values_are_the_generated_fields_and_the_seed_password) ... ok +test_kernel_peak_counts (__main__.SwapRecorded.test_kernel_peak_counts) ... ok +test_nothing_readable_is_none (__main__.SwapRecorded.test_nothing_readable_is_none) ... ok +test_venue_swap_read (__main__.SwapRecorded.test_venue_swap_read) ... ok +test_venue_swap_unreadable_is_none_not_zero (__main__.SwapRecorded.test_venue_swap_unreadable_is_none_not_zero) ... ok + +---------------------------------------------------------------------- +Ran 42 tests in 0.059s + +OK diff --git a/documentation/audits/night-2026-10-28/bench/plex/upgrade_fixtures.diff b/documentation/audits/night-2026-10-28/bench/plex/upgrade_fixtures.diff new file mode 100644 index 00000000..6735b530 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/plex/upgrade_fixtures.diff @@ -0,0 +1,124 @@ +diff --git a/scripts/upgrade_fixtures.py b/scripts/upgrade_fixtures.py +index 062a803..f88caa6 100644 +--- a/scripts/upgrade_fixtures.py ++++ b/scripts/upgrade_fixtures.py +@@ -852,6 +852,113 @@ class NZBGet: + return True + + ++class Plex: ++ """Plex's own HTTP API makes a library section and the same API proves it survived (felhom.eu R-1238). ++ ++ NO plex.tv ACCOUNT. An UNCLAIMED Plex answers a request from a private address with no token (felhom.eu ++ `architecture/13-threat-model.md`, the R-1045 row) - the harness calls from the bench host at the container's own ++ IP, so the caller is the docker bridge gateway (172.x.0.1), a private address. Measured on the bench 2026-10-28 ++ night (felhom.eu audits/night-2026-10-28/bench/plex/): POST /library/sections answered 201 with the new section; ++ GET read it back before and after 1.41.4.9463 -> 1.43.5.11029, with PLEX_CLAIM absent AND with a junk PLEX_CLAIM ++ (the image's first-run claim exchange fails and the server stays unclaimed). A CLAIMED server would answer 401 to ++ this route - seed() then records why and returns None, it never reads that as "no data". ++ ++ The server says it is ready only when /identity carries no `startState`: the healthcheck (curl -f /identity) is ++ healthy while `startState` is `startingPlugins`, and a POST then answers 400 "the server is still starting up" ++ (measured, the first spike attempt). JSON only with `Accept: application/json` - XML otherwise. ++ ++ What is proven: the section (its name, uuid and folder) lives in Plex's own database in the config volume. No media ++ is scanned; the scanner and the metadata agents are NOT measured, and no record claims they are.""" ++ port = 32400 ++ container = "plex" ++ J = ("-H", "Accept: application/json") ++ ++ def _base(self, ipfn): ++ ip = ipfn(self.container) ++ return f"http://{ip}:{self.port}" if ip else "" ++ ++ def _ready(self, base, say, tries=90, delay=4): ++ """(True, identity) once /identity answers 200 JSON with no startState; (False, last answer) otherwise.""" ++ last = "" ++ for _ in range(tries): ++ rc, code, out = _curl(base + "/identity", *self.J, timeout=15) ++ last = f"rc={rc} http={code} {out[:160]}" ++ if rc == 0 and code == "200": ++ try: ++ mc = json.loads(out).get("MediaContainer") or {} ++ except ValueError: ++ mc = {} ++ if "claimed" in mc and "startState" not in mc: ++ return True, mc ++ time.sleep(delay) ++ say(f" plex: never finished starting ({last})") ++ return False, None ++ ++ def _sections(self, base): ++ rc, code, out = _curl(base + "/library/sections", *self.J, timeout=30) ++ if rc != 0 or code != "200": ++ return None, f"rc={rc} http={code} {out[:160]}" ++ try: ++ return (json.loads(out).get("MediaContainer") or {}).get("Directory") or [], out ++ except ValueError: ++ return None, f"non-JSON http={code} {out[:160]}" ++ ++ def seed(self, ipfn, say): ++ base = self._base(ipfn) ++ if not base: ++ say(" plex: no container IP") ++ return None ++ ok, ident = self._ready(base, say) ++ if not ok: ++ return None ++ if ident.get("claimed") is not False: ++ self.tried = "the unclaimed private-address route: the server reports claimed=%r" % ident.get("claimed") ++ say(f" plex: {self.tried} - this route needs an UNCLAIMED server") ++ return None ++ name = "felhom" + secrets.token_hex(4) ++ q = ("name=%s&type=movie&agent=tv.plex.agents.movie&scanner=Plex%%20Movie&language=en-US&location=%%2Fmedia" % name) ++ rc, code, out = _curl(base + "/library/sections?" + q, *self.J, method="POST", timeout=60) ++ say(f" plex: POST /library/sections {name} http={code} (server {ident.get('version')}, claimed=false)") ++ if rc != 0 or code not in ("200", "201"): ++ self.tried = f"POST /library/sections answered rc={rc} http={code}: {out[:160]}" ++ say(f" plex: the app refused the section ({out[:160]})") ++ return None ++ try: ++ made = [d for d in (json.loads(out).get("MediaContainer") or {}).get("Directory") or [] if d.get("title") == name] ++ except ValueError: ++ made = [] ++ if not made or not made[0].get("uuid"): ++ say(f" plex: the create answer does not carry the section ({out[:160]})") ++ return None ++ say(f" plex: seeded library section {name} key={made[0].get('key')} uuid={made[0]['uuid']}") ++ return {"name": name, "uuid": made[0]["uuid"]} ++ ++ def verify(self, ipfn, seeded, say): ++ base = self._base(ipfn) ++ if not base: ++ return False ++ ok, ident = self._ready(base, say) ++ if not ok: ++ return False ++ secs, out = self._sections(base) ++ if secs is None: ++ say(f" plex: the app would not list its sections ({out})") ++ return False ++ # negative control first: a name nobody created must be absent ++ ghost = "felhom" + secrets.token_hex(4) ++ if any(d.get("title") == ghost for d in secs) or ghost in out: ++ say(" plex: READBACK IS UNUSABLE - a section nobody created is present") ++ return False ++ hit = [d for d in secs if d.get("title") == seeded["name"]] ++ if not hit: ++ say(f" plex: the seeded section {seeded['name']} is GONE ({len(secs)} section(s) listed)") ++ return False ++ same = hit[0].get("uuid") == seeded["uuid"] ++ locs = [l.get("path") for l in hit[0].get("Location") or []] ++ say(f" plex: section {seeded['name']} read back (server {ident.get('version')}), same uuid={same}, folders={locs}") ++ return same and "/media" in locs ++ ++ + FIXTURES = { + "privatebin": PrivateBin(), + "docmost": Docmost(), +@@ -865,4 +972,5 @@ FIXTURES = { + "transmission": Transmission(), + "sabnzbd": SABnzbd(), + "nzbget": NZBGet(), ++ "plex": Plex(), + } diff --git a/documentation/audits/night-2026-10-28/bench/tools/bx.py b/documentation/audits/night-2026-10-28/bench/tools/bx.py new file mode 100644 index 00000000..aca62f34 --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/tools/bx.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +"""bx.py [timeout] — run a bash script inside the bench (LXC 9401 on demo-hp) via retest-floating's bench().""" +import importlib.util, os, sys +H = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts" +spec = importlib.util.spec_from_file_location("rf", os.path.join(H, "retest-floating.py")) +rf = importlib.util.module_from_spec(spec); sys.path.insert(0, H); spec.loader.exec_module(rf) +r = rf.bench(open(sys.argv[1]).read(), timeout=int(sys.argv[2]) if len(sys.argv) > 2 else 600) +sys.stdout.write(r.stdout); sys.stderr.write(r.stderr); sys.exit(r.returncode) diff --git a/documentation/audits/night-2026-10-28/bench/tools/edge_run.py b/documentation/audits/night-2026-10-28/bench/tools/edge_run.py new file mode 100644 index 00000000..7a4aec2d --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/tools/edge_run.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""edge_run.py [soak] — one upgrade-test.py edge on the bench whose FROM is +NOT the template (an older tag -> the current pin), through upgrade-test.py's own run_edge(); log + evidence copied back. +The bench-side runner registers EDGES['MV-'] and calls main(), so the method is the harness's own, unchanged.""" +import base64, importlib.util, io, os, sys, tarfile +H = "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts" +spec = importlib.util.spec_from_file_location("rf", os.path.join(H, "retest-floating.py")) +rf = importlib.util.module_from_spec(spec); sys.path.insert(0, H); spec.loader.exec_module(rf) +app, ev, frm, to = sys.argv[1:5] +soak = sys.argv[5] if len(sys.argv) > 5 else "600" +f = dict(x.split("=", 1) for x in frm.split(",")); t = dict(x.split("=", 1) for x in to.split(",")) +py = ("import importlib.util,sys; sys.path.insert(0,'/opt/upg'); s=importlib.util.spec_from_file_location('ut','/opt/upg/upgrade-test.py'); " + "ut=importlib.util.module_from_spec(s); s.loader.exec_module(ut); " + "ut.EDGES['MV-%s']=dict(app=%r, note=%r, frm=%r, to=%r); " + "sys.exit(ut.main(['--soak', %r, 'MV-%s']))" % (app, app, os.environ.get('NOTE', 'older tag -> current pin'), f, t, soak, app)) +os.makedirs(ev, exist_ok=True) +coll = os.environ.get('COLLECT') # ' ' -> /opt/upg/MV--collect.csv beside the run +if coll: + rf.bench("cat > /opt/upg/meili_collect.sh <<'SHEOF'\n%s\nSHEOF\n" % open(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'meili_collect.sh')).read(), timeout=120) +r = rf.bench(("(nohup sh -c \"sleep 90; bash /opt/upg/meili_collect.sh %s\" > /opt/upg/MV-%s-collect.csv 2>&1 < /dev/null &) ; " % (coll, app) if coll else "") + "cd /opt/upg && rm -rf evidence/MV-%s && cat > /opt/upg/edge_run_inner.py <<'PYEOF'\n%s\nPYEOF\n" + "timeout 3300 python3 /opt/upg/edge_run_inner.py > /opt/upg/MV-%s.log 2>&1; echo rc=$?" % (app, py, app), timeout=3500) +print(r.stdout.strip()[-200:], r.stderr.strip()[-300:]) +pull = rf.bench("cd /opt/upg && tar czf - MV-%s.log MV-%s-collect.csv edge_run_inner.py evidence/MV-%s 2>/dev/null | base64 -w0" % (app, app, app), timeout=600) +tarfile.open(fileobj=io.BytesIO(base64.b64decode(pull.stdout.strip()))).extractall(ev) +print("evidence ->", ev) diff --git a/documentation/audits/night-2026-10-28/bench/tools/meili_bench.py b/documentation/audits/night-2026-10-28/bench/tools/meili_bench.py new file mode 100644 index 00000000..a458f66a --- /dev/null +++ b/documentation/audits/night-2026-10-28/bench/tools/meili_bench.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""meili_bench.py