Expose stalled local agent failures

This commit is contained in:
Codex committed 2026-07-07 16:04:40 -04:00
1 parent 49d0827650
commit 73d888f4a1
6 files changed
+47 -8

No files matched your search

+16
View File
@@ -6223,6 +6223,11 @@ tempest.operators.<span class="function">spawn</span>(<span class="string">'scan
return;
}
if (status.paused && status.stallReason) {
resolve(status);
return;
}
// Update kill chain from mission phase
if (status.mission?.currentPhase) {
const phaseMap = {
@@ -18252,6 +18257,14 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
// Poll for mission completion (SSE provides real-time updates, poll for completion detection)
const finalStatus = await BackendDispatch.pollUntilComplete((status) => {
if (status.paused && status.stallReason) {
const reason = status.stallReason;
document.getElementById('cmdMissionStatus').textContent = `Stalled — ${reason}`;
document.getElementById('cmdMissionStatus').style.color = '#ff6b6b';
addMissionLog('error', reason);
addIntel('ERROR', reason, 'danger');
return;
}
// Periodic status update
if (status.operators?.summary) {
const ops = status.operators.summary;
@@ -18269,6 +18282,9 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
}, 3000);
// Mission complete
if (finalStatus.paused && finalStatus.stallReason) {
throw new Error(finalStatus.stallReason);
}
const findingCount = finalStatus.findings?.length || 0;
completeMission(null);
addIntel('COMMAND', `Backend mission complete. ${findingCount} findings.`, 'success');
+8 -4
View File
@@ -419,7 +419,7 @@ describe('Wedged-dispatch timeout backstop', () => {
expect(stopped).toBe(true);
});
it('a wedged mission reaches phase advancement once the per-dispatch backstop fires', async () => {
it('a wedged mission stalls instead of advancing after required recon dispatches time out', async () => {
// Tiny backstop so the test is fast and deterministic.
const prev = process.env.T3MP3ST_TASK_TIMEOUT_MS;
process.env.T3MP3ST_TASK_TIMEOUT_MS = '30';
@@ -442,14 +442,18 @@ describe('Wedged-dispatch timeout backstop', () => {
command.start();
// Poll (real timers): the 1s tick loop must dispatch, wedge, then reap the
// dispatch via the backstop and advance past RECON.
// dispatch via the backstop. Failed required recon work should stall instead
// of advancing the mission with no successful backend/model work.
const deadline = Date.now() + 8000;
while (Date.now() < deadline && !phaseAdvanced) {
while (Date.now() < deadline && !command.getStatus().paused) {
await new Promise(r => setTimeout(r, 100));
}
const status = command.getStatus();
command.stop();
expect(phaseAdvanced).toBe(true);
expect(phaseAdvanced).toBe(false);
expect(status.paused).toBe(true);
expect(status.stallReason).toContain('stalled in reconnaissance');
// The wedged operator was reset back to idle (or re-tasked in a later phase),
// never left stuck in 'executing' with no current task.
const stuck = command.cell.getAllOperators().some(
+6 -2
View File
@@ -65,6 +65,8 @@ export interface AgentResult {
durationMs: number;
/** Whether the agent hit the iteration limit */
hitLimit: boolean;
/** Error from the forced final-summary call after hitting limits, if any */
finalSummaryError?: string;
}
export interface AgentEvents {
@@ -267,6 +269,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
});
let summary = 'Agent reached iteration limit without producing a final summary.';
let finalSummaryError: string | undefined;
try {
const finalResponse = await this.llm.chat(messages, { maxTokens: 2048 });
summary = finalResponse.content;
@@ -275,8 +278,8 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
for (const f of this.parseFinalFindings(summary)) {
if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
}
} catch {
// Use what we have
} catch (error) {
finalSummaryError = error instanceof Error ? error.message : String(error);
}
const result: AgentResult = {
@@ -288,6 +291,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
tokensUsed,
durationMs: Date.now() - startTime,
hitLimit,
finalSummaryError,
};
this.emit('agent:complete', result);
+7 -1
View File
@@ -268,6 +268,9 @@ import {
// TEMPEST COMMAND
// =============================================================================
const DEFAULT_AGENT_MAX_ITERATIONS = 15;
const LOCAL_AGENT_MAX_ITERATIONS = Number(process.env.T3MP3ST_LOCAL_AGENT_MAX_ITERATIONS || 30);
/**
* TEMPEST Command - Main orchestration controller
*/
@@ -1158,8 +1161,11 @@ export class TempestCommand extends EventEmitter<CommandEvents> {
// Attach the agent loop scoped to this archetype's SPECIALIZED role toolkit (defaultTools =
// the curated per-operator tool allowlist). toolCategories stays as a coarse fallback.
const profile = ARCHETYPE_PROFILES[archetype];
const maxIterations = this.llm.getProvider() === 'local-agent'
? LOCAL_AGENT_MAX_ITERATIONS
: DEFAULT_AGENT_MAX_ITERATIONS;
const agentLoop = new AgentLoop(this.llm, this.arsenal, {
maxIterations: 15,
maxIterations,
maxTokens: 50000,
toolCategories: profile.toolCategories,
tools: profile.defaultTools,
+3
View File
@@ -461,6 +461,9 @@ export class OperatorAgent extends EventEmitter<OperatorEvents> {
return {
success: result.success,
output: result.summary,
error: result.finalSummaryError
? `${result.summary} Final summary failed: ${result.finalSummaryError}`
: undefined,
findings: result.findings.map(f => f.title),
nextTasks: undefined,
};
+7 -1
View File
@@ -271,6 +271,7 @@ export class TargetEnvironment extends EventEmitter<TargetEvents> {
export function createTargetFromUrl(url: string, zone: TargetZone = 'external'): Target {
const parsed = new URL(url);
const normalizedUrl = parsed.toString();
return {
id: randomUUID(),
@@ -278,12 +279,17 @@ export function createTargetFromUrl(url: string, zone: TargetZone = 'external'):
type: 'web_application',
zone,
status: 'discovered',
address: url,
address: parsed.hostname,
port: parsed.port ? parseInt(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
protocol: parsed.protocol.replace(':', ''),
services: [],
vulnerabilities: [],
credentials: [],
metadata: {
url: normalizedUrl,
origin: parsed.origin,
path: `${parsed.pathname}${parsed.search}${parsed.hash}`,
},
discoveredAt: Date.now(),
};
}