c997d79246
EjectDisk/Decommission switched from c.post (drops non-2xx body) to
postWithStatus + shared refusalError, so the agent's informative 403 body
("…decommission refused (role: X)") reaches the operator instead of a bare
"HTTP 403" (campaign F2 evidence gap). Generic post + other callers untouched.
Tests T-D1/T-D2/T-D3 + ok:false case; T-D1 red-proof shows the pre-fix bare
"HTTP 403". Bundles the v0.101.0 CHANGELOG entry (this + the F3 sync deadline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
62 lines
4.3 KiB
Markdown
62 lines
4.3 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
|
||
(Filled in during Phase 4/5 of this session — see below; secrets-safe.)
|
||
|
||
- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.101.0`; deployed to guest 9201.
|
||
- Scenario A (F1 acceptance): fresh email-off vaultwarden deploy healthy, `_ENABLE_SMTP=false`.
|
||
- Scenario B (regression): app-email ON→healthy (`_ENABLE_SMTP=true`, host+from set) → OFF→healthy.
|
||
- Sync smoke: `POST /api/sync` 2xx.
|
||
- F2 diagnosis verdict: see the felhom.eu audit addendum.
|
||
|
||
## 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.
|