Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
10 KiB
REPORT — v0.113.0: NAS verify-before-commit + page redesign + protocol-honest guidance
Date: 2026-07-11 · Version: controller v0.113.0 (from v0.112.0) · Pairs with: agent
v0.81.0 + felhom.eu host-install v1.13.0
Spec: TASK — NAS verify-before-commit + page redesign + protocol-honest guidance
Evidence base: felhom.eu/documentation/audits/SPIKE-nas-verify-2026-07-11.md (b57f6c1)
Confirmed baselines → shipped
| Repo | Baseline (main) |
Shipped commits | Version |
|---|---|---|---|
| felhom-agent | 300f06722b (v0.80.0) |
added9d |
v0.81.0 — deployed on felhom-pve |
| felhom-controller | 3db9126121 (v0.112.0) |
bb8737a (orchestration) + a65dcff (UI) + this docs commit |
v0.113.0 — live on 9201 |
| felhom.eu | e80e14d6 |
27e2fb0 |
host-install v1.13.0 + feature doc |
What shipped (files)
Agent (added9d) — internal/storage/netmount.go (NFS retry=0 + exported
NetworkMountedAt/NetworkEndpointReachable), NEW internal/storage/netverify.go
(ClassifyNetVerifyFailure, Q4-verbatim table), NEW internal/localapi/netverifyjob.go (in-memory
single-slot detached verify + auto-rollback + GET /netstorage/verify-status),
internal/localapi/netstorage.go (sync fast-fail: full validation + 2 s TCP pre-probe before ANY
install; verify-job start), server.go (seams + route). No new sudoers grants — journal read is
unprivileged via the systemd-journal group.
Controller (bb8737a + a65dcff) — internal/agentapi/client.go (verify fields, typed
NetAddRefusedError, NetVerifyStatus), NEW internal/web/netprobe.go + netprobe_linux.go +
netprobe_other.go (uid-1000 re-exec probe; --netprobe hidden mode in cmd/controller/main.go),
NEW internal/web/netstorage_job.go (detached single-flight orchestration + §3.2 Hungarian map),
netstorage_handlers.go (job-start add + status endpoint + orphan rows + netListFn seam),
storage_handlers.go (route), templates/storage_network.html (full redesign).
felhom.eu (27e2fb0) — scripts/felhom-host-install.sh v1.13.0 (usermod -aG systemd-journal felhom-agent, idempotent; header/const drift v1.11.0-vs-1.12.0 fixed), NEW
documentation/controller/network-storage-nas.md (authoritative feature doc).
Tests + companion red-proofs (all: mutation run → FAIL observed → reverted → suite green)
Test funcs added: agent storage +3 (netverify_test.go incl. the pure networkMountedIn table),
agent localapi +5 (netverifyjob_test.go), controller web +9 (netstorage_job_test.go +
storage_network_template_test.go).
| # | Test | Red-proof mutant | Observed failure (verbatim core) |
|---|---|---|---|
| A1 | TestMountOptions_NFSRetry0_SMBWithout (+ NFS rendering test) |
reverted retry=0 |
NFS options missing retry=0 (Q4-vi): "vers=4.1,...,_netdev" |
| A2 | TestClassifyNetVerifyFailure (Q4-verbatim table incl. merged nfs_export + empty-journal degradation) |
exit-code classifier (return mount_failed — everything is rc=32) |
every non-generic row: = "mount_failed", want "unreachable"/... — an exit-code mutant cannot split smb_auth/smb_share |
| A3 | TestNetVerify_MountFailed_RollsBackAndClassifies (+ journal-unavailable sibling) |
rollback call dropped | RemoveNetworkMount not called for the failed install: removed=[] + creds file must be removed |
| A4 | TestNetVerify_TruthTable (§8: ReadDir-ok+unmounted=FAIL; EACCES+mounted=OK) |
readability-based verdict (terr == nil) |
BOTH rows failed: a readable-but-unmounted path must FAIL verify, got done / unreadable-but-mounted must PASS, got failed |
| A5 | TestNetVerify_UnreachablePreProbe_InstallsNothing |
pre-probe skipped | unreachable add: got 200 want 502 (the install would have run) |
| A6 | TestNetVerify_SingleFlight_AndNoJobShape |
running-check dropped | second add while verifying: got 200 want 409 |
| C1 | TestNetAdd_HappyPath_RegisterOnlyAfterProbe |
register moved BEFORE the probe | phase = failed (category=register_failed detail=... already registered) + C2's must NOT be registered (got 1 paths) |
| C2 | TestNetAdd_ProbeFail_RollsBackNotRegistered |
rollback dropped | probe-fail must roll the agent install back: removes=[] |
| C3 | TestNetAdd_AgentVerifyFailed_MappedMessage |
(kill-surface shared with C4's mutant) | asserts the EXACT §3.2 merged nfs_export string + probe-not-run + no controller double-remove |
| C4 | TestNetAdd_VerifyLost_RollsBack |
none treated as success |
probe must not run when the verify was lost |
| C5 | TestNetProbeChild (ok / unwritable→2→not_writable / nonce-tamper→3→probe_io / cleanup-fail→4=OK+warn) |
— (pure child body + verdict table; Credential wiring live-validated in Scenario C) | — |
| C6 | TestNetAdd_SingleFlight |
acquire check dropped | second add: got 200 want 409 |
| C7 | TestNetStorage_OrphanRow |
orphan sweep disabled (registry-only filter) | items = 1, want 2 (registered + orphan) |
| C8 | TestStorageNetworkTemplate_CanonicalClasses (renders through the PRODUCTION template tree) |
form-input reintroduced |
rendered page still contains the banned pattern "form-input" |
Green gates: agent go build && go vet && go test ./... PASS (one hit of the KNOWN
TestGenerateRecoveryCode wordlist flake — clean on re-run, pre-documented); controller full suite
PASS; template_id_gate.py + emoji_gate.py green.
Deployed + verified
- Agent v0.81.0 on felhom-pve:
.bak-0.80.0kept;usermod -aG systemd-journal felhom-agentapplied BEFORE restart;felhom-agent --version→ 0.81.0;id felhom-agent→groups=990(felhom-agent),999(systemd-journal); journal shows a clean start (enrolled drive bound under shared parent, local-api listening, hub desired-state gen 10, no capability degradation). - Controller v0.113.0 on 9201 (golden/bootstrap mechanism):
docker ps→gitea.dooplex.hu/admin/felhom-controller:0.113.0 Up (healthy).
Live validation (anti-F9: the exact endpoint the UI invokes)
Method: curl from inside guest 9201 to the controller container
(http://172.17.0.2:8080 + Host: felhom.demo-felhom.eu — the dashboard is host-routed; the demo
has no password so auth/CSRF do not apply) — POST /api/storage/netstorage/add + status-poll, the
byte-identical server pipeline behind the UI; the residual is client-side rendering only.
Sim NAS = isolated /srv/nas-spike3/* on DooPlex (.bak-nasspike3 backups; pre-counts exports=2,
smb_sections=6; NO iptables — the unreachable case used the ping+neigh-verified-unused
192.168.0.199). Throwaway users spike3b (uid 1060) + spike3smb; passwords never committed.
| Scenario | Result | Key evidence |
|---|---|---|
| A bogus export on a reachable server (THE bug) | PASS — failed in 4 s, category nfs_export, the exact merged Hungarian message, the REAL journal in detail (reason given by server: No such file or directory — the unprivileged journal read works), list EMPTY, zero spike3 units left on felhom-pve — the "Készenlét forever" behavior is dead |
|
| B SMB wrong password / wrong share | PASS — smb_auth („Hibás SMB felhasználónév vagy jelszó.") vs smb_share (distinct message), both 4 s, creds file gone after each, no units |
|
C squash trap (anonuid=1000, NO all_squash — mounts fine) |
PASS — agent verify passed → uid-1000 probe refused → not_writable with the Route-A message incl. the computed 101000; full rollback (no units/mounts/registration); no probe file left on the export |
|
| D happy paths | PASS — d1 (NFS anonuid=101000), d2 (NFS Route A, alien uid 1060), d3 (SMB plain user, no force user): all done in 4 s, registered Schedulable, health ok. d2 production proof: the installed unit carries retry=0; a uid-1000 write shows guest-view 65534:65534 and lands server-side 1060:1060 |
|
| E unreachable IP | PASS — failed in 2.0 s (the agent's sync TCP pre-probe), category unreachable, message names the IP, nothing installed |
|
| G single-flight | unit-proven (A6 + C6); not re-proven live — the 4 s happy windows make a live race impractical |
Teardown verified: d1/d2/d3 removed via the real remove endpoint (list empty); 180 restored from
.bak-nasspike3 (post-counts exports=2, smb_sections=6, 0 spike3 exports, both users deleted,
scratch gone); felhom-pve: 0 spike3 mounts/units/creds, mountpoint dirs removed,
systemctl reset-failed cleared the residual failed-unit listings; guest 9201: 0 spike3 mounts,
controller healthy.
NOT yet live-validated
- Guest-restart trigger survival (Q1c follow-up — needs a restart window; no guest restart allowed).
- Synology/QNAP appliance pass (virtual-dsm) — the sim-vs-real caveat stands.
- Peti rollout: floor bump + his
usermod -aG systemd-journal felhom-agentstep — when the NAS feature reaches him. STOP honored: no publish train (agent 0.81.0 NOT published to Gitea, Day-0 manifest untouched), Peti's box untouched, guest 9201 never restarted. - Scenario F live (agent restart mid-verify) — covered by unit tests A6/C4 + the orphan surface (C7); a live kill inside a 4 s verify window is impractical.
Observations (documented, NOT acted on)
- The demo host had NO pre-existing
nas-mediaunit (the spike-era shares were fully torn down) — nothing predatingretry=0exists; the "don't rewrite installed units" rule was moot in practice. RemoveNetworkMountleaves the EMPTY mountpoint dir under/mnt/felhom-drives/and does notreset-faileda failed mount unit's residual state (the unit FILES are removed; anot-found failedlisting lingers until reset-failed/reboot). Cosmetic; cleaned by hand this run.- Pre-existing (since 2026-07-08, unrelated to this task):
lanresolver: cannot list provisioned guests: permission denied—/var/lib/felhom-agent/guestsis root-0700 while the agent runs non-root. - host-install header/SCRIPT_VERSION drift (v1.11.0 vs 1.12.0): the v1.12.0 bump had shipped with no changelog entry and no header sync; both fixed at v1.13.0.
- The controller container publishes NO ports (bridge-only, traefik-fronted): in-guest API testing
needs the container IP +
Host:header — the "POST to in-guest 127.0.0.1:8080" note in older session memory is stale.