Files
felhom-controller/REPORT.md
T
admin 9d5a588ca3 docs: REPORT live-validation evidence + F2 verdict; CONTEXT v0.101.0
Scenario A/B env excerpts (restarts=0, _ENABLE_SMTP off/on), sync smoke 2xx,
F2 real-finding verdict. CONTEXT dated to v0.101.0 with open follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-06 14:26:45 +02:00

5.7 KiB
Raw Blame History

REPORT — v0.101.0: campaign findings F3 (sync deadline) + F2 evidence gap (agent refusal surfacing)

Date: 2026-07-06 · Class: implementation (code + tests + deploy + live validation) Baseline: main @ af7ea0b (v0.100.0, matched the task's confirmed baseline exactly). Provenance: felhom.eu/documentation/audits/CAMPAIGN-nomercy-2026-07-06.md findings F3, F2. Companion repos: catalog d86e256 (F1 vaultwarden fix); felhom.eu audit addendum (F1/F2/F3 verdicts).

What changed

F3 — git subprocess deadline (internal/sync/sync.go)

runGitInDir took no context; a hung remote parked the sync goroutine in cmd.Run(), the doSync defer never ran, syncing stayed true, and every subsequent manual + periodic sync was refused ("Szinkronizálás már folyamatban") until a controller restart. Now: const gitCmdTimeout = 120s; a gitCmd wrapper derives a fresh per-command context.WithTimeout; runGitInDir takes a context.Context and uses exec.CommandContext; on ctx.Err() the error names the timeout + the masked git args. All three call sites (clone via runGit, fetch, reset) route through it. Debounce and failed-sync-arms-debounce semantics unchanged.

F2 evidence gap — agent refusal surfacing (internal/agentapi/client.go)

EjectDisk + Decommission used c.post, which drops any non-2xx body, flattening the agent's informative refusal to a bare HTTP 403 (the exact campaign evidence: agentapi: POST /disks/decommission: HTTP 403). Both switched to the purpose-built postWithStatus + a new shared refusalError(path, status, env) that returns agentapi: POST <path>: HTTP <code>: <agent reason, truncated ~300> (and handles the ok:false-on-2xx business-refusal case). truncateErr mirrors stacks.truncateStr. The generic post and every other caller are untouched. internal/web/storage_handlers.go already wraps the client error into its Hungarian message ("a meghajtó leszerelése sikertelen: %w" / eject err.Error()), so the role reason now reaches the UI/API response verbatim — verified by reading the handler paths (no re-truncation layer).

Tests + red-proofs (non-hollow)

Test Asserts Red-proof (pre-fix shape)
TestRunGitInDir_CancelledContextKillsSubprocess (T-C1) cancelled ctx → error names the ctx cause + "deadline/killed", returns < 5s reverted to exec.CommandFAIL: "got nil (git --version ran to completion — the pre-fix behavior)"
TestTriggerSync_FailureReleasesSyncingAndAllowsRetry (T-C2) failed sync carries "Git hiba", syncing false after, immediate retry hits debounce, post-debounce retry EXECUTES (behavioral; guards the lockup)
TestDecommission_RefusalReasonSurfaced (T-D1) error contains "(role: system)" + "HTTP 403" reverted to c.postFAIL: "agent refusal reason discarded — operator sees only: agentapi: POST /disks/decommission: HTTP 403"
TestEjectDisk_RefusalReasonSurfaced (T-D2) error contains "(role: backup)"
TestDecommission_SuccessUnchanged (T-D3) 200 envelope decodes to result
TestDecommission_OkFalseBusinessRefusal ok:false-on-2xx reason surfaced

Both red-proofs demonstrated live (revert → FAIL with the wrong value shown → restore → pass; git diff clean). Green gate go build ./... && go vet ./... && go test ./... = PASS (exit 0); 30 passing tests across internal/sync + internal/agentapi (+2 new test files, +6 new tests).

Deploy + live validation (executed, real UI pipeline)

  • Built + pushed gitea.dooplex.hu/admin/felhom-controller:0.101.0 on 180; deployed to guest 9201 via golden/bootstrap → docker ps :0.101.0 Up (healthy).
  • Catalog synced (startup sync pulled the F1 template; grep _ENABLE_SMTP present in the guest docker-compose.yml + .felhom.yml).
  • Scenario A (F1 acceptance): stopped + removed the crash-looping vaultwarden via the UI /stop + /remove endpoints (remove correctly 409'd until stopped — "still running"); fresh POST /api/stacks/vaultwarden/deploy (app-email default off) → 202 → running in ~70s, docker inspect restarts=0; env: _ENABLE_SMTP=false, SMTP_HOST=, SMTP_FROM= (empty). No crash-loop — the pre-fix instance that was Restarting is gone.
  • Scenario B (regression): POST /stacks/vaultwarden/app-email app_email_enabled=on → recreate → healthy, restarts=0, _ENABLE_SMTP=true, SMTP_HOST=felhom-controller, SMTP_FROM=vaultwarden@felhom.eu; then app_email_enabled=false → healthy, _ENABLE_SMTP=false. Left OFF. The send-tested ON path is unchanged.
  • Sync smoke (F3): POST /api/sync → HTTP 200 ("Sablonok naprakészek") — the deadline change is transparent on the healthy path.

F2 verdict (real finding — agent fix deferred)

Campaign-time agent journal (persistent, survived the P6 reboots): "local-api: protected — decommission refused by role" where=/mnt/teszt_enroll role=system. roleForMountPath (agent disks.go:1031) fail-safe-returns system when no PVE storage target matches the mount — but /disks already classifies teszt_enroll as user-data. Two role paths disagree; a bind-mounted user-data drive that isn't a standalone PVE storage can't be decommissioned. Over-refusal (fail-safe), not a hole → MEDIUM. Full write-up: felhom.eu audit addendum. Agent untouched (read-only per scope); fix is a separate follow-up task.

NOT yet live-validated

Clean backup/restore (F3-volume-tar / C2 Tier-2) coverage on the now-healthy vaultwarden — awaits the separate targeted P1P3 campaign re-run (a runbook, not this task).

Not changed

felhom-agent (read-only; F2 is diagnosis only). The generic agentapi.post error format, the sync debounce, and the other five smtp-mapped catalog templates.