9d5a588ca3
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
81 lines
5.7 KiB
Markdown
81 lines
5.7 KiB
Markdown
# 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.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 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.
|