diff --git a/REPORT.md b/REPORT.md index e445d07..31a0f66 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,58 +1,38 @@ -# REPORT — agent v0.81.0: NAS verify-before-commit (retry=0 + classifier + detached verify job) — DEPLOYED +# REPORT — agent v0.82.0: local-API version channel (X-Felhom-Agent-Version) — DEPLOYED -**Date:** 2026-07-11 · **Class:** implementation (agent half of the cross-repo "NAS -verify-before-commit + page redesign" task). Baseline v0.80.0 (`300f06722b`) → **v0.81.0** -(`added9d`), live on felhom-pve. Pairs with controller v0.113.0 + host-install v1.13.0. -**Full task-wide record (red-proof outcomes, live A–E transcripts, teardown):** -`felhom-controller/REPORT.md`. Evidence base: SPIKE-nas-verify-2026-07-11 (b57f6c1). +**Date:** 2026-07-11 · **Class:** implementation (agent slice of the NAS/coupling backlog-clear +task, Part B). Baseline v0.81.0 (`1e60e88`) → **v0.82.0** (`fa9c7fe`), live on felhom-pve. +Pairs with controller v0.115.0 (version-aware Supports) + hub v0.45.0. -## What shipped (agent) +## What shipped -- **`retry=0` in the production NFS option string** (`netmount.go mountOptions`, Q4-vi): a dead-NAS - on-demand access fails clean in ~3.8 s (ENODEV) instead of a 91 s wedge; verify failures classify - as `No route to host` instead of a diagnostic-free systemd timeout. SMB unchanged (mount.cifs - rejects `retry=`). Installed units are NOT rewritten (none existed on the demo anyway). -- **`internal/storage/netverify.go`** — `ClassifyNetVerifyFailure(journalTail, tcpReachable)`: - pure, first-match table on the Q4 VERBATIM substrings → `unreachable | nfs_export | smb_auth | - smb_share | timeout | mount_failed`; `nfs_export` deliberately merges not-found/not-permitted - (NFSv4 identical strings). String-based BY DESIGN — every mount failure exits rc=32. -- **Detached verify job** (`localapi/netverifyjob.go`, formatjob shape but IN-MEMORY single slot — - a restart empties the slot; `GET /netstorage/verify-status` phase `none` is the CONTROLLER's - rollback signal, Scenario F): add = decode → role-gate → **sync fast-fail** (full spec validation - + 2 s TCP pre-probe; unreachable = HTTP 502 `{code:unreachable}`, NOTHING installed) → SMB creds - (0600) → EnsureNetworkMount → detached verify: trigger read through the automount, success judged - from **/proc/mounts ONLY** (§8: EACCES on a 0700 export is a GOOD mount), failure = journal - classification + **auto-rollback** (RemoveNetworkMount + creds). Single-flight: second add = 409. -- **Unprivileged journal read** (`journalctl -u -n 20 -o cat`, NO sudo, NO new sudoers - grant): needs the `systemd-journal` group (host-install v1.13.0; existing hosts: - `usermod -aG systemd-journal felhom-agent`). Unavailable ⇒ degrade to `mount_failed` + hint, - still rolled back. -- New exports: `storage.NetworkMountedAt`, `storage.NetworkEndpointReachable`. REUSE.md updated - (helpers + the in-memory-vs-persisted job pattern row + the four Server net-verify seams). +- **`X-Felhom-Agent-Version` on EVERY local-API response** (`localapi/server.go`: the mux is wrapped + so the header is set on all routes + all statuses, INCLUDING auth failures and 404s — the + controller learns the agent version passively from ordinary traffic). Value = `main.version` via + the new `localapi.Options.AgentVersion`. No new routes, no envelope changes, **no sudoers changes**. +- The controller (v0.115.0) reads it and capability-gates by version comparison; header-less + (≤0.81.0) agents keep working — the controller falls back to the v0.114.0 route probe unchanged. -## Tests + red-proofs (agent side; outcomes verbatim in felhom-controller/REPORT.md) +## Tests + red-proof -A1 retry=0 (revert → fail) · A2 classifier table incl. merged nfs_export + empty-journal -degradation (exit-code mutant → every non-generic row fails) · A3 rollback effects via fakes -(drop rollback → `removed=[]` + creds linger) · A4 §8 truth table (readability mutant → both rows -fail) · A5 pre-probe zero-install (skip probe → 200-with-install) · A6 single-flight + no-job shape -(drop check → 409 lost). Green gate `go build && go vet && go test ./...` PASS (known -TestGenerateRecoveryCode wordlist flake hit once, clean on re-run). +`TestVersionHeader_OnEveryResponse` (authed / unauthed / 404 all carry the header) + +`TestVersionHeader_OmittedWhenUnset`. Red-proof: dropping the Handler() wrap (return the bare mux) → +every row fails with an empty header (observed: `X-Felhom-Agent-Version = "", want "9.9.9-test"`), +reverted → green. Full `go build && go vet && go test ./...` PASS (the known +TestGenerateRecoveryCode wordlist flake did not recur this run). -## Deployed + verified (felhom-pve) +## Deployed + verified -`.bak-0.80.0` kept → `install -m0755` → `usermod -aG systemd-journal felhom-agent` → -`systemctl restart felhom-agent`. Verified: `felhom-agent 0.81.0`; `id felhom-agent` → -`…,999(systemd-journal)`; clean start journal (enrolled drive re-bound, local-api listening, -desired-state gen 10). Live proof the journal path works: Scenario A's failure detail carried the -REAL `mount.nfs4 … reason given by server: No such file or directory` lines read unprivileged. +felhom-pve: `.bak-0.81.0` kept → `install -m0755` → restart; `felhom-agent --version` → 0.82.0; +clean start journal (enrolled drive re-bound, local-api listening). Wire proof: an authed +`GET /storage` to the live agent returns `x-felhom-agent-version: 0.82.0`. **NOT published** — +demo-deploy only; Peti stays 0.81.0 (the live probe-fallback path). Hub now shows demo agent 0.82.0, +Peti 0.81.0. -**NOT published:** 0.81.0 is deployed on the demo only — no Gitea publish, Day-0 manifest untouched -(the publish train is the operator's separate decision); Peti's box untouched. +## Observations (documented, NOT acted on) -## Observations - -- Pre-existing (2026-07-08, unrelated): `lanresolver: cannot list provisioned guests: permission - denied` — `/var/lib/felhom-agent/guests` is root-0700 under the non-root agent. -- `RemoveNetworkMount` leaves the empty mountpoint dir + a `not-found failed` residual unit listing - (until `reset-failed`/reboot) — cosmetic, documented in the task report. +- **Q1c FAIL surfaced during this task's Part E** (supervised, agent 0.82.0 live): a NAS automount + trigger does NOT survive a guest reboot — the guest sees an empty dir; the agent has no + network-mount reassert on guest reboot (unlike drive `ReassertGuestBinds`). Follow-up spec'd: + `felhom.eu/documentation/backlog/FOLLOWUP-nas-automount-guest-reboot-reassert.md`. NOT this + release — a distinct agent task. diff --git a/REUSE.md b/REUSE.md index c7e6db1..770414f 100644 --- a/REUSE.md +++ b/REUSE.md @@ -103,6 +103,7 @@ | Injectable seam funcs on Server | internal/localapi/server.go (`reresolveWipe`, `deviceDurableID`, `boundCheck`, net-verify: `netTrigger`/`netMounted`/`netJournal`/`netReachable`) | prod default wired in `NewServer`; tests override — no real /dev, /proc/mounts, journalctl or TCP in tests | | Detached IN-MEMORY verify job (single slot, deliberately unpersisted) | internal/localapi/netverifyjob.go | claim slot sync (single-flight 409) → detached pipeline off baseCtx → auto-rollback on fail; restart ⇒ slot empty ⇒ the CALLER rolls back (Scenario F) — contrast formatjob (persisted+recovered) | | Optional dependency degradation | internal/localapi/server.go (`Options`) | nil dep ⇒ endpoint answers "not configured" (503), never a crash | +| Version channel (v0.82.0) | internal/localapi/server.go (`Options.AgentVersion`; `Handler()` mux wrap) | sets `X-Felhom-Agent-Version` on EVERY response (all routes/statuses, incl. auth-fail/404) — the controller's capability-comparison source; empty version ⇒ header omitted | | Root-file install via random temp | internal/localapi/intermediary.go (`installSharedParentUnit`) | `stageTemp` (os.CreateTemp) → sudoers-globbed `install -m` → pinned destination | | Detached destructive job + restart recovery | internal/localapi/formatjob.go | persist `running` → run off baseCtx → record outcome; recovery re-resolves durable id, never a path | | Signed-op verify pipeline | internal/authz/verifier.go (`Verify`) | armor→namespace→key-material allowlist→crypto over RAW bytes→blob→target→window→nonce LAST |