v0.287.0: CHANGELOG, README, REUSE (the family gate)
gates / gates (push) Successful in 29s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-02 07:43:49 +02:00
parent 977665d8c0
commit c8d5ee204d
3 changed files with 37 additions and 0 deletions
+29
View File
@@ -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) ## 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** **MinAgent: 0.131.0** (unchanged). Needs hub v0.123.0 (unchanged). No new strings. **v0.286.0 was never floored**
+1
View File
@@ -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.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.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 | | `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.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.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 | | `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 |
+7
View File
@@ -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 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/<slug>/close-signup` for an app installed before the rule: lock record loop re-applies it. `POST /apps/<slug>/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`. (`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-<app>.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 - **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 (`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). loop sets the app's own switch. An installed app the household never closed keeps what it had (decision 49).