Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
8.6 KiB
REPORT — agent v0.50.0: NAS network storage Part A1 (NFS/SMB automount foundation)
Date: 2026-06-30
Task: TASK.md — NAS network storage Part A1 (agent foundation: automount + bind + liveness).
Class: Risky / supervised (creates real network mounts on the Proxmox host). Implemented on main,
sim-validated live, sim torn down.
Baseline (confirmed live)
| value | |
|---|---|
Agent main before |
v0.49.0 (commit 7aeb7ca), live on felhom-pve |
| Agent after | v0.50.0 (commit 63aa63d) |
| Deployed binary sha256 (felhom-pve) | 6ddb6ff2efe6b10377e00ff7612f738ff437dfbcbcc35e3fc566e40d139aedf1 |
| Host client tooling | mount.nfs (nfs-common) + mount.cifs (cifs-utils) present |
What was built
The agent foundation of SPIKE-nas-storage-2026-06-29.md (verdict READY) — a customer NAS can serve
bulk media to a media app. The agent mounts a NAS share host-side under /mnt/felhom-drives/<name>
via a systemd .automount (+ .mount) pair; it propagates into guest 9201 for free through the existing
shared mp8 bind. A NAS is a distinct storage class — no durable-id, never in the drive
enroll/eject/decommission/wipe/SMART/watchdog machinery. Bulk-media class only.
Files changed (commit 63aa63d, +1351/-5)
internal/storage/netmount.go(NEW, 511) —NetworkMountSpec, the locked NFS/SMB option sets, the+100000uid recipe,.mount/.automountrendering, validation, per-share liveness, and theSudoHostOpsEnsureNetworkMount/RemoveNetworkMount/ListNetworkMountsmethods.internal/storage/mountunit.go— drive-machinery guard:parseFelhomMountUnitexplicitly refuses any unit carrying the network marker (Scenario D).internal/localapi/netstorage.go(NEW, 204) — self-scopedPOST /netstorage/add,GET /netstorage,POST /netstorage/remove; role gate; out-of-band 0600 SMB creds file.internal/localapi/server.go— wireNetStorage+SmbCredsDirinto Options + routes.cmd/felhom-agent/main.go—newHostOpsreturns*storage.SudoHostOps; wireNetStorage; v0.50.0.internal/config/config.go—privileged.smb_creds_dir(default/var/lib/felhom-agent/smb-creds).configs/felhom-agent.sudoers— new narrowFELHOM_NETMOUNTalias (.automountinstall/enable/ disable/stop + felhom mount-unit removal);visudo -cfclean on the live host.
Tests — go build ./... && go vet ./... && go test ./... GREEN (build server, Linux)
Unit tests with companion red-proofs:
- Exact option-set string-asserts — NFS
vers=4.1,soft,timeo=50,retrans=2,noatime,_netdev(nohard, no client uid); SMBvers=3.0,credentials=…,uid=101000,gid=101000,forceuid,forcegid,file_mode=0664, dir_mode=0775,_netdev. - +100000 companion — container uid 1000 → the SMB unit renders
uid=101000; a+0impl (uid=1000) FAILS the test (the documented non-writable trap).dir_modemust be plain0775, never setgid2775. - Validation matrix — bad name/traversal/slash/space, bad protocol, server metachar, NFS relative/ traversal export, uid/gid range, SMB-without-creds, SMB bad share name.
- Role gate — under
/mnt/felhom-drives→ user-data; anything else → system (refused). - Drive-machinery guard (Scenario D) + companion —
parseFelhomMountUnitrefuses a network unit even when contrived with a by-uuidWhat=; the same content with the drive marker DOES parse — proving the guard (not luck) is the discriminator. - Unit round-trip + health + isNetworkMounted; Ensure/Remove command-sequence (mkdir → install ×2
→ daemon-reload → enable
--nowthe.automount, never the.mount; remove = stop/disable automount → stop mount → rm ×2 → daemon-reload); bad spec → ZERO commands. - localapi: add NFS/SMB happy-path; SMB writes a 0600 creds file + no secret in the response; SMB missing creds → 400; role-gate refusal → 403, surface never touched; list; remove (creds cleaned); not-configured → 503; auth required → 401.
(The two SudoHostOps command-sequence tests skip on Windows — the systemd-escaped unit filename embeds a
backslash — and run on the Linux build server, where the full suite is green.)
Live sim validation (Scenarios A–D)
Isolated sim NAS: a throwaway privileged LXC (VMID 9300, nas-sim, 192.168.0.114) on felhom-pve
running nfs-kernel-server + samba with its own config — it did NOT modify any production host's
/etc/exports or smb.conf (the spike's near-miss avoided). Export squashed to anonuid/anongid=101000;
SMB force user/group = nasguest (uid/gid 101000). Throwaway creds, out-of-band. Sim destroyed after.
Endpoints driven exactly as A2 will (the controller's own local-api token + the real agent server pipeline).
A — NFS host-side mount → propagates → container read+write (the +100000 recipe) ✅
POST /netstorage/add(nfs) → host automount installed; first access mountednfs4with effective optsvers=4.1,soft,timeo=50,retrans=2,noatime(exact recipe).- Guest 9201 saw the
nfs4mount with NO restart (shared-bind propagation); guest view ownership1000:1000(the +100000 idmap). - A uid-1000 Docker container READ
readme.txtAND WROTEctest.txt(WRITE_OK); the new file landed on the NAS asnasguest:nasguest(101000:101000).
B — SMB fallback read+write ✅
POST /netstorage/add(smb) → creds staged 0600 (felhom-agent-owned, out-of-band); mountedcifswithvers=3.0,uid=101000,forceuid,gid=101000,forcegid,file_mode=0664,dir_mode=0775(exact recipe).- uid-1000 container READ + WROTE (
SMB_WRITE_OK); landed on the NAS asnasguest(101000). No secret in any response, log, or committed file.
C — failure isolation (the risk) ✅
- NAS black-holed from felhom-pve (
iptables DROP, both directions — a dead-NAS black hole). - Clean fail-soft: uncached
stat→ errorNo such devicein 15s;catdata read → error in 16s (≈ the spike's ~16s). It errors, does not hang. df /did NOT hang (box-wide health intact); guest 9201 responsive +felhom-controllerhealthy throughout — blast radius contained to the process touching the mount.- Per-share liveness reported
unreachablefor the affected shares only (never box-wide); the endpoint TCP-probe never touched the wedged mount. - Automatic recovery on NAS return: a fresh read succeeded with no remount; liveness returned to
ok/idle. - Note: a first C run hit a 30s ceiling — it coincided with a concurrent scheduled
vzdumpof 9201 (load 4.7); the clean re-run after the backup matched the spike (~15–16s). The box-wide isolation held even under that backup load.
D — the drive machinery ignores the NAS mount ✅
GET /diskslisted only real drives (felhom-pbs,felhom-usb,local,felhom-flash,local-lvm) — nevermedia/vids; the NAS shares carry no durable-id.- The 20 s drive reconcile/
ReassertEnrolledMountstick re-bound only the enrolled drives (felhom-flash/felhom-usb) and never touched the NAS mounts; they remained present and unmodified.
Lifecycle + teardown
POST /netstorage/remove(both) → units gone, creds file gone, mounts gone, list empty.- Sim LXC 9300 destroyed; all iptables DROP rules removed; helper scripts removed;
/mnt/felhom-drivesback to onlyfelhom-flash+felhom-usb; agent v0.50.0 active; guest 9201 + controller healthy.
Deploy + clean restart
sudoers (FELHOM_NETMOUNT, visudo -cf clean) and the v0.50.0 binary installed on felhom-pve (prior
binary backed up .bak-0.49.0). Restart: capabilities self-check ok=46 total=46 degraded=0,
local-api server listening 192.168.0.162:8443, no errors — ReassertEnrolledMounts ran without touching
any network mount.
Operational note (pre-existing, not this feature)
During validation df / on felhom-pve showed the root fs at ~100% (vzdump output under /var/lib/vz/ dump on pve-root). Unrelated to network storage (the NAS data lives on the sim, the agent state is
tiny) — flagging it as a host backup-retention/disk-pressure item for follow-up.
STOP — not yet built (per the task)
- A2 (controller "network storage" registry kind + UI + per-share health surface) — NOT built; A2 will drive these A1 endpoints.
- B (restic-over-SFTP NAS backup target, class 2) — NOT built (separate task).
- No real customer media app wired to a NAS path — awaiting A2 + a real app.
- Real-Synology/QNAP (virtual-dsm) GA-fidelity confirmation — out of scope (later pass).
SMB/NFS test credentials were throwaway and out-of-band only. No secrets in any committed file.