R-108: network storage may not host an app's data namespace (v0.187.0)
This is D5's precondition and it is now met. An app's namespace root IS its backup root: namespaceRoot returns a non-system drive path as-is, so the recovery unit lands at <HDD_PATH>/backups/primary/<stack>/. On a NAS that sits inside the share, which FileBrowser binds WHOLE — share root, :rslave, download:true. The bind was NOT narrowed, and establishing why inverted the fix. The share-root :rslave bind is load-bearing (a 2026-07-22 probe proved an in-container access through it wakes the idle automount trigger), and scoping is undefinable anyway: apps on a share store at <share>/<app>, there is no userdata/ layer, and creating one would write Felhom convention onto a customer's own NAS, which R-67 forbids. So the browsing surface cannot be narrowed and the backup tree must never be placed under it. Operator ruling: refuse the placement, keep the browse bind. Tier 2 already refuses network targets for this reason (F-6C-1). Nothing stranded: zero apps on network storage across all six hub customers including Peti. R-67's browse capability is byte-identical. FIVE surfaces, not the four the register named — settings.RefuseAsAppNamespace is the single predicate. The deploy POST is the real boundary (it accepts any caller-supplied HDD_PATH; DeployStack validates only os.Stat). Surface 4, handleStorageDecommission mode=migrate, guarded only its SOURCE, so a whole namespace could be decommissioned ONTO a NAS — that one is not in the register. Fails closed: /mnt/felhom-drives holds both kinds, Kind exists only on a registered path, so an unregistered path under that root refuses. Supersedes README's "NAS backup locality — decision A" (v0.118.0). 9 tests, all non-effect (nil stackMgr, so a guard that misses panics rather than passing). 4 red-proofs, each mutation asserted to have landed. Suite rc=0, 27 packages, 0 FAIL. vet rc=0. Template + emoji gates OK.
This commit is contained in:
@@ -1,5 +1,79 @@
|
||||
## Changelog
|
||||
|
||||
### v0.187.0 — R-108: network storage may not host an app's data namespace (2026-07-30) — MinAgent 0.113.0 (unchanged)
|
||||
|
||||
**This is D5's precondition, and it is now met.** D5 moves app secrets into the local recovery unit so
|
||||
Tier-1/Tier-2 restore stop needing the guest; that is safe only once no browsing surface can reach the
|
||||
backup tree. One could.
|
||||
|
||||
**The chain, confirmed at source end to end.** `namespaceRoot(drivePath)` returns any non-system drive
|
||||
path AS-IS (`internal/backup/backup.go:262`), so an app's namespace root IS its `HDD_PATH`. Its recovery
|
||||
unit therefore lands at `<HDD_PATH>/backups/primary/<stack>/` (`appbackup.RecoveryUnitPath`). Put an app
|
||||
on a NAS and that directory sits inside the share, which FileBrowser binds **whole** — share ROOT,
|
||||
`:rslave`, `download: true`. Live on demo-hp, the asymmetry visible in one glance:
|
||||
|
||||
- /mnt/felhom-drives/nvme-1tb/userdata:/srv/nvme-1tb <- drive: userdata-SCOPED
|
||||
- /mnt/felhom-drives/Felhom-Share:/srv/Felhom-Share:rslave <- share: ROOT
|
||||
|
||||
**Why the bind was NOT narrowed** (this was the real finding, and it inverted the fix). The share-root
|
||||
`:rslave` bind is **load-bearing**, not an oversight: a Phase-0 probe (2026-07-22) proved an in-container
|
||||
access through it wakes the idle automount trigger, so narrowing it breaks NAS access itself. And
|
||||
scoping is not even definable — apps on a share store at `<share>/<app>`, there is no `userdata/` layer,
|
||||
and creating one would write Felhom's directory convention onto a customer's own NAS, which R-67
|
||||
forbids outright. So the browsing surface cannot be narrowed, and the backup tree must therefore never
|
||||
be placed under it. **Operator ruling 2026-07-30: refuse the placement, keep the browse bind.** Tier 2
|
||||
already refuses network targets for this same class of reason (`F-6C-1`); this closes the PRIMARY
|
||||
namespace, which was the last way a `backups/` tree could appear inside a share-root bind.
|
||||
|
||||
**Nothing is stranded.** Verified across all six hub customers including Peti: zero apps on network
|
||||
storage. demo-hp's `Felhom-Share` holds only the customer's own files (no `backups/`). R-67's browse
|
||||
capability is untouched — same bind, same `:rslave`, byte-identical.
|
||||
|
||||
**FIVE surfaces, not the four the register named.** `settings.RefuseAsAppNamespace` is the single
|
||||
predicate all of them consult:
|
||||
|
||||
1. **the deploy POST** (`internal/api/router.go`) — **this is the boundary.** The R-108 row says "the
|
||||
deploy dropdown has no `IsNetwork()` filter", which understates it: the dropdown is a UI list, and
|
||||
this endpoint accepts whatever `HDD_PATH` a caller supplies, with `DeployStack` validating only that
|
||||
it EXISTS (`os.Stat`, `internal/stacks/deploy.go`). Filtering the list alone would have left the
|
||||
surface open.
|
||||
2. **per-app migrate targets** (`internal/web/handlers.go`) — dropped from the offered list.
|
||||
3. **`handleStorageMigrateApp`** — refused before `MigrateApp`, so no job starts.
|
||||
4. **`handleStorageDecommission` mode=migrate, the TARGET** — **not in the register.** The existing
|
||||
`refuseNetworkLifecycle` guards `req.Where`, the SOURCE; the target was unchecked, so a whole
|
||||
namespace could be decommissioned ONTO a NAS. Found by enumerating the set rather than trusting the
|
||||
four that were named.
|
||||
5. **the FileBrowser bind** — deliberately unchanged, and now pinned by a test so it cannot drift.
|
||||
|
||||
**FAIL CLOSED, and the non-obvious part is why this is a function and not an `IsNetwork()` call:**
|
||||
`/mnt/felhom-drives` holds BOTH kinds in-guest (`.../hdd_1` is a local drive, `.../Felhom-Share` is a
|
||||
NAS), so a path prefix cannot classify — `Kind` is the only discriminator and it exists only on a
|
||||
REGISTERED path. An unregistered path under that root is therefore un-classifiable, and un-classifiable
|
||||
refuses. Every share is registered under that root by construction, so the network set is completely
|
||||
covered without touching drives.
|
||||
|
||||
**UI (§5): marked, not hidden.** A registered NAS stays in the deploy dropdown, `disabled`, labelled
|
||||
`(hálózati tárhely — alkalmazáshoz nem választható)`, and never pre-selected even when it is the
|
||||
registry default. A share the customer registered themselves, silently missing from the list they
|
||||
expect it in, reads as a bug and generates a support question; present-with-a-reason answers it in
|
||||
place. Follows the existing `(nem elérhető)` disabled-option precedent.
|
||||
|
||||
Tests: 9 new, all asserting the **non-effect**. The refusal tests run against a Server with a
|
||||
deliberately **nil `stackMgr`**, so a guard that fails to fire reaches the mutation and PANICS rather
|
||||
than passing quietly. They assert no job id, no `started` flag, no `MigratedTo` written, and — for
|
||||
decommission — that the source was NOT soft-marked. Fixtures are demo-hp's real two-class storage set
|
||||
(both paths under the same mount root, which is the trap). Red-proofs: 4, each mutation asserted to
|
||||
have landed first — drop either migrate guard → panic; break fail-closed → 3 tests; userdata-scope the
|
||||
share → the R-67 regression guard fires, quoting the broken bind.
|
||||
|
||||
Suite rc=0, 27 packages, 0 FAIL; `go vet` rc=0; `template_id_gate.py` + `emoji_gate.py` both OK.
|
||||
|
||||
- `internal/settings/settings.go` — `RefuseAsAppNamespace` + the two Hungarian refusal reasons.
|
||||
- `internal/api/router.go` — the deploy-POST refusal.
|
||||
- `internal/web/storage_handlers.go` — `refuseAppNamespaceTarget`; wired into migrate-app + decommission.
|
||||
- `internal/web/handlers.go` — migrate-target list filter; `DeployStoragePath.NotAllowed`.
|
||||
- `internal/web/templates/deploy.html` — disabled option + reason; no pre-select of a disabled default.
|
||||
|
||||
### v0.186.0 — R-114 + R-112: tell the truth about the backup target, then show it (2026-07-29) — MinAgent 0.113.0 (unchanged)
|
||||
|
||||
Two defects E-2d found on a real box, fixed in this order deliberately: the message is corrected
|
||||
|
||||
Reference in New Issue
Block a user