diff --git a/CONTEXT.md b/CONTEXT.md index 38b2c5f1..f3a0b911 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -22,7 +22,7 @@ > `min_controller`; catalog gate `check-family-gate.py` (refuses a family app while the newest baked golden < 0.287.0 — > why this golden was baked off-cadence). `01` §5 carries the design paragraph. Licence read of all 58 templates > (R-787 CLOSED): `documentation/audits/licences-2026-10-02/TABLE.md`; operator rows R-789..R-795; SparkyFitness request -> drafted, not sent (R-784, decision 65). Register 422 → 435. Report: `REPORT-family-gate-2026-10-02.md`. +> drafted, not sent (R-784, decision 65). Register 422 → 436 (R-801: the volume-persistence gate never exercised an app — fixed, re-sweep owed; R-800: remove-with-data keeps userdata, operator). Report: `REPORT-family-gate-2026-10-02.md`. > **Rulings 2026-10-02 (morning, operator) — `09` §3 decisions 64, 65, recorded before the work.** **64 (R-780):** the > family gate — GO, both sessions. **65 (R-784 B):** SparkyFitness stays offered; the operator asks the author; no written diff --git a/REPORT-family-gate-2026-10-02.md b/REPORT-family-gate-2026-10-02.md new file mode 100644 index 00000000..00a10996 --- /dev/null +++ b/REPORT-family-gate-2026-10-02.md @@ -0,0 +1,77 @@ +# REPORT — the family gate built; Grimmory and MeTube published; every app's licence read (2026-10-02) + +Brief: "the family gate, built (controller + catalog), Grimmory and MeTube published behind it; every app's licence read; +SparkyFitness's licence ruling recorded; one controller release and a golden bake". Architecture read: +`01-topology-and-trust.md` §5 (the family-gate paragraph is new), `09` §3 decisions 46, 47, 63, 64, 65. +Evidence root: `documentation/audits/family-gate-2026-10-02/`. + +| Part | State | One line | +|---|---|---| +| **A** — the controller | **DONE** | v0.287.0: family members with own logins, a door per family app, anchored exceptions, the Család card. Exit items 1–5 live on 9202. | +| **B** — the catalog | **DONE** | Format + gate `family-gate`; Grimmory (4 e-reader exceptions) and MeTube (none) published (catalog `96829d0`) with complete records; on 9202 and demo-hp through the product. | +| **C** — licences | **DONE** | 58 templates, every image, one table (`audits/licences-2026-10-02/TABLE.md`); short list in STATUS; nothing hidden or changed. | +| **D** — SparkyFitness | **DONE (not sent)** | Request drafted (`audits/licences-2026-10-02/EMAIL-DRAFT-sparkyfitness.md`); decision 65 + trigger in STATUS. | +| **E** — release | **DONE** | Floor 0.287.0 (min_agent 0.131.0), both demo boxes in ~25 s; golden 0.287.0 baked, round-tripped, vouched; currency gate OK. Phone test SKIPPED (operator not present). | + +Not stopped between sessions: Part A was proven live on 9202 before Part B started (`A/items.txt`). + +## Claims in the brief, checked + +- **"The family session is separable with the existing cookie scheme"** — right, with one addition: a separate cookie + name (`felhom_family`) scoped to `Path=/__family` and a separate store; RequireAuth never reads it (test + `TestFamilyGate_MemberPassesButNeverTheDashboard`, red-proof RP-F1; live: the member's cookies at `/launcher` → the + dashboard login, also through the real internet). +- **"Grimmory's e-reader paths keep their own auth behind an exception"** — right, measured per path as a stranger + through the simulated tunnel: OPDS 401 (Grimmory's), Kobo made-up token 401, KOReader wrong key 401, Komga API 401; + the right credentials 200; look-alikes (`/api/v1/opdsx`, `/api/koreaderx`, `../`) → the gate. +- **"MeTube's websocket passes forwardAuth"** — right: a member's upgrade → 101; a stranger's websocket and polling → 401. +- **"Whether n8n's licence limits paid services"** — partly: own internal or personal use is allowed; providing it to + others is allowed only free of charge for non-commercial purposes. Pulling the unchanged image onto the household's + box is arguably not that — a grey zone, now an operator row (R-791). + +## Proof + +- **Controller:** suites green; red-proofs RP-F1..F7 (`A/RP-F-family-gate-mutants.txt`). Live (`A/items.txt`): a stranger + 0 app answers of 36; members in; reset/remove/logout end access at the next request; a stranger's 7 guesses lock only + the stranger; controller down → 500, never the app; the gate costs 0.49 ms. **Through the real internet on demo-hp** + (`B4/member-internet.txt`): a member's browser → the family sign-in → MeTube; logout → refused; a stranger 401. +- **Catalog:** `catalog_gates.py grimmory` and `metube` exit 0 on the bench, every gate OK (`B/catalog-gates-*.txt`). + Decoys of the new gate seen red (`B/family-gate-decoys-red.txt`). +- **Stranger per exception (Grimmory):** `A/items.txt` item 4; R-775 re-measured: 6 tries → the gate's 401, then the + household signs in 200 — the 15-minute lock can no longer be aimed from outside. +- **9202 lifecycle** (`B/box/`): Grimmory step 3.4.1 → 3.5.0 behind the gate (58.5 s, book read back); MeTube fresh + install (a stranger's 63 polls: 404 → 401, never the app), step .28 → .29 (34.9 s), night backup, remove keeping data, + restore (64.8 s / 38.5 s), seeds read back, the gate files and the stranger's refusal back by themselves. +- **demo-hp, live catalog** (`B4/`): both installed through the product, strangers 401 through the real internet and + the LAN, removed with data; the apps were removed after (STATUS asks whether to keep them). + +## Found on the way (all register rows) + +- **R-801 (P2, fixed in the catalog):** the volume-persistence gate never sent a request to ANY app — it read the port + from label values, the port is in the label name. Fixed (`routed_ports()`), red-proofed (`B/RP-R801-routed-ports.txt`). + **A re-sweep of all 58 templates is owed.** Before the fix MeTube answered UNDETERMINED; after, CLEAN with its own + download (`B/volume-persistence-after-R801.txt`). +- **R-800 (P2, operator):** "remove with data" keeps an app's files in userdata, and the dialog does not say so. +- R-788 (`APP_EXERCISE`), R-796 (MeTube's send-to helpers cannot pass the gate), R-797 (rule 3 not checkable in CI), + R-798 (Grimmory's dead env line), R-799 (fixture field). Licence rows R-789..R-795. +- Harness only (no row): box_walk cached "not gated" while an app had no router (fixed); Cloudflare refuses Python's + user agent (error 1010) — a test client detail, the box was never reached. + +## Rows and gates + +- **Register 422 → 436:** closed R-767, R-780, R-787; updated R-775 (narrowed), R-784 (decided B, draft ready), + R-788; opened R-788..R-801 (14). +- **Releases:** controller v0.287.0 (one release). Catalog `96829d0` (+ the record amendment). Golden 0.287.0. +- **CI by head_sha:** catalog 96829d0 → job 1201 success; felhom.eu e6d1ebd → 1200 success; controller 9821690 → 1199 + success (all pushes of the session green). +- `unproven.py --summary`: unchanged (not walked 35 of 55). + +## Teardown + +- **9202:** both apps removed (keep-data remove; the harness removed its own test folders by hand — R-442 refuses + with-data there); back on the live catalog (`A/repoint-drill.txt` controls); family members anna/bela remain on 9202's + card (scratch box, harmless). +- **Bench 9401:** rebuilt and destroyed four times; `pct list` 9201 + 9202 only; nvme-scratch back to its baseline. +- **demo-hp 9201:** apps removed, test files removed by hand, the test member removed (`B4/teardown.txt`). +- **Hub:** the floor and the artifact manifest changed on purpose; nothing else. **Secrets:** scratch files shredded at + the end; evidence scanned for every secret value used (0 hits, positive control 1). diff --git a/STATUS.md b/STATUS.md index 3e066fe7..0beaea25 100644 --- a/STATUS.md +++ b/STATUS.md @@ -2,61 +2,69 @@ **Ready for the first real tester (Tester-2): yes. You confirmed the tunnel route and the connect mails (2026-09-30).** -**Updated 2026-10-01 (late night). Both demo boxes run controller 0.286.1 and host agent 0.138.0. Hub 0.126.0. New -installs get golden 0.286.1 with agent 0.138.0.** +**Updated 2026-10-02. Both demo boxes run controller 0.287.0 and host agent 0.138.0. Hub 0.126.0. New installs get +golden 0.287.0 with agent 0.138.0.** **Tester-2 — read only, from the hub.** The customer record exists. Tester-2's box has not registered yet. -## Tonight: the box tells visitors apart +## Today: the family gate is built -- **Before:** through the internet every visitor looked like the same address. A stranger's 5 wrong passwords locked the - whole family out of the box's own dashboard for a minute, and out of some apps. -- **Now:** the box sees each visitor's real address, and a stranger cannot fake it. Measured on demo-hp through the real - internet path: a stranger's wrong passwords locked only that stranger. -- **Apps:** 19 apps would have believed a faked address. They are protected, in the same update. BookStack now locks - only the stranger, measured. Home Assistant no longer treats every internet visitor as "at home". -- **Not measured:** a second outside address on the real internet path. The test machine abroad was refused by - Cloudflare before it reached the box. A 2-minute test from your phone would close it. +- **What it is:** a door in front of chosen apps. Only the family gets in. Each family member has their own name and + password. You add, reset and remove them on the dashboard: Settings → Security → **Család** card. +- **Grimmory (e-books) is published behind it.** Strangers cannot reach its login any more, so they cannot lock you out. + E-readers (Kobo, KOReader, OPDS apps) still work with Grimmory's own login. +- **MeTube (video downloads) is published behind it.** It has no login of its own, so the gate is its only door. The page + says: download only what you have the right to. +- **Measured on the scratch box and on demo-hp:** a stranger reached nothing; a stranger's wrong guesses locked only that + stranger; a removed family member was out at the next click; backup, remove and restore kept the gate. +- **Phone test: skipped** — you were not here. A 2-minute test from your phone off wifi would prove the real-internet + family sign-in by a person, not by me. -## The permanent family gate: tested, NOT built +## Licences: all 58 apps read -- A gate with a login for each family member, in front of Grimmory and MeTube, passed every test on the scratch box. -- **Strangers:** they reached nothing, and their wrong guesses locked only themselves. -- **Family:** members stayed signed in for 30 days, logout worked, and the family login does not open the dashboard. -- **E-readers:** Kobo, KOReader and OPDS readers kept working with Grimmory's own login. -- **Cost:** under 1 ms per page. If the box's controller is down, the gated apps refuse — they never open. +Most are open source. These need your word (nothing was hidden or changed): -## Also tonight +| App | The licence says | Choose: keep / hide / ask | I would | +|---|---|---|---| +| **Tandoor** | You may not sell a service whose value comes mostly from it (paid hosting and support named). | | ask the authors; keep meanwhile | +| **Emby** | Personal, non-commercial use only. Jellyfin does the same job. | | hide for new installs | +| **SparkyFitness** | Commercial use needs the author's written permission. (Decided: keep, ask.) | — | send the e-mail draft | +| n8n | Own or personal use is fine; giving it to others only free of charge. | | keep | +| Plex | Your household is the licensee with its own Plex account. | | keep | +| recipe-importer | Our own app — it has no licence file. | | pick one | -- **SparkyFitness passed most of the checklist.** One problem: its **licence forbids commercial use** without the - author's written permission (below). -- **Two box fixes:** - - The health check now says "not checked" instead of "healthy" when it could not look. - - A removed and restored app keeps its sign-up lock. -- **A new golden.** Fresh installs get today's version. +Four apps carry paid "enterprise" parts that stay switched off (Cal.com, Docmost, Outline, search in Wanderer), and seven +apps use Redis 7.4 as a private cache. Both are fine as they run; I watch them. -**Rows.** 4 closed, 12 opened. The list went from 410 to 422 rows. +**SparkyFitness e-mail:** drafted, NOT sent. The author has no public e-mail; the draft suggests their Discord. +If no written permission arrives before the first paying customer, SparkyFitness gets hidden from new installs. + +## Also today + +- **A new golden (0.287.0).** Baked early, because a box from the old golden would have shown the two apps without the + gate until its first update. The catalog check now refuses that case. +- **A bug in one catalog check, fixed:** the check "does the app keep its data where we back it up" never actually + talked to any app. Fixed and proven; all apps must be checked again (my next work). +- **Rows.** 3 closed, 14 opened (most are your licence choices). The list went from 422 to 436 rows. ## What needs you -1. **The permanent family gate: build it or not?** - - **Go:** 2 work sessions — the box first, then Grimmory and MeTube published behind it. - - **No-go:** Grimmory stays unpublished, and MeTube is not built. - - **If you say nothing:** nothing is built, and the next sessions do other work. - - I would choose **go**. It also answers Grimmory and MeTube. -2. **SparkyFitness's licence** says "non-commercial use only". - - **A:** hide it from new installs and ask the author for permission. - - **B:** keep it offered while asking. - - I would choose **A**. - - **If you say nothing:** it stays offered. -3. **Grimmory:** publish behind the family gate (if go), or as it is now. Its lock is per visitor since tonight; read in - its code, not yet measured. -4. **One phone test (2 minutes):** sign in to demo-hp's dashboard from your phone off wifi, while I lock out my own - address. If you skip it, the second-address proof stays the simulated one. -5. Still open from before: Invidious and moonlight-web — I advise no to both. MeTube and Pinchflat are stopped. +1. **The licence table above:** keep, hide or ask, per row. **If you say nothing:** everything stays offered. +2. **Send the SparkyFitness request** (the draft is ready). **If you do nothing:** it gets hidden before the first paying + customer. +3. **Removing an app "with data" keeps its files** (Grimmory's books, MeTube's videos) in the household folder, and + the dialog does not say so. **Choose:** keep them and say so on the dialog (I would), or delete the app's own folder + too. **If you say nothing:** files stay, the dialog stays silent. +4. **Demo box apps:** I installed Grimmory and MeTube on demo-hp for the test and removed them after. Want them kept + there for showing? **If you say nothing:** they stay removed. +5. **One phone test (2 minutes), only if you want it:** say so, and I put MeTube back on demo-hp and give you a family + login; you open `video.enkisfelhom.hu` on your phone off wifi. **If you say nothing:** the proof stays my real-internet + test from here. +6. Still open: Invidious and moonlight-web — I advise no to both. Pinchflat stays stopped (upstream paused). ## Standing steps - **Monthly security re-test: last run 2026-10-01, next due ~2026-11-01.** (You start it with the standing brief.) -- **Weekly:** the golden bake (around 8 October; tonight's bake was 0.286.1). +- **Weekly:** the golden bake (next around 9 October; today's bake was 0.287.0). +- **SparkyFitness:** written permission from the author before the first paying customer, or `lifecycle: hidden`. - **Registry clean-up:** only when the registry disk fills; "show me" mode first, then a person decides. diff --git a/documentation/audits/family-gate-2026-10-02/B4/attempt1/demo-common.txt b/documentation/audits/family-gate-2026-10-02/B4/attempt1/demo-common.txt new file mode 100644 index 00000000..cbfb9c84 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/attempt1/demo-common.txt @@ -0,0 +1,5 @@ +##### B4 on demo-hp 9201 — controller gitea.dooplex.hu/admin/felhom-controller:0.287.0 | catalog 96829d0 Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decis | 2026-10-02T07:38:25Z +before: gate files: none | drive: /dev/nvme0n1 938G 74G 817G 9% /mnt/felhom-drives/hdd_1 +the public names resolve to: 2a06:98c1:3121::3 video.enkisfelhom.hu +2a06:98c1:3120::3 video.enkisfelhom.hu (Cloudflare — the REAL internet path) +Család card: add b4teszt -> ok=True (password returned once, len 19, not printed) diff --git a/documentation/audits/family-gate-2026-10-02/B4/attempt1/grimmory-demo.txt b/documentation/audits/family-gate-2026-10-02/B4/attempt1/grimmory-demo.txt new file mode 100644 index 00000000..264829b6 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/attempt1/grimmory-demo.txt @@ -0,0 +1,12 @@ +deploy grimmory (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} + gate files right after the press: family-gate-grimmory.yml setup-gate-grimmory.yml + deployed in 85 s: state=running pinned={'grimmory': 'ghcr.io/grimmory-tools/grimmory:v3.5.0', 'grimmory-db': 'mariadb:11.4'} + family_gate record: family_gate: since: "2026-10-02T07:38:31Z" hosts: - library.enkisfelhom.hu +the household makes the first admin through the setup gate -> 200 + setup gate opened by its probe after ~10 s; gate files: family-gate-grimmory.yml +NOTE: attempt 1 stopped on a HARNESS error: after Grimmory's setup gate opened, box_walk still held the SETUP-gate cookie, which the family gate rightly refused (401 on an API path, no redirect, so no retry). The product behaved as designed. Cleaned up through the product (remove with data, member removed) before attempt 2. +cleanup: remove grimmory WITH data -> ('200', {'ok': True, 'data': {'removed': 'grimmory', 'volumes_removed': ['grimmory_grimmory_db'], 'hdd_paths_removed': ['/mnt/felhom-drives/hdd_1/appdata/grimmory/data (4.0K)'], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/felhom-drives/hdd_1/backups/primary/grimmory (32K)'], 'verified': True}, 'message': 'Stack grimmory removed'}) +cleanup: remove member b4teszt -> {"data":{"members":[],"name":"b4teszt"},"error":"","ok":true} + +left: /mnt/felhom-drives/hdd_1/appdata/grimmory +/mnt/felhom-drives/hdd_1/userdata/media/grimmory diff --git a/documentation/audits/family-gate-2026-10-02/B4/attempt1/metube-demo.txt b/documentation/audits/family-gate-2026-10-02/B4/attempt1/metube-demo.txt new file mode 100644 index 00000000..e69de29b diff --git a/documentation/audits/family-gate-2026-10-02/B4/demo-common.txt b/documentation/audits/family-gate-2026-10-02/B4/demo-common.txt new file mode 100644 index 00000000..c2b13d48 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/demo-common.txt @@ -0,0 +1,7 @@ +##### B4 on demo-hp 9201 — controller gitea.dooplex.hu/admin/felhom-controller:0.287.0 | catalog 96829d0 Grimmory and MeTube published behind the family gate (controller >= 0.287.0, decis | 2026-10-02T08:10:07Z +before: gate files: none | drive: /dev/nvme0n1 938G 74G 817G 9% /mnt/felhom-drives/hdd_1 +the public names resolve to: 2a06:98c1:3120::3 video.enkisfelhom.hu +2a06:98c1:3121::3 video.enkisfelhom.hu (Cloudflare — the REAL internet path) +Család card: add b4teszt -> ok=True (password returned once, len 19, not printed) +Család card: remove b4teszt -> True | members now: [] +after: gate files: none | stacks: 0 diff --git a/documentation/audits/family-gate-2026-10-02/B4/grimmory-demo.txt b/documentation/audits/family-gate-2026-10-02/B4/grimmory-demo.txt new file mode 100644 index 00000000..a7029092 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/grimmory-demo.txt @@ -0,0 +1,17 @@ +deploy grimmory (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} + gate files right after the press: family-gate-grimmory.yml setup-gate-grimmory.yml + deployed in 80 s: state=running pinned={'grimmory': 'ghcr.io/grimmory-tools/grimmory:v3.5.0', 'grimmory-db': 'mariadb:11.4'} + family_gate record: family_gate: since: "2026-10-02T08:10:13Z" hosts: - library.enkisfelhom.hu +the household makes the first admin through the setup gate -> 200 + setup gate opened by its probe after ~10 s; gate files: family-gate-grimmory.yml + grimmory: readback http=200 found=True (controls: no token 401, wrong 401, second setup 403) +a book uploaded -> 204 | read back: True +on the drive: /mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub | +a STRANGER / internet 401 | LAN 401 +a STRANGER /api/v1/books internet 401 | LAN 401 +a STRANGER /api/v1/opdsx internet 401 | LAN 401 +a STRANGER /api/v1/opds internet 401 | LAN 401 +the family member b4teszt through the REAL internet: the app's address → the family sign-in → back: 403 at library.enkisfelhom.hu/ (the app's own page: False) +2.6 remove WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'grimmory', 'volumes_removed': ['grimmory_grimmory_db'], 'hdd_paths_removed': ['/mnt/felhom-drives/hdd_1/appdata/grimmory/data (4.0K)'], 'hdd_paths_preserved': [], 'backup_paths_removed': ['/mnt/felhom-drives/hdd_1/backups/primary/grimmory (32K)'], 'verified': True}, + left after: /mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub | /mnt/felhom-drives/hdd_1/appdata/grimmory | /mnt/felhom-drives/hdd_1/userdata/media/grimmory + gate files: family-gate-grimmory.yml family-gate-metube.yml | a stranger at library/ -> internet 404 | LAN 404 diff --git a/documentation/audits/family-gate-2026-10-02/B4/member-internet.txt b/documentation/audits/family-gate-2026-10-02/B4/member-internet.txt new file mode 100644 index 00000000..f7ea70b5 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/member-internet.txt @@ -0,0 +1,13 @@ +##### B4 member through the real internet — 2026-10-02T08:40:07Z +control: Python's user agent at the edge -> 403 | a browser's -> 404 (MeTube not installed yet: 404 from the box) +deploy metube -> 202 + state=running +Család card: add b4teszt -> ok=True (password returned once, not printed) +a member's browser opens video.enkisfelhom.hu (no cookie) -> 200 at https://felhom.enkisfelhom.hu/__family/start (the family sign-in page: True) +the member signs in on the box's own page -> 200 at https://video.enkisfelhom.hu/ (MeTube's own page: True) +the member reads /history -> 200 (True) +the same browser at the DASHBOARD (/launcher) -> 200 at https://felhom.enkisfelhom.hu/login (sent to the dashboard login: True) +logout -> 200; then /history -> 200 at https://felhom.enkisfelhom.hu/__family/start (refused: True) +a STRANGER (no cookie, browser UA) at video.enkisfelhom.hu/history -> 401 +remove metube WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'metube', 'volumes_removed': ['metube_metube_state'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adatot külső megh +Család card: remove b4teszt -> ok=True members now [] diff --git a/documentation/audits/family-gate-2026-10-02/B4/metube-demo.txt b/documentation/audits/family-gate-2026-10-02/B4/metube-demo.txt new file mode 100644 index 00000000..852f7bb5 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/metube-demo.txt @@ -0,0 +1,16 @@ +deploy metube (fields beyond DOMAIN/SUBDOMAIN: ['HDD_PATH'], values not printed) -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'} + gate files right after the press: family-gate-grimmory.yml family-gate-metube.yml + deployed in 40 s: state=running pinned={'metube': 'ghcr.io/alexta69/metube:2026.09.29'} + family_gate record: family_gate: since: "2026-10-02T08:12:13Z" hosts: - video.enkisfelhom.hu + metube: POST /add http=200 {"status": "ok"} + metube: the download finished (991017 bytes) + metube: history has it=True; GET /download/ http=200 +the seed reads back: True +on the drive: /mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4 | +a STRANGER / internet 401 | LAN 401 +a STRANGER /history internet 401 | LAN 401 +a STRANGER /socket.io/?EIO=4&transport=polling internet 401 | LAN 401 +the family member b4teszt through the REAL internet: the app's address → the family sign-in → back: 403 at video.enkisfelhom.hu/ (the app's own page: False) +2.6 remove WITH data and backups -> 200 {'ok': True, 'data': {'removed': 'metube', 'volumes_removed': ['metube_metube_state'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'hdd_note': 'Az alkalmazás nem tárolt saját adatot külső meghajtón, így ott nem volt mit törölni.', 'verified': True}, 'message': 'Stack metube removed'} + left after: /mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4 | /mnt/felhom-drives/hdd_1/userdata/media/metube + gate files: none | a stranger at video/ -> internet 404 | LAN 404 diff --git a/documentation/audits/family-gate-2026-10-02/B4/teardown.txt b/documentation/audits/family-gate-2026-10-02/B4/teardown.txt new file mode 100644 index 00000000..0ea3f985 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/B4/teardown.txt @@ -0,0 +1,14 @@ +## 2026-10-02T08:41:34Z demo-hp 9201 teardown (three layers) +machine — before the hand tidy: +/mnt/felhom-drives/hdd_1/userdata/media/grimmory/Felhom/Felhom Demo 7ca212a4/Felhom Demo 7ca212a4 - Felhom.epub +/mnt/felhom-drives/hdd_1/userdata/media/metube/Big_Buck_Bunny_360_10s_1MB.mp4 +0 +0 +1 +the test files above are this session's own (a test EPUB, the 1 MB test clip) — removed by hand with the empty test folders; the product kept them on purpose (userdata = the household's files, R-800): +audiobooks books comics movies music photos podcasts tv +adventurelog adventurelog-frontend adventurelog-postgres bentopdf bookstack bookstack-db calibre-web cloudflared docmost docmost-postgres docmost-redis felhom-controller filebrowser kimai kimai-db opengist paperless-postgres paperless-redis paperless-webserver privatebin romm romm-db romm-redis traefik +host — no guest, storage or file created on demo-hp itself (9201 only). +hub — read only; the floor and the artifact manifest changed on purpose (E/floor.txt, golden record); no customer or appliance record created. +Család card on demo-hp: members [] (b4teszt removed). +note: the 1 family-gate file counted above was MeTube's, read ~10 s after its removal; the box's tick removed it at 08:41:36 ("removed the family-gate file of an app that is not installed"); video.enkisfelhom.hu answers 404 after. Read 2026-10-02T08:42Z. diff --git a/documentation/audits/family-gate-2026-10-02/tools/fg_b4.py b/documentation/audits/family-gate-2026-10-02/tools/fg_b4.py index e67b46bf..d5773cd2 100644 --- a/documentation/audits/family-gate-2026-10-02/tools/fg_b4.py +++ b/documentation/audits/family-gate-2026-10-02/tools/fg_b4.py @@ -69,6 +69,7 @@ def deploy(app, sub): w.login() +w.sync_rescan("metube", "ghcr.io/alexta69/metube:2026.09.29") say(None, f"##### B4 on demo-hp 9201 — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()} | catalog " + w.guest("cd /opt/docker/felhom-controller/data/catalog-cache 2>/dev/null || cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache; git log --oneline -1").strip()[:90] + f" | {time.strftime('%FT%TZ', time.gmtime())}") @@ -95,6 +96,7 @@ for app, (sub, fx) in APPS.items(): if "setup-gate-grimmory" not in gate_files(): break time.sleep(5) say(app, f" setup gate opened by its probe after ~{5*i} s; gate files: {gate_files()}") + w.GATE.pop(sub, None) # the SETUP-gate cookie is useless now; the family gate wants a family cookie (attempt 1) code, tok = fx._token(w, sub, "admin", pw) auth = ["-H", f"Authorization: Bearer {tok}"] rc, c, out = w.app_curl(sub, "/api/v1/libraries", *auth, "-H", "Content-Type: application/json", diff --git a/documentation/audits/family-gate-2026-10-02/tools/fg_b4_lib.py b/documentation/audits/family-gate-2026-10-02/tools/fg_b4_lib.py new file mode 100644 index 00000000..b1363052 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/tools/fg_b4_lib.py @@ -0,0 +1,54 @@ +"""fg_b4_lib.py — the demo-box helpers fg_b4_member.py uses (deploy, the Család card, remove), product endpoints only.""" +import json, os, secrets, subprocess, sys, time +os.environ.setdefault("GUEST", "9201"); os.environ.setdefault("BASE", "https://192.168.0.155") +sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts") +import box_walk as w +w.HP = "hp" +DOM = w.DOMAIN; CTL = f"felhom.{DOM}"; DRIVE = "/mnt/felhom-drives/hdd_1" + + +def _card(action, name): + sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip(); csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip() + r = subprocess.run(["curl", "-sk", "-H", f"Host: {CTL}", "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}", "-X", "POST", + "--data", f"name={name}", f"{w.BASE}/family/members/{action}"], capture_output=True, text=True) + return json.loads(r.stdout) + + +def add_member(name, say): + d = _card("add", name) + say(f"Család card: add {name} -> ok={d['ok']} (password returned once, not printed)") + return d["data"]["password"] + + +def remove_member(name, say): + d = _card("remove", name) + say(f"Család card: remove {name} -> ok={d['ok']} members now {d['data'].get('members')}") + + +def deploy(app, sub, say): + code, d = w.ctl("GET", f"/api/stacks/{app}/deploy-fields") + fields = (((d.get("data") or {}).get("metadata") or {}).get("deploy_fields")) or [] + values = {"DOMAIN": DOM, "SUBDOMAIN": sub} + for f in fields: + ev, ty = f.get("env_var"), f.get("type") + if ev in values or (not f.get("required") and ty != "password"): + continue + values[ev] = DRIVE if ty == "path" else ("Demo-" + secrets.token_hex(12) if ty in ("secret", "password") else (f.get("default") or app)) + code, d = w.ctl("POST", f"/api/stacks/{app}/deploy", {"values": values}) + say(f"deploy {app} -> {code}") + for _ in range(120): + time.sleep(5) + st = w.stack(app) + if st.get("deployed") and (st.get("app_config") or {}).get("pinned_images") and st.get("state") == "running": + break + say(f" state={st.get('state')}") + + +def remove(app, say): + w.ctl("POST", f"/api/stacks/{app}/stop") + for _ in range(24): + time.sleep(5) + if w.stack(app).get("state") != "running": + break + code, d = w.ctl("POST", f"/api/stacks/{app}/remove", {"remove_hdd_data": True, "remove_backups": True}) + say(f"remove {app} WITH data and backups -> {code} {str(d)[:200]}") diff --git a/documentation/audits/family-gate-2026-10-02/tools/fg_b4_member.py b/documentation/audits/family-gate-2026-10-02/tools/fg_b4_member.py new file mode 100644 index 00000000..a4921145 --- /dev/null +++ b/documentation/audits/family-gate-2026-10-02/tools/fg_b4_member.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""fg_b4_member.py — B4 part 2: a family member signs in through the REAL internet (Cloudflare → demo-hp's tunnel) with a +browser's user agent. Attempt 2 of fg_b4.py used Python's urllib, whose user agent Cloudflare refuses at its edge (403, +"error code 1010") before the box — measured. MeTube is installed through the product, a test member is added on the +Család card, the member signs in with curl (cookie jar, browser UA) on the box's own page, reaches MeTube, then logs out +(and is refused after); a stranger's cookie-less request is refused through the same path. Then MeTube is removed with +its data and the member removed. Secrets never printed. Evidence: $EV/B4/member-internet.txt.""" +import html, json, os, re, secrets, subprocess, sys, tempfile, time +os.environ.setdefault("GUEST", "9201"); os.environ.setdefault("BASE", "https://192.168.0.155") +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fg_b4_lib as L +w = L.w +log = open(f"{w.EV}/B4/member-internet.txt", "a", buffering=1) +def say(*a): + w.say(*a); log.write(" ".join(map(str, a)) + "\n") +UA = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0 Safari/537.36" +jar = tempfile.mktemp(prefix="b4jar-") +def c(url, *a, data=None): + args = ["curl", "-s", "-L", "--max-time", "30", "-A", UA, "-b", jar, "-c", jar, "-H", "Accept: text/html", "-o", "/tmp/b4body", + "-w", "%{http_code} %{url_effective}"] + if data is not None: args += ["--data", data] + r = subprocess.run(args + list(a) + [url], capture_output=True, text=True) + code, _, eff = r.stdout.strip().partition(" ") + body = open("/tmp/b4body", encoding="utf-8", errors="replace").read() if os.path.exists("/tmp/b4body") else "" + return code, eff.split("?")[0], body + +w.login() +say(f"##### B4 member through the real internet — {time.strftime('%FT%TZ', time.gmtime())}") +say("control: Python's user agent at the edge ->", subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", "Python-urllib/3.13", "https://video.enkisfelhom.hu/"], capture_output=True, text=True).stdout, + "| a browser's ->", subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", UA, "https://video.enkisfelhom.hu/"], capture_output=True, text=True).stdout, "(MeTube not installed yet: 404 from the box)") +L.deploy("metube", "video", say) +fpw = L.add_member("b4teszt", say) +code, url, body = c("https://video.enkisfelhom.hu/") +say(f"a member's browser opens video.enkisfelhom.hu (no cookie) -> {code} at {url} (the family sign-in page: {'name=\"_ft\"' in body})") +ft = re.search(r'name="_ft" value="([^"]+)"', body); rd = re.search(r'name="rd" value="([^"]*)"', body) +form = "&".join(f"{k}={__import__('urllib.parse').parse.quote(v)}" for k, v in + (("_ft", ft.group(1) if ft else ""), ("rd", html.unescape(rd.group(1)) if rd else ""), ("name", "b4teszt"), ("password", fpw))) +code, url, body = c("https://felhom.enkisfelhom.hu/__family/login", data=form) +say(f"the member signs in on the box's own page -> {code} at {url} (MeTube's own page: {'MeTube' in body and 'name=\"_ft\"' not in body})") +code, url, body = c("https://video.enkisfelhom.hu/history") +say(f"the member reads /history -> {code} ({'done' in body})") +code, url, body = c("https://felhom.enkisfelhom.hu/launcher") +say(f"the same browser at the DASHBOARD (/launcher) -> {code} at {url} (sent to the dashboard login: {'/login' in url})") +code, url, body = c("https://felhom.enkisfelhom.hu/__family/logout", data="") +code2, url2, _ = c("https://video.enkisfelhom.hu/history") +say(f"logout -> {code}; then /history -> {code2} at {url2} (refused: {'video.' not in url2 or code2 != '200'})") +say("a STRANGER (no cookie, browser UA) at video.enkisfelhom.hu/history ->", + subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "-A", UA, "https://video.enkisfelhom.hu/history"], capture_output=True, text=True).stdout) +L.remove("metube", say) +L.remove_member("b4teszt", say) +os.unlink(jar) diff --git a/documentation/backlog/OPEN-ITEMS.md b/documentation/backlog/OPEN-ITEMS.md index cf3dfa41..ba31af7c 100644 --- a/documentation/backlog/OPEN-ITEMS.md +++ b/documentation/backlog/OPEN-ITEMS.md @@ -911,7 +911,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server` | **R-797** | **[P3-LOW] `check-family-gate.py` rule 3 (a family_gate template needs a baked golden ≥ 0.287.0) is checked only where the felhom.eu sibling exists — CI's single clone cannot.** MEASURED 2026-10-02 (`audits/family-gate-2026-10-02/C-metube-bench/C3b-family-gate-with-sibling.txt`): without the sibling the gate printed NOT CHECKED and its summary read OK. The summary line now says "rule 3 … NOT CHECKED here". The pre-push hook (with the sibling) is where it bites. **Needs:** nothing more unless CI gets the sibling. | **WATCHING — rank P3-LOW; owner: CC** | | **R-798** | **[P3-LOW] Grimmory's template sets `SWAGGER_ENABLED=false`, which v3.5.0 does not read (it reads `API_DOCS_ENABLED`, default false).** READ 2026-10-02 (`audits/family-gate-2026-10-02/B/grimmory-reads/upstream-v3.5.0.txt`). Harmless today — the API docs are off by default. **Needs:** remove the dead line or set the right name, on the next Grimmory step. | **READY — rank P3-LOW; owner: CC** | | **R-799** | **[P3-LOW] The MeTube fixture's `POST /add` leaves out `download_type`, which upstream's validator lists as required.** MEASURED 2026-10-02 (`audits/family-gate-2026-10-02/C-metube-bench/`): both 2026.09.28 and .29 answered 200 and downloaded anyway. Fragile if a later tag enforces it. **Needs:** add `download_type: video` to the fixture with the next MeTube step. | **READY — rank P3-LOW; owner: CC** | -| **R-800** | **[P3-LOW] Removing MeTube "keeping data" answers `hdd_paths_preserved: []` although its downloads stay on the drive.** MEASURED 2026-10-02 on 9202 (`audits/family-gate-2026-10-02/B/box/life.txt`, `audits/family-gate-2026-10-02/B/box/final.txt`): the remove parses MeTube's compose as "0 HDD mounts" — its only drive mount is `${USERDATA_PATH}`, not `${HDD_PATH}` — so the result lists nothing kept; the files WERE kept and the restore used them. Other userdata-only apps (jellyfin, komga, …) have the same shape. **Needs:** check what the household's remove dialog says for such an app; list userdata mounts as kept. | **READY — rank P3-LOW; owner: CC** | +| **R-800** | **[P2-MEDIUM] "Delete your data from the hard drive" keeps the app's files in the household's userdata folder — and neither the dialog nor the result says so.** MEASURED 2026-10-02. On demo-hp (live catalog, `audits/family-gate-2026-10-02/B4/grimmory-demo.txt`, `metube-demo.txt`): remove WITH data removed Grimmory's database volume, appdata and backups but KEPT the uploaded EPUB in `userdata/media/grimmory`; for MeTube the dialog offers no data checkbox at all and the result says "the app stored no data of its own on a drive" while the downloaded video stays in `userdata/media/metube`. On 9202 "keeping data" answers `hdd_paths_preserved: []` for the same reason (`audits/family-gate-2026-10-02/B/box/life.txt`): the remove reads `${HDD_PATH}` binds only. The model behind it may be right — userdata is the household's own files (a shared `media/` tree must never go with one app) — but then the dialog must say the files stay and where. **Needs (operator):** keep userdata on remove (and say so on the dialog), or delete an app's OWN userdata subfolder when the household ticks the box. | **WAITING-ON-OPERATOR — rank P2-MEDIUM; owner: operator (the rule), CC (the dialog)** | | **R-801** | **[P2-MEDIUM] The volume-persistence gate never sent a request to ANY app: it read the routed port from label VALUES while `docker compose config --format json` puts the port in the label NAME.** MEASURED 2026-10-02 on the bench (`audits/family-gate-2026-10-02/B/volume-persistence-metube-exerciser-diag.txt`: `"ports": []`, `"exercise": []` for MeTube; the label shape read on Compose 2.26.1). So every verdict since the gate exists came only from what an app writes at start by itself; the GET exercise and the deep second pass never ran. **Fixed in the catalog** (`routed_ports()`, key=value; unit tests, red-proofed: `audits/family-gate-2026-10-02/B/RP-R801-routed-ports.txt`). **Still owed:** a full re-sweep of all 58 templates on the bench with the fixed gate — an app whose data path is written only on a request may now read differently. | **READY — rank P2-MEDIUM; owner: CC (the re-sweep)** |