Add phenotype cloning, failure atlas, AI court session, and clearance L0-L4
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

This commit is contained in:
AetherForge
2026-06-07 02:41:54 -07:00
parent 4b94776432
commit d9f36f182c
47 changed files with 4859 additions and 94 deletions

View File

@@ -0,0 +1,287 @@
/**
* Network Topology Map — pure logic helpers.
*
* No React, no DOM — fully unit-testable.
* All data is derived from the existing Agent type; no backend changes needed.
*/
import type { Agent } from '../types';
// ── Subnet parsing ─────────────────────────────────────────────────────────
/**
* Extract the /24 subnet string from an IPv4 address.
* "192.168.1.42" → "192.168.1.0/24"
* Returns "unknown" when the IP is missing or non-IPv4.
*/
export function parseSubnet(ip: string | undefined): string {
if (!ip) return 'unknown';
const parts = ip.split('.');
if (parts.length !== 4 || parts.some((p) => isNaN(Number(p)))) return 'unknown';
return `${parts[0]}.${parts[1]}.${parts[2]}.0/24`;
}
/** Human-friendly label: "192.168.1.x" */
export function subnetLabel(subnet: string): string {
if (subnet === 'unknown') return 'Unknown';
return subnet.replace('.0/24', '.x');
}
/** Group agents by their /24 subnet. */
export function groupBySubnet(agents: Agent[]): Map<string, Agent[]> {
const map = new Map<string, Agent[]>();
for (const a of agents) {
const s = parseSubnet(a.ip);
if (!map.has(s)) map.set(s, []);
map.get(s)!.push(a);
}
return map;
}
// ── Layout ─────────────────────────────────────────────────────────────────
/** Stable hash from a string → 0..1 float. */
function stableHash(seed: string): number {
let h = 2166136261 >>> 0;
for (let i = 0; i < seed.length; i++) {
h ^= seed.charCodeAt(i);
h = Math.imul(h, 16777619) >>> 0;
}
return (h % 10000) / 10000;
}
/** Two independent stable floats for x/y from one seed. */
function stableXY(seed: string): { x: number; y: number } {
return {
x: stableHash(seed + ':x'),
y: stableHash(seed + ':y'),
};
}
export interface SubnetLayout {
subnet: string;
label: string;
cx: number; // center x, 0-100 viewBox
cy: number; // center y, 0-100 viewBox
r: number; // radius of the bubble ring
color: string;
}
const SUBNET_PALETTE = [
'#00e8f5', // cyan
'#b24bf3', // violet
'#39ff14', // neon green
'#ff2da6', // magenta
'#ffb020', // amber
'#ff6b35', // orange
'#3a86ff', // blue
'#06d6a0', // teal
'#ffd60a', // yellow
'#f72585', // hot pink
];
export function subnetColor(index: number): string {
return SUBNET_PALETTE[index % SUBNET_PALETTE.length];
}
/**
* Place subnet bubbles in a circle around the center.
* Single subnet gets center position.
*/
export function layoutSubnets(subnets: string[]): SubnetLayout[] {
const total = subnets.length;
const BASE_R = 18;
const ORBIT_R = total === 1 ? 0 : 28;
return subnets.map((subnet, i) => {
const angle = total === 1 ? 0 : (i / total) * Math.PI * 2 - Math.PI / 2;
const cx = 50 + Math.cos(angle) * ORBIT_R;
const cy = 50 + Math.sin(angle) * ORBIT_R;
return {
subnet,
label: subnetLabel(subnet),
cx,
cy,
r: BASE_R,
color: subnetColor(i),
};
});
}
export interface TopoNode {
id: string;
agentId: string;
label: string;
x: number;
y: number;
subnet: string;
subnetColor: string;
online: boolean;
platform: string;
ip: string;
hashrate: number;
latencyMs?: number;
joinLane?: string;
canSpread: boolean;
capabilities: Agent['capabilities'];
}
/**
* Compute pixel positions for all nodes.
* Nodes within a subnet scatter around the subnet center.
*/
export function layoutNodes(
agents: Agent[],
subnetLayouts: SubnetLayout[],
): TopoNode[] {
const subnetMap = new Map(subnetLayouts.map((s) => [s.subnet, s]));
const subnetAgentCounts = new Map<string, number>();
for (const a of agents) {
const s = parseSubnet(a.ip);
subnetAgentCounts.set(s, (subnetAgentCounts.get(s) ?? 0) + 1);
}
const subnetCounters = new Map<string, number>();
return agents.map((agent): TopoNode => {
const subnet = parseSubnet(agent.ip);
const layout = subnetMap.get(subnet) ?? { cx: 50, cy: 50, r: 18, color: '#00e8f5', subnet, label: 'Unknown' };
const total = subnetAgentCounts.get(subnet) ?? 1;
const idx = subnetCounters.get(subnet) ?? 0;
subnetCounters.set(subnet, idx + 1);
// Stable jitter within the bubble radius
const jitter = stableXY(`${subnet}:${agent.id}`);
const angle = (idx / Math.max(total, 1)) * Math.PI * 2 + (jitter.x - 0.5) * 0.8;
const dist = (total === 1 ? 0 : 4 + Math.sqrt(total) * 2.5) * (0.6 + jitter.y * 0.4);
const maxDist = layout.r * 0.75;
return {
id: agent.id,
agentId: agent.id,
label: agent.name,
x: layout.cx + Math.cos(angle) * Math.min(dist, maxDist),
y: layout.cy + Math.sin(angle) * Math.min(dist, maxDist),
subnet,
subnetColor: layout.color,
online: agent.status === 'online',
platform: agent.platform ?? 'unknown',
ip: agent.ip ?? '—',
hashrate: agent.hashrate_15m ?? 0,
latencyMs: agent.latency_ms,
joinLane: agent.join_lane,
canSpread: !!(agent.capabilities?.auto_spread),
capabilities: agent.capabilities,
};
});
}
// ── Edge graph ──────────────────────────────────────────────────────────────
export type EdgeKind =
| 'subnet' // same /24 — implies reachability
| 'spread' // lateral spread candidate
| 'smb' // SMB admin$ path (Windows + port 445)
| 'winrm' // WinRM (Windows + 5985/5986)
| 'ssh' // SSH lateral (Linux/Darwin)
| 'cross_subnet'; // different subnets, but both spread-capable
export interface TopoEdge {
id: string;
sourceId: string;
targetId: string;
kind: EdgeKind;
/** Highlight when either endpoint is selected. */
active: boolean;
}
/** Determine what spread lanes exist between two agents. */
export function spreadLanesBetween(a: Agent, b: Agent): EdgeKind[] {
if (!a.capabilities?.auto_spread || !b.capabilities?.auto_spread) return [];
const lanes: EdgeKind[] = ['spread'];
const aWin = (a.platform ?? '').toLowerCase().includes('win');
const bWin = (b.platform ?? '').toLowerCase().includes('win');
const aLin = (a.platform ?? '').toLowerCase().includes('linux') || (a.platform ?? '').toLowerCase().includes('darwin');
const bLin = (b.platform ?? '').toLowerCase().includes('linux') || (b.platform ?? '').toLowerCase().includes('darwin');
if (aWin && bWin) { lanes.push('smb'); lanes.push('winrm'); }
if ((aLin && bWin) || (aWin && bLin) || (aLin && bLin)) lanes.push('ssh');
return lanes;
}
/**
* Build all edges for the topology graph.
*
* Rules:
* - Same subnet + both online → `subnet` edge
* - Both have auto_spread + online → additional `spread` / protocol edges
* - Cross-subnet + both spread-capable → `cross_subnet`
*/
export function buildEdges(agents: Agent[], selectedIds: Set<string>): TopoEdge[] {
const online = agents.filter((a) => a.status === 'online');
const edges: TopoEdge[] = [];
const seen = new Set<string>();
for (let i = 0; i < online.length; i++) {
for (let j = i + 1; j < online.length; j++) {
const a = online[i];
const b = online[j];
const subA = parseSubnet(a.ip);
const subB = parseSubnet(b.ip);
const sameSubnet = subA === subB && subA !== 'unknown';
const edgeKey = [a.id, b.id].sort().join('::');
if (seen.has(edgeKey)) continue;
seen.add(edgeKey);
const active = selectedIds.has(a.id) || selectedIds.has(b.id);
if (sameSubnet) {
edges.push({ id: edgeKey + ':subnet', sourceId: a.id, targetId: b.id, kind: 'subnet', active });
}
// Spread lanes (may add on top of subnet edge)
const lanes = spreadLanesBetween(a, b);
for (const lane of lanes) {
if (lane === 'spread' && sameSubnet) continue; // subnet edge covers it
const spreadKey = edgeKey + ':' + lane;
if (seen.has(spreadKey)) continue;
seen.add(spreadKey);
edges.push({
id: spreadKey,
sourceId: a.id,
targetId: b.id,
kind: sameSubnet ? lane : 'cross_subnet',
active,
});
}
}
}
return edges;
}
/**
* Which agents can be reached laterally from a source agent?
* Returns agent IDs with the best available lane.
*/
export function spreadCandidates(
source: Agent,
allAgents: Agent[],
): { agentId: string; lane: EdgeKind }[] {
if (!source.capabilities?.auto_spread || source.status !== 'online') return [];
const results: { agentId: string; lane: EdgeKind }[] = [];
for (const target of allAgents) {
if (target.id === source.id || target.status !== 'online') continue;
const lanes = spreadLanesBetween(source, target);
if (lanes.length === 0) continue;
// Prefer most specific lane
const preferred = lanes.find((l) => l !== 'spread') ?? lanes[0];
results.push({ agentId: target.id, lane: preferred });
}
return results;
}