docs(roadmap): mark what is built, and record the fork-bomb postmortem
The phasing table now reflects reality rather than intent: 0, 1, 2 and 4 are done, 5 and 6 are partial (the migrate "unknown location" prompt and the Disks WebUI page are the gaps), 3 is untouched. §10.8 records why the app->location index detonated, because the mistake is easy to repeat and the blast radius was the whole machine. The file's stated requirements — manager-owned, on the system disk — are both true of configs/, which is why it looked like the right home. The property it violated is that configs/ is SOURCED, so a TSV of "<slug> <path>" is a script, and the row for the app named `libreportal` made it a self-invoking one. The generalisable rule is in there too: ask what a directory's contract is, not just who owns it. configs/** is executed; containers/<app>/**.config is executed. Both are load-bearing conventions a plain data file silently breaks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8fad6c6a4d
commit
70ca195f30
@ -1,6 +1,6 @@
|
||||
# LibrePortal — Storage Locations (per-app data placement)
|
||||
|
||||
**Status:** Proposal — **not built.** · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24)
|
||||
**Status:** Phases 0-2, 4 and 5 **built**; the two WebUI surfaces (setup-wizard step, Disks page) are not. · **Audience:** us, future-self · **Scope:** register more than one filesystem root for live app data, choose one per app, move an app between them, and resolve the right one on restore/migrate · **Origin:** "add different locations to set up LibrePortal on, with control per app" (2026-08-24)
|
||||
|
||||
---
|
||||
|
||||
@ -345,6 +345,44 @@ The WebUI's own dir stays pinned permanently and *structurally* (`webuiDir`), no
|
||||
|
||||
**10.7 — Footprint version.** New root helper + changed baked helpers ⇒ **bump `footprint_version`** (currently 5), or existing installs update the manager-owned code and keep root helpers that don't know about the registry.
|
||||
|
||||
## 10.8 — Postmortem: the index that became a fork bomb
|
||||
|
||||
Recorded because the mistake was subtle and the blast radius was the whole
|
||||
machine.
|
||||
|
||||
`storageIndexSet` cached the app→root map at `configs/storage/app_locations`. The
|
||||
file's stated requirements are "manager-owned" and "not on a removable disk", and
|
||||
`configs/` satisfies both — which is exactly why it looked like the right home.
|
||||
It carries a third property the file violated: **`sourceScanFiles` sources what it
|
||||
finds under `configs/`, and sourcing means executing.**
|
||||
|
||||
The index is a TSV of `<slug><TAB><root>`. bash reads that as a command and its
|
||||
argument. It stayed harmless while no slug matched a real executable — and armed
|
||||
the instant a row existed for the app named `libreportal`, because that IS the CLI
|
||||
on `PATH`. Sourcing ran `libreportal /libreportal-containers`, which re-entered the
|
||||
scan, which sourced the file again: one process pair per level, from every CLI
|
||||
invocation on the box including the task processor's own poll. 514 `start.sh`
|
||||
processes within 100 seconds of boot; the kernel killed plasmashell, kwin and sddm
|
||||
as collateral.
|
||||
|
||||
Two fixes, deliberately at both ends:
|
||||
|
||||
- `scan_files.sh` now requires a `.category` marker before sourcing anything in a
|
||||
`configs/` subdirectory — the contract `commandReloadConfigs` already enforced,
|
||||
and one all five real categories already satisfied.
|
||||
- the index moved to `$system_dir/storage/app_locations`, with a one-shot
|
||||
migration. A machine-written data file has no business in the one tree whose
|
||||
contract is "everything here is executed", guard or no guard.
|
||||
|
||||
`scripts/dev/lp-configs-guard-test` pins both: a file of the exact detonating
|
||||
shape placed in an unmarked `configs/` subdirectory must not execute, while a
|
||||
marked category must still load.
|
||||
|
||||
**The generalisable rule:** before putting a file anywhere in this repo, ask what
|
||||
the directory's *contract* is, not just who owns it. `configs/**` is executed.
|
||||
`containers/<app>/**.config` is executed (see the note in `scan_files.sh`). Both
|
||||
are load-bearing conventions that a plain data file silently violates.
|
||||
|
||||
## 11. What doesn't get a clean answer
|
||||
|
||||
Most of §10 collapses into a small number of good primitives — the marker file, `appDir`, the emptiness rule, a comment marker. Four things don't, and pretending otherwise would set us up to be surprised later.
|
||||
@ -360,13 +398,13 @@ Each phase is independently shippable and independently verifiable. Phase 0 carr
|
||||
|
||||
| Phase | Deliverable | Visible? |
|
||||
|---|---|---|
|
||||
| **0** | `appDir` / `storageRoots` / `pathIsContainerData` / `webuiDir`; sweep the ~200 call sites; helpers read a registry that contains exactly one root. Full install + backup + restore verified. | No |
|
||||
| **1** | Root-owned registry + `libreportal-storage` helper + the §6 fitness checks + `libreportal storage {list,add,remove,check,scan}`. Locations can exist; nothing uses them yet. | CLI only |
|
||||
| **2** | `CFG_<APP>_STORAGE` + the `**READONLY**` comment marker, resolved path in the comment (§5.2), install-time placement, per-location config panels and disk gauges. | Yes |
|
||||
| **3** | Setup-wizard **Storage** step (§7) + `--storage-dir=` for unattended installs. Depends on 1 and 2; deliberately after them, so first-run drives a path that already works headlessly. | Yes |
|
||||
| **4** | `libreportal app move`. | Yes |
|
||||
| **5** | Manifest `storage` block, staged restore + path rewrite (fixes §9's latent bug), the "unknown location" prompt in migrate. | Yes |
|
||||
| **6** | **Disks view** (§7.1) — extend `/admin/system/storage` into one row per filesystem with roles, badges and actions; `libreportal storage disks`. Periodic `storage check` + dashboard degradation warnings. Replaces the separate *Storage* page this phase used to propose: with disks as the frame, a second locations page would reintroduce exactly the split §13.1 resolved. | Yes |
|
||||
| **0** ✅ | `appDir` / `storageRoots` / `pathIsContainerData` / `webuiDir`; ~260 call sites swept. `scripts/dev/lp-storage-test`. | No |
|
||||
| **1** ✅ | Root-owned registry + `libreportal-storage` + fitness checks + `libreportal storage {list,add,remove,check,scan,apps,disks}`. | CLI only |
|
||||
| **2** ✅ | `CFG_<APP>_STORAGE` in 37 templates + the `**READONLY**` marker + resolved path in the comment (§5.2) + per-location config panel emit. | Yes |
|
||||
| **3** ⬜ | Setup-wizard **Storage** step (§7) + `--storage-dir=`. **Not built.** The headless path it would drive already works, so this is UI on top of a working feature, not a prerequisite. | Yes |
|
||||
| **4** ✅ | `libreportal app move` — stop, snapshot, copy as root, verify, then delete the source. | Yes |
|
||||
| **5** ◐ | Manifest `storage` block and the staged restore + path rewrite are done (§9's latent bug is fixed). The "unknown location" **prompt** is not — an unresolvable location currently falls back to this host's default rather than asking. | Yes |
|
||||
| **6** ◐ | The Disks **data layer** is built and `libreportal storage disks` renders it (including not-attached rows). The WebUI page at `/admin/system/storage` and the periodic `storage check` cron are **not**. | Yes |
|
||||
|
||||
## 13. Open questions
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user