feat(worker): disclose scan coverage and make reporting auditable

Build on the retry-safe finalization foundation to preserve correct identities, source locations, scan dates,
partial-coverage limitations, and consistent report JSON, Markdown, SARIF, and PDF output.

Report Agentic SAST, reconciliation wall-clock time, stage usage, retry spend, and background work without duplicate
or hardcoded totals. Keep report findings canonical, drop cross-class restatements, name enrichment losses, and render
the executive-summary narrative in the PDF.
This commit is contained in:
ajmallesh
2026-08-26 20:18:44 -07:00
parent c3864c9785
commit 85d5cbd657
40 changed files with 1336 additions and 185 deletions
+24 -8
View File
@@ -248,19 +248,35 @@ export function derivePipeline(input: RenderInput, now: number): DerivedPhase[]
};
});
// The synthetic phase appears only when there is operational work to show, so a scan
// The synthetic phase(s) appear only when there is operational work to show, so a scan
// with no recorded operational stages keeps the plain agent tree.
if (operationalAgents.length === 0) return agentPhases;
return [
...agentPhases,
{
// Agentic SAST is a pluggable analysis engine — a peer to the pentest, not background plumbing —
// so it stands in its own phase; reconciliation and report steps remain under "Background work".
const engineAgents = operationalAgents.filter((agent) => operationFamilyKey(agent.name) === 'agentic-sast');
const backgroundAgents = operationalAgents.filter((agent) => operationFamilyKey(agent.name) !== 'agentic-sast');
const syntheticPhases: DerivedPhase[] = [];
if (engineAgents.length > 0) {
syntheticPhases.push({
key: 'analysis-engines',
label: 'Analysis Engines',
parallel: true,
state: phaseGlyphState(engineAgents.map((operation) => operation.state)),
agents: engineAgents,
});
}
if (backgroundAgents.length > 0) {
syntheticPhases.push({
key: 'operational-work',
label: 'Background work',
parallel: true,
state: phaseGlyphState(operationalAgents.map((operation) => operation.state)),
agents: operationalAgents,
},
];
state: phaseGlyphState(backgroundAgents.map((operation) => operation.state)),
agents: backgroundAgents,
});
}
return [...agentPhases, ...syntheticPhases];
}
export { agentError };
+2
View File
@@ -344,6 +344,8 @@ export interface PipelineState {
readonly failedStageLabel?: string;
readonly error?: string;
readonly errorCode?: string;
/** Usage-accounting warnings projected by the worker; empty when the ledger reconciled. */
readonly warnings?: readonly string[];
};
readonly nonFatalFailures?: { readonly phase: string; readonly error: string }[];
/** Ordered durable degradation reasons with safe messages; empty or absent for full success. */
+52 -2
View File
@@ -1,4 +1,12 @@
/** Closed-field projection for Temporal values displayed by the CLI. */
/**
* Closed-field projection for Temporal values displayed by the CLI.
*
* PipelineState travels through Temporal from a worker container this process does not
* control, so free-text fields are treated as unvetted: this module either matches a
* value against a known closed set (safe to print as-is) or collapses it to a fixed,
* bounded message. A value with no case here should fail closed to something generic,
* never pass through untouched.
*/
import type { PartialReasonView, PipelineState } from './pipeline.js';
@@ -49,6 +57,23 @@ const CAPELLA_FAILURE_MESSAGES = new Set([
'Agentic SAST had not finished when the scan stopped.',
]);
// Mirrors apps/worker/src/types/errors.ts. The CLI cannot import from the worker package,
// so keep this exact closed set in sync with ProviderFailureCategory.
const PROVIDER_FAILURE_CATEGORIES = new Set([
'rate_limit',
'overloaded',
'transport',
'context_limit',
'quota',
'authentication',
'configuration',
'unknown',
]);
function isProviderFailureCategory(value: unknown): value is string {
return typeof value === 'string' && PROVIDER_FAILURE_CATEGORIES.has(value);
}
const OPERATION_LABELS = new Set([
'Agentic SAST',
'Miscellaneous findings',
@@ -141,12 +166,27 @@ export function safePartialReasons(reasons: readonly PartialReasonView[]): reado
});
}
/** Upper bounds on the warning array crossing into cli.status.json, so a malformed state cannot bloat it. */
const MAX_AGENTIC_SAST_WARNINGS = 20;
const MAX_AGENTIC_SAST_WARNING_LENGTH = 2_000;
/** Sanitize the worker's usage-accounting warnings: strings only, bounded count and length. */
function safeAgenticSastWarnings(value: PipelineState['agenticSast']): readonly string[] {
const warnings = value?.warnings;
if (!Array.isArray(warnings)) return [];
return warnings
.filter((warning): warning is string => typeof warning === 'string')
.slice(0, MAX_AGENTIC_SAST_WARNINGS)
.map((warning) => warning.slice(0, MAX_AGENTIC_SAST_WARNING_LENGTH));
}
export function safeAgenticSast(value: PipelineState['agenticSast']):
| {
readonly status: string;
readonly failedStageLabel?: string;
readonly error?: string;
readonly errorCode?: string;
readonly warnings: readonly string[];
}
| undefined {
if (value === undefined || !['disabled', 'running', 'succeeded', 'failed'].includes(value.status)) return undefined;
@@ -158,12 +198,16 @@ export function safeAgenticSast(value: PipelineState['agenticSast']):
error = 'An agentic SAST step failed.';
}
const errorCode =
value.errorCode !== undefined && /^[A-Z][A-Z0-9_]{0,63}$/u.test(value.errorCode) ? value.errorCode : undefined;
value.errorCode !== undefined &&
(/^[A-Z][A-Z0-9_]{0,63}$/u.test(value.errorCode) || isProviderFailureCategory(value.errorCode))
? value.errorCode
: undefined;
return {
status: value.status,
...(failedStageLabel !== undefined && { failedStageLabel }),
...(error !== undefined && { error }),
...(errorCode !== undefined && { errorCode }),
warnings: safeAgenticSastWarnings(value),
};
}
@@ -183,6 +227,11 @@ export function safeOperationKey(value: string): string {
return 'background-task';
}
/**
* A workspace or workflow id is printed straight into the progress display, so this
* confines it to a plain identifier charset before that happens: no control or escape
* characters survive to reach the terminal.
*/
export function safeCliIdentifier(value: string): string {
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value) ? value : 'unknown';
}
@@ -210,6 +259,7 @@ export function safeFailureDetail(hasFailure: boolean): string | undefined {
return hasFailure ? 'This scan step could not be completed.' : undefined;
}
/** Same closed-set trade-off as safeFailureDetail, for the scan-level (not per-agent) failure. */
export function safeTerminalFailure(hasFailure: boolean): string | undefined {
return hasFailure ? 'The scan could not be completed.' : undefined;
}
+8 -1
View File
@@ -38,7 +38,13 @@ export interface StatusJson {
/** Ordered durable degradation reasons with safe messages; present only when non-empty. */
readonly partialReasons?: readonly PartialReasonView[];
/** Agentic SAST outcome, with the worker's sanitized failure sentence and bounded code. */
readonly agenticSast?: { readonly status: string; readonly error?: string; readonly errorCode?: string };
readonly agenticSast?: {
readonly status: string;
readonly error?: string;
readonly errorCode?: string;
/** Usage-accounting warnings; always present (empty when the ledger reconciled) so it is never null. */
readonly warnings: readonly string[];
};
/** False when operational (Capella/reconciliation) spend is known to be incomplete. */
readonly usageAccountingComplete?: boolean;
readonly phases: readonly DerivedPhase[];
@@ -90,6 +96,7 @@ export function toStatusJson(input: RenderInput, now: number): StatusJson {
status: agenticSast.status,
...(agenticSast.error !== undefined && { error: agenticSast.error }),
...(agenticSast.errorCode !== undefined && { errorCode: agenticSast.errorCode }),
warnings: [...agenticSast.warnings],
},
}),
...(usageAccountingComplete !== undefined && { usageAccountingComplete }),