diff --git a/CHANGELOG.md b/CHANGELOG.md index c9ecc0d..90f6e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## v0.287.0 — the family gate: family members with their own logins in front of chosen apps (decisions 63/64, R-780) (2026-10-02) + +**MinAgent: 0.131.0** (unchanged). Needs hub v0.123.0 (unchanged). New strings: `family_gate.*`, `err.stacks.family_gate_failed`, +`err.stacks.needs_newer_controller` (both languages). **Catalog companion:** `family_gate:`, `family_gate_except:` and +`min_controller:` in `.felhom.yml` (a template with `family_gate` must say `min_controller: "0.287.0"`). + +- **The family list** (`internal/family`): the household's dashboard admin adds, resets and removes family members on the + security page („Család" card). Each has their OWN name and a generated password (4×4 letters/digits, shown once, in the + answer to the press — never in a page, never logged). Sessions last 30 days and survive a restart (`family.json`, + 0600, atomic). A reset (the member's generation moves on), a removal or a logout ends access at the next request. +- **The door** (`internal/stacks/family_gate.go`): an app whose template says `family_gate: true` gets a traefik + forwardAuth file BEFORE its first start (also when a removed app is restored); a life record (`family_gate:` in + app.yaml) keeps it while the app is installed — a catalog change never gates or un-gates an installed app. Priority + below the install hold, the setup gate and the sign-up block. `family_gate_except:` lists literal path prefixes + (e-reader and phone apps) routed WITHOUT the door — **anchored** `^/prefix(/|$)` (the spike's finding F1: an unanchored + `PathPrefix(/api/v1/opds)` let `/api/v1/opdsx` through); a matcher or regex in the template refuses the install. +- **The answerer** (`internal/web/family_gate.go`): `/__felhom_gate/family` (forwardAuth; the app cookie + `felhom_famgate` is host-only and names a store session); `/__family/start|login|logout` on the dashboard host, + outside the dashboard's auth (the family session cookie `felhom_family` is scoped to `Path=/__family`). The sign-in is + counted per VISITOR (`clientIP`, R-753: 5 per minute) and per NAME (10 per 10 minutes) — a stranger locks only himself; + a name under a spread attack waits minutes. The household's dashboard session vouches (decision 46's rule) as a + household session in the family store. **`RequireAuth` never reads a family cookie; no family page ever sets the + dashboard cookie.** While the controller is down a gated app answers an error (traefik's forwardAuth), never the app. +- **`min_controller:`** — a template that needs a newer box is refused before anything is written. +- Tests: `TestFamily_*` (store), `TestFamilyGate_*` (web: stranger, member-not-dashboard, reset/remove/logout, locks, + household, token, setup gate untouched, messages, card), `TestFamilyExceptRegexp_Anchored`, `TestFamilyGate_*` + (stacks: before the first start, bad exception refuses, restore + loop), `TestMinController`. Red-proofs RP-F1..RP-F7, + each seen failing (`felhom.eu/documentation/audits/family-gate-2026-10-02/A/`). + ## v0.286.1 — R-772 found live: a stopped probe container is seen on the next tick (2026-10-01) **MinAgent: 0.131.0** (unchanged). Needs hub v0.123.0 (unchanged). No new strings. **v0.286.0 was never floored** diff --git a/REUSE.md b/REUSE.md index ff51ac2..51c37ed 100644 --- a/REUSE.md +++ b/REUSE.md @@ -28,6 +28,7 @@ | `stacks.RunAfterInstall` / `expandAfterInstall` / `web.defaultLoginInEffect` (v0.279.0, decision 45) | controller/internal/stacks/after_install.go · controller/internal/web/known_login.go | `(name, wait)` / `(cmd, allowed, env)` / `(meta, cfg, installed)` | A fresh install replaces a known default login; the page says when a default is still in effect | **Only from the deploy-done hook** — never after a restore/kept load (R-694). A `success:` marker is required (exit 0 lies). Never log the expanded command | | `stacks.OpenSetupGate` / `SetupGateTick` / `SetupGateHost` · `web.ServeGateAuth` / `ServeGateStart` (v0.280.0, decision 46) | controller/internal/stacks/setup_gate.go · controller/internal/web/setup_gate.go | `(name, by)` / `()` / `(host)` · handlers | The setup gate: a `setup_gate: true` install is closed to everyone but the household until its probe or the household's press opens it | **Write the gate BEFORE the first start** (spike F2). Open = record first, then remove the file. Never widen the dashboard cookie — the handshake mints a host-bound one-use token | | `stacks.OpenSignupWindow` / `SignupBlocked` / `SetupGateProbe` · `web.ServeSignupClosed` (v0.281.0, decision 47) | controller/internal/stacks/signup_block.go · controller/internal/web/setup_gate.go | `(name)` | Sign-up closed at the app's own address once the gate opens; the household's 15-minute window; the press asks the probe | **The block goes up BEFORE the gate comes down** (a failed write keeps the gate closed). Never on an app this box did not gate | +| `family.Store` · `stacks.FamilyGateHost` / `familyGateTick` / `FamilyExceptRegexp` · `web.ServeFamilyGateAuth` / `ServeFamilyStart` / `ServeFamilyLogin` / `ServeFamilyLogout` (v0.287.0, decisions 63/64) | controller/internal/family/family.go · controller/internal/stacks/family_gate.go · controller/internal/web/family_gate.go | store `Add/Reset/Remove/Verify/NewSession/Valid/EndSession` · `(host)` / `()` / `(prefix)` · handlers | The PERMANENT family gate: family members with their own logins in front of a `family_gate: true` app | **A family cookie never opens the dashboard** (RequireAuth reads only `felhom_session`); the app cookie names a STORE session, so the store is asked on every request (reset/remove/logout end access at once); **every exception goes through `FamilyExceptRegexp`** (anchored — never a hand-written PathPrefix); door written before the first start, like the setup gate. `familyStoreOverride` is the test seam. Fifth atomic-write helper (family.json, fsync) — see §6 | | `stacks.OpenInstallHold` / `installHoldTick` (v0.284.0, R-741) | controller/internal/stacks/install_hold.go | `(name, by)` / `()` | An `after_install` app held behind the setup gate's door until its known login is replaced | **Written before the first start**, like the gate; opens on `after_install` success or the household's "I changed it"; the door (`SetupGateHost`) reads holds first | | `stacks.RetainImagesAfterUpdate` / `RetainImagesAfterRemove` / `RunImageRetentionOnce` · seam `imageDocker` (v0.284.0, decision 53) | controller/internal/stacks/image_retention.go | `(name, previous)` / `(name, repos)` / `()` | Deletes an app's images older than its running + previous one | **The keep set is box-wide and read at delete time** (containers, installed composes, installed/previous records); exact id, never forced or pruned; skipped while any update runs; tests use the `imageDocker` seam, never Docker | | `stacks.RetainControllerImages(ControllerImageRecord)` · `selfupdate.UpdateState.RecordedPrevious` (v0.285.0, decision 56) | controller/internal/stacks/controller_image_retention.go | `({Repo, Running, Previous})` | Deletes controller images older than the running + previous one | The previous comes from the SWAP RECORD (success onto the running version), version order only as the fallback; versions above the running one and non-version tags are kept; skipped while the controller swaps itself; same `imageDocker` seam | diff --git a/controller/README.md b/controller/README.md index ffe61d6..b535482 100644 --- a/controller/README.md +++ b/controller/README.md @@ -1972,6 +1972,13 @@ that folder is never a dead end, and an install never runs into it silently (R-6 into app.yaml + one `compose up -d`, or a command) set when the gate opens, after the block. The window lifts and the loop re-applies it. `POST /apps//close-signup` for an app installed before the rule: lock record (`opened_by: close-signup`), block, switch; never a gate. Code: `internal/stacks/after_setup.go`. +- **The family gate (v0.287.0, decisions 63/64)** — `.felhom.yml` `family_gate: true` puts a PERMANENT door in front of + the app: only the household's family members (each with their own name and password, „Család" card on the security + page) and the household itself get through; `family_gate_except:` lists literal path prefixes left to the app's own + login (anchored `^/prefix(/|$)`). Door file `family-gate-.yml`, written before the first start; answerer + `/__felhom_gate/family`; sign-in pages `/__family/start|login|logout` on the dashboard host; sessions 30 days in + `family.json`. A family cookie never opens the dashboard. `min_controller:` refuses a template too new for the box. + Code: `internal/family/`, `internal/stacks/family_gate.go`, `internal/web/family_gate.go`. - **A removed app restored from its backup gets its lock back (v0.286.0, R-773)** — with no app.yaml left, the restore (`PersistUnitRedeployConfig`) writes the lock record (`opened_by: restore`) and the block BEFORE anything starts; the loop sets the app's own switch. An installed app the household never closed keeps what it had (decision 49).