feat: agent-capability gate for coupled features — typed StatusError + Supports probe/cache + netstorage add gate (option-1)
- agentapi: non-2xx GETs now surface as typed *StatusError (same text); features.go adds Feature/SupportState/SupportCache (route probe, TTL 5m, Yes/No cached, Unknown never cached or refused) + Client.Supports - web: handleNetStorageAdd refuses up front (412, code agent_outdated, Hungarian message) when the agent predates /netstorage/verify-status (= pre-0.81 add semantics); gate runs BEFORE the single-flight claim; SupportUnknown passes through to the existing agent-error paths - netAddSupport page-render helper lands here; its template consumer follows - tests: T1 gate refusal (job never starts, slot free), T2 unchanged happy path + warm-cache negative assertion, T3 indeterminate never 'too old', T4 classification incl. the string-match trap, T6 TTL, wire-level 404-typing; red-proofs RP1-RP4 run and reverted Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -22,6 +22,31 @@ import (
|
||||
// agent applies the +100000 host offset; this is the in-guest id the share is mapped to.
|
||||
const defaultMediaUID = 1000
|
||||
|
||||
// netAddOutdatedMsg is the sync add-time refusal (machine code "agent_outdated") when the agent
|
||||
// predates the coupled verify-before-commit add semantics (pre-v0.81.0).
|
||||
const netAddOutdatedMsg = "Az ügynök frissítése szükséges ehhez a funkcióhoz — a frissítés megérkezése után próbáld újra."
|
||||
|
||||
// netAddSupport evaluates the coupled-feature probe for the settings-page render with a SHORT
|
||||
// budget — a down agent must not stall the page (the cache usually answers instantly). Returns the
|
||||
// template vocabulary: "yes" | "no" | "unknown"; only "no" swaps the add form for the banner —
|
||||
// flaky states belong to the add-time handling.
|
||||
func (s *Server) netAddSupport() string {
|
||||
agent, err := s.netAgentForAdd()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
switch s.netFeatures.Supports(ctx, agent, agentapi.FeatureNetstorageVerify) {
|
||||
case agentapi.SupportYes:
|
||||
return "yes"
|
||||
case agentapi.SupportNo:
|
||||
return "no"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// networkStorageItem is the UI row: the registered descriptor + live per-share health from the agent.
|
||||
// Orphan marks an agent-configured share with NO registry entry (a crash-window leftover — Scenario
|
||||
// F's visible closure): the row renders with ONLY the remove action.
|
||||
@@ -91,6 +116,16 @@ func (s *Server) handleNetStorageAdd(w http.ResponseWriter, r *http.Request) {
|
||||
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
||||
return
|
||||
}
|
||||
// Capability gate (the publish-train backstop): the coupled add semantics shipped with agent
|
||||
// v0.81.0 together with GET /netstorage/verify-status — on an older agent, refuse up front
|
||||
// instead of failing mid-pipeline in `verifying` with a misleading rollback. Runs BEFORE the
|
||||
// single-flight claim (a refused add must not consume the slot). SupportUnknown passes: a down
|
||||
// agent speaks through the existing agent-error paths, never as "too old".
|
||||
if s.netFeatures.Supports(r.Context(), agent, agentapi.FeatureNetstorageVerify) == agentapi.SupportNo {
|
||||
s.logger.Printf("[WARN] [web] netstorage add %q refused: agent predates %s (probe 404)", name, agentapi.FeatureNetstorageVerify)
|
||||
writeDiskJSON(w, http.StatusPreconditionFailed, false, netAddOutdatedMsg, map[string]any{"code": "agent_outdated"})
|
||||
return
|
||||
}
|
||||
label := strings.TrimSpace(req.Label)
|
||||
if label == "" {
|
||||
label = "Hálózati tárhely: " + name
|
||||
|
||||
Reference in New Issue
Block a user