Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
9.3 KiB
REPORT — v0.114.0: agent-capability gate for coupled features (option-1) + publish-train rules (option-2)
Date: 2026-07-11 · Version: controller v0.114.0 (from v0.113.0) · MinAgent: — (the gate itself works against agents 0.79–0.81 as they exist; that is the point) · Agent/hub: UNTOUCHED.
What shipped
The 0.81/0.113 publish train's 9-minute controller-before-agent skew (Peti's box) answered at the
box level: the controller detects whether its agent supports a coupled feature (route probe on
GET /netstorage/verify-status — the route that shipped WITH the v0.81.0 coupled add semantics)
and refuses the NAS add up front with an honest Hungarian message instead of failing mid-pipeline
with a misleading rollback. Riding along: the publish-train conventions codified in
felhom.eu/documentation/runbooks/publish-train-rules.md.
Baselines (verified live before work)
| Repo | main @ start | → main @ end |
|---|---|---|
| felhom-controller | 5d06ecf374 (v0.113.0) |
53ab971 (v0.114.0) |
| felhom.eu | a487ab2fc4 |
4eaabb2 |
| felhom-agent | 1e60e88eb2 (v0.81.0) — read-only, NOT touched |
unchanged |
Commits
| Hash | Content |
|---|---|
d347dc4 |
Commit 1 — agentapi typed StatusError (1.1) + features.go (Feature/SupportState/featureProbes/SupportCache TTL 5m/Client.Supports) + the add gate before the single-flight claim + T1–T4, T6 |
50da732 |
Commit 2 — NetAddSupport threaded to storage_network.html (banner on no; list+remove in every state; JS load guard) + T5 |
4a5c40b |
Pre-existing scheduling flake fixed in TestBackupTier2Restore_DoubleClickRefused (IsRunning flips before StopStack lands — wait for the first stop, then assert) |
d9da5a0 |
Docs — CHANGELOG v0.114.0 (MinAgent header convention starts), README NAS gate paragraph, CONTEXT, REUSE rows, CLAUDE.md coupled-feature rule |
53ab971 |
[DEBUG] gate-verdict log per add + SupportState.String() |
felhom.eu 4eaabb2 |
documentation/runbooks/publish-train-rules.md (the 4 standing gates, each citing its incident) + CONTEXT entry — pushed |
Files touched (controller)
internal/agentapi/client.go (StatusError; get non-200 branch — same message text),
internal/agentapi/features.go + features_test.go (NEW),
internal/web/server.go (netFeatures field), internal/web/netstorage_handlers.go (gate +
netAddOutdatedMsg + netAddSupport), internal/web/handlers.go (networkStoragePageData),
internal/web/templates/storage_network.html, internal/web/netstorage_gate_test.go (NEW),
internal/web/storage_network_template_test.go (T5 + NetAddSupport in C8's data),
internal/web/async_restore_test.go (flake fix). Docs: CHANGELOG/CONTEXT/REUSE/CLAUDE/README.
Verify-first (1.1) finding
Client.get returned a generic fmt.Errorf("agentapi: GET %s: HTTP %d") on non-200 — a 404 was
NOT typed. Added StatusError{Path, Code} returned by that branch (identical message text, so no
behavior shift elsewhere); classification uses errors.As + Code==404 only. Pinned wire-level by
TestClient_404IsTypedAndSupportsSaysNo (httptest TLS + real leaf pin, routeless mux).
Tests + red-proofs (every red-proof run → predicted failure observed → reverted; final tree clean)
Counts: web 76 → 80 test funcs; agentapi 28 → 33.
| Test | Scenario | Result | Red-proof (verbatim failure) |
|---|---|---|---|
TestNetAddGate_OldAgent_RefusedUpFront (T1) |
A | PASS — 412 + "code":"agent_outdated" + the §Part-3 message; job slot nil; 0 agent add calls; 0 removes; 0 registered; slot re-acquirable |
RP1 gate disabled → gate: got 200 want 412 ({"data":{"name":"media","started":true},"ok":true}) — the exact pre-gate WRONG behavior |
TestNetAddGate_CurrentAgent_UnchangedAndProbeCached (T2) |
B | PASS — two adds both done, registered, no rollback; probe count across both = 1 |
RP2 cache disabled → probe (verify-status) calls across two adds = 2, want exactly 1 (cached) |
TestNetAddGate_ProbeIndeterminate_PassesThrough (T3) |
C | PASS — transport error AND 5xx: 200 {started:true}, job fails agent_error (the existing path), never agent_outdated |
RP3 classify-any-error-as-No → both subtests: indeterminate probe must pass the gate: got 412 ({"data":{"code":"agent_outdated"},"error":"Az ügynök frissítése szükséges…"}) — the false "too old" trap, caught |
TestSupports_Classification (T4) |
2xx/404/wrapped-404/401/500/502/refused/timeout/plain-error-with-404-text → Yes/No/No/U/U/U/U/U/U | PASS | RP4 string-match mutant → plain_error_with_404_text: Supports(agentapi: GET /netstorage/verify-status: HTTP 404) = 2, want 0 (RP3 also failed 6 T4 cases) |
TestStorageNetworkTemplate_AgentOutdatedBanner (T5) |
D | PASS — no: banner present, ns-add-form absent, list+Eltávolítás present; yes/unknown: form, no banner |
RP5 conditional dropped → the §Part-3 banner is missing on an old agent + the add form must NOT render on an old agent |
TestSupports_CacheTTL (T6) |
warm-cache negative (calls stays 1), re-fire after TTL+1s, negative polarity cached, Unknown NEVER cached | PASS | covered by RP2 (probe calls on a warm cache = 2, want 1; after TTL expiry = 3, want 2; negative verdict not cached (calls=2)) |
TestClient_404IsTypedAndSupportsSaysNo / TestClient_SupportsYesOnLiveRoute |
wire-level 1.1 | PASS — real TLS+pin client: routeless mux ⇒ *StatusError{404} ⇒ SupportNo; live route ⇒ SupportYes |
— (pins the verify-first finding) |
TestSupports_UnknownFeature |
table gap fails open, no probe fired | PASS | — |
Green gate go build && go vet && go test ./...: PASS per commit (template_id_gate + emoji_gate
green for the template commit). One unrelated failure surfaced under parallel load —
TestBackupTier2Restore_DoubleClickRefused — diagnosed as a PRE-EXISTING race in the test itself
(asserted StopStack count right after IsRunning flips; goroutine not yet parked), fixed in
4a5c40b; full suite green since.
Deployed + verified
Guest 9201 (golden/bootstrap mechanism, built on 180, image pushed):
gitea.dooplex.hu/admin/felhom-controller:0.114.0 Up (healthy); startup log clean; selfupdate:
Current version 0.114.0 is up to date. Agent on felhom-pve: 0.81.0 (untouched).
Live validation (the supported branch; anti-F9 — the exact endpoint the UI invokes)
Method: curl from inside guest 9201 to the controller container (http://172.17.0.2:8080 +
Host: felhom.demo-felhom.eu; the demo has no password so auth/CSRF do not apply) — the residual
vs a browser is client-side rendering only. Demo agent = 0.81 ⇒ the SUPPORTED branch. Done inside
one cache window.
- Gate-pass + real pipeline — POST add
gatecheck114(nfs, server 192.168.0.180 = reachable, export/srv/nonexistent-gate-check= bogus): response{"data":{"name":"gatecheck114", "started":true},"ok":true}(identical shape to 0.113.0); controller log[DEBUG] [web] netstorage add "gatecheck114" capability gate: netstorage_verify=yes(the probe verdict, visible); status poll →failedin ~2.5 s, categorynfs_export, the exact merged Hungarian message, real journal detail — byte-for-byte the 0.113.0 behavior. - Nothing changed state — share list
{"network_storage":[]}; felhom-pve: 0 unit files; the known residuals only (onenot-found failedunit listing →systemctl reset-failed; the empty/mnt/felhom-drives/gatecheck114dir → removed; both documented v0.113.0 observations, re-confirmed, no new gaps). Host+guest now show onlyfelhom-usb. - Page render — GET
/storage/network:ns-add-formpresent (2 occurrences), the banner textrégebbi verziójú0 occurrences ⇒ the SupportYes render; list intact.
NOT live-validated
- The
agent_outdatedrefusal branch (SupportNo) — test-proven only (T1/T4/T5 with red-proofs RP1/RP3/RP4/RP5). The demo agent is current (0.81.0); downgrading a live agent to prove a banner is not justified. Stays on this list until a future old-agent window shows it naturally. - Concurrent double-probe on a cold cache (documented harmless: one extra cheap GET) — not raced live.
STOP honored
NOT published (Day-0 manifest untouched, no publish-agent.sh, no golden bake), NO floor movement,
NO hub changes, NO agent changes, Peti untouched on 0.113.0. The gate is inert protection until the
next train. Note: the 0.114.0 registry tag was rebuilt once (the [DEBUG] gate-log commit) before
any vouch existed — demo-only, nothing references it.
Observations (out of scope, documented NOT acted on)
- Hub floor-UI separation (decoupling the floor field from the manifest-save screen / staging it) — its own task; the rules doc only references it (rule 2's "save LAST" is the interim control).
- Agent version-in-envelope — the documented future upgrade of
Supports(prefer version comparison over route probing); thefeatureProbestable comment + rules doc carry the pointer. Client.Supportsexists as the spec'd public API; the web layer drives the sameSupportCachemachinery through itsnetAgentseam (Server.netFeatures) so unit tests fake the probe — one code path in production and tests.- The known v0.113.0 remove-residuals (failed-unit listing needs
reset-failed; empty mountpoint dir lingers) reproduced on the failed-add rollback path too — same cleanup, still LOW.