diff --git a/REPORT.md b/REPORT.md index 963da03..bc69fc7 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,123 +1,122 @@ -# REPORT — v0.113.0: NAS verify-before-commit + page redesign + protocol-honest guidance +# REPORT — v0.114.0: agent-capability gate for coupled features (option-1) + publish-train rules (option-2) -**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) +**Date:** 2026-07-11 · **Version:** controller v0.114.0 (from v0.113.0) · **MinAgent:** — (the gate +itself works against agents 0.79–0.81 as they exist; that is the point) · **Agent/hub: UNTOUCHED.** -## Confirmed baselines → shipped +## What shipped -| Repo | Baseline (`main`) | Shipped commits | Version | +The 0.81/0.113 publish train's 9-minute controller-before-agent skew (Peti's box) answered at the +box level: the controller detects whether its agent supports a coupled feature (route probe on +`GET /netstorage/verify-status` — the route that shipped WITH the v0.81.0 coupled add semantics) +and refuses the NAS add up front with an honest Hungarian message instead of failing mid-pipeline +with a misleading rollback. Riding along: the publish-train conventions codified in +`felhom.eu/documentation/runbooks/publish-train-rules.md`. + +## Baselines (verified live before work) + +| Repo | main @ start | → main @ end | +|---|---|---| +| felhom-controller | `5d06ecf374` (v0.113.0) | `53ab971` (v0.114.0) | +| felhom.eu | `a487ab2fc4` | `4eaabb2` | +| felhom-agent | `1e60e88eb2` (v0.81.0) — read-only, NOT touched | unchanged | + +## Commits + +| Hash | Content | +|---|---| +| `d347dc4` | Commit 1 — agentapi typed `StatusError` (1.1) + `features.go` (Feature/SupportState/`featureProbes`/`SupportCache` TTL 5m/`Client.Supports`) + the add gate before the single-flight claim + T1–T4, T6 | +| `50da732` | Commit 2 — `NetAddSupport` threaded to `storage_network.html` (banner on `no`; list+remove in every state; JS load guard) + T5 | +| `4a5c40b` | Pre-existing scheduling flake fixed in `TestBackupTier2Restore_DoubleClickRefused` (IsRunning flips before StopStack lands — wait for the first stop, then assert) | +| `d9da5a0` | Docs — CHANGELOG v0.114.0 (MinAgent header convention starts), README NAS gate paragraph, CONTEXT, REUSE rows, CLAUDE.md coupled-feature rule | +| `53ab971` | `[DEBUG]` gate-verdict log per add + `SupportState.String()` | +| felhom.eu `4eaabb2` | `documentation/runbooks/publish-train-rules.md` (the 4 standing gates, each citing its incident) + CONTEXT entry — pushed | + +## Files touched (controller) + +`internal/agentapi/client.go` (StatusError; `get` non-200 branch — same message text), +`internal/agentapi/features.go` + `features_test.go` (NEW), +`internal/web/server.go` (`netFeatures` field), `internal/web/netstorage_handlers.go` (gate + +`netAddOutdatedMsg` + `netAddSupport`), `internal/web/handlers.go` (`networkStoragePageData`), +`internal/web/templates/storage_network.html`, `internal/web/netstorage_gate_test.go` (NEW), +`internal/web/storage_network_template_test.go` (T5 + `NetAddSupport` in C8's data), +`internal/web/async_restore_test.go` (flake fix). Docs: CHANGELOG/CONTEXT/REUSE/CLAUDE/README. + +## Verify-first (1.1) finding + +`Client.get` returned a generic `fmt.Errorf("agentapi: GET %s: HTTP %d")` on non-200 — a 404 was +NOT typed. Added `StatusError{Path, Code}` returned by that branch (identical message text, so no +behavior shift elsewhere); classification uses `errors.As` + `Code==404` only. Pinned wire-level by +`TestClient_404IsTypedAndSupportsSaysNo` (httptest TLS + real leaf pin, routeless mux). + +## Tests + red-proofs (every red-proof run → predicted failure observed → reverted; final tree clean) + +Counts: web 76 → 80 test funcs; agentapi 28 → 33. + +| Test | Scenario | Result | Red-proof (verbatim failure) | |---|---|---|---| -| 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 | +| `TestNetAddGate_OldAgent_RefusedUpFront` (T1) | A | PASS — 412 + `"code":"agent_outdated"` + the §Part-3 message; job slot nil; 0 agent add calls; 0 removes; 0 registered; slot re-acquirable | RP1 gate disabled → `gate: got 200 want 412 ({"data":{"name":"media","started":true},"ok":true})` — the exact pre-gate WRONG behavior | +| `TestNetAddGate_CurrentAgent_UnchangedAndProbeCached` (T2) | B | PASS — two adds both `done`, registered, no rollback; probe count across both = 1 | RP2 cache disabled → `probe (verify-status) calls across two adds = 2, want exactly 1 (cached)` | +| `TestNetAddGate_ProbeIndeterminate_PassesThrough` (T3) | C | PASS — transport error AND 5xx: 200 `{started:true}`, job fails `agent_error` (the existing path), never `agent_outdated` | RP3 classify-any-error-as-No → both subtests: `indeterminate probe must pass the gate: got 412 ({"data":{"code":"agent_outdated"},"error":"Az ügynök frissítése szükséges…"})` — the false "too old" trap, caught | +| `TestSupports_Classification` (T4) | 2xx/404/wrapped-404/401/500/502/refused/timeout/**plain-error-with-404-text** → Yes/No/No/U/U/U/U/U/**U** | PASS | RP4 string-match mutant → `plain_error_with_404_text: Supports(agentapi: GET /netstorage/verify-status: HTTP 404) = 2, want 0` (RP3 also failed 6 T4 cases) | +| `TestStorageNetworkTemplate_AgentOutdatedBanner` (T5) | D | PASS — `no`: banner present, `ns-add-form` absent, list+Eltávolítás present; `yes`/`unknown`: form, no banner | RP5 conditional dropped → `the §Part-3 banner is missing on an old agent` + `the add form must NOT render on an old agent` | +| `TestSupports_CacheTTL` (T6) | warm-cache negative (calls stays 1), re-fire after TTL+1s, negative polarity cached, Unknown NEVER cached | PASS | covered by RP2 (`probe calls on a warm cache = 2, want 1`; `after TTL expiry = 3, want 2`; `negative verdict not cached (calls=2)`) | +| `TestClient_404IsTypedAndSupportsSaysNo` / `TestClient_SupportsYesOnLiveRoute` | wire-level 1.1 | PASS — real TLS+pin client: routeless mux ⇒ `*StatusError{404}` ⇒ `SupportNo`; live route ⇒ `SupportYes` | — (pins the verify-first finding) | +| `TestSupports_UnknownFeature` | table gap fails open, no probe fired | PASS | — | -## 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. +Green gate `go build && go vet && go test ./...`: PASS per commit (template_id_gate + emoji_gate +green for the template commit). One unrelated failure surfaced under parallel load — +`TestBackupTier2Restore_DoubleClickRefused` — diagnosed as a PRE-EXISTING race in the test itself +(asserted StopStack count right after IsRunning flips; goroutine not yet parked), fixed in +`4a5c40b`; full suite green since. ## Deployed + verified -- **Agent v0.81.0** on felhom-pve: `.bak-0.80.0` kept; `usermod -aG systemd-journal felhom-agent` - applied 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)`. +Guest 9201 (golden/bootstrap mechanism, built on 180, image pushed): +`gitea.dooplex.hu/admin/felhom-controller:0.114.0 Up (healthy)`; startup log clean; selfupdate: +`Current version 0.114.0 is up to date`. Agent on felhom-pve: 0.81.0 (untouched). -## Live validation (anti-F9: the exact endpoint the UI invokes) +## Live validation (the supported branch; 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. +Method: `curl` from inside guest 9201 to the controller container (`http://172.17.0.2:8080` + +`Host: felhom.demo-felhom.eu`; the demo has no password so auth/CSRF do not apply) — the residual +vs a browser is client-side rendering only. Demo agent = 0.81 ⇒ the SUPPORTED branch. Done inside +one cache window. -| 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 | +1. **Gate-pass + real pipeline** — POST add `gatecheck114` (nfs, server 192.168.0.180 = reachable, + export `/srv/nonexistent-gate-check` = bogus): response `{"data":{"name":"gatecheck114", + "started":true},"ok":true}` (identical shape to 0.113.0); controller log + `[DEBUG] [web] netstorage add "gatecheck114" capability gate: netstorage_verify=yes` (the probe + verdict, visible); status poll → `failed` in ~2.5 s, category `nfs_export`, the exact merged + Hungarian message, real journal detail — byte-for-byte the 0.113.0 behavior. +2. **Nothing changed state** — share list `{"network_storage":[]}`; felhom-pve: 0 unit files; + the known residuals only (one `not-found failed` unit listing → `systemctl reset-failed`; the + empty `/mnt/felhom-drives/gatecheck114` dir → removed; both documented v0.113.0 observations, + re-confirmed, no new gaps). Host+guest now show only `felhom-usb`. +3. **Page render** — GET `/storage/network`: `ns-add-form` present (2 occurrences), the banner text + `régebbi verziójú` 0 occurrences ⇒ the SupportYes render; list intact. -**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 live-validated -## NOT yet live-validated +- **The `agent_outdated` refusal branch (SupportNo)** — test-proven only (T1/T4/T5 with red-proofs + RP1/RP3/RP4/RP5). The demo agent is current (0.81.0); downgrading a live agent to prove a banner + is not justified. Stays on this list until a future old-agent window shows it naturally. +- Concurrent double-probe on a cold cache (documented harmless: one extra cheap GET) — not raced live. -- 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-agent` step — 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. +## STOP honored -## Observations (documented, NOT acted on) +NOT published (Day-0 manifest untouched, no `publish-agent.sh`, no golden bake), NO floor movement, +NO hub changes, NO agent changes, Peti untouched on 0.113.0. The gate is inert protection until the +next train. Note: the `0.114.0` registry tag was rebuilt once (the `[DEBUG]` gate-log commit) before +any vouch existed — demo-only, nothing references it. -- The demo host had NO pre-existing `nas-media` unit (the spike-era shares were fully torn down) — - nothing predating `retry=0` exists; the "don't rewrite installed units" rule was moot in practice. -- `RemoveNetworkMount` leaves the EMPTY mountpoint dir under `/mnt/felhom-drives/` and does not - `reset-failed` a failed mount unit's residual state (the unit FILES are removed; a `not-found - failed` listing 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/guests` is 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. +## Observations (out of scope, documented NOT acted on) + +- **Hub floor-UI separation** (decoupling the floor field from the manifest-save screen / staging + it) — its own task; the rules doc only references it (rule 2's "save LAST" is the interim control). +- **Agent version-in-envelope** — the documented future upgrade of `Supports` (prefer version + comparison over route probing); the `featureProbes` table comment + rules doc carry the pointer. +- `Client.Supports` exists as the spec'd public API; the web layer drives the same `SupportCache` + machinery through its `netAgent` seam (`Server.netFeatures`) so unit tests fake the probe — one + code path in production and tests. +- The known v0.113.0 remove-residuals (failed-unit listing needs `reset-failed`; empty mountpoint + dir lingers) reproduced on the failed-add rollback path too — same cleanup, still LOW.