mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-10-05 15:56:51 +02:00
feat(status): show Preflight and Cyber access verification rows for gated providers
This commit is contained in:
1 parent
ab092e0731
commit
48097f673b
5 files changed
+59
-13
No files matched your search
@@ -363,6 +363,33 @@ function agenticSastPhase(operations: readonly DerivedAgent[]): DerivedPhase | u
|
||||
};
|
||||
}
|
||||
|
||||
/** Preflight rows shown at the top of the tree, in run order. Each is its own single-line phase. */
|
||||
const PREFLIGHT_ROW_KEYS = ['preflight', 'cyber-access'] as const;
|
||||
|
||||
/**
|
||||
* The two preflight gates the worker persists — the preflight checks and the cyber-access probe —
|
||||
* as top-of-tree rows. Each appears once its stage is recorded (running, then done or failed); a
|
||||
* run that never reaches a gate simply omits its row.
|
||||
*/
|
||||
function preflightPhases(operations: readonly DerivedAgent[]): DerivedPhase[] {
|
||||
const byKey = new Map(operations.map((operation) => [operation.name, operation]));
|
||||
const phases: DerivedPhase[] = [];
|
||||
for (const key of PREFLIGHT_ROW_KEYS) {
|
||||
const operation = byKey.get(key);
|
||||
if (operation === undefined) continue;
|
||||
phases.push({
|
||||
key: operation.name,
|
||||
label: operation.label,
|
||||
children: false,
|
||||
meta: 'duration',
|
||||
state: operation.state,
|
||||
summary: operation,
|
||||
agents: [operation],
|
||||
});
|
||||
}
|
||||
return phases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookkeeping rows worth showing. A deterministic stage that has completed says nothing —
|
||||
* it can only ever read 0s — but one that is still running, or that failed, is exactly what
|
||||
@@ -408,13 +435,14 @@ function assemblePhases(agentPhases: readonly DerivedPhase[], operations: readon
|
||||
return phase;
|
||||
});
|
||||
|
||||
const preflight = preflightPhases(operations);
|
||||
const sast = agenticSastPhase(operations);
|
||||
if (sast === undefined) return phases;
|
||||
if (sast === undefined) return [...preflight, ...phases];
|
||||
|
||||
// Agentic SAST starts with the scan and runs alongside the pentest, so it reads after
|
||||
// the login check rather than appended past Reporting where it never ran.
|
||||
const afterAuth = phases.findIndex((phase) => phase.key === 'auth-validation') + 1;
|
||||
return [...phases.slice(0, afterAuth), sast, ...phases.slice(afterAuth)];
|
||||
return [...preflight, ...phases.slice(0, afterAuth), sast, ...phases.slice(afterAuth)];
|
||||
}
|
||||
|
||||
export { agentError };
|
||||
@@ -138,8 +138,8 @@ const AGENTIC_SAST_PARENT_KEY = 'agentic-sast';
|
||||
// apps/worker/src/temporal/reconcile-activity-types.ts, and
|
||||
// apps/worker/src/ai/sast/capella/temporal/activity-types.ts.
|
||||
const OPERATION_ACTIVITY_PROGRESS: Readonly<Record<string, ActivityProgressSpec>> = {
|
||||
runPreflightValidation: { key: 'preflight', label: 'Preflight validation', kind: 'operation' },
|
||||
runExploitReadinessProbe: { key: 'preflight', label: 'Exploit-workload readiness', kind: 'operation' },
|
||||
runPreflightValidation: { key: 'preflight', label: 'Preflight', kind: 'operation' },
|
||||
runExploitReadinessProbe: { key: 'cyber-access', label: 'Cyber access verification', kind: 'operation' },
|
||||
syncPlaywrightStealthConfig: { key: 'preflight', label: 'Browser setup', kind: 'operation' },
|
||||
initDeliverableGit: { key: 'scan-initialization', label: 'Initialize deliverables', kind: 'operation' },
|
||||
syncCodePathDenyRules: { key: 'scan-initialization', label: 'Apply source rules', kind: 'operation' },
|
||||
|
||||
@@ -75,6 +75,8 @@ function isProviderFailureCategory(value: unknown): value is string {
|
||||
}
|
||||
|
||||
const OPERATION_LABELS = new Set([
|
||||
'Preflight',
|
||||
'Cyber access verification',
|
||||
'Agentic SAST',
|
||||
// Capella stage rows, signalled up from the SAST child workflow. Mirrors
|
||||
// CAPELLA_STAGE_LABELS in apps/worker/src/ai/sast/types.ts, minus the deterministic
|
||||
@@ -228,7 +230,7 @@ export function safeOperationLabel(value: string): string {
|
||||
|
||||
export function safeOperationKey(value: string): string {
|
||||
if (
|
||||
/^(?:agentic-sast|miscellaneous-pipeline|report:(?:initialize|assemble|compact|checkpoint|finalize|finalize-degraded|terminal|surface))$/u.test(
|
||||
/^(?:preflight|cyber-access|agentic-sast|miscellaneous-pipeline|report:(?:initialize|assemble|compact|checkpoint|finalize|finalize-degraded|terminal|surface))$/u.test(
|
||||
value,
|
||||
) ||
|
||||
/^agentic-sast:(?:architecture|threat-model|plan|research|dedupe|review|critic|confirm|calibrate)$/u.test(value) ||
|
||||
|
||||
@@ -878,8 +878,11 @@ function cyberAccessErrorType(providerId: string): string {
|
||||
* Exploit-workload readiness probe activity. For OpenAI/Anthropic, hands the model a slice of the
|
||||
* exploit agent's workload and gates on a decline (`stopReason: error`), failing the scan with the
|
||||
* provider's own message. A setup/transport fault is not a decline and never gates.
|
||||
*
|
||||
* Returns `{ gated }` — true only for a provider that actually gates security workloads, so the
|
||||
* caller records the cyber-access stage for those alone (a non-gated provider ran a no-op probe).
|
||||
*/
|
||||
export async function runExploitReadinessProbe(_input: ActivityInput): Promise<void> {
|
||||
export async function runExploitReadinessProbe(_input: ActivityInput): Promise<{ gated: boolean }> {
|
||||
const startTime = Date.now();
|
||||
const attemptNumber = Context.current().info.attempt;
|
||||
|
||||
@@ -897,7 +900,7 @@ export async function runExploitReadinessProbe(_input: ActivityInput): Promise<v
|
||||
// Only OpenAI and Anthropic gate security workloads — never probe any other provider.
|
||||
if (!isCyberGatedProvider(selection.providerId)) {
|
||||
logger.info(`Exploit-workload readiness: skipped (provider ${selection.providerId})`);
|
||||
return;
|
||||
return { gated: false };
|
||||
}
|
||||
|
||||
logger.info('Checking exploit-workload readiness via pi...');
|
||||
@@ -906,14 +909,14 @@ export async function runExploitReadinessProbe(_input: ActivityInput): Promise<v
|
||||
// Setup/transport fault, not a decline — never gates the scan.
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
logger.info(`Exploit-workload readiness: probe skipped (${message.slice(0, 200)})`);
|
||||
return;
|
||||
return { gated: false };
|
||||
} finally {
|
||||
clearInterval(heartbeatInterval);
|
||||
}
|
||||
|
||||
if (result.error !== undefined) {
|
||||
logger.info(`Exploit-workload readiness: ${result.providerId} inconclusive (${result.error.slice(0, 200)})`);
|
||||
return;
|
||||
return { gated: true };
|
||||
}
|
||||
|
||||
if (result.response?.stopReason === 'error') {
|
||||
@@ -924,7 +927,7 @@ export async function runExploitReadinessProbe(_input: ActivityInput): Promise<v
|
||||
// Gate only on a confirmed cyber decline; any other errored turn is inconclusive.
|
||||
if (!isCyberSafeguardDecline(result.providerId, result.response)) {
|
||||
logger.info(`Exploit-workload readiness: ${result.providerId} inconclusive (errored turn, not a cyber decline)`);
|
||||
return;
|
||||
return { gated: true };
|
||||
}
|
||||
|
||||
// Gate with the provider-specific type (for the CLI guidance), bounded message.
|
||||
@@ -938,6 +941,7 @@ export async function runExploitReadinessProbe(_input: ActivityInput): Promise<v
|
||||
|
||||
const structured = result.structuredOutput !== undefined ? result.structuredValid : 'none';
|
||||
logger.info(`Exploit-workload readiness: ${result.providerId} OK (structured=${structured})`);
|
||||
return { gated: true };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1343,9 +1343,21 @@ export async function pentestPipeline(input: PipelineInput): Promise<PipelineSta
|
||||
|
||||
state.currentPhase = 'preflight';
|
||||
state.currentAgent = null;
|
||||
await preflightActs.runPreflightValidation(activityInput);
|
||||
// The probe gates the exploitation workload, which an auth-only run never reaches.
|
||||
if (!authOnly) await preflightActs.runExploitReadinessProbe(activityInput);
|
||||
await runOperation('preflight', 'Preflight', () => preflightActs.runPreflightValidation(activityInput));
|
||||
if (!authOnly) {
|
||||
const startedAt = startOperation('cyber-access', 'Cyber access verification');
|
||||
try {
|
||||
const probe = await preflightActs.runExploitReadinessProbe(activityInput);
|
||||
if (probe.gated) {
|
||||
completeOperation('cyber-access', 'Cyber access verification', startedAt);
|
||||
} else {
|
||||
delete state.operationalStages['cyber-access'];
|
||||
}
|
||||
} catch (error) {
|
||||
failOperation('cyber-access', 'Cyber access verification', startedAt);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (validateModel) {
|
||||
state.status = 'completed';
|
||||
|
||||
Reference in new issue
Block a user