v0.115.0: version-aware Supports (agent header channel) + DSM-validated NFS guidance — MinAgent: 0.81.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
|
||||
)
|
||||
|
||||
// Agent-capability probing (the publish-train backstop). A controller release that depends on
|
||||
@@ -53,13 +55,12 @@ type SupportProber interface {
|
||||
NetVerifyStatus(ctx context.Context) (NetVerifyStatus, error)
|
||||
}
|
||||
|
||||
// featureProbes maps each coupled feature to its route probe.
|
||||
// featureProbes maps each coupled feature to its route probe (the FALLBACK path for agents that
|
||||
// predate the v0.82.0 version header).
|
||||
//
|
||||
// CONVENTION (publish-train rules doc, felhom.eu/documentation/runbooks/publish-train-rules.md):
|
||||
// every future coupled feature adds a row here plus a Supports gate call at its entry point, and
|
||||
// declares MinAgent in its CHANGELOG header. When the agent someday reports an explicit version in
|
||||
// its envelope, Supports should prefer that version comparison over route probing — that
|
||||
// enhancement is roadmap, not built yet.
|
||||
// every future coupled feature adds a row here AND a featureMinAgent row, plus a Supports gate
|
||||
// call at its entry point, and declares MinAgent in its CHANGELOG header.
|
||||
var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error{
|
||||
FeatureNetstorageVerify: func(ctx context.Context, p SupportProber) error {
|
||||
_, err := p.NetVerifyStatus(ctx)
|
||||
@@ -67,6 +68,20 @@ var featureProbes = map[Feature]func(ctx context.Context, p SupportProber) error
|
||||
},
|
||||
}
|
||||
|
||||
// featureMinAgent maps each coupled feature to the MINIMUM agent version that carries its coupled
|
||||
// semantics (the CHANGELOG `MinAgent:` header value). Used by Supports when the agent's version is
|
||||
// KNOWN (the v0.82.0 X-Felhom-Agent-Version channel) — a direct comparison, no probe traffic. A
|
||||
// feature missing here (or an unparseable table value) falls back to the probe.
|
||||
var featureMinAgent = map[Feature]string{
|
||||
FeatureNetstorageVerify: "0.81.0",
|
||||
}
|
||||
|
||||
// AgentVersionReporter is optionally implemented by a SupportProber (*Client is one): it reports
|
||||
// the last strictly-validated agent version seen on its traffic ("" = unknown → probe fallback).
|
||||
type AgentVersionReporter interface {
|
||||
AgentVersion() string
|
||||
}
|
||||
|
||||
// supportTTL bounds how long a probe verdict (either polarity) is trusted. An agent updated
|
||||
// mid-window flips within this — no invalidation plumbing by design.
|
||||
const supportTTL = 5 * time.Minute
|
||||
@@ -85,14 +100,24 @@ type SupportCache struct {
|
||||
entries map[Feature]supportEntry
|
||||
}
|
||||
|
||||
// Supports reports whether the agent behind p provides feature f, answering from the cache inside
|
||||
// the TTL window and probing otherwise. The probe runs OUTSIDE the lock — concurrent misses may
|
||||
// double-probe (harmless: the probe is one cheap GET).
|
||||
// Supports reports whether the agent behind p provides feature f.
|
||||
//
|
||||
// Order (v0.115.0): (1) the agent's VERSION is known (the v0.82.0 header channel, strictly
|
||||
// validated at capture) AND the feature has a MinAgent row → pure semver comparison, NO probe
|
||||
// traffic, no cache involvement (the cache stays probe-only); (2) otherwise — pre-0.82 agent, no
|
||||
// traffic yet, or a table gap — the v0.114.0 probe path, byte-identical (cache inside the TTL
|
||||
// window, probe on miss). The probe runs OUTSIDE the lock — concurrent misses may double-probe
|
||||
// (harmless: the probe is one cheap GET).
|
||||
func (sc *SupportCache) Supports(ctx context.Context, p SupportProber, f Feature) SupportState {
|
||||
probe, ok := featureProbes[f]
|
||||
if !ok {
|
||||
return SupportUnknown // unregistered feature — never refuse on a table gap
|
||||
}
|
||||
if vr, hasVer := p.(AgentVersionReporter); hasVer {
|
||||
if state, decided := supportsByVersion(vr.AgentVersion(), f); decided {
|
||||
return state
|
||||
}
|
||||
}
|
||||
sc.mu.Lock()
|
||||
nowFn := sc.now
|
||||
if nowFn == nil {
|
||||
@@ -123,6 +148,31 @@ func (c *Client) Supports(ctx context.Context, f Feature) SupportState {
|
||||
return c.features.Supports(ctx, c, f)
|
||||
}
|
||||
|
||||
// supportsByVersion decides a feature by version comparison alone. decided=false (unknown/garbage
|
||||
// version, missing MinAgent row, unparseable table value) sends the caller to the probe fallback —
|
||||
// a bad version string must never be trusted in EITHER direction.
|
||||
func supportsByVersion(agentVer string, f Feature) (state SupportState, decided bool) {
|
||||
if agentVer == "" {
|
||||
return SupportUnknown, false
|
||||
}
|
||||
minStr, ok := featureMinAgent[f]
|
||||
if !ok {
|
||||
return SupportUnknown, false
|
||||
}
|
||||
av, err := util.ParseVersion(agentVer)
|
||||
if err != nil {
|
||||
return SupportUnknown, false // capture validates the shape, but stay defensive
|
||||
}
|
||||
mv, err := util.ParseVersion(minStr)
|
||||
if err != nil {
|
||||
return SupportUnknown, false // a broken table row falls back to probing, never refuses
|
||||
}
|
||||
if av.Compare(mv) >= 0 {
|
||||
return SupportYes, true
|
||||
}
|
||||
return SupportNo, true
|
||||
}
|
||||
|
||||
// classifySupportErr maps a probe outcome to a SupportState. ONLY a typed HTTP 404 means
|
||||
// "unsupported" — every other error (transport, timeout, 401, 5xx, envelope problems) is Unknown,
|
||||
// so a merely-down agent is never reported as outdated. Typed errors only; never string-match.
|
||||
|
||||
Reference in New Issue
Block a user