feat: add web sentinel session invariance checks

This commit is contained in:
Codex
2026-06-26 12:00:13 +00:00
parent ad7043f3f4
commit dee36326b9
9 changed files with 622 additions and 25 deletions
+77 -1
View File
@@ -1654,6 +1654,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
});
}
let promptIndex = 0;
const sessionInvarianceChecks = sessionInvarianceChecksByRound(scenario);
for (const item of arrayAt(scenario, "commandSequence").map(record)) {
const type = stringAt(item, "type");
const repeat = Math.max(1, typeof item.repeat === "number" && Number.isFinite(item.repeat) ? Math.trunc(item.repeat) : 1);
@@ -1711,6 +1712,21 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
warnings: mergeWarnings(Array.isArray(waitResult.warnings) ? waitResult.warnings : [], elapsedWarnings()),
}));
}
const invariantResult = runQuickVerifySessionInvarianceChecks(state, observerId, sessionInvarianceChecks.get(promptIndex) ?? [], deadline, promptIndex, steps);
if (invariantResult.ok !== true) {
return recordQuickVerify(state, finalizeQuickVerifyFailure(state, {
runId,
scenarioId,
reason,
observerId,
promptIndex,
steps,
failure: text(invariantResult.failure ?? "observe-session-invariance-check-failed"),
promptSource: prompts.summary,
elapsedMs: elapsedMs(),
warnings: elapsedWarnings(),
}));
}
}
}
}
@@ -1756,6 +1772,65 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou
});
}
function sessionInvarianceChecksByRound(scenario: Record<string, unknown>): Map<number, Record<string, unknown>[]> {
const checks = new Map<number, Record<string, unknown>[]>();
const items = Array.isArray(scenario.sessionInvarianceChecks) ? scenario.sessionInvarianceChecks.map(record) : [];
for (const item of items) {
const afterRound = typeof item.afterRound === "number" && Number.isInteger(item.afterRound) ? item.afterRound : null;
if (afterRound === null || afterRound < 0) continue;
const list = checks.get(afterRound) ?? [];
list.push(item);
checks.set(afterRound, list);
}
return checks;
}
function runQuickVerifySessionInvarianceChecks(
state: SentinelCicdState,
observerId: string,
checks: readonly Record<string, unknown>[],
deadline: number,
promptIndex: number,
steps: Record<string, unknown>[],
): Record<string, unknown> {
for (const check of checks) {
const checkId = nonEmptyString(check.id) ?? `after-round-${promptIndex}`;
const commands: { readonly type: string; readonly enabled: boolean }[] = [
{ type: "refreshCurrentSession", enabled: check.refreshCurrent === true },
{ type: "switchAwayAndBack", enabled: check.switchAwayAndBack === true },
{ type: "assertSessionInvariant", enabled: check.assertSessionInvariant !== false },
];
for (const command of commands) {
if (!command.enabled) continue;
const args = [
"web-probe", "observe", "command", observerId,
"--node", state.spec.nodeId,
"--lane", state.spec.lane,
"--type", command.type,
"--after-round", String(promptIndex),
"--wait-ms", "55000",
"--command-timeout-seconds", String(remainingSeconds(deadline, 55)),
];
const severity = nonEmptyString(check.severity);
const findingId = nonEmptyString(check.findingId);
const expectedSentinelRange = nonEmptyString(check.expectedSentinelRange);
const alternateSessionStrategy = nonEmptyString(check.alternateSessionStrategy);
if (severity !== null) args.push("--severity", severity);
if (findingId !== null) args.push("--finding-id", findingId);
if (expectedSentinelRange !== null) args.push("--expected-sentinel-range", expectedSentinelRange);
if (command.type === "switchAwayAndBack" && alternateSessionStrategy !== null) args.push("--alternate-session-strategy", alternateSessionStrategy);
if (command.type === "assertSessionInvariant" && check.requireComposerReady === true) args.push("--require-composer-ready");
args.push(check.blocking === true ? "--blocking" : "--non-blocking");
const result = runChildCli(args, remainingSeconds(deadline, 60));
steps.push({ phase: `observe-session-invariance-${command.type}`, ok: result.ok, promptIndex, checkId, result: result.result });
if (!result.ok) {
return { ok: false, failure: `observe-session-invariance-${command.type}-failed`, checkId, promptIndex, valuesRedacted: true };
}
}
}
return { ok: true, promptIndex, checkCount: checks.length, valuesRedacted: true };
}
function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
readonly runId: string;
readonly scenarioId: string;
@@ -2172,7 +2247,7 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
"let artifactCount=0; let screenshot=null;",
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
"walk(stateDir);",
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220)};});",
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
"console.log(JSON.stringify({ok:!!report,reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
"NODE",
@@ -2438,6 +2513,7 @@ function readPromptSetForScenario(scenario: Record<string, unknown>): { ok: true
summary: {
...summary,
promptCount: parsed.length,
promptMarkers: parsed.map((item) => Array.from(new Set(Array.from(item.matchAll(/\bsentinel-(?:0[1-9]|10)\b/giu)).map((match) => match[0].toLowerCase())))),
promptTextHashes: parsed.map((item) => `sha256:${createHash("sha256").update(item).digest("hex").slice(0, 16)}`),
promptTextBytes: parsed.map((item) => Buffer.byteLength(item)),
valuesRedacted: true,