Expose stalled local agent failures
This commit is contained in:
1 parent
49d0827650
commit
73d888f4a1
6 files changed
+47
-8
No files matched your search
@@ -6223,6 +6223,11 @@ tempest.operators.<span class="function">spawn</span>(<span class="string">'scan
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status.paused && status.stallReason) {
|
||||||
|
resolve(status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update kill chain from mission phase
|
// Update kill chain from mission phase
|
||||||
if (status.mission?.currentPhase) {
|
if (status.mission?.currentPhase) {
|
||||||
const phaseMap = {
|
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)
|
// Poll for mission completion (SSE provides real-time updates, poll for completion detection)
|
||||||
const finalStatus = await BackendDispatch.pollUntilComplete((status) => {
|
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
|
// Periodic status update
|
||||||
if (status.operators?.summary) {
|
if (status.operators?.summary) {
|
||||||
const ops = 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);
|
}, 3000);
|
||||||
|
|
||||||
// Mission complete
|
// Mission complete
|
||||||
|
if (finalStatus.paused && finalStatus.stallReason) {
|
||||||
|
throw new Error(finalStatus.stallReason);
|
||||||
|
}
|
||||||
const findingCount = finalStatus.findings?.length || 0;
|
const findingCount = finalStatus.findings?.length || 0;
|
||||||
completeMission(null);
|
completeMission(null);
|
||||||
addIntel('COMMAND', `Backend mission complete. ${findingCount} findings.`, 'success');
|
addIntel('COMMAND', `Backend mission complete. ${findingCount} findings.`, 'success');
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ describe('Wedged-dispatch timeout backstop', () => {
|
|||||||
expect(stopped).toBe(true);
|
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.
|
// Tiny backstop so the test is fast and deterministic.
|
||||||
const prev = process.env.T3MP3ST_TASK_TIMEOUT_MS;
|
const prev = process.env.T3MP3ST_TASK_TIMEOUT_MS;
|
||||||
process.env.T3MP3ST_TASK_TIMEOUT_MS = '30';
|
process.env.T3MP3ST_TASK_TIMEOUT_MS = '30';
|
||||||
@@ -442,14 +442,18 @@ describe('Wedged-dispatch timeout backstop', () => {
|
|||||||
command.start();
|
command.start();
|
||||||
|
|
||||||
// Poll (real timers): the 1s tick loop must dispatch, wedge, then reap the
|
// 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;
|
const deadline = Date.now() + 8000;
|
||||||
while (Date.now() < deadline && !phaseAdvanced) {
|
while (Date.now() < deadline && !command.getStatus().paused) {
|
||||||
await new Promise(r => setTimeout(r, 100));
|
await new Promise(r => setTimeout(r, 100));
|
||||||
}
|
}
|
||||||
|
const status = command.getStatus();
|
||||||
command.stop();
|
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),
|
// The wedged operator was reset back to idle (or re-tasked in a later phase),
|
||||||
// never left stuck in 'executing' with no current task.
|
// never left stuck in 'executing' with no current task.
|
||||||
const stuck = command.cell.getAllOperators().some(
|
const stuck = command.cell.getAllOperators().some(
|
||||||
|
|||||||
+6
-2
@@ -65,6 +65,8 @@ export interface AgentResult {
|
|||||||
durationMs: number;
|
durationMs: number;
|
||||||
/** Whether the agent hit the iteration limit */
|
/** Whether the agent hit the iteration limit */
|
||||||
hitLimit: boolean;
|
hitLimit: boolean;
|
||||||
|
/** Error from the forced final-summary call after hitting limits, if any */
|
||||||
|
finalSummaryError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentEvents {
|
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 summary = 'Agent reached iteration limit without producing a final summary.';
|
||||||
|
let finalSummaryError: string | undefined;
|
||||||
try {
|
try {
|
||||||
const finalResponse = await this.llm.chat(messages, { maxTokens: 2048 });
|
const finalResponse = await this.llm.chat(messages, { maxTokens: 2048 });
|
||||||
summary = finalResponse.content;
|
summary = finalResponse.content;
|
||||||
@@ -275,8 +278,8 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
for (const f of this.parseFinalFindings(summary)) {
|
for (const f of this.parseFinalFindings(summary)) {
|
||||||
if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
|
if (!allFindings.some((x) => x.title === f.title)) allFindings.push(f);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
// Use what we have
|
finalSummaryError = error instanceof Error ? error.message : String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: AgentResult = {
|
const result: AgentResult = {
|
||||||
@@ -288,6 +291,7 @@ export class AgentLoop extends EventEmitter<AgentEvents> {
|
|||||||
tokensUsed,
|
tokensUsed,
|
||||||
durationMs: Date.now() - startTime,
|
durationMs: Date.now() - startTime,
|
||||||
hitLimit,
|
hitLimit,
|
||||||
|
finalSummaryError,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.emit('agent:complete', result);
|
this.emit('agent:complete', result);
|
||||||
|
|||||||
+7
-1
@@ -268,6 +268,9 @@ import {
|
|||||||
// TEMPEST COMMAND
|
// 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
|
* 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 =
|
// 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.
|
// the curated per-operator tool allowlist). toolCategories stays as a coarse fallback.
|
||||||
const profile = ARCHETYPE_PROFILES[archetype];
|
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, {
|
const agentLoop = new AgentLoop(this.llm, this.arsenal, {
|
||||||
maxIterations: 15,
|
maxIterations,
|
||||||
maxTokens: 50000,
|
maxTokens: 50000,
|
||||||
toolCategories: profile.toolCategories,
|
toolCategories: profile.toolCategories,
|
||||||
tools: profile.defaultTools,
|
tools: profile.defaultTools,
|
||||||
|
|||||||
@@ -461,6 +461,9 @@ export class OperatorAgent extends EventEmitter<OperatorEvents> {
|
|||||||
return {
|
return {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
output: result.summary,
|
output: result.summary,
|
||||||
|
error: result.finalSummaryError
|
||||||
|
? `${result.summary} Final summary failed: ${result.finalSummaryError}`
|
||||||
|
: undefined,
|
||||||
findings: result.findings.map(f => f.title),
|
findings: result.findings.map(f => f.title),
|
||||||
nextTasks: undefined,
|
nextTasks: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-1
@@ -271,6 +271,7 @@ export class TargetEnvironment extends EventEmitter<TargetEvents> {
|
|||||||
|
|
||||||
export function createTargetFromUrl(url: string, zone: TargetZone = 'external'): Target {
|
export function createTargetFromUrl(url: string, zone: TargetZone = 'external'): Target {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
|
const normalizedUrl = parsed.toString();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
@@ -278,12 +279,17 @@ export function createTargetFromUrl(url: string, zone: TargetZone = 'external'):
|
|||||||
type: 'web_application',
|
type: 'web_application',
|
||||||
zone,
|
zone,
|
||||||
status: 'discovered',
|
status: 'discovered',
|
||||||
address: url,
|
address: parsed.hostname,
|
||||||
port: parsed.port ? parseInt(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
|
port: parsed.port ? parseInt(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80),
|
||||||
protocol: parsed.protocol.replace(':', ''),
|
protocol: parsed.protocol.replace(':', ''),
|
||||||
services: [],
|
services: [],
|
||||||
vulnerabilities: [],
|
vulnerabilities: [],
|
||||||
credentials: [],
|
credentials: [],
|
||||||
|
metadata: {
|
||||||
|
url: normalizedUrl,
|
||||||
|
origin: parsed.origin,
|
||||||
|
path: `${parsed.pathname}${parsed.search}${parsed.hash}`,
|
||||||
|
},
|
||||||
discoveredAt: Date.now(),
|
discoveredAt: Date.now(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user