feat(scan): add live progress and recon bootstrap (#81)
* Add live scan progress view * Make local-agent recon report real tool progress * Feed recon bootstrap into local agent ReAct loop * Remove stale local-agent JSON tool parser * Label backend-seeded scan progress --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
8959a288e5
commit
089591b18f
6 files changed
+476
-11
No files matched your search
+251
-1
@@ -295,6 +295,11 @@
|
|||||||
|
|
||||||
.page { display: none; }
|
.page { display: none; }
|
||||||
.page.active { display: block; animation: fadeIn 0.3s ease; }
|
.page.active { display: block; animation: fadeIn 0.3s ease; }
|
||||||
|
.live-scan-grid { display: grid; grid-template-columns: minmax(260px, 0.9fr) minmax(360px, 1.4fr); gap: 14px; align-items: start; }
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.live-scan-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
@@ -3305,6 +3310,9 @@
|
|||||||
<div class="nav-item active" data-page="warroom">
|
<div class="nav-item active" data-page="warroom">
|
||||||
<span class="icon">⚔️</span> War Room
|
<span class="icon">⚔️</span> War Room
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item" data-page="live-scan">
|
||||||
|
<span class="icon">📡</span> Live Scan
|
||||||
|
</div>
|
||||||
<div class="nav-item" data-page="operators">
|
<div class="nav-item" data-page="operators">
|
||||||
<span class="icon">🤖</span> Operatives
|
<span class="icon">🤖</span> Operatives
|
||||||
<span class="badge" id="activeOperatorCount">0</span>
|
<span class="badge" id="activeOperatorCount">0</span>
|
||||||
@@ -4173,6 +4181,48 @@
|
|||||||
<div id="targetList" style="display: none;"></div>
|
<div id="targetList" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Live Scan - per-agent execution progress -->
|
||||||
|
<div class="page" id="page-live-scan">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:2px;margin-bottom:6px;">Live Scan</div>
|
||||||
|
<div style="font-size:24px;font-weight:800;color:var(--text-primary);line-height:1.15;">Agent Progress</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="refreshLiveScanPage()">Refresh</button>
|
||||||
|
<button class="btn btn-secondary btn-sm" onclick="clearLiveScanFeed()">Clear Feed</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="liveScanSummary" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:14px;"></div>
|
||||||
|
|
||||||
|
<div class="live-scan-grid">
|
||||||
|
<section style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;overflow:hidden;">
|
||||||
|
<div style="padding:12px 14px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;gap:10px;">
|
||||||
|
<div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:1px;">Operators</div>
|
||||||
|
<div id="liveScanOperatorCount" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);">0 active</div>
|
||||||
|
</div>
|
||||||
|
<div id="liveScanOperators" style="max-height:360px;overflow:auto;"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;overflow:hidden;">
|
||||||
|
<div style="padding:12px 14px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;gap:10px;">
|
||||||
|
<div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:1px;">Task Queue</div>
|
||||||
|
<div id="liveScanTaskCount" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);">0 tasks</div>
|
||||||
|
</div>
|
||||||
|
<div id="liveScanTasks" style="max-height:360px;overflow:auto;"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section style="margin-top:14px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;overflow:hidden;">
|
||||||
|
<div style="padding:12px 14px;border-bottom:1px solid var(--border-color);display:flex;justify-content:space-between;align-items:center;gap:10px;">
|
||||||
|
<div style="font-size:11px;color:var(--text-secondary);text-transform:uppercase;letter-spacing:1px;">Reasoning And Tool Stream</div>
|
||||||
|
<div id="liveScanFeedCount" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--accent);">0 events</div>
|
||||||
|
</div>
|
||||||
|
<div id="liveScanFeed" style="height:min(48vh,520px);min-height:300px;overflow:auto;font-family:'JetBrains Mono',monospace;background:rgba(0,0,0,0.18);"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Operators - Tactical Unit Command -->
|
<!-- Operators - Tactical Unit Command -->
|
||||||
<div class="page" id="page-operators">
|
<div class="page" id="page-operators">
|
||||||
<!-- ═══ OPERATIVES ROSTER — click any specialist to view + edit its prompt / tools / config ═══ -->
|
<!-- ═══ OPERATIVES ROSTER — click any specialist to view + edit its prompt / tools / config ═══ -->
|
||||||
@@ -5981,6 +6031,14 @@ tempest.operators.<span class="function">spawn</span>(<span class="string">'scan
|
|||||||
if (data.operators) {
|
if (data.operators) {
|
||||||
this._updateOperatorStatusUI(data.operators);
|
this._updateOperatorStatusUI(data.operators);
|
||||||
}
|
}
|
||||||
|
window.updateLiveScanStatus?.(data);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
|
||||||
|
this._eventSource.addEventListener('scan:progress', (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.data);
|
||||||
|
window.recordLiveScanEvent?.(data);
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6909,13 +6967,205 @@ Correlating findings across agents, identifying patterns, producing actionable i
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LiveScanState = {
|
||||||
|
status: null,
|
||||||
|
events: [],
|
||||||
|
seen: new Set()
|
||||||
|
};
|
||||||
|
|
||||||
|
function liveScanEscape(value) {
|
||||||
|
return String(value ?? '').replace(/[&<>"']/g, ch => ({
|
||||||
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||||
|
}[ch]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveScanTime(ts) {
|
||||||
|
const date = ts ? new Date(ts) : new Date();
|
||||||
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveScanStatusColor(status) {
|
||||||
|
const s = String(status || '').toLowerCase();
|
||||||
|
if (s === 'completed' || s === 'idle') return '#00ff88';
|
||||||
|
if (s === 'failed' || s === 'burned') return '#ff4444';
|
||||||
|
if (s === 'running' || s === 'tasked' || s === 'executing' || s === 'in_progress') return '#00aaff';
|
||||||
|
if (s === 'pending' || s === 'cooldown') return '#ffaa00';
|
||||||
|
return 'var(--text-secondary)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveScanKindLabel(kind) {
|
||||||
|
return ({
|
||||||
|
task_started: 'START',
|
||||||
|
thinking: 'THINK',
|
||||||
|
tool_call: 'TOOL',
|
||||||
|
tool_result: 'RESULT',
|
||||||
|
task_completed: 'DONE',
|
||||||
|
task_failed: 'FAIL'
|
||||||
|
})[kind] || String(kind || 'EVENT').toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function liveScanMergeEvents(events) {
|
||||||
|
if (!Array.isArray(events)) return;
|
||||||
|
events.forEach(ev => {
|
||||||
|
const id = ev?.id || `${ev?.timestamp || Date.now()}-${ev?.operatorId || ''}-${ev?.kind || ''}-${ev?.detail || ''}`;
|
||||||
|
if (LiveScanState.seen.has(id)) return;
|
||||||
|
LiveScanState.seen.add(id);
|
||||||
|
LiveScanState.events.push({ ...ev, id });
|
||||||
|
});
|
||||||
|
LiveScanState.events.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
|
||||||
|
if (LiveScanState.events.length > 300) {
|
||||||
|
const trimmed = LiveScanState.events.splice(0, LiveScanState.events.length - 300);
|
||||||
|
trimmed.forEach(ev => LiveScanState.seen.delete(ev.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.recordLiveScanEvent = function recordLiveScanEvent(event) {
|
||||||
|
liveScanMergeEvents([event]);
|
||||||
|
renderLiveScanPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.updateLiveScanStatus = function updateLiveScanStatus(status) {
|
||||||
|
LiveScanState.status = status || null;
|
||||||
|
liveScanMergeEvents(status?.progress);
|
||||||
|
renderLiveScanPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.refreshLiveScanPage = async function refreshLiveScanPage() {
|
||||||
|
const status = await T3MP3ST_API.get('/api/mission/status');
|
||||||
|
window.updateLiveScanStatus(status);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.clearLiveScanFeed = function clearLiveScanFeed() {
|
||||||
|
LiveScanState.events = [];
|
||||||
|
LiveScanState.seen = new Set();
|
||||||
|
renderLiveScanPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderLiveScanPage() {
|
||||||
|
const page = document.getElementById('page-live-scan');
|
||||||
|
if (!page) return;
|
||||||
|
|
||||||
|
const status = LiveScanState.status || {};
|
||||||
|
const active = status.active ?? status.running ?? false;
|
||||||
|
const mission = status.mission || {};
|
||||||
|
const operators = Array.isArray(status.operators?.details) ? status.operators.details : [];
|
||||||
|
const operatorSummary = status.operators?.summary || status.operators || {};
|
||||||
|
const tasks = Array.isArray(status.tasks) ? status.tasks : [];
|
||||||
|
const events = LiveScanState.events.slice().reverse();
|
||||||
|
const currentPhase = mission.currentPhase || status.currentPhase || 'none';
|
||||||
|
const progress = typeof mission.progress === 'number' ? `${Math.round(mission.progress)}%` : '-';
|
||||||
|
|
||||||
|
const summary = document.getElementById('liveScanSummary');
|
||||||
|
if (summary) {
|
||||||
|
const cells = [
|
||||||
|
['State', status.paused ? 'Paused' : (active ? 'Running' : 'Stopped'), active ? '#00ff88' : '#ffaa00'],
|
||||||
|
['Phase', currentPhase, '#00aaff'],
|
||||||
|
['Progress', progress, '#00ff88'],
|
||||||
|
['Operators', String(operatorSummary.total ?? operators.length ?? 0), '#ffaa00'],
|
||||||
|
['Tasks', String(tasks.length), '#00aaff'],
|
||||||
|
['Stall', status.stallReason || '-', status.stallReason ? '#ff4444' : 'var(--text-secondary)']
|
||||||
|
];
|
||||||
|
summary.innerHTML = cells.map(([label, value, color]) => `
|
||||||
|
<div style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:8px;padding:12px;min-height:74px;">
|
||||||
|
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;">${liveScanEscape(label)}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace;font-size:18px;color:${color};overflow-wrap:anywhere;">${liveScanEscape(value)}</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const opCount = document.getElementById('liveScanOperatorCount');
|
||||||
|
if (opCount) opCount.textContent = `${operatorSummary.busy ?? operators.filter(op => ['tasked', 'executing'].includes(op.status)).length ?? 0} busy`;
|
||||||
|
const opEl = document.getElementById('liveScanOperators');
|
||||||
|
if (opEl) {
|
||||||
|
if (!operators.length) {
|
||||||
|
opEl.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:12px;">No operator details available yet.</div>';
|
||||||
|
} else {
|
||||||
|
opEl.innerHTML = operators.map(op => {
|
||||||
|
const statusColor = liveScanStatusColor(op.status);
|
||||||
|
return `
|
||||||
|
<div style="padding:12px 14px;border-bottom:1px solid rgba(255,255,255,0.06);display:grid;grid-template-columns:1fr auto;gap:8px;align-items:start;">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:13px;color:var(--text-primary);font-weight:700;">${liveScanEscape(op.callsign || op.id)}</div>
|
||||||
|
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">${liveScanEscape(op.archetype || 'operator')}</div>
|
||||||
|
<div style="font-size:10px;color:var(--text-secondary);margin-top:7px;">${Number(op.completedTasks || 0)} done · ${Number(op.failedTasks || 0)} failed · risk ${Number(op.detectionRisk || 0)}</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:${statusColor};text-transform:uppercase;">${liveScanEscape(op.status || 'unknown')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskCount = document.getElementById('liveScanTaskCount');
|
||||||
|
if (taskCount) taskCount.textContent = `${tasks.length} tasks`;
|
||||||
|
const taskEl = document.getElementById('liveScanTasks');
|
||||||
|
if (taskEl) {
|
||||||
|
if (!tasks.length) {
|
||||||
|
taskEl.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:12px;">No mission tasks are queued.</div>';
|
||||||
|
} else {
|
||||||
|
taskEl.innerHTML = tasks.map(task => {
|
||||||
|
const color = liveScanStatusColor(task.status);
|
||||||
|
const output = task.result?.error || task.result?.output || '';
|
||||||
|
return `
|
||||||
|
<div style="padding:11px 14px;border-bottom:1px solid rgba(255,255,255,0.06);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;">
|
||||||
|
<div style="min-width:0;">
|
||||||
|
<div style="font-size:12px;color:var(--text-primary);font-weight:700;overflow-wrap:anywhere;">${liveScanEscape(task.name || task.id)}</div>
|
||||||
|
<div style="font-size:10px;color:var(--text-muted);margin-top:4px;">${liveScanEscape(task.phase || '-')} · ${liveScanEscape(task.operatorType || '-')} ${task.assignedTo ? '· ' + liveScanEscape(task.assignedTo) : ''}</div>
|
||||||
|
${output ? `<div style="font-size:10px;color:var(--text-secondary);margin-top:6px;overflow-wrap:anywhere;">${liveScanEscape(output).slice(0, 260)}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:${color};text-transform:uppercase;">${liveScanEscape(task.status || 'unknown')}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedCount = document.getElementById('liveScanFeedCount');
|
||||||
|
if (feedCount) feedCount.textContent = `${LiveScanState.events.length} events`;
|
||||||
|
const feed = document.getElementById('liveScanFeed');
|
||||||
|
if (feed) {
|
||||||
|
if (!events.length) {
|
||||||
|
feed.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:12px;">No live reasoning events yet. Start or refresh a scan to populate this stream.</div>';
|
||||||
|
} else {
|
||||||
|
feed.innerHTML = events.map(ev => {
|
||||||
|
const color = ev.kind === 'task_failed' || ev.success === false ? '#ff6666'
|
||||||
|
: ev.kind === 'tool_call' ? '#ffaa00'
|
||||||
|
: ev.kind === 'tool_result' ? '#00aaff'
|
||||||
|
: ev.kind === 'thinking' ? '#cfd6e6'
|
||||||
|
: '#00ff88';
|
||||||
|
const sourceBadge = ev.source === 'backend_seeded'
|
||||||
|
? '<span style="display:inline-block;margin-left:6px;padding:1px 5px;border:1px solid rgba(255,170,0,0.45);border-radius:3px;color:#ffaa00;font-size:9px;text-transform:uppercase;letter-spacing:0.4px;">backend-seeded</span>'
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
|
<div style="padding:10px 14px;border-bottom:1px solid rgba(255,255,255,0.055);display:grid;grid-template-columns:76px 74px minmax(110px,180px) minmax(0,1fr);gap:10px;align-items:start;">
|
||||||
|
<div style="font-size:10px;color:var(--text-muted);">${liveScanTime(ev.timestamp)}</div>
|
||||||
|
<div style="font-size:10px;color:${color};font-weight:800;">${liveScanEscape(liveScanKindLabel(ev.kind))}</div>
|
||||||
|
<div style="font-size:10px;color:var(--accent);overflow-wrap:anywhere;">${liveScanEscape(ev.callsign || ev.archetype || ev.operatorId || '-')}</div>
|
||||||
|
<div style="min-width:0;">
|
||||||
|
<div style="font-size:11px;color:var(--text-secondary);margin-bottom:4px;overflow-wrap:anywhere;">${liveScanEscape(ev.taskName || ev.toolName || '-')}${sourceBadge}</div>
|
||||||
|
<div style="font-size:12px;color:var(--text-primary);line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;">${liveScanEscape(ev.detail || '')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
if (document.getElementById('page-live-scan')?.classList.contains('active')) {
|
||||||
|
window.refreshLiveScanPage?.();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
function navigateTo(page) {
|
function navigateTo(page) {
|
||||||
document.querySelectorAll('.nav-item').forEach(i => i.classList.toggle('active', i.dataset.page === page));
|
document.querySelectorAll('.nav-item').forEach(i => i.classList.toggle('active', i.dataset.page === page));
|
||||||
document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === `page-${page}`));
|
document.querySelectorAll('.page').forEach(p => p.classList.toggle('active', p.id === `page-${page}`));
|
||||||
const titles = { warroom: 'War Room', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' };
|
const titles = { warroom: 'War Room', 'live-scan': 'Live Scan', operators: 'Operatives', evidence: 'Evidence Vault', arsenal: 'Arsenal', terminal: 'Terminal', benchmarks: 'Benchmarks', configs: 'Config Library', 'ctf-range': 'CTF Range', general: 'Op Admiral', selfimprove: 'Self-Improvement', settings: 'Settings', about: 'About' };
|
||||||
document.getElementById('pageTitle').textContent = titles[page] || 'War Room';
|
document.getElementById('pageTitle').textContent = titles[page] || 'War Room';
|
||||||
document.getElementById('sidebar').classList.remove('open');
|
document.getElementById('sidebar').classList.remove('open');
|
||||||
|
if (page === 'live-scan') setTimeout(() => window.refreshLiveScanPage?.(), 50);
|
||||||
if (page === 'operators' && typeof window.loadOperativesRoster === 'function') setTimeout(window.loadOperativesRoster, 80);
|
if (page === 'operators' && typeof window.loadOperativesRoster === 'function') setTimeout(window.loadOperativesRoster, 80);
|
||||||
if (page === 'evidence' && typeof window.hydrateFindings === 'function') setTimeout(window.hydrateFindings, 60);
|
if (page === 'evidence' && typeof window.hydrateFindings === 'function') setTimeout(window.hydrateFindings, 60);
|
||||||
if (page === 'selfimprove' && typeof window.renderSelfImprove === 'function') setTimeout(window.renderSelfImprove, 60);
|
if (page === 'selfimprove' && typeof window.renderSelfImprove === 'function') setTimeout(window.renderSelfImprove, 60);
|
||||||
|
|||||||
+70
-4
@@ -71,8 +71,8 @@ export interface AgentResult {
|
|||||||
|
|
||||||
export interface AgentEvents {
|
export interface AgentEvents {
|
||||||
'agent:step': AgentStep;
|
'agent:step': AgentStep;
|
||||||
'agent:tool_call': { name: string; args: Record<string, unknown> };
|
'agent:tool_call': { name: string; args: Record<string, unknown>; source?: 'agent' | 'backend_seeded' };
|
||||||
'agent:tool_result': { name: string; result: ToolResult };
|
'agent:tool_result': { name: string; result: ToolResult; source?: 'agent' | 'backend_seeded' };
|
||||||
'agent:thinking': { content: string };
|
'agent:thinking': { content: string };
|
||||||
'agent:complete': AgentResult;
|
'agent:complete': AgentResult;
|
||||||
'agent:error': { error: Error; step: number };
|
'agent:error': { error: Error; step: number };
|
||||||
@@ -133,6 +133,39 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
{ role: 'user', content: this.buildTaskPrompt(task, target, toolDefs, sourceContext, sharedContext) },
|
{ role: 'user', content: this.buildTaskPrompt(task, target, toolDefs, sourceContext, sharedContext) },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const bootstrapCall = this.getLocalReconBootstrap(task, target, toolDefs);
|
||||||
|
if (bootstrapCall) {
|
||||||
|
const toolStep = await this.executeTool(bootstrapCall, target, -1);
|
||||||
|
steps.push(toolStep);
|
||||||
|
seenCalls.set(
|
||||||
|
`${bootstrapCall.name}:${JSON.stringify(bootstrapCall.arguments || {})}`,
|
||||||
|
String(toolStep.toolResult?.output || toolStep.toolResult?.error || 'no output').replace(/\s+/g, ' ').slice(0, 160)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (toolStep.toolResult?.findings) {
|
||||||
|
const out = String(toolStep.toolResult.output ?? '').slice(0, 4000);
|
||||||
|
for (const tf of toolStep.toolResult.findings) {
|
||||||
|
allFindings.push({ ...tf, provenance: 'tool', toolName: bootstrapCall.name, toolOutput: out || tf.details });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: 'Running an initial low-noise recon check before planning deeper actions.',
|
||||||
|
toolCalls: [bootstrapCall],
|
||||||
|
});
|
||||||
|
messages.push({
|
||||||
|
role: 'tool',
|
||||||
|
content: this.formatToolResult(toolStep.toolResult),
|
||||||
|
toolCallId: bootstrapCall.id,
|
||||||
|
name: bootstrapCall.name,
|
||||||
|
});
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: 'Use the bootstrap observation above as your first tool observation. Continue through the normal ACTION CONTRACT: request another listed tool only if it will materially improve the evidence; otherwise provide the final recon debrief.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < this.options.maxIterations; i++) {
|
for (let i = 0; i < this.options.maxIterations; i++) {
|
||||||
try {
|
try {
|
||||||
// Ask the LLM what to do next
|
// Ask the LLM what to do next
|
||||||
@@ -298,6 +331,38 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getLocalReconBootstrap(
|
||||||
|
task: Task,
|
||||||
|
target: Target | undefined,
|
||||||
|
tools: LLMToolDefinition[]
|
||||||
|
): LLMToolCall | undefined {
|
||||||
|
if (this.llm.getProvider() !== 'local-agent') return undefined;
|
||||||
|
if (!target?.address) return undefined;
|
||||||
|
if (task.phase !== 'reconnaissance' && task.operatorType !== 'recon') return undefined;
|
||||||
|
|
||||||
|
const hasTool = (name: string) => tools.some((tool) => tool.name === name);
|
||||||
|
const taskName = task.name.toLowerCase();
|
||||||
|
const id = `local-agent-bootstrap-${Date.now()}`;
|
||||||
|
|
||||||
|
if (taskName.includes('dns') && hasTool('dns_lookup')) {
|
||||||
|
return { id, name: 'dns_lookup', arguments: { domain: target.address, type: 'A' } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskName.includes('port') && hasTool('port_scan')) {
|
||||||
|
return { id, name: 'port_scan', arguments: { target: target.address, ports: '80,443', timeout: 2000 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((taskName.includes('web') || taskName.includes('fingerprint')) && hasTool('header_analysis')) {
|
||||||
|
return { id, name: 'header_analysis', arguments: { url: `https://${target.address}` } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((taskName.includes('content') || taskName.includes('discover')) && hasTool('robots_txt_fetch')) {
|
||||||
|
return { id, name: 'robots_txt_fetch', arguments: { url: `https://${target.address}` } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a single tool call via the Arsenal
|
* Execute a single tool call via the Arsenal
|
||||||
*/
|
*/
|
||||||
@@ -306,7 +371,8 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
target: Target | undefined,
|
target: Target | undefined,
|
||||||
iteration: number
|
iteration: number
|
||||||
): Promise<AgentStep> {
|
): Promise<AgentStep> {
|
||||||
this.emit('agent:tool_call', { name: toolCall.name, args: toolCall.arguments });
|
const source = iteration < 0 ? 'backend_seeded' : 'agent';
|
||||||
|
this.emit('agent:tool_call', { name: toolCall.name, args: toolCall.arguments, source });
|
||||||
|
|
||||||
let toolResult: ToolResult;
|
let toolResult: ToolResult;
|
||||||
try {
|
try {
|
||||||
@@ -330,7 +396,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('agent:tool_result', { name: toolCall.name, result: toolResult });
|
this.emit('agent:tool_result', { name: toolCall.name, result: toolResult, source });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
iteration,
|
iteration,
|
||||||
|
|||||||
+100
@@ -212,6 +212,8 @@ import type {
|
|||||||
OperatorArchetype,
|
OperatorArchetype,
|
||||||
CommandEvents,
|
CommandEvents,
|
||||||
Finding,
|
Finding,
|
||||||
|
ScanProgressEvent,
|
||||||
|
Task,
|
||||||
} from './types/index.js';
|
} from './types/index.js';
|
||||||
|
|
||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
@@ -271,6 +273,7 @@ import {
|
|||||||
|
|
||||||
const DEFAULT_AGENT_MAX_ITERATIONS = 15;
|
const DEFAULT_AGENT_MAX_ITERATIONS = 15;
|
||||||
const LOCAL_AGENT_MAX_ITERATIONS = Number(process.env.T3MP3ST_LOCAL_AGENT_MAX_ITERATIONS || 30);
|
const LOCAL_AGENT_MAX_ITERATIONS = Number(process.env.T3MP3ST_LOCAL_AGENT_MAX_ITERATIONS || 30);
|
||||||
|
const MAX_PROGRESS_EVENTS = 300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TEMPEST Command - Main orchestration controller
|
* TEMPEST Command - Main orchestration controller
|
||||||
@@ -544,6 +547,40 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
* Setup event forwarding for an operator
|
* Setup event forwarding for an operator
|
||||||
*/
|
*/
|
||||||
private setupOperatorEvents(operator: OperatorAgent): void {
|
private setupOperatorEvents(operator: OperatorAgent): void {
|
||||||
|
operator.on('task:started', ({ task }) => {
|
||||||
|
this.recordProgress('task_started', operator, task, `Started ${task.name}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
operator.on('task:completed', ({ task, result }) => {
|
||||||
|
const findings = result.findings?.length ? ` Findings: ${result.findings.join(', ')}` : '';
|
||||||
|
this.recordProgress(
|
||||||
|
'task_completed',
|
||||||
|
operator,
|
||||||
|
task,
|
||||||
|
`${result.success === false ? 'Finished unsuccessfully' : 'Completed'} ${task.name}.${findings}`,
|
||||||
|
{ success: result.success !== false }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
operator.on('task:failed', ({ task, error }) => {
|
||||||
|
this.recordProgress('task_failed', operator, task, error, { success: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
operator.on('agent:thinking', ({ task, content }) => {
|
||||||
|
this.recordProgress('thinking', operator, task, content);
|
||||||
|
});
|
||||||
|
|
||||||
|
operator.on('agent:tool_call', ({ task, name, args, source }) => {
|
||||||
|
this.recordProgress('tool_call', operator, task, `${name} ${JSON.stringify(args || {})}`, { toolName: name, source });
|
||||||
|
});
|
||||||
|
|
||||||
|
operator.on('agent:tool_result', ({ task, name, result, source }) => {
|
||||||
|
const detail = result.success
|
||||||
|
? (result.output || 'Tool completed')
|
||||||
|
: (result.error || result.output || 'Tool failed');
|
||||||
|
this.recordProgress('tool_result', operator, task, detail, { toolName: name, success: result.success, source });
|
||||||
|
});
|
||||||
|
|
||||||
operator.on('finding:discovered', ({ finding }) => {
|
operator.on('finding:discovered', ({ finding }) => {
|
||||||
this.vault.addFinding(finding);
|
this.vault.addFinding(finding);
|
||||||
this.emit('finding:discovered', { finding, operatorId: operator.id });
|
this.emit('finding:discovered', { finding, operatorId: operator.id });
|
||||||
@@ -628,6 +665,34 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private recordProgress(
|
||||||
|
kind: ScanProgressEvent['kind'],
|
||||||
|
operator: OperatorAgent,
|
||||||
|
task: Task | undefined,
|
||||||
|
detail: string,
|
||||||
|
extra: Partial<Pick<ScanProgressEvent, 'toolName' | 'success' | 'source'>> = {}
|
||||||
|
): void {
|
||||||
|
const compact = String(detail || '').replace(/\s+/g, ' ').trim().slice(0, 2000);
|
||||||
|
const event: ScanProgressEvent = {
|
||||||
|
id: `progress-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
kind,
|
||||||
|
operatorId: operator.id,
|
||||||
|
callsign: operator.callsign,
|
||||||
|
archetype: operator.archetype,
|
||||||
|
taskId: task?.id,
|
||||||
|
taskName: task?.name,
|
||||||
|
detail: compact,
|
||||||
|
...extra,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.progressEvents.push(event);
|
||||||
|
if (this.progressEvents.length > MAX_PROGRESS_EVENTS) {
|
||||||
|
this.progressEvents.splice(0, this.progressEvents.length - MAX_PROGRESS_EVENTS);
|
||||||
|
}
|
||||||
|
this.emit('scan:progress', event);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a finding and update the target's services/vulnerabilities.
|
* Parse a finding and update the target's services/vulnerabilities.
|
||||||
* This is the intelligence pipeline that feeds data between phases.
|
* This is the intelligence pipeline that feeds data between phases.
|
||||||
@@ -826,6 +891,8 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
* CLI work; it only fires on truly-hung dispatches. Override via
|
* CLI work; it only fires on truly-hung dispatches. Override via
|
||||||
* T3MP3ST_TASK_TIMEOUT_MS.
|
* T3MP3ST_TASK_TIMEOUT_MS.
|
||||||
*/
|
*/
|
||||||
|
private progressEvents: ScanProgressEvent[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the dispatch timeout from the environment, falling back to the
|
* Resolve the dispatch timeout from the environment, falling back to the
|
||||||
* provider-specific default. Guards against a non-numeric / non-positive override.
|
* provider-specific default. Guards against a non-numeric / non-positive override.
|
||||||
@@ -951,6 +1018,7 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
|
|
||||||
// Auto-spawn an operator if none exists for this archetype
|
// Auto-spawn an operator if none exists for this archetype
|
||||||
if (!operator) {
|
if (!operator) {
|
||||||
|
if (this.llm.getProvider() === 'local-agent') continue;
|
||||||
const allOps = this.cell.getAllOperators();
|
const allOps = this.cell.getAllOperators();
|
||||||
const archetypeCount = allOps.filter(op => op.archetype === task.operatorType).length;
|
const archetypeCount = allOps.filter(op => op.archetype === task.operatorType).length;
|
||||||
// Spawn up to 3 operators per archetype for parallelism
|
// Spawn up to 3 operators per archetype for parallelism
|
||||||
@@ -1099,6 +1167,9 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
* Auto-spawn operators needed for a given kill chain phase
|
* Auto-spawn operators needed for a given kill chain phase
|
||||||
*/
|
*/
|
||||||
private autoSpawnForPhase(phase: KillChainPhase): void {
|
private autoSpawnForPhase(phase: KillChainPhase): void {
|
||||||
|
if (this.llm.getProvider() === 'local-agent') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const phaseOperators: Record<string, OperatorArchetype[]> = {
|
const phaseOperators: Record<string, OperatorArchetype[]> = {
|
||||||
[KillChainPhase.RECON]: ['recon'],
|
[KillChainPhase.RECON]: ['recon'],
|
||||||
[KillChainPhase.WEAPONIZE]: ['scanner'],
|
[KillChainPhase.WEAPONIZE]: ['scanner'],
|
||||||
@@ -1139,6 +1210,7 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
this.on('detection:triggered', (data) => broadcast('detection', data));
|
this.on('detection:triggered', (data) => broadcast('detection', data));
|
||||||
this.on('mission:phase_changed', (data) => broadcast('phase_changed', data));
|
this.on('mission:phase_changed', (data) => broadcast('phase_changed', data));
|
||||||
this.on('approval:decision', (data) => broadcast('arsenal.approval', data));
|
this.on('approval:decision', (data) => broadcast('arsenal.approval', data));
|
||||||
|
this.on('scan:progress', (data) => broadcast('scan:progress', data));
|
||||||
this.on('tick', (count) => {
|
this.on('tick', (count) => {
|
||||||
// Broadcast status every 5 ticks to avoid flooding
|
// Broadcast status every 5 ticks to avoid flooding
|
||||||
if (typeof count === 'number' && count % 5 === 0) {
|
if (typeof count === 'number' && count % 5 === 0) {
|
||||||
@@ -1232,8 +1304,19 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
opsec: ReturnType<OpsecController['getStats']>;
|
opsec: ReturnType<OpsecController['getStats']>;
|
||||||
activeMission: string | null;
|
activeMission: string | null;
|
||||||
stallReason: string | null;
|
stallReason: string | null;
|
||||||
|
progress: ScanProgressEvent[];
|
||||||
|
tasks: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
phase: string;
|
||||||
|
status: string;
|
||||||
|
operatorType: string;
|
||||||
|
assignedTo?: string;
|
||||||
|
result?: { success: boolean; output?: string; error?: string; findings?: string[] };
|
||||||
|
}>;
|
||||||
} {
|
} {
|
||||||
const activeMission = this.mission.getActiveMission();
|
const activeMission = this.mission.getActiveMission();
|
||||||
|
const taskQueue = this.mission.getTaskQueue();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
@@ -1246,6 +1329,23 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
|
|||||||
opsec: this.opsec.getStats(),
|
opsec: this.opsec.getStats(),
|
||||||
activeMission: activeMission?.id || null,
|
activeMission: activeMission?.id || null,
|
||||||
stallReason: this.stallReason,
|
stallReason: this.stallReason,
|
||||||
|
progress: [...this.progressEvents],
|
||||||
|
tasks: activeMission
|
||||||
|
? taskQueue.getForMission(activeMission.id).map(task => ({
|
||||||
|
id: task.id,
|
||||||
|
name: task.name,
|
||||||
|
phase: task.phase,
|
||||||
|
status: task.status,
|
||||||
|
operatorType: task.operatorType,
|
||||||
|
assignedTo: task.assignedTo,
|
||||||
|
result: task.result ? {
|
||||||
|
success: task.result.success,
|
||||||
|
output: task.result.output,
|
||||||
|
error: task.result.error,
|
||||||
|
findings: task.result.findings,
|
||||||
|
} : undefined,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+36
-5
@@ -22,6 +22,7 @@ import {
|
|||||||
import type { LLMBackbone } from '../llm/index.js';
|
import type { LLMBackbone } from '../llm/index.js';
|
||||||
import type { Arsenal } from '../arsenal/index.js';
|
import type { Arsenal } from '../arsenal/index.js';
|
||||||
import type { AgentLoop } from '../agent/index.js';
|
import type { AgentLoop } from '../agent/index.js';
|
||||||
|
import type { ToolResult } from '../types/index.js';
|
||||||
import type { Target } from '../types/index.js';
|
import type { Target } from '../types/index.js';
|
||||||
import { OPERATOR_SYSTEM_PROMPTS } from '../prompts/index.js';
|
import { OPERATOR_SYSTEM_PROMPTS } from '../prompts/index.js';
|
||||||
import { gateLiveFinding } from '../evidence/gate.js';
|
import { gateLiveFinding } from '../evidence/gate.js';
|
||||||
@@ -36,6 +37,9 @@ export interface OperatorEvents {
|
|||||||
'task:completed': { task: Task; result: TaskResult };
|
'task:completed': { task: Task; result: TaskResult };
|
||||||
'task:failed': { task: Task; error: string };
|
'task:failed': { task: Task; error: string };
|
||||||
'task:decomposed': { parent: Task; subtasks: Task[]; reason: string };
|
'task:decomposed': { parent: Task; subtasks: Task[]; reason: string };
|
||||||
|
'agent:thinking': { task: Task; content: string };
|
||||||
|
'agent:tool_call': { task: Task; name: string; args: Record<string, unknown>; source?: 'agent' | 'backend_seeded' };
|
||||||
|
'agent:tool_result': { task: Task; name: string; result: ToolResult; source?: 'agent' | 'backend_seeded' };
|
||||||
'finding:discovered': { finding: Finding };
|
'finding:discovered': { finding: Finding };
|
||||||
'finding:gate-blocked': { finding: Finding; reasons: string[] };
|
'finding:gate-blocked': { finding: Finding; reasons: string[] };
|
||||||
'credential:harvested': { credential: Credential };
|
'credential:harvested': { credential: Credential };
|
||||||
@@ -422,11 +426,38 @@ export class OperatorAgent extends EventEmitter<OperatorEvents> {
|
|||||||
// Pass the white-box source excerpt (if any) so the model sees the target's
|
// Pass the white-box source excerpt (if any) so the model sees the target's
|
||||||
// real source alongside the task; empty string keeps black-box behavior.
|
// real source alongside the task; empty string keeps black-box behavior.
|
||||||
if (this.agentLoop && this.llm) {
|
if (this.agentLoop && this.llm) {
|
||||||
// [Phase-2] Register as live on the board and pull the shared situation report so this
|
const onThinking = ({ content }: { content: string }): void => {
|
||||||
// operator sees teammates' verified leads/claims — it builds on them instead of running blind.
|
this.emit('agent:thinking', { task, content });
|
||||||
this.board?.heartbeat(this.id, 'hunting', task.name);
|
};
|
||||||
const sharedContext = this.board?.situationReport(this.id);
|
const onToolCall = ({ name, args, source }: { name: string; args: Record<string, unknown>; source?: 'agent' | 'backend_seeded' }): void => {
|
||||||
const result = await this.agentLoop.run(task, this.profile.systemPrompt, target, this.whiteboxSource, sharedContext);
|
this.emit('agent:tool_call', { task, name, args, source });
|
||||||
|
};
|
||||||
|
const onToolResult = ({ name, result, source }: { name: string; result: ToolResult; source?: 'agent' | 'backend_seeded' }): void => {
|
||||||
|
this.emit('agent:tool_result', { task, name, result, source });
|
||||||
|
};
|
||||||
|
const canForwardAgentEvents =
|
||||||
|
typeof this.agentLoop.on === 'function' &&
|
||||||
|
typeof this.agentLoop.off === 'function';
|
||||||
|
if (canForwardAgentEvents) {
|
||||||
|
this.agentLoop.on('agent:thinking', onThinking);
|
||||||
|
this.agentLoop.on('agent:tool_call', onToolCall);
|
||||||
|
this.agentLoop.on('agent:tool_result', onToolResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
// [Phase-2] Register as live on the board and pull the shared situation report so this
|
||||||
|
// operator sees teammates' verified leads/claims — it builds on them instead of running blind.
|
||||||
|
this.board?.heartbeat(this.id, 'hunting', task.name);
|
||||||
|
const sharedContext = this.board?.situationReport(this.id);
|
||||||
|
result = await this.agentLoop.run(task, this.profile.systemPrompt, target, this.whiteboxSource, sharedContext);
|
||||||
|
} finally {
|
||||||
|
if (canForwardAgentEvents) {
|
||||||
|
this.agentLoop.off('agent:thinking', onThinking);
|
||||||
|
this.agentLoop.off('agent:tool_call', onToolCall);
|
||||||
|
this.agentLoop.off('agent:tool_result', onToolResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Convert agent findings to operator findings.
|
// Convert agent findings to operator findings.
|
||||||
// PROVENANCE-HONEST: only a tool-backed finding gets tool-output evidence (the raw
|
// PROVENANCE-HONEST: only a tool-backed finding gets tool-output evidence (the raw
|
||||||
|
|||||||
+3
-1
@@ -6293,7 +6293,7 @@ app.post('/api/mission/resume', (_req: Request, res: Response) => {
|
|||||||
app.get('/api/mission/status', (_req: Request, res: Response) => {
|
app.get('/api/mission/status', (_req: Request, res: Response) => {
|
||||||
const cmd = getTempestCommand();
|
const cmd = getTempestCommand();
|
||||||
if (!cmd) {
|
if (!cmd) {
|
||||||
res.json({ active: false });
|
res.json({ active: false, progress: [], tasks: [] });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6323,6 +6323,8 @@ app.get('/api/mission/status', (_req: Request, res: Response) => {
|
|||||||
targets: status.targets,
|
targets: status.targets,
|
||||||
vault: status.vault,
|
vault: status.vault,
|
||||||
opsec: status.opsec,
|
opsec: status.opsec,
|
||||||
|
tasks: status.tasks,
|
||||||
|
progress: status.progress,
|
||||||
findings: findings.map(f => ({
|
findings: findings.map(f => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
title: f.title,
|
title: f.title,
|
||||||
|
|||||||
@@ -534,6 +534,21 @@ export interface Appendix {
|
|||||||
// EVENT TYPES
|
// EVENT TYPES
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface ScanProgressEvent {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
kind: 'task_started' | 'thinking' | 'tool_call' | 'tool_result' | 'task_completed' | 'task_failed';
|
||||||
|
operatorId: string;
|
||||||
|
callsign: string;
|
||||||
|
archetype: OperatorArchetype;
|
||||||
|
taskId?: string;
|
||||||
|
taskName?: string;
|
||||||
|
toolName?: string;
|
||||||
|
source?: 'agent' | 'backend_seeded';
|
||||||
|
detail: string;
|
||||||
|
success?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommandEvents {
|
export interface CommandEvents {
|
||||||
'command:started': void;
|
'command:started': void;
|
||||||
'command:stopped': void;
|
'command:stopped': void;
|
||||||
@@ -547,6 +562,7 @@ export interface CommandEvents {
|
|||||||
'target:owned': { target: Target; operatorId: string };
|
'target:owned': { target: Target; operatorId: string };
|
||||||
'detection:triggered': DetectionEvent;
|
'detection:triggered': DetectionEvent;
|
||||||
'mission:phase_changed': { missionId: string; phase: KillChainPhase };
|
'mission:phase_changed': { missionId: string; phase: KillChainPhase };
|
||||||
|
'scan:progress': ScanProgressEvent;
|
||||||
'abort:recommended': string;
|
'abort:recommended': string;
|
||||||
/** A capability-approval gate decision (allowed/denied) on an intrusive/dangerous tool — bridged to
|
/** A capability-approval gate decision (allowed/denied) on an intrusive/dangerous tool — bridged to
|
||||||
* the dashboard's live approval/audit feed. Structural match for arsenal/approval.ts ApprovalRecord. */
|
* the dashboard's live approval/audit feed. Structural match for arsenal/approval.ts ApprovalRecord. */
|
||||||
|
|||||||
Reference in new issue
Block a user