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
5.7 KiB
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.Command → FAIL: "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.post → FAIL: "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.0on 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_SMTPpresent in the guestdocker-compose.yml+.felhom.yml). - Scenario A (F1 acceptance): stopped + removed the crash-looping vaultwarden via the UI
/stop+/removeendpoints (remove correctly 409'd until stopped — "still running"); freshPOST /api/stacks/vaultwarden/deploy(app-email default off) → 202 →runningin ~70s,docker inspectrestarts=0; env:_ENABLE_SMTP=false,SMTP_HOST=,SMTP_FROM=(empty). No crash-loop — the pre-fix instance that wasRestartingis 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; thenapp_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 P1–P3 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.