From 48097f673b68145a7614bd44daa49072a72c004f Mon Sep 17 00:00:00 2001 From: ezl-keygraph Date: Mon, 5 Oct 2026 03:25:27 +0530 Subject: [PATCH] feat(status): show Preflight and Cyber access verification rows for gated providers --- apps/cli/src/scan/derive.ts | 32 ++++++++++++++++++++++++-- apps/cli/src/scan/pipeline.ts | 4 ++-- apps/cli/src/scan/safe-fields.ts | 4 +++- apps/worker/src/temporal/activities.ts | 14 +++++++---- apps/worker/src/temporal/workflows.ts | 18 ++++++++++++--- 5 files changed, 59 insertions(+), 13 deletions(-) diff --git a/apps/cli/src/scan/derive.ts b/apps/cli/src/scan/derive.ts index eccb2968..dd9fb81f 100644 --- a/apps/cli/src/scan/derive.ts +++ b/apps/cli/src/scan/derive.ts @@ -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 }; diff --git a/apps/cli/src/scan/pipeline.ts b/apps/cli/src/scan/pipeline.ts index ef7b656a..e2562f01 100644 --- a/apps/cli/src/scan/pipeline.ts +++ b/apps/cli/src/scan/pipeline.ts @@ -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> = { - 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' }, diff --git a/apps/cli/src/scan/safe-fields.ts b/apps/cli/src/scan/safe-fields.ts index 85081804..f625aea2 100644 --- a/apps/cli/src/scan/safe-fields.ts +++ b/apps/cli/src/scan/safe-fields.ts @@ -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) || diff --git a/apps/worker/src/temporal/activities.ts b/apps/worker/src/temporal/activities.ts index 08df1bec..a22d3f50 100644 --- a/apps/worker/src/temporal/activities.ts +++ b/apps/worker/src/temporal/activities.ts @@ -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 { +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 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';