|
|
|
@@ -326,6 +326,7 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
|
|
|
|
|
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
|
|
|
|
|
const visibilityFindings = quickVerifyAnalysisVisibilityFindings(analysis, artifactSummary);
|
|
|
|
|
const cleanupStep = stopQuickVerifyObserver(state, observerId, "observe-stop-after-terminal");
|
|
|
|
|
const cleanup = classifyQuickVerifyCleanupStep(cleanupStep);
|
|
|
|
|
const cleanupFindings = quickVerifyCleanupFindings(cleanupStep);
|
|
|
|
|
const findings = mergeFindingRecords(
|
|
|
|
|
mergeFindingRecords(
|
|
|
|
@@ -361,17 +362,18 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
|
|
|
|
|
promptSource: prompts.summary,
|
|
|
|
|
accountEnv: accountEnv.summary,
|
|
|
|
|
steps: [...steps, cleanupStep],
|
|
|
|
|
cleanup,
|
|
|
|
|
analysis: artifactSummary,
|
|
|
|
|
views: {
|
|
|
|
|
summary: { renderedText: renderQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, findings, findingCount: findings.length, steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
|
|
|
|
|
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, steps, findings, accountEnv: accountEnv.summary, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
|
|
|
|
|
summary: { renderedText: renderQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, findings, findingCount: findings.length, steps, cleanup, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
|
|
|
|
|
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, steps, cleanup, findings, accountEnv: accountEnv.summary, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) },
|
|
|
|
|
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
|
|
|
|
|
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
|
|
|
|
|
},
|
|
|
|
|
findings,
|
|
|
|
|
screenshot: record(artifactSummary).screenshot,
|
|
|
|
|
publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"),
|
|
|
|
|
warnings: mergeWarnings(analysisWarnings, nonBlockingCanaryWarnings, cleanupFindings.length > 0 ? ["quick verify observer stop failed; runner lifecycle cleanup is a blocking finding."] : [], elapsedWarnings()),
|
|
|
|
|
warnings: mergeWarnings(analysisWarnings, nonBlockingCanaryWarnings, quickVerifyCleanupWarnings(cleanup), elapsedWarnings()),
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@@ -570,9 +572,14 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
|
|
|
|
const artifactSummaryRecord = record(artifactSummary);
|
|
|
|
|
const artifactFindings = Array.isArray(artifactSummaryRecord.findings) ? artifactSummaryRecord.findings.map(record) : [];
|
|
|
|
|
const visibilityFindings = quickVerifyAnalysisVisibilityFindings(analysis, artifactSummary);
|
|
|
|
|
const cleanup = quickVerifyCleanupFromSteps(cleanupSteps);
|
|
|
|
|
const cleanupFindings = cleanupSteps.flatMap((step) => quickVerifyCleanupFindings(step));
|
|
|
|
|
const findings = mergeFindingRecords(
|
|
|
|
|
mergeFindingRecords(artifactFindings, controlFindings),
|
|
|
|
|
visibilityFindings,
|
|
|
|
|
mergeFindingRecords(
|
|
|
|
|
mergeFindingRecords(artifactFindings, controlFindings),
|
|
|
|
|
visibilityFindings,
|
|
|
|
|
),
|
|
|
|
|
cleanupFindings,
|
|
|
|
|
);
|
|
|
|
|
const blockingFindings = findings.filter(isQuickVerifyBlockingFinding);
|
|
|
|
|
const recoveredWaitFailure = durableBusinessTurn
|
|
|
|
@@ -602,10 +609,11 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
|
|
|
|
runnerFailure: recoveredWaitFailure ? null : input.failure,
|
|
|
|
|
promptSource: input.promptSource,
|
|
|
|
|
steps: [...input.steps, ...cleanupSteps],
|
|
|
|
|
cleanup,
|
|
|
|
|
analysis: artifactSummary,
|
|
|
|
|
views: {
|
|
|
|
|
summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, findings, findingCount: findings.length, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) },
|
|
|
|
|
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, findings, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) },
|
|
|
|
|
summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, findings, findingCount: findings.length, steps: input.steps, cleanup, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) },
|
|
|
|
|
"auth-session-switch-summary": { renderedText: renderAuthSessionSwitchQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, cleanup, findings, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), failure: recoveredWaitFailure ? null : input.failure, failureTitleZh: recoveredWaitFailure ? null : failureTitle, timeoutDisplay: recoveredWaitFailure ? null : failureDisplay }) },
|
|
|
|
|
"turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok },
|
|
|
|
|
"trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok },
|
|
|
|
|
},
|
|
|
|
@@ -615,6 +623,7 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
|
|
|
|
|
warnings: mergeWarnings(
|
|
|
|
|
Array.isArray(input.warnings) ? input.warnings : [],
|
|
|
|
|
recoveredWaitFailure ? ["quick verify wait command timed out, but collected turn-summary/trace-frame artifacts show a durable completed business turn; treating the wait timeout as a non-blocking tool finding."] : [],
|
|
|
|
|
quickVerifyCleanupWarnings(cleanup),
|
|
|
|
|
targetValidationElapsedWarnings(input.elapsedMs ?? null, "quick verify confirm-wait", targetValidationSeconds),
|
|
|
|
|
),
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
@@ -627,24 +636,125 @@ function stopQuickVerifyObserver(state: SentinelCicdState, observerId: string, p
|
|
|
|
|
"--node", state.spec.nodeId,
|
|
|
|
|
"--lane", state.spec.lane,
|
|
|
|
|
"--force",
|
|
|
|
|
"--raw",
|
|
|
|
|
"--wait-ms", "55000",
|
|
|
|
|
"--command-timeout-seconds", "55",
|
|
|
|
|
], 60);
|
|
|
|
|
return { phase, ok: stop.ok, result: stop.result };
|
|
|
|
|
const step = { phase, ok: stop.ok, payload: cliDataPayload(stop.parsed), result: stop.result };
|
|
|
|
|
return { ...step, cleanup: classifyQuickVerifyCleanupStep(step) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quickVerifyCleanupFindings(cleanupStep: Record<string, unknown>): Record<string, unknown>[] {
|
|
|
|
|
if (cleanupStep.ok === true) return [];
|
|
|
|
|
export function quickVerifyCleanupFindings(cleanupStep: Record<string, unknown>): Record<string, unknown>[] {
|
|
|
|
|
const cleanup = classifyQuickVerifyCleanupStep(cleanupStep);
|
|
|
|
|
if (stringAtNullable(cleanup, "cleanupClass") !== "observer-stop-failed") return [];
|
|
|
|
|
return [{
|
|
|
|
|
id: "quick-verify-observer-stop-failed",
|
|
|
|
|
severity: "red",
|
|
|
|
|
count: 1,
|
|
|
|
|
summary: "quick verify completed target analysis but failed to stop its observer runner; this can leak Chrome process trees on cadence runs.",
|
|
|
|
|
cleanupPhase: cleanupStep.phase,
|
|
|
|
|
cleanup,
|
|
|
|
|
evidenceSummary: formatQuickVerifyCleanupLine(cleanup),
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function classifyQuickVerifyCleanupStep(cleanupStep: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
const explicit = record(cleanupStep.cleanup);
|
|
|
|
|
if (stringAtNullable(explicit, "cleanupClass") !== null) return explicit;
|
|
|
|
|
const payload = quickVerifyCleanupPayload(cleanupStep);
|
|
|
|
|
const observer = record(payload.observer);
|
|
|
|
|
const forceResult = record(payload.forceResult);
|
|
|
|
|
const forceReason = stringAtNullable(payload, "forceReason");
|
|
|
|
|
const status = stringAtNullable(payload, "status");
|
|
|
|
|
const forceStopAttempted = forceReason !== null
|
|
|
|
|
|| status === "forced-stopped"
|
|
|
|
|
|| stringAtNullable(observer, "command") === "web-probe-observe force-stop"
|
|
|
|
|
|| numberAtNullable(forceResult, "exitCode") !== null;
|
|
|
|
|
const aliveBefore = booleanAtNullable(observer, "aliveBefore");
|
|
|
|
|
const aliveAfter = booleanAtNullable(observer, "aliveAfter");
|
|
|
|
|
const termSent = booleanAtNullable(observer, "termSent");
|
|
|
|
|
const killSent = booleanAtNullable(observer, "killSent");
|
|
|
|
|
const pendingAbandoned = numberAtNullable(observer, "pendingAbandoned");
|
|
|
|
|
const processingAbandoned = numberAtNullable(observer, "processingAbandoned");
|
|
|
|
|
const forceStopOk = forceStopAttempted && aliveAfter === false;
|
|
|
|
|
const gracefulStopOk = !forceStopAttempted && (cleanupStep.ok === true || payload.ok === true);
|
|
|
|
|
const cleanupClass = gracefulStopOk
|
|
|
|
|
? "stopped"
|
|
|
|
|
: forceStopOk
|
|
|
|
|
? "bounded-force-stop"
|
|
|
|
|
: "observer-stop-failed";
|
|
|
|
|
return {
|
|
|
|
|
cleanupClass,
|
|
|
|
|
phase: stringAtNullable(cleanupStep, "phase"),
|
|
|
|
|
commandOk: cleanupStep.ok === true ? true : cleanupStep.ok === false ? false : null,
|
|
|
|
|
status,
|
|
|
|
|
forceReason,
|
|
|
|
|
gracefulStopOk,
|
|
|
|
|
forceStopOk,
|
|
|
|
|
aliveBefore,
|
|
|
|
|
aliveAfter,
|
|
|
|
|
termSent,
|
|
|
|
|
killSent,
|
|
|
|
|
pendingAbandoned,
|
|
|
|
|
processingAbandoned,
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quickVerifyCleanupPayload(cleanupStep: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
const payload = record(cleanupStep.payload);
|
|
|
|
|
if (Object.keys(payload).length > 0) return payload;
|
|
|
|
|
const result = record(cleanupStep.result);
|
|
|
|
|
for (const key of ["stdoutPreview", "stdoutTail"]) {
|
|
|
|
|
const parsed = parseJsonObject(stringAtNullable(result, key) ?? "");
|
|
|
|
|
const data = cliDataPayload(parsed);
|
|
|
|
|
if (Object.keys(data).length > 0) return data;
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quickVerifyCleanupFromSteps(steps: readonly Record<string, unknown>[]): Record<string, unknown> | null {
|
|
|
|
|
for (const step of steps.slice().reverse()) {
|
|
|
|
|
const phase = stringAtNullable(step, "phase");
|
|
|
|
|
if (phase !== null && phase.includes("observe-stop")) return classifyQuickVerifyCleanupStep(step);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quickVerifyCleanupWarnings(cleanup: Record<string, unknown> | null): string[] {
|
|
|
|
|
if (cleanup === null) return [];
|
|
|
|
|
const cleanupClass = stringAtNullable(cleanup, "cleanupClass");
|
|
|
|
|
if (cleanupClass === "bounded-force-stop") {
|
|
|
|
|
return [`quick verify observer graceful stop did not drain, but force stop completed with aliveAfter=false; recorded bounded cleanup evidence (${formatQuickVerifyCleanupLine(cleanup)}).`];
|
|
|
|
|
}
|
|
|
|
|
if (cleanupClass === "observer-stop-failed") {
|
|
|
|
|
return ["quick verify observer stop failed; runner lifecycle cleanup is a blocking finding."];
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatQuickVerifyCleanupLine(cleanup: Record<string, unknown> | null): string {
|
|
|
|
|
if (cleanup === null) return "cleanup=-";
|
|
|
|
|
const fields = [
|
|
|
|
|
["cleanup", stringAtNullable(cleanup, "cleanupClass")],
|
|
|
|
|
["aliveBefore", cleanup.aliveBefore],
|
|
|
|
|
["aliveAfter", cleanup.aliveAfter],
|
|
|
|
|
["termSent", cleanup.termSent],
|
|
|
|
|
["killSent", cleanup.killSent],
|
|
|
|
|
["pendingAbandoned", cleanup.pendingAbandoned],
|
|
|
|
|
["processingAbandoned", cleanup.processingAbandoned],
|
|
|
|
|
["gracefulStopOk", cleanup.gracefulStopOk],
|
|
|
|
|
["forceStopOk", cleanup.forceStopOk],
|
|
|
|
|
];
|
|
|
|
|
return fields.map(([key, value]) => `${key}=${value ?? "-"}`).join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function booleanAtNullable(value: Record<string, unknown>, key: string): boolean | null {
|
|
|
|
|
const raw = value[key];
|
|
|
|
|
return raw === true ? true : raw === false ? false : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function quickVerifyAnalysisVisibilityFindings(analysis: ChildCliResult, artifactSummary: Record<string, unknown>): Record<string, unknown>[] {
|
|
|
|
|
if (analysis.ok !== true) return [];
|
|
|
|
|
if (stringAtNullable(artifactSummary, "reportJsonSha256") !== null) return [];
|
|
|
|
@@ -773,6 +883,7 @@ function recordQuickVerify(state: SentinelCicdState, payload: Record<string, unk
|
|
|
|
|
timeoutDisplay: payload.timeoutDisplay ?? null,
|
|
|
|
|
warnings: Array.isArray(payload.warnings) ? payload.warnings : [],
|
|
|
|
|
analysis: compactQuickVerifyRecordAnalysis(payload.analysis),
|
|
|
|
|
cleanup: compactQuickVerifyCleanupEvidence(payload.cleanup),
|
|
|
|
|
promptSource: payload.promptSource,
|
|
|
|
|
steps: Array.isArray(payload.steps) ? payload.steps.map(compactQuickVerifyRecordStep) : [],
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
@@ -1119,11 +1230,33 @@ function compactQuickVerifyRecordStep(value: unknown): Record<string, unknown> {
|
|
|
|
|
promptIndex: numberAtNullable(item, "promptIndex"),
|
|
|
|
|
checkId: stringAtNullable(item, "checkId"),
|
|
|
|
|
failure: stringAtNullable(item, "failure"),
|
|
|
|
|
cleanup: compactQuickVerifyCleanupEvidence(item.cleanup),
|
|
|
|
|
result: compactQuickVerifyRecordStepResult(record(item.result)),
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactQuickVerifyCleanupEvidence(value: unknown): Record<string, unknown> | null {
|
|
|
|
|
const item = record(value);
|
|
|
|
|
if (stringAtNullable(item, "cleanupClass") === null) return null;
|
|
|
|
|
return {
|
|
|
|
|
cleanupClass: stringAtNullable(item, "cleanupClass"),
|
|
|
|
|
phase: stringAtNullable(item, "phase"),
|
|
|
|
|
commandOk: booleanAtNullable(item, "commandOk"),
|
|
|
|
|
status: stringAtNullable(item, "status"),
|
|
|
|
|
forceReason: stringAtNullable(item, "forceReason"),
|
|
|
|
|
gracefulStopOk: booleanAtNullable(item, "gracefulStopOk"),
|
|
|
|
|
forceStopOk: booleanAtNullable(item, "forceStopOk"),
|
|
|
|
|
aliveBefore: booleanAtNullable(item, "aliveBefore"),
|
|
|
|
|
aliveAfter: booleanAtNullable(item, "aliveAfter"),
|
|
|
|
|
termSent: booleanAtNullable(item, "termSent"),
|
|
|
|
|
killSent: booleanAtNullable(item, "killSent"),
|
|
|
|
|
pendingAbandoned: numberAtNullable(item, "pendingAbandoned"),
|
|
|
|
|
processingAbandoned: numberAtNullable(item, "processingAbandoned"),
|
|
|
|
|
valuesRedacted: true,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function compactQuickVerifyRecordStepResult(value: Record<string, unknown>): Record<string, unknown> {
|
|
|
|
|
return {
|
|
|
|
|
ok: value.ok === true ? true : value.ok === false ? false : null,
|
|
|
|
@@ -1862,6 +1995,7 @@ function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
|
|
|
|
|
"quick-verify-analysis-summary-unreadable",
|
|
|
|
|
"quick-verify-command-sequence-failed",
|
|
|
|
|
"quick-verify-observer-start-failed",
|
|
|
|
|
"quick-verify-observer-stop-failed",
|
|
|
|
|
"quick-verify-account-secret-missing",
|
|
|
|
|
"prompt-chat-submit-failed",
|
|
|
|
|
"workbench-turn-state-triad-inconsistent",
|
|
|
|
@@ -2369,12 +2503,14 @@ function renderQuickVerifySummary(input: Record<string, unknown>): string {
|
|
|
|
|
: [];
|
|
|
|
|
const findingCount = numberAtNullable(input, "findingCount") ?? numberAtNullable(artifact, "findingCount") ?? findings.length;
|
|
|
|
|
const failureTitle = stringAtNullable(input, "failureTitleZh") ?? stringAtNullable(input, "errorTitleZh") ?? stringAtNullable(record(input.timeoutDisplay), "titleZh");
|
|
|
|
|
const cleanup = compactQuickVerifyCleanupEvidence(input.cleanup);
|
|
|
|
|
return [
|
|
|
|
|
"Web Probe Sentinel Quick Verify",
|
|
|
|
|
"=======================================================",
|
|
|
|
|
`run=${input.runId ?? "-"} scenario=${input.scenarioId ?? "-"} observer=${input.observerId ?? "-"}`,
|
|
|
|
|
failureTitle === null ? "" : `errorTitle=${failureTitle} failure=${input.failure ?? "-"}`,
|
|
|
|
|
`report=${artifact.reportJsonSha256 ?? "-"} artifacts=${artifact.artifactCount ?? "-"} findings=${findingCount}`,
|
|
|
|
|
cleanup === null ? "" : formatQuickVerifyCleanupLine(cleanup),
|
|
|
|
|
`publicOrigin=${input.publicOrigin ?? "-"}`,
|
|
|
|
|
"",
|
|
|
|
|
"Findings",
|
|
|
|
@@ -2385,6 +2521,7 @@ function renderQuickVerifySummary(input: Record<string, unknown>): string {
|
|
|
|
|
function renderAuthSessionSwitchQuickVerifySummary(input: Record<string, unknown>): string {
|
|
|
|
|
const artifact = record(input.artifactSummary);
|
|
|
|
|
const accountEnv = record(input.accountEnv);
|
|
|
|
|
const cleanup = compactQuickVerifyCleanupEvidence(input.cleanup);
|
|
|
|
|
const findingRows = Array.isArray(input.findings)
|
|
|
|
|
? input.findings.map(record).slice(0, 8)
|
|
|
|
|
: Array.isArray(artifact.findings)
|
|
|
|
@@ -2409,6 +2546,7 @@ function renderAuthSessionSwitchQuickVerifySummary(input: Record<string, unknown
|
|
|
|
|
`run=${input.runId ?? "-"} scenario=${input.scenarioId ?? "-"} observer=${input.observerId ?? "-"}`,
|
|
|
|
|
stringAtNullable(input, "failureTitleZh") === null && stringAtNullable(input, "errorTitleZh") === null ? "" : `errorTitle=${stringAtNullable(input, "failureTitleZh") ?? stringAtNullable(input, "errorTitleZh") ?? "-"}`,
|
|
|
|
|
`status=${artifact.ok === true ? "ok" : "blocked"} report=${artifact.reportJsonSha256 ?? "-"} publicOrigin=${input.publicOrigin ?? "-"}`,
|
|
|
|
|
cleanup === null ? "" : formatQuickVerifyCleanupLine(cleanup),
|
|
|
|
|
`accountEnv=${accountEnv.envCount ?? "-"} valuesRedacted=true`,
|
|
|
|
|
"",
|
|
|
|
|
"Command Sequence",
|
|
|
|
|