Files
Genarrative/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/assertions/runtime.mjs
T
kdletters dedff81475 修复AGC无人值守生成阻断的交付收口与验收
- 可信 code-prototype 父 Run 认领并观察直属美术 delivery,普通失败立即收束,合法安全默认 marker 由 Runtime 确定性执行唯一同合同返工

- 补齐 suppressed 无 child 与父身份链丢失时的 completion 失败关闭边界

- 修复 Windows project.lock delete-pending 竞争并收紧 HTML 内联 JS 语法 fail-open

- 同步技术方案、决策记录与实施计划,并完成确定性及真实 Provider 分层验证
2026-08-15 15:12:05 +08:00

2619 lines
89 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash, fs, path } from '../dependencies.mjs';
import { isPlainObject } from '../harness/config.mjs';
import {
isPathInside,
readJson,
readJsonl,
resolveProjectRelative,
} from '../harness/io.mjs';
import { runProcess } from '../harness/process.mjs';
import { seededGameHtml } from '../harness/project.mjs';
import {
agentConversationPath,
assertStructuredPlanContextSnapshot,
inspectStructuredPlanSnapshot,
readTargetDurableActions,
targetMainTaskRunIds,
} from '../harness/runtime.mjs';
import {
commandRootErrorMarker,
configFileName,
gitCommitReflogMessage,
gitSensitivePath,
idempotentObservationTools,
mainAgentId,
patchedText,
patchsetCreatedContent,
patchsetCreatedMarker,
patchsetCreatedPath,
sentinelFileName,
state,
steerInstruction,
supportedToolPlanProtocols,
toolPlanNormalizationKinds,
toolPlanProtocolAuditSafeFields,
toolPlanProtocolErrorKinds,
toolPlanProtocolErrorKindSet,
toolPlanRepairAuditSafeFields,
visibleText,
} from '../runtime-state.mjs';
import { goalStaleActionReceiptMatchesOriginalAction } from '../suites/goal.mjs';
import { assert, codedError, hashValue } from './core.mjs';
export function validateMainRunToolPlanProtocols(records) {
const protocols = records.filter(
(record) =>
record.recordType === 'agent.runtime.tool_plan.protocol' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
);
assert(protocols.length > 0, 'main-tool-plan-protocol-missing');
assert(
protocols.every(
(record) =>
supportedToolPlanProtocols.has(record.protocol) &&
hasSafeToolPlanAuditPayload(record),
),
'main-tool-plan-protocol-invalid',
);
return protocols.length;
}
export function emptyToolPlanRepairCountsByProtocolErrorKind() {
return Object.fromEntries(
toolPlanProtocolErrorKinds.map((kind) => [kind, 0]),
);
}
export function isCatalogBoundToolPlanFunctionName(name, protocol) {
if (!/^[a-z][a-z0-9_]{0,127}$/u.test(name)) return false;
if (protocol === 'native_function') {
return name === 'submit_agent_tool_plan';
}
if (protocol !== 'native_runtime_tools') return false;
return (
['update_agent_plan', 'respond_to_user'].includes(name) ||
name.startsWith('runtime_tool_') ||
name.startsWith('mcp_tool_')
);
}
export function hasValidToolPlanProtocolCallProjection(record) {
const isHash = (value) => /^[0-9a-f]{64}$/u.test(value);
if (
!Number.isSafeInteger(record.functionCallCount) ||
record.functionCallCount < 0 ||
!Array.isArray(record.callIdSha256s) ||
record.callIdSha256s.length !== record.functionCallCount ||
!record.callIdSha256s.every(isHash) ||
new Set(record.callIdSha256s).size !== record.callIdSha256s.length ||
!Array.isArray(record.functionNames) ||
record.functionNames.length !== record.functionCallCount
) {
return false;
}
if (record.protocol === 'text_json') {
return record.functionCallCount === 0;
}
if (record.protocol === 'native_function') {
return (
record.functionCallCount === 1 &&
record.functionNames.every((name) =>
isCatalogBoundToolPlanFunctionName(name, record.protocol),
)
);
}
return (
record.protocol === 'native_runtime_tools' &&
record.functionCallCount > 0 &&
record.functionNames.every((name) =>
isCatalogBoundToolPlanFunctionName(name, record.protocol),
)
);
}
export function hasSafeToolPlanAuditPayload(record) {
const safeFields =
record?.recordType === 'agent.runtime.tool_plan.protocol'
? toolPlanProtocolAuditSafeFields
: record?.recordType === 'agent.runtime.tool_plan.repair'
? toolPlanRepairAuditSafeFields
: null;
const isHash = (value) => /^[0-9a-f]{64}$/u.test(value);
if (
!safeFields ||
!isPlainObject(record) ||
Object.keys(record).length !== safeFields.size ||
Object.keys(record).some((field) => !safeFields.has(field)) ||
record.schemaVersion !== 'game-creator-agent-db.v1' ||
!Number.isSafeInteger(record.updatedAt) ||
record.updatedAt <= 0 ||
!['agentId', 'taskId', 'sessionId', 'runId', 'source'].every((field) =>
isNonEmptyString(record[field]),
) ||
!supportedToolPlanProtocols.has(record.protocol) ||
!Number.isSafeInteger(record.loopIteration) ||
record.loopIteration < 0 ||
!Number.isSafeInteger(record.repairAttempt) ||
record.repairAttempt < 0 ||
record.requestSlot !==
`loop-${record.loopIteration}-repair-${record.repairAttempt}` ||
!isHash(record.responseFingerprint) ||
!isHash(record.providerRequestIdSha256)
) {
return false;
}
if (record.recordType === 'agent.runtime.tool_plan.repair') {
const hasCallIdentity =
isHash(record.callIdSha256) && isHash(record.functionNameSha256);
const hasNoCallIdentity =
record.callIdSha256 == null && record.functionNameSha256 == null;
return (
toolPlanProtocolErrorKindSet.has(record.protocolErrorKind) &&
Number.isSafeInteger(record.attempt) &&
record.attempt > 0 &&
record.attempt === record.repairAttempt + 1 &&
Number.isSafeInteger(record.maxAttempts) &&
record.maxAttempts >= record.attempt &&
isHash(record.protocolErrorSha256) &&
isHash(record.responsePreviewSha256) &&
['protocolErrorChars', 'responsePreviewChars'].every(
(field) => Number.isSafeInteger(record[field]) && record[field] >= 0,
) &&
(record.protocol === 'text_json' ? hasNoCallIdentity : hasCallIdentity)
);
}
if (
!hasValidToolPlanProtocolCallProjection(record) ||
!Number.isSafeInteger(record.responseIdChars) ||
record.responseIdChars < 0 ||
(record.responseIdSha256 == null
? record.responseIdChars !== 0
: !isHash(record.responseIdSha256) || record.responseIdChars === 0) ||
!Array.isArray(record.normalizationKinds) ||
!Number.isSafeInteger(record.normalizationCount) ||
record.normalizationCount < 0 ||
!Number.isSafeInteger(record.normalizedTextChars) ||
record.normalizedTextChars < 0
) {
return false;
}
return record.normalizationCount === 0
? record.normalizationKinds.length === 0 &&
record.normalizedTextChars === 0 &&
record.normalizedTextSha256 == null
: record.normalizationKinds.length > 0 &&
record.normalizationKinds.length <= toolPlanNormalizationKinds.size &&
new Set(record.normalizationKinds).size ===
record.normalizationKinds.length &&
record.normalizationKinds.every((kind) =>
toolPlanNormalizationKinds.has(kind),
) &&
record.normalizationCount >= record.normalizationKinds.length &&
record.normalizedTextChars > 0 &&
isHash(record.normalizedTextSha256);
}
export function collectToolPlanRepairAuditEvidence(audits) {
const repairs = audits.filter(
(record) => record.recordType === 'agent.runtime.tool_plan.repair',
);
const countsByKind = emptyToolPlanRepairCountsByProtocolErrorKind();
const repairedLoops = new Set();
let secondRepairCount = 0;
for (const repair of repairs) {
assert(
hasSafeToolPlanAuditPayload(repair) &&
toolPlanProtocolErrorKindSet.has(repair.protocolErrorKind) &&
isNonEmptyString(repair.agentId) &&
isNonEmptyString(repair.runId) &&
Number.isSafeInteger(repair.loopIteration) &&
repair.loopIteration >= 0 &&
Number.isSafeInteger(repair.attempt) &&
repair.attempt > 0 &&
Number.isSafeInteger(repair.maxAttempts) &&
repair.maxAttempts >= repair.attempt,
'tool-plan-repair-classification-metadata-invalid',
);
countsByKind[repair.protocolErrorKind] += 1;
repairedLoops.add(
`${repair.agentId}\0${repair.runId}\0${repair.loopIteration}`,
);
if (repair.attempt === 2) secondRepairCount += 1;
}
assert(
sumObjectValues(countsByKind) === repairs.length,
'tool-plan-repair-classification-total-mismatch',
);
return {
toolPlanRepairCount: repairs.length,
nativeRuntimeToolPlanRepairCount: repairs.filter(
(record) => record.protocol === 'native_runtime_tools',
).length,
toolPlanRepairedLoopCount: repairedLoops.size,
toolPlanSecondRepairCount: secondRepairCount,
toolPlanRepairCountsByProtocolErrorKind: countsByKind,
toolPlanAuditPayloadLeakCount: audits.filter(
(record) => !hasSafeToolPlanAuditPayload(record),
).length,
};
}
export function toolPlanRepairEvidenceHasNoFatalLocalRepair(evidence) {
return (
evidence.toolPlanRepairCountsByProtocolErrorKind['catalog-binding'] === 0
);
}
export function collectNativeRuntimeToolPlanProtocolEvidence(records) {
const protocols = records.filter(
(record) =>
record.recordType === 'agent.runtime.tool_plan.protocol' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
);
const repairs = records.filter(
(record) =>
record.recordType === 'agent.runtime.tool_plan.repair' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
);
const audits = [...protocols, ...repairs];
return {
toolPlanProtocolCount: protocols.length,
nativeRuntimeToolPlanCount: protocols.filter(
(record) => record.protocol === 'native_runtime_tools',
).length,
wrapperToolPlanFallbackCount: audits.filter(
(record) => record.protocol === 'native_function',
).length,
textJsonToolPlanFallbackCount: audits.filter(
(record) => record.protocol === 'text_json',
).length,
...collectToolPlanRepairAuditEvidence(audits),
};
}
export function validateNativeRuntimeToolPlanProtocolEvidence(records) {
const protocolCount = validateMainRunToolPlanProtocols(records);
const evidence = collectNativeRuntimeToolPlanProtocolEvidence(records);
const protocols = records.filter(
(record) =>
record.recordType === 'agent.runtime.tool_plan.protocol' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
);
assert(
evidence.toolPlanProtocolCount === protocolCount &&
evidence.nativeRuntimeToolPlanCount === protocolCount &&
evidence.nativeRuntimeToolPlanRepairCount ===
evidence.toolPlanRepairCount &&
evidence.wrapperToolPlanFallbackCount === 0 &&
evidence.textJsonToolPlanFallbackCount === 0 &&
evidence.toolPlanAuditPayloadLeakCount === 0 &&
toolPlanRepairEvidenceHasNoFatalLocalRepair(evidence) &&
protocols.every(
(record) =>
hasValidToolPlanProtocolCallProjection(record) &&
Array.isArray(record.functionNames) &&
record.functionNames.length === record.functionCallCount &&
record.functionNames.every(
(name) =>
isNonEmptyString(name) && name !== 'submit_agent_tool_plan',
),
),
'goal-native-tool-plan-protocol-required',
);
return evidence;
}
export async function validateSameRunSteerEvidence({
agentDb,
activity,
contextBundle,
conversationEntries,
events,
initial,
output,
runtimeState,
taskSnapshot,
}) {
assert(state.steer, 'same-run-steer-state-missing');
assert(
state.steer.providerInterrupted === true &&
state.steer.providerWaitStatus === 'running' &&
state.steer.providerWaitPhase === 'planning' &&
Number.isSafeInteger(state.steer.providerWaitUpdatedAt) &&
JSON.stringify(state.steer.taskRunIdsBefore) ===
JSON.stringify(state.steer.taskRunIdsAfter) &&
state.steer.taskRunIdsBefore.includes(state.initialRunId) &&
state.steer.taskRunSetHash ===
hashValue(JSON.stringify(state.steer.taskRunIdsBefore)) &&
Number.isSafeInteger(state.steer.planRevisionAtAcceptance) &&
runtimeState.planRevision > state.steer.planRevisionAtAcceptance,
'same-run-steer-task-queue-invalid',
);
const finalMainRunIds = targetMainTaskRunIds(
taskSnapshot,
state.steer.taskIdentity,
);
assert(
JSON.stringify(finalMainRunIds) ===
JSON.stringify(state.steer.taskRunIdsBefore),
'same-run-steer-final-target-run-set-invalid',
);
const ledgerPath = path.join(
state.projectRoot,
'.agent/runtime/steers',
mainAgentId,
`${state.initialRunId}.jsonl`,
);
const ledger = await readJsonl(ledgerPath);
const entryRecords = ledger.filter(
(record) => record.steerId === state.steer.steerId,
);
const closedRecords = ledger.filter(
(record) => record.steerId == null && record.status === 'closed',
);
assert(
ledger.length === 5 &&
entryRecords.length === 4 &&
JSON.stringify(entryRecords.map((record) => record.status)) ===
JSON.stringify([
'prepared',
'conversation-persisted',
'queued',
'applied',
]) &&
closedRecords.length === 1 &&
ledger.at(-1) === closedRecords[0],
'same-run-steer-ledger-lifecycle-invalid',
);
const prepared = entryRecords[0];
const expectedMessageId = prepared.messageId;
assert(
isNonEmptyString(expectedMessageId) &&
entryRecords.every(
(record) =>
record.schemaVersion === 'game-creator-runtime-steer.v1' &&
isNonEmptyString(record.projectId) &&
record.agentId === mainAgentId &&
record.taskId === initial.taskId &&
record.sessionId === state.initialSessionId &&
record.runId === state.initialRunId &&
record.source === initial.source &&
record.sequence === state.steer.sequence &&
record.messageId === expectedMessageId &&
record.instructionSha256 === state.steer.instructionSha256 &&
record.contentChars === [...steerInstruction].length &&
record.contentBytes === Buffer.byteLength(steerInstruction) &&
record.acceptedVia === 'cli',
) &&
prepared.instruction === steerInstruction &&
entryRecords.slice(1).every((record) => record.instruction == null) &&
Number.isSafeInteger(entryRecords.at(-1).appliedAt),
'same-run-steer-ledger-entry-invalid',
);
const closed = closedRecords[0];
assert(
closed.schemaVersion === prepared.schemaVersion &&
closed.projectId === prepared.projectId &&
closed.agentId === mainAgentId &&
closed.taskId === initial.taskId &&
closed.sessionId === state.initialSessionId &&
closed.runId === state.initialRunId &&
closed.source === initial.source &&
closed.sequence === state.steer.sequence &&
closed.steerId == null &&
closed.messageId == null &&
closed.instructionSha256 == null &&
closed.instruction == null &&
closed.contentChars === 0 &&
closed.contentBytes === 0 &&
closed.acceptedVia === 'runtime-finalization',
'same-run-steer-ledger-closed-invalid',
);
const runtimeRefs = runtimeState.appliedSteerRefs ?? [];
const contextRefs = contextBundle.appliedSteerRefs ?? [];
assert(
runtimeState.agentId === mainAgentId &&
runtimeState.sessionId === state.initialSessionId &&
runtimeState.runId === state.initialRunId &&
runtimeState.appliedSteerCursor === state.steer.sequence &&
runtimeState.queuedSteerCount === 0 &&
runtimeRefs.length === 1 &&
runtimeRefs[0].steerId === state.steer.steerId &&
runtimeRefs[0].sequence === state.steer.sequence &&
runtimeRefs[0].messageId === expectedMessageId &&
runtimeRefs[0].instructionSha256 === state.steer.instructionSha256 &&
runtimeRefs[0].contentChars === [...steerInstruction].length &&
contextBundle.appliedSteerCursor === runtimeState.appliedSteerCursor &&
JSON.stringify(contextRefs) === JSON.stringify(runtimeRefs),
'same-run-steer-runtime-state-invalid',
);
const targetConversationPath = path.resolve(
agentConversationPath(mainAgentId, state.initialSessionId),
);
const steerMessages = conversationEntries.filter(
({ file, message }) =>
file === targetConversationPath &&
message.messageId === expectedMessageId,
);
assert(
steerMessages.length === 1 &&
steerMessages[0].message.role === 'user' &&
steerMessages[0].message.agentId === mainAgentId &&
steerMessages[0].message.content === steerInstruction &&
hashValue(steerMessages[0].message.content) ===
state.steer.instructionSha256,
'same-run-steer-conversation-invalid',
);
const steerConversationAudits = agentDb.filter(
(record) =>
record.recordType === 'conversation.message' &&
record.role === 'user' &&
record.agentId === mainAgentId &&
record.sessionId === state.initialSessionId &&
record.messageId === expectedMessageId,
);
assert(
steerConversationAudits.length === 1 &&
isNonEmptyString(steerConversationAudits[0].path) &&
path.resolve(resolveProjectRelative(steerConversationAudits[0].path)) ===
targetConversationPath,
'same-run-steer-conversation-audit-invalid',
);
const indexedSteerAudits = agentDb
.map((record, index) => ({ index, record }))
.filter(
({ record }) =>
record.recordType === 'agent.runtime.steer' &&
record.agentId === mainAgentId &&
record.taskId === initial.taskId &&
record.sessionId === state.initialSessionId &&
record.runId === state.initialRunId &&
record.steerId === state.steer.steerId,
);
const steerAudits = indexedSteerAudits.map(({ record }) => record);
assert(
steerAudits.length === 2 &&
JSON.stringify(steerAudits.map((record) => record.status)) ===
JSON.stringify(['queued', 'applied']) &&
steerAudits.every(
(record) =>
record.sequence === state.steer.sequence &&
record.messageId === expectedMessageId &&
record.instructionSha256 === state.steer.instructionSha256 &&
record.contentChars === [...steerInstruction].length,
),
'same-run-steer-audit-invalid',
);
const appliedAuditIndex = indexedSteerAudits.find(
({ record }) => record.status === 'applied',
)?.index;
assert(
Number.isSafeInteger(appliedAuditIndex) &&
appliedAuditIndex >= state.steer.agentDbSequenceBefore,
'same-run-steer-applied-sequence-invalid',
);
const postSteerPlanUpdates = agentDb
.map((record, index) => ({ index, record }))
.filter(
({ index, record }) =>
index > appliedAuditIndex &&
record.recordType === 'agent.runtime.plan_update' &&
record.agentId === mainAgentId &&
record.taskId === initial.taskId &&
record.sessionId === state.initialSessionId &&
record.runId === state.initialRunId,
);
assert(postSteerPlanUpdates.length > 0, 'same-run-steer-replan-missing');
const firstPostSteerPlan = postSteerPlanUpdates[0].record;
const completedBeforeSteer = new Set(
state.steer.completedStepHashesAtAcceptance,
);
const completedAfterSteer = new Set(
firstPostSteerPlan.steps
.filter((step) => step.status === 'completed')
.map((step) => step.stepSha256),
);
const incompleteAfterSteer = firstPostSteerPlan.steps
.map((step, index) => ({
index,
status: step.status,
stepHash: step.stepSha256,
}))
.filter((step) => step.status !== 'completed');
assert(
firstPostSteerPlan.planRevision > state.steer.planRevisionAtAcceptance &&
completedBeforeSteer.size > 0 &&
[...completedBeforeSteer].every((stepHash) =>
completedAfterSteer.has(stepHash),
) &&
incompleteAfterSteer.length > 0 &&
hashValue(JSON.stringify(incompleteAfterSteer)) !==
state.steer.incompletePlanSignatureAtAcceptance,
'same-run-steer-incomplete-plan-not-reordered',
);
const oldActionIds = new Set(
state.steer.activeActionsAtAcceptance.map((action) => action.actionId),
);
const oldPendingExecutionCount = agentDb
.slice(state.steer.agentDbSequenceBefore)
.filter(
(record) =>
oldActionIds.has(record.actionId) &&
[
'agent.runtime.tool_action.executing',
'agent.runtime.action_receipt',
].includes(record.recordType),
).length;
const acceptanceWindowExecutionCount = agentDb
.slice(state.steer.agentDbSequenceBefore, appliedAuditIndex + 1)
.filter(
(record) =>
record.recordType === 'agent.runtime.tool_action.executing' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
).length;
const durableActionIdsAtAcceptance = new Set(
state.steer.durableActionsAtAcceptance.map((action) => action.actionId),
);
const oldPlanMaterializedActions = (await readTargetDurableActions()).filter(
(action) =>
action.plannedSteerCursor < state.steer.sequence &&
!durableActionIdsAtAcceptance.has(action.actionId),
);
assert(
oldPendingExecutionCount === 0 &&
acceptanceWindowExecutionCount === 0 &&
oldPlanMaterializedActions.length === 0,
'same-run-steer-old-pending-action-executed',
);
let preSteerSideEffectReplayCount = 0;
for (const latched of state.steer.sideEffectReceiptsAtAcceptance) {
const matches = agentDb.filter(
(record) =>
record.recordType === 'agent.runtime.action_receipt' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === latched.actionId &&
record.actionFingerprint === latched.actionFingerprint &&
record.tool === latched.tool &&
record.status === latched.status &&
hashValue(actionReceiptIdentity(record)) === latched.identityHash,
);
if (matches.length > 1) preSteerSideEffectReplayCount += matches.length - 1;
assert(matches.length === 1, 'same-run-steer-side-effect-receipt-changed');
}
const finalRevision = await readJson(
path.join(state.projectRoot, '.agent/runtime/project-revision.json'),
);
assert(
Number.isSafeInteger(state.steer.projectRevisionAtAcceptance) &&
finalRevision.revision >= state.steer.projectRevisionAtAcceptance &&
/^[0-9a-f]{64}$/u.test(
state.steer.projectSideEffectFingerprintAtAcceptance ?? '',
) &&
preSteerSideEffectReplayCount === 0,
'same-run-steer-side-effect-snapshot-invalid',
);
const publicInstructionLeakCount = countExactSecrets(
Buffer.from(
JSON.stringify([
taskSnapshot.all,
events,
agentDb,
activity,
output,
runtimeState,
]),
),
[steerInstruction],
);
assert(
publicInstructionLeakCount === 0,
'same-run-steer-public-instruction-leak',
);
return {
planRevisionAtAcceptance: state.steer.planRevisionAtAcceptance,
sequence: state.steer.sequence,
steerIdHash: state.steer.steerIdHash,
messageId: expectedMessageId,
messageIdHash: hashValue(expectedMessageId),
instructionSha256: state.steer.instructionSha256,
providerInterrupted: state.steer.providerInterrupted,
providerPlanningWaitMatched: true,
completedStepCountAtAcceptance: completedBeforeSteer.size,
incompleteStepCountAtAcceptance:
state.steer.incompleteStepsAtAcceptance.length,
firstPostSteerPlanRevision: firstPostSteerPlan.planRevision,
firstPostSteerIncompleteStepCount: incompleteAfterSteer.length,
incompletePlanReordered: true,
oldPendingActionCount: oldActionIds.size,
oldPendingActionSetHash: hashValue(
JSON.stringify(
state.steer.activeActionsAtAcceptance.map((action) => [
action.actionId,
action.actionFingerprint,
action.tool,
action.status,
action.plannedSteerCursor,
]),
),
),
oldPendingExecutionCount,
oldPlanMaterializedActionCount: oldPlanMaterializedActions.length,
acceptanceWindowExecutionCount,
sideEffectReceiptCountAtAcceptance:
state.steer.sideEffectReceiptsAtAcceptance.length,
sideEffectSnapshotHash: hashValue(
JSON.stringify(
state.steer.sideEffectReceiptsAtAcceptance.map(
(receipt) => receipt.identityHash,
),
),
),
preSteerSideEffectReplayCount,
ledgerRecordCount: ledger.length,
appliedCount: entryRecords.filter((record) => record.status === 'applied')
.length,
closedCount: closedRecords.length,
auditCount: steerAudits.length,
taskRunCountBefore: state.steer.taskRunIdsBefore.length,
taskRunCountAfter: state.steer.taskRunIdsAfter.length,
taskRunSetHash: state.steer.taskRunSetHash,
publicInstructionLeakCount,
};
}
export function validateStructuredPlanEvidence(
records,
runtimeState,
contextBundle,
) {
const indexedUpdates = records
.map((record, index) => ({ index, record }))
.filter(
({ record }) =>
record.recordType === 'agent.runtime.plan_update' &&
record.agentId === mainAgentId &&
record.sessionId === state.initialSessionId &&
record.runId === state.initialRunId,
);
const updates = indexedUpdates.map(({ record }) => record);
assert(updates.length >= 3, 'structured-plan-update-count-invalid');
const completed = new Set();
let regressionCount = 0;
let previousPlanSignature = null;
for (const [index, update] of updates.entries()) {
assert(
update.planRevision === index + 1 &&
Number.isSafeInteger(update.updatedAt) &&
/^[0-9a-f]{64}$/u.test(update.explanationSha256 ?? '') &&
Number.isInteger(update.explanationChars) &&
update.explanationChars > 0 &&
Array.isArray(update.steps) &&
update.steps.length >= 3 &&
update.steps.length <= 8,
'structured-plan-update-invalid',
);
const planSignature = hashValue(
JSON.stringify({
explanationSha256: update.explanationSha256,
steps: update.steps,
}),
);
assert(
planSignature !== previousPlanSignature,
'structured-plan-idempotent-revision-incremented',
);
previousPlanSignature = planSignature;
const stepNames = new Set();
let inProgressCount = 0;
const byName = new Map();
for (const step of update.steps) {
assert(
/^[0-9a-f]{64}$/u.test(step.stepSha256 ?? '') &&
['pending', 'in_progress', 'completed'].includes(step.status) &&
!stepNames.has(step.stepSha256),
'structured-plan-step-invalid',
);
stepNames.add(step.stepSha256);
byName.set(step.stepSha256, step.status);
if (step.status === 'in_progress') inProgressCount += 1;
}
assert(inProgressCount <= 1, 'structured-plan-multiple-in-progress');
for (const step of completed) {
if (byName.get(step) !== 'completed') regressionCount += 1;
}
for (const step of update.steps) {
if (step.status === 'completed') completed.add(step.stepSha256);
}
}
assert(regressionCount === 0, 'structured-plan-terminal-regression');
const timeline = validatePlanUpdateActionTimeline(records, indexedUpdates);
const finalUpdate = updates.at(-1);
assert(
finalUpdate.steps.every((step) => step.status === 'completed'),
'structured-plan-final-not-completed',
);
const finalSnapshot = inspectStructuredPlanSnapshot(
runtimeState,
'final-structured-plan',
);
assert(
runtimeState.planRevision === finalUpdate.planRevision &&
createHash('sha256')
.update(runtimeState.planExplanation)
.digest('hex') === finalUpdate.explanationSha256 &&
Array.isArray(runtimeState.planSteps) &&
runtimeState.planSteps.length === finalUpdate.steps.length &&
runtimeState.planSteps.every(
(step, index) =>
step.index === index &&
createHash('sha256').update(step.title).digest('hex') ===
finalUpdate.steps[index].stepSha256 &&
step.status === 'completed',
) &&
runtimeState.activePlanStepIndex === null,
'structured-plan-runtime-state-invalid',
);
assertStructuredPlanContextSnapshot(
runtimeState,
contextBundle,
'structured-plan-final-context',
);
const recovery = state.planRecovery;
assert(
recovery &&
Number.isSafeInteger(recovery.preKillRevision) &&
recovery.preKillRevision > 0 &&
Number.isSafeInteger(recovery.recoveredRevision) &&
recovery.recoveredRevision >= recovery.preKillRevision &&
Array.isArray(recovery.preKillCompletedStepHashes) &&
recovery.preKillCompletedStepHashes.length > 0 &&
recovery.preKillIncompleteStepCount > 0 &&
/^[0-9a-f]{64}$/u.test(recovery.preKillTerminalStepHash ?? '') &&
Array.isArray(recovery.recoveredCompletedStepHashes) &&
/^[0-9a-f]{64}$/u.test(recovery.recoveredTerminalStepHash ?? ''),
'structured-plan-recovery-state-invalid',
);
const preKillUpdate = updates.find(
(update) => update.planRevision === recovery.preKillRevision,
);
const recoveredUpdate = updates.find(
(update) => update.planRevision === recovery.recoveredRevision,
);
assert(
preKillUpdate &&
recoveredUpdate &&
preKillUpdate.steps.length >= 3 &&
preKillUpdate.steps.some((step) => step.status !== 'completed'),
'structured-plan-recovery-revision-missing',
);
const preKillCompleted = preKillUpdate.steps
.filter((step) => step.status === 'completed')
.map((step) => step.stepSha256)
.sort();
const recoveredCompleted = recoveredUpdate.steps
.filter((step) => step.status === 'completed')
.map((step) => step.stepSha256)
.sort();
assert(
JSON.stringify(preKillCompleted) ===
JSON.stringify(recovery.preKillCompletedStepHashes) &&
recovery.preKillIncompleteStepCount ===
preKillUpdate.steps.length - preKillCompleted.length &&
recovery.preKillTerminalStepHash ===
hashValue(JSON.stringify(preKillCompleted)) &&
JSON.stringify(recoveredCompleted) ===
JSON.stringify(recovery.recoveredCompletedStepHashes) &&
recovery.recoveredTerminalStepHash ===
hashValue(JSON.stringify(recoveredCompleted)) &&
preKillCompleted.every((stepHash) =>
recoveredCompleted.includes(stepHash),
) &&
recoveredCompleted.every((stepHash) =>
finalSnapshot.completedStepHashes.includes(stepHash),
),
'structured-plan-recovery-terminal-step-invalid',
);
return {
updateCount: updates.length,
planRevision: runtimeState.planRevision,
completedStepCount: runtimeState.planSteps.length,
regressionCount,
preKillRevision: recovery.preKillRevision,
preKillCompletedStepCount: preKillCompleted.length,
preKillIncompleteStepCount: recovery.preKillIncompleteStepCount,
preKillTerminalStepHash: recovery.preKillTerminalStepHash,
recoveredRevision: recovery.recoveredRevision,
recoveredCompletedStepCount: recoveredCompleted.length,
recoveredTerminalStepHash: recovery.recoveredTerminalStepHash,
timelineUpdateCount: timeline.updateCount,
timelineAnchoredUpdateCount: timeline.anchoredUpdateCount,
completionTransitionCount: timeline.completionTransitionCount,
completionObservationCount: timeline.completionObservationCount,
prematureCompletionCount: timeline.prematureCompletionCount,
timelineHash: timeline.timelineHash,
};
}
export function validatePlanUpdateActionTimeline(records, indexedUpdates) {
const terminalActions = records
.map((record, index) => ({ index, record }))
.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.status !== 'waiting-for-confirmation' &&
isNonEmptyString(record.actionId) &&
isNonEmptyString(record.actionFingerprint) &&
isNonEmptyString(record.tool) &&
Number.isSafeInteger(record.updatedAt),
)
.map(({ index: observationIndex, record: observation }) => {
const receiptIndex = records.findIndex(
(record) =>
record.recordType === 'agent.runtime.action_receipt' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === observation.actionId &&
record.actionFingerprint === observation.actionFingerprint &&
record.tool === observation.tool &&
record.status === observation.status,
);
const actionIndex = records.findIndex(
(record) =>
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === observation.actionId &&
record.actionFingerprint === observation.actionFingerprint &&
record.tool === observation.tool &&
[
'agent.runtime.tool_action.executing',
'agent.runtime.tool_confirmation_required',
'agent.runtime.tool_confirmation.approved',
'agent.runtime.tool_confirmation.rejected',
'agent.runtime.tool_action.observed',
].includes(record.recordType),
);
const receipt = records[receiptIndex];
const action = records[actionIndex];
assert(
actionIndex >= 0 &&
actionIndex < observationIndex &&
receiptIndex >= 0 &&
receiptIndex < observationIndex &&
Number.isSafeInteger(action.updatedAt) &&
Number.isSafeInteger(receipt.updatedAt) &&
action.updatedAt <= receipt.updatedAt &&
receipt.updatedAt <= observation.updatedAt,
'structured-plan-terminal-action-lifecycle-invalid',
);
return {
actionIdentityHash: hashValue(
`${observation.actionId}\0${observation.actionFingerprint}\0${observation.tool}`,
),
actionIndex,
observation,
observationIndex,
receiptIndex,
};
});
assert(
duplicateCount(
terminalActions.map((action) => action.observation.actionId),
) === 0,
'structured-plan-terminal-observation-duplicate',
);
const appliedSteerIndexes = records
.map((record, index) => ({ index, record }))
.filter(
({ record }) =>
record.recordType === 'agent.runtime.steer' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.status === 'applied',
)
.map(({ index }) => index);
const timeline = [];
let anchoredUpdateCount = 0;
let completionTransitionCount = 0;
let completionObservationCount = 0;
let prematureCompletionCount = 0;
let previousCompleted = new Set();
let previousUpdateIndex = -1;
for (const [updateIndex, indexedUpdate] of indexedUpdates.entries()) {
const update = indexedUpdate.record;
const completedNow = new Set(
update.steps
.filter((step) => step.status === 'completed')
.map((step) => step.stepSha256),
);
const newlyCompleted = [...completedNow].filter(
(stepHash) => !previousCompleted.has(stepHash),
);
const intervalActions = terminalActions.filter(
(action) =>
action.observationIndex > previousUpdateIndex &&
action.observationIndex < indexedUpdate.index,
);
const intervalSteerIndexes = appliedSteerIndexes.filter(
(index) => index > previousUpdateIndex && index < indexedUpdate.index,
);
if (updateIndex === 0) {
assert(
newlyCompleted.length === 0,
'structured-plan-initial-update-precompleted-step',
);
} else {
assert(
intervalActions.length > 0 || intervalSteerIndexes.length > 0,
'structured-plan-update-without-action-or-steer-anchor',
);
anchoredUpdateCount += 1;
}
if (newlyCompleted.length > intervalActions.length) {
prematureCompletionCount +=
newlyCompleted.length - intervalActions.length;
}
assert(
newlyCompleted.length <= intervalActions.length,
'structured-plan-completed-before-terminal-observation',
);
const completionActions =
newlyCompleted.length === 0
? []
: intervalActions.slice(-newlyCompleted.length);
for (const action of completionActions) {
assert(
action.observation.updatedAt <= update.updatedAt &&
action.receiptIndex < action.observationIndex &&
action.observationIndex < indexedUpdate.index,
'structured-plan-completion-timestamp-invalid',
);
}
completionTransitionCount += newlyCompleted.length;
completionObservationCount += completionActions.length;
timeline.push({
actionIdentityHashes: completionActions.map(
(action) => action.actionIdentityHash,
),
completedStepHashes: newlyCompleted.sort(),
planRevision: update.planRevision,
planSequence: indexedUpdate.index + 1,
steerSequences: intervalSteerIndexes.map((index) => index + 1),
terminalObservationSequences: completionActions.map(
(action) => action.observationIndex + 1,
),
updatedAt: update.updatedAt,
});
previousCompleted = completedNow;
previousUpdateIndex = indexedUpdate.index;
}
assert(
completionTransitionCount > 0 &&
completionTransitionCount === completionObservationCount &&
prematureCompletionCount === 0,
'structured-plan-completion-observation-coverage-invalid',
);
return {
updateCount: indexedUpdates.length,
anchoredUpdateCount,
completionTransitionCount,
completionObservationCount,
prematureCompletionCount,
timelineHash: hashValue(JSON.stringify(timeline)),
};
}
export function validateConfirmedActionLifecycles(records) {
const indexed = records.map((record, index) => ({ record, index }));
const approvals = indexed.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_confirmation.approved',
);
const approvedActionIds = new Set(
approvals.map(({ record }) => record.actionId),
);
assert(
state.confirmedActionIds.size > 0,
'confirmed-action-evidence-missing',
);
assert(
approvals.length === approvedActionIds.size &&
approvedActionIds.size === state.confirmedActionIds.size &&
[...approvedActionIds].every((actionId) =>
state.confirmedActionIds.has(actionId),
) &&
[...state.confirmedActionIds].every((actionId) =>
approvedActionIds.has(actionId),
),
'confirmed-action-set-mismatch',
);
for (const actionId of state.confirmedActionIds) {
const lifecycle = indexed.filter(
({ record }) => record.actionId === actionId,
);
const waiting = lifecycle.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.status === 'waiting-for-confirmation',
);
const observed = lifecycle.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.decision === 'approved' &&
record.status !== 'waiting-for-confirmation',
);
const required = lifecycle.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_confirmation_required',
);
const approved = lifecycle.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_confirmation.approved',
);
assert(
waiting.length === 1 &&
observed.length === 1 &&
required.length === 1 &&
approved.length === 1,
'confirmed-action-lifecycle-count-invalid',
);
const tool = required[0].record.tool;
const agentId = required[0].record.agentId;
const runId = required[0].record.runId;
const actionFingerprint = required[0].record.actionFingerprint;
assert(
isNonEmptyString(tool) &&
isNonEmptyString(agentId) &&
isNonEmptyString(runId) &&
isNonEmptyString(actionFingerprint) &&
[waiting[0], observed[0], approved[0]].every(
({ record }) => record.agentId === agentId && record.runId === runId,
) &&
waiting[0].record.tool === tool &&
observed[0].record.tool === tool &&
approved[0].record.tool === tool &&
waiting[0].record.actionFingerprint === actionFingerprint &&
approved[0].record.actionFingerprint === actionFingerprint &&
approved[0].record.confirmedRunId === runId &&
canonicalAuditInputSummary(required[0].record.inputSummary) ===
canonicalAuditInputSummary(approved[0].record.inputSummary),
'confirmed-action-lifecycle-identity-invalid',
);
assert(
waiting[0].index < required[0].index &&
required[0].index < approved[0].index &&
approved[0].index < observed[0].index,
'confirmed-action-lifecycle-order-invalid',
);
}
return state.confirmedActionIds.size;
}
export function validateMainRunActionReceipts(
records,
mainTask,
historyExecution,
imageInspectExecution,
commandOutputReadExecution,
failedCommandActionId,
gitCommitExecution,
) {
const receiptRecords = records.filter(
(record) => record.recordType === 'agent.runtime.action_receipt',
);
assertNoPersistedImagePayload('action-receipt', receiptRecords);
const terminalObservations = records.filter(
(record) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.status !== 'waiting-for-confirmation' &&
isNonEmptyString(record.actionId),
);
const mainActionIds = new Set(
terminalObservations.map((record) => record.actionId),
);
const mainRunReceipts = receiptRecords.filter((record) =>
mainActionIds.has(record.actionId),
);
const declaredMainRunReceipts = receiptRecords.filter(
(record) => record.runId === state.initialRunId,
);
assert(mainActionIds.size > 0, 'main-run-terminal-actions-missing');
assert(
mainRunReceipts.length === mainActionIds.size &&
declaredMainRunReceipts.length === mainRunReceipts.length,
'main-run-action-receipt-count-invalid',
);
assert(
mainRunReceipts.every(
(record) =>
record.agentId === mainAgentId &&
record.taskId === mainTask.taskId &&
record.sessionId === state.initialSessionId &&
record.runId === state.initialRunId &&
/^action-[0-9a-f]{24}$/u.test(record.actionId) &&
/^[0-9a-f]{64}$/u.test(record.actionFingerprint) &&
isNonEmptyString(record.tool) &&
isNonEmptyString(record.executionMode) &&
isNonEmptyString(record.status) &&
Object.hasOwn(record, 'inputSummary') &&
(record.inputSummary == null ||
isNonEmptyString(record.inputSummary)) &&
isNonEmptyString(record.summary) &&
Object.hasOwn(record, 'safeDetail') &&
(record.safeDetail == null || typeof record.safeDetail === 'string') &&
typeof record.detailUnavailable === 'boolean' &&
Number.isSafeInteger(record.updatedAt),
),
'main-run-action-receipt-identity-invalid',
);
const duplicateIdentityCount = duplicateCount(
mainRunReceipts.map(
(record) => `${record.actionId}\0${record.actionFingerprint}`,
),
);
assert(
duplicateIdentityCount === 0 &&
duplicateCount(mainRunReceipts.map((record) => record.actionId)) === 0,
'main-run-action-receipt-duplicate',
);
for (const observation of terminalObservations) {
const matches = mainRunReceipts.filter(
(record) => record.actionId === observation.actionId,
);
const fingerprints = new Set(
records
.filter(
(record) =>
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === observation.actionId &&
isNonEmptyString(record.actionFingerprint),
)
.map((record) => record.actionFingerprint),
);
assert(
matches.length === 1 &&
matches[0].tool === observation.tool &&
matches[0].status === observation.status &&
fingerprints.size === 1 &&
fingerprints.has(matches[0].actionFingerprint),
'terminal-action-receipt-mismatch',
);
}
const requiredTools = new Set([
'project.patchset',
'git.inspect',
'image.inspect',
'command.output_read',
'agent.action_history',
'project.git_commit',
]);
const coveredTools = new Set(mainRunReceipts.map((record) => record.tool));
assert(
[...requiredTools].every((tool) => coveredTools.has(tool)),
'required-action-receipt-tools-missing',
);
const historyReceipts = mainRunReceipts.filter(
(record) =>
record.actionId === historyExecution.actionId &&
record.actionFingerprint === historyExecution.actionFingerprint &&
record.tool === 'agent.action_history' &&
record.status === 'ok',
);
assert(historyReceipts.length === 1, 'action-history-receipt-count-invalid');
const imageInspectReceipts = mainRunReceipts.filter(
(record) =>
record.actionId === imageInspectExecution.actionId &&
record.actionFingerprint === imageInspectExecution.actionFingerprint &&
record.tool === 'image.inspect' &&
record.executionMode === imageInspectExecution.mode &&
record.status === 'ok' &&
record.detailUnavailable === false &&
isNonEmptyString(record.safeDetail),
);
assert(
imageInspectReceipts.length === 1,
'image-inspect-receipt-count-invalid',
);
let imageInspectSafeDetail;
try {
imageInspectSafeDetail = JSON.parse(imageInspectReceipts[0].safeDetail);
} catch (error) {
throw codedError('image-inspect-receipt-detail-invalid', error);
}
const commandOutputReadReceipts = mainRunReceipts.filter(
(record) =>
record.tool === 'command.output_read' &&
record.status === 'ok' &&
record.detailUnavailable === false &&
isNonEmptyString(record.safeDetail),
);
assert(
commandOutputReadReceipts.some(
(record) =>
record.actionId === commandOutputReadExecution.actionId &&
record.actionFingerprint ===
commandOutputReadExecution.actionFingerprint,
),
'command-output-read-receipt-missing',
);
const commandOutputReadSafeDetails = commandOutputReadReceipts.map(
(record) => {
let detail;
try {
detail = JSON.parse(record.safeDetail);
} catch (error) {
throw codedError('command-output-read-receipt-detail-invalid', error);
}
assert(
detail.sourceActionId === failedCommandActionId &&
isNonEmptyString(detail.sourceRunId) &&
/^[0-9a-f]{64}$/u.test(detail.sourceActionFingerprint) &&
isNonEmptyString(detail.outputRef) &&
/^[0-9a-f]{64}$/u.test(detail.outputSha256) &&
Number.isSafeInteger(detail.startLine) &&
detail.startLine >= 1 &&
Number.isSafeInteger(detail.totalLines) &&
!Object.hasOwn(detail, 'lines'),
'command-output-read-receipt-safe-detail-invalid',
);
return detail;
},
);
const gitCommitReceipts = mainRunReceipts.filter(
(record) =>
record.actionId === gitCommitExecution.actionId &&
record.actionFingerprint === gitCommitExecution.actionFingerprint &&
record.tool === 'project.git_commit' &&
record.executionMode === 'confirmation' &&
record.status === 'ok' &&
record.detailUnavailable === false &&
isNonEmptyString(record.safeDetail),
);
assert(gitCommitReceipts.length === 1, 'git-commit-receipt-count-invalid');
let gitCommitSafeDetail;
try {
gitCommitSafeDetail = JSON.parse(gitCommitReceipts[0].safeDetail);
} catch (error) {
throw codedError('git-commit-receipt-detail-invalid', error);
}
assert(
hasExactKeys(gitCommitSafeDetail, [
'branch',
'commitHead',
'messageSha256',
'parentHead',
'pathCount',
'paths',
'remainingChangedCount',
]) &&
matchesGitObjectId(gitCommitSafeDetail.parentHead) &&
matchesGitObjectId(gitCommitSafeDetail.commitHead) &&
/^[0-9a-f]{64}$/u.test(gitCommitSafeDetail.messageSha256) &&
isNonEmptyString(gitCommitSafeDetail.branch) &&
gitCommitSafeDetail.pathCount === 2 &&
pathListsEqual(gitCommitSafeDetail.paths, [
'game/index.html',
patchsetCreatedPath,
]) &&
gitCommitSafeDetail.remainingChangedCount === 1,
'git-commit-receipt-safe-detail-invalid',
);
const serializedReceipts = Buffer.from(
receiptRecords.map((record) => JSON.stringify(record)).join('\n'),
);
const secretLeakCount = countExactSecrets(serializedReceipts, state.secrets);
const lureLeakCount = countExactSecrets(serializedReceipts, state.lures);
const commandOutputMarkerLeakCount = countExactSecrets(serializedReceipts, [
commandRootErrorMarker,
]);
assert(secretLeakCount === 0, 'action-receipt-secret-leak-detected');
assert(lureLeakCount === 0, 'action-receipt-lure-leak-detected');
assert(
commandOutputMarkerLeakCount === 0,
'action-receipt-command-output-marker-leak-detected',
);
return {
receiptCount: receiptRecords.length,
mainRunReceiptCount: mainRunReceipts.length,
requiredToolCount: requiredTools.size,
duplicateIdentityCount,
secretLeakCount,
lureLeakCount,
actionHistoryReceiptIndex: records.indexOf(historyReceipts[0]),
imageInspectReceiptCount: imageInspectReceipts.length,
imageInspectReceiptIndex: records.indexOf(imageInspectReceipts[0]),
imageInspectSafeDetail,
commandOutputReadReceiptCount: commandOutputReadReceipts.length,
commandOutputReadSafeDetails,
commandOutputMarkerLeakCount,
gitCommitReceiptCount: gitCommitReceipts.length,
gitCommitReceiptIndex: records.indexOf(gitCommitReceipts[0]),
gitCommitSafeDetail,
};
}
export function validatePreviewScreenshotPaths(screenshots, code) {
assert(
Array.isArray(screenshots) &&
screenshots.length === 2 &&
screenshots.every(
(entry) =>
isNonEmptyString(entry) &&
!path.isAbsolute(entry) &&
!entry.includes('\\'),
) &&
screenshots[0].endsWith('/desktop.png') &&
screenshots[1].endsWith('/mobile.png') &&
new Set(screenshots).size === screenshots.length,
code,
);
return screenshots;
}
export function validateImageInspectAudit(
record,
expectedImages,
receiptDetail,
) {
assert(
hasExactKeys(record, [
'agentId',
'conclusionChars',
'images',
'recordType',
'responseId',
'runId',
'schemaVersion',
'updatedAt',
]) &&
isNonEmptyString(record.schemaVersion) &&
Number.isSafeInteger(record.updatedAt) &&
isNonEmptyString(record.responseId) &&
Number.isSafeInteger(record.conclusionChars) &&
record.conclusionChars > 0 &&
record.conclusionChars <= 7_000,
'image-inspect-dedicated-audit-fields-invalid',
);
validateImageInspectMetadata(
record.images,
expectedImages,
'image-inspect-dedicated-audit-images-invalid',
);
assert(
hasExactKeys(receiptDetail, ['conclusionChars', 'images', 'responseId']) &&
receiptDetail.responseId === record.responseId &&
receiptDetail.conclusionChars === record.conclusionChars,
'image-inspect-receipt-fields-invalid',
);
validateImageInspectMetadata(
receiptDetail.images,
expectedImages,
'image-inspect-receipt-images-invalid',
);
}
export function validateImageInspectMetadata(actual, expected, code) {
assert(
Array.isArray(actual) &&
actual.length === expected.length &&
actual.every(
(image, index) =>
hasExactKeys(image, ['bytes', 'path', 'sha256']) &&
image.path === expected[index].path &&
image.sha256 === expected[index].sha256 &&
image.bytes === expected[index].bytes &&
/^[0-9a-f]{64}$/u.test(image.sha256) &&
Number.isSafeInteger(image.bytes) &&
image.bytes > 0,
),
code,
);
}
export function hasExactKeys(value, expectedKeys) {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
return (
actual.length === expected.length &&
actual.every((key, index) => key === expected[index])
);
}
export function matchesGitObjectId(value) {
return (
typeof value === 'string' &&
[40, 64].includes(value.length) &&
/^[0-9a-f]+$/u.test(value)
);
}
export function pathListsEqual(actual, expected) {
if (
!Array.isArray(actual) ||
actual.some((entry) => typeof entry !== 'string')
) {
return false;
}
const left = [...actual].sort();
const right = [...expected].sort();
return (
left.length === right.length &&
left.every((entry, index) => entry === right[index])
);
}
export function assertNoPersistedImagePayload(surface, records) {
const serialized = records.map((record) => JSON.stringify(record)).join('\n');
assert(
!/data:image(?:\/|%2f)/iu.test(serialized),
`${surface}-data-image-payload-leak`,
);
assert(
!/(?:;|%3b)base64(?:,|%2c)[a-z0-9+/=\r\n]{128,}/iu.test(serialized) &&
!/[a-z0-9+/]{512,}={0,2}/iu.test(serialized),
`${surface}-base64-image-payload-leak`,
);
}
export function validateActionHistoryObservations(
events,
contextObservations,
historyExecution,
patchsetExecution,
mainTask,
) {
const eventObservations = events.filter(
(event) =>
event.agentId === mainAgentId &&
event.runId === state.initialRunId &&
event.actionId === historyExecution.actionId &&
event.eventType === 'observation' &&
String(event.summary ?? '').startsWith('agent.action_historyok') &&
isNonEmptyString(event.detail),
);
const bundledObservations = (contextObservations ?? []).filter(
(observation) =>
observation?.tool === 'agent.action_history' &&
observation?.status === 'ok' &&
isNonEmptyString(observation.detail) &&
observation.detail.includes(patchsetExecution.actionId),
);
assert(
eventObservations.length === 1 && bundledObservations.length === 1,
'action-history-observation-count-invalid',
);
const payloads = [eventObservations[0], bundledObservations[0]].map(
(observation) => {
let payload;
try {
payload = JSON.parse(observation.detail);
} catch (error) {
throw codedError('action-history-observation-json-invalid', error);
}
const actions = Array.isArray(payload.actions) ? payload.actions : [];
const patchsetActions = actions.filter(
(action) => action.actionId === patchsetExecution.actionId,
);
assert(
payload.runId === state.initialRunId &&
payload.count === actions.length &&
payload.truncated === false &&
actions.length === 1 &&
patchsetActions.length === 1 &&
patchsetActions[0].agentId === mainAgentId &&
patchsetActions[0].taskId === mainTask.taskId &&
patchsetActions[0].sessionId === state.initialSessionId &&
patchsetActions[0].actionFingerprint ===
patchsetExecution.actionFingerprint &&
patchsetActions[0].runId === state.initialRunId &&
patchsetActions[0].tool === 'project.patchset' &&
patchsetActions[0].status === 'ok' &&
!actions.some(
(action) =>
action.actionId === historyExecution.actionId ||
action.tool === 'agent.action_history',
),
'action-history-observation-evidence-invalid',
);
return payload;
},
);
const actions = payloads[0].actions;
return {
resultCount: actions.length,
recursiveResultCount: actions.filter(
(action) => action.tool === 'agent.action_history',
).length,
};
}
export function validateToolActionReplays(records) {
const attemptsByActionId = new Map();
for (const record of records) {
if (
![
'agent.runtime.tool_action.executing',
'agent.runtime.tool_confirmation_required',
'agent.runtime.action_receipt',
].includes(record.recordType)
) {
continue;
}
assert(
isNonEmptyString(record.agentId) &&
isNonEmptyString(record.runId) &&
isNonEmptyString(record.actionId) &&
isNonEmptyString(record.actionFingerprint) &&
isNonEmptyString(record.tool),
'tool-action-replay-identity-missing',
);
const attempt = {
recordType: record.recordType,
agentId: record.agentId,
runId: record.runId,
actionId: record.actionId,
actionFingerprint: record.actionFingerprint,
tool: record.tool,
status: record.status ?? null,
rawInputSummary: record.inputSummary ?? null,
inputSummary: canonicalAuditInputSummary(record.inputSummary),
};
const existing = attemptsByActionId.get(attempt.actionId);
if (existing) {
const sameIdentity =
existing.agentId === attempt.agentId &&
existing.runId === attempt.runId &&
existing.actionFingerprint === attempt.actionFingerprint &&
existing.tool === attempt.tool &&
existing.inputSummary === attempt.inputSummary;
assert(
sameIdentity ||
goalStaleActionReceiptMatchesOriginalAction(existing, attempt),
'tool-action-replay-identity-conflict',
);
} else {
attemptsByActionId.set(attempt.actionId, attempt);
}
}
const sideEffectsByIdentity = new Map();
const observationsByIdentity = new Map();
for (const attempt of attemptsByActionId.values()) {
const terminalReceipt = records.find(
(record) =>
record.recordType === 'agent.runtime.action_receipt' &&
record.agentId === attempt.agentId &&
record.runId === attempt.runId &&
record.actionId === attempt.actionId &&
record.tool === attempt.tool,
);
const commandTerminalStatus =
attempt.tool === 'command.exec'
? (terminalReceipt?.status ?? '[missing-terminal-status]')
: '';
const identity = `${attempt.agentId}\0${attempt.runId}\0${attempt.tool}\0${attempt.actionFingerprint}\0${commandTerminalStatus}`;
const idempotentObservation = idempotentObservationTools.has(attempt.tool);
const sideEffectOccurred =
terminalReceipt?.status === 'ok' ||
(attempt.tool === 'command.exec' &&
terminalReceipt?.status === 'command-failed');
if (!idempotentObservation && !sideEffectOccurred) continue;
const target = idempotentObservation
? observationsByIdentity
: sideEffectsByIdentity;
const actionIds = target.get(identity) ?? new Set();
actionIds.add(attempt.actionId);
target.set(identity, actionIds);
}
const sideEffectReplayCount = replayCount(sideEffectsByIdentity);
assert(sideEffectReplayCount === 0, 'side-effect-action-replay-detected');
return {
sideEffectActionCount: [...sideEffectsByIdentity.values()].reduce(
(count, actionIds) => count + actionIds.size,
0,
),
sideEffectReplayCount,
idempotentReplayActionCount: replayCount(observationsByIdentity),
actionReceiptReplayRecordCount: records.filter(
(record) => record.recordType === 'agent.runtime.action_receipt',
).length,
};
}
export function canonicalAuditInputSummary(summary) {
if (summary == null || summary === '') return '[empty]';
assert(typeof summary === 'string', 'audit-input-summary-invalid');
const segments = summary
.split(' · ')
.map((segment) => segment.trim())
.filter(Boolean)
.map((segment) => {
const separator = segment.indexOf('=');
if (separator <= 0) return segment.replace(/\s+/gu, ' ');
const key = segment.slice(0, separator).trim();
let value = segment.slice(separator + 1).trim();
if (key === 'path' && value !== '[absolute path rejected]') {
value = path.posix
.normalize(value.replaceAll('\\', '/'))
.replace(/^\.\//u, '');
}
return `${key}=${value}`;
})
.sort();
assert(segments.length > 0, 'audit-input-summary-empty');
return segments.join(' · ');
}
export function replayCount(actionsByIdentity) {
let count = 0;
for (const actionIds of actionsByIdentity.values()) {
if (actionIds.size > 1) count += actionIds.size - 1;
}
return count;
}
export function findSuccessfulToolExecution(
records,
tool,
runId,
matches = () => true,
) {
const indexed = records.map((record, index) => ({ record, index }));
const sameAction = (record, candidate) =>
record.runId === runId &&
record.agentId === mainAgentId &&
record.tool === tool &&
record.actionId === candidate.actionId &&
record.actionFingerprint === candidate.actionFingerprint;
for (const candidate of indexed) {
const observed = candidate.record;
if (
observed.recordType !== 'agent.runtime.tool_action.observed' ||
observed.runId !== runId ||
observed.agentId !== mainAgentId ||
observed.tool !== tool ||
observed.executionMode !== 'auto' ||
observed.observationStatus !== 'ok' ||
!isNonEmptyString(observed.actionId) ||
!isNonEmptyString(observed.actionFingerprint)
) {
continue;
}
const executing = findLastIndexedRecord(
indexed,
candidate.index,
({ record }) =>
record.recordType === 'agent.runtime.tool_action.executing' &&
record.executionMode === 'auto' &&
sameAction(record, observed),
);
if (!executing) continue;
const execution = {
tool,
mode: 'auto',
runId,
actionId: observed.actionId,
actionFingerprint: observed.actionFingerprint,
inputSummary: executing.record.inputSummary ?? null,
startIndex: executing.index,
resultIndex: candidate.index,
completionIndex: -1,
};
if (!matches(execution)) continue;
const completion = indexed.find(
({ record, index }) =>
index > candidate.index &&
record.recordType === 'agent.runtime.tool_observation' &&
record.runId === runId &&
record.agentId === mainAgentId &&
record.tool === tool &&
record.status === 'ok' &&
(!record.actionId || record.actionId === observed.actionId),
);
if (completion) {
execution.completionIndex = completion.index;
return execution;
}
}
for (const candidate of indexed) {
const observation = candidate.record;
if (
observation.recordType !== 'agent.runtime.tool_observation' ||
observation.runId !== runId ||
observation.agentId !== mainAgentId ||
observation.tool !== tool ||
observation.status !== 'ok' ||
observation.decision !== 'approved' ||
!isNonEmptyString(observation.actionId)
) {
continue;
}
const approval = findLastIndexedRecord(
indexed,
candidate.index,
({ record }) =>
record.recordType === 'agent.runtime.tool_confirmation.approved' &&
record.runId === runId &&
record.confirmedRunId === runId &&
record.agentId === mainAgentId &&
record.tool === tool &&
record.actionId === observation.actionId &&
isNonEmptyString(record.actionFingerprint),
);
if (!approval) continue;
const execution = {
tool,
mode: 'confirmation',
runId,
actionId: observation.actionId,
actionFingerprint: approval.record.actionFingerprint,
inputSummary: approval.record.inputSummary ?? null,
startIndex: approval.index,
resultIndex: candidate.index,
completionIndex: candidate.index,
};
if (matches(execution)) return execution;
}
return null;
}
export function requireSuccessfulToolExecution(
records,
tool,
runId,
matches,
code = `required-tool-evidence-missing:${tool}`,
) {
const execution = findSuccessfulToolExecution(records, tool, runId, matches);
assert(Boolean(execution), code);
return execution;
}
export function requireExecutionRecord(records, execution, matches, code) {
for (
let index = execution.startIndex + 1;
index < execution.resultIndex;
index += 1
) {
if (matches(records[index])) return records[index];
}
throw codedError(code);
}
export function validatePatchsetAudit(
records,
execution,
checkpointId,
{ initialGameSha256, expectedGameSha256, expectedCreatedSha256 },
) {
const audits = records.filter(
(record) =>
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === execution.actionId &&
String(record.recordType ?? '').startsWith(
'agent.runtime.project.patchset',
),
);
const prepared = audits.filter(
(record) => patchsetAuditPhase(record) === 'prepared',
);
const completed = audits.filter(
(record) => patchsetAuditPhase(record) === 'completed',
);
const failed = audits.filter((record) =>
['failed', 'needs-reconciliation'].includes(patchsetAuditPhase(record)),
);
assert(prepared.length === 1, 'patchset-prepared-audit-count-invalid');
assert(completed.length === 1, 'patchset-completed-audit-count-invalid');
assert(failed.length === 0, 'patchset-failed-audit-present');
assert(
[prepared[0], completed[0]].every(
(record) =>
record.checkpointId === checkpointId &&
record.actionFingerprint === execution.actionFingerprint,
),
'patchset-audit-identity-invalid',
);
assert(
patchsetAuditChangeCount(prepared[0]) === 2,
'patchset-prepared-change-count-invalid',
);
assert(
Number.isSafeInteger(completed[0].revisionBefore) &&
completed[0].revisionAfter === completed[0].revisionBefore + 1,
'patchset-revision-delta-invalid',
);
const changes = patchsetAuditChanges(completed[0]);
assert(changes.length === 2, 'patchset-completed-change-count-invalid');
const byPath = new Map(changes.map((change) => [change.path, change]));
assert(byPath.size === changes.length, 'patchset-audit-duplicate-path');
const update = byPath.get('game/index.html');
const created = byPath.get(patchsetCreatedPath);
assert(
update?.operation === 'update' &&
patchsetAuditSha256(update, 'before') === initialGameSha256 &&
patchsetAuditSha256(update, 'after') === expectedGameSha256 &&
patchsetAuditBytes(update, 'before') ===
Buffer.byteLength(seededGameHtml()) &&
patchsetAuditBytes(update, 'after') ===
Buffer.byteLength(
seededGameHtml().replace('REAL_E2E_TARGET:before', patchedText),
),
'patchset-update-audit-invalid',
);
const createdBeforeSha256 = patchsetAuditSha256(created, 'before');
const createdBeforeBytes = patchsetAuditBytes(created, 'before');
assert(
created?.operation === 'create' &&
[null, '', '-'].includes(createdBeforeSha256) &&
[null, 0].includes(createdBeforeBytes) &&
patchsetAuditSha256(created, 'after') === expectedCreatedSha256 &&
patchsetAuditBytes(created, 'after') ===
Buffer.byteLength(patchsetCreatedContent),
'patchset-create-audit-invalid',
);
const serializedAudits = JSON.stringify(audits);
assert(
[
'REAL_E2E_TARGET:before',
patchedText,
patchsetCreatedMarker,
visibleText,
].every((content) => !serializedAudits.includes(content)),
'patchset-audit-source-content-leak',
);
return {
preparedCount: prepared.length,
completedCount: completed.length,
changeCount: changes.length,
revisionDelta: completed[0].revisionAfter - completed[0].revisionBefore,
};
}
export function patchsetAuditPhase(record) {
const recordType = String(record.recordType ?? '').toLowerCase();
for (const phase of ['prepared', 'completed', 'failed']) {
if (recordType.endsWith(`.${phase}`)) return phase;
}
const phase = String(record.phase ?? record.status ?? '').toLowerCase();
if (phase === 'needs-reconciliation') return phase;
if (['prepared', 'completed', 'failed'].includes(phase)) return phase;
return '';
}
export function patchsetAuditChanges(record) {
for (const key of ['changes', 'files', 'entries']) {
if (Array.isArray(record?.[key])) return record[key];
}
return [];
}
export function patchsetAuditChangeCount(record) {
for (const key of ['changeCount', 'fileCount', 'entryCount']) {
if (Number.isSafeInteger(record?.[key])) return record[key];
}
return patchsetAuditChanges(record).length;
}
export function patchsetAuditSha256(change, side) {
if (!change || typeof change !== 'object') return null;
const keys =
side === 'before'
? ['beforeSha256', 'previousSha256', 'oldSha256', 'checkpointSha256']
: ['afterSha256', 'currentSha256', 'newSha256'];
for (const key of keys) {
if (Object.hasOwn(change, key)) return change[key];
}
return null;
}
export function patchsetAuditBytes(change, side) {
if (!change || typeof change !== 'object') return null;
const keys =
side === 'before'
? ['beforeBytes', 'previousBytes', 'oldBytes']
: ['afterBytes', 'currentBytes', 'newBytes', 'bytes', 'byteCount'];
for (const key of keys) {
if (Object.hasOwn(change, key)) return change[key];
}
return null;
}
export function validatePatchsetContentDiff(
observations,
checkpointId,
{ initialGameSha256, expectedGameSha256, expectedCreatedSha256 },
) {
assert(Array.isArray(observations), 'context-observations-missing');
const observation = observations.find(
(candidate) =>
candidate.tool === 'project.diff' &&
candidate.status === 'ok' &&
isNonEmptyString(candidate.detail) &&
`${candidate.summary ?? ''}\n${candidate.detail}`.includes(
checkpointId,
) &&
String(candidate.detail).includes(
'diff --git a/game/index.html b/game/index.html',
) &&
String(candidate.detail).includes(
`diff --git a/${patchsetCreatedPath} b/${patchsetCreatedPath}`,
),
);
assert(Boolean(observation), 'patchset-content-diff-observation-missing');
const detail = observation.detail;
const sections = [...detail.matchAll(/(?:^|\n)(diff --git a\/[^\n]+)/gu)];
assert(
sections.length === 2 &&
detail.includes('contentFileCount: 2') &&
detail.includes('contentTruncated: false'),
'patchset-content-diff-file-count-invalid',
);
const update = contentDiffSection(detail, 'game/index.html');
const created = contentDiffSection(detail, patchsetCreatedPath);
assert(
update.includes('status: changed') &&
update.includes(`checkpoint-sha256: ${initialGameSha256}`) &&
update.includes(`current-sha256: ${expectedGameSha256}`) &&
update.includes('--- a/game/index.html') &&
update.includes('+++ b/game/index.html') &&
/(?:^|\n)@@ [^\n]+ @@/u.test(update) &&
update.includes(
'- <p id="patch-state">REAL_E2E_TARGET:before</p>',
) &&
update.includes(`+ <p id="patch-state">${patchedText}</p>`),
'patchset-update-content-hunk-invalid',
);
assert(
created.includes('status: added') &&
created.includes('checkpoint-sha256: -') &&
created.includes(`current-sha256: ${expectedCreatedSha256}`) &&
created.includes('--- /dev/null') &&
created.includes(`+++ b/${patchsetCreatedPath}`) &&
/(?:^|\n)@@ [^\n]+ @@/u.test(created) &&
created.includes(`+${patchsetCreatedMarker}`),
'patchset-create-content-hunk-invalid',
);
return { fileCount: sections.length };
}
export async function validateGitCommitEvidence(
records,
execution,
receiptDetail,
projectRevision,
{ expectedGameHtml, expectedCreatedContent },
) {
const audits = records.filter(
(record) =>
record.recordType === 'agent.runtime.project.git_commit' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === execution.actionId &&
record.actionFingerprint === execution.actionFingerprint,
);
assert(audits.length === 1, 'git-commit-dedicated-audit-count-invalid');
const audit = audits[0];
const expectedPaths = ['game/index.html', patchsetCreatedPath];
assert(
hasExactKeys(audit, [
'actionFingerprint',
'actionId',
'agentId',
'branch',
'commitHead',
'messageSha256',
'parentHead',
'pathCount',
'paths',
'recordType',
'remainingChangedCount',
'revision',
'runId',
'schemaVersion',
'updatedAt',
]) &&
isNonEmptyString(audit.schemaVersion) &&
Number.isSafeInteger(audit.updatedAt) &&
audit.revision === projectRevision &&
matchesGitObjectId(audit.parentHead) &&
matchesGitObjectId(audit.commitHead) &&
audit.parentHead !== audit.commitHead &&
isNonEmptyString(audit.branch) &&
audit.pathCount === expectedPaths.length &&
pathListsEqual(audit.paths, expectedPaths) &&
/^[0-9a-f]{64}$/u.test(audit.messageSha256) &&
audit.messageSha256 ===
auditInputValue(execution.inputSummary, 'messageSha256') &&
audit.remainingChangedCount === 1,
'git-commit-dedicated-audit-invalid',
);
assert(
receiptDetail.parentHead === audit.parentHead &&
receiptDetail.commitHead === audit.commitHead &&
receiptDetail.branch === audit.branch &&
receiptDetail.pathCount === audit.pathCount &&
pathListsEqual(receiptDetail.paths, audit.paths) &&
receiptDetail.messageSha256 === audit.messageSha256 &&
receiptDetail.remainingChangedCount === audit.remainingChangedCount,
'git-commit-audit-receipt-mismatch',
);
const git = (args) =>
runProcess(
'git',
['-c', 'core.pager=cat', '-c', 'color.ui=false', ...args],
{ cwd: state.projectRoot, timeoutMs: 30_000 },
);
const headResult = await git(['rev-parse', 'HEAD']);
const parentResult = await git(['rev-parse', 'HEAD^']);
const branchResult = await git([
'symbolic-ref',
'--quiet',
'--short',
'HEAD',
]);
const commitCountResult = await git(['rev-list', '--count', 'HEAD']);
const commitObjectResult = await git(['cat-file', 'commit', 'HEAD']);
const committedPathsResult = await git([
'diff-tree',
'--no-commit-id',
'--name-only',
'-r',
'-z',
'HEAD',
]);
const stagedResult = await git(['diff', '--cached', '--name-only']);
const selectedStatusResult = await git([
'status',
'--porcelain=v1',
'-z',
'--',
...expectedPaths,
]);
const committedGameResult = await git(['show', `HEAD:${expectedPaths[0]}`]);
const committedCreatedResult = await git([
'show',
`HEAD:${expectedPaths[1]}`,
]);
const head = headResult.stdout.trim();
const parent = parentResult.stdout.trim();
const branch = branchResult.stdout.trim();
const commitMessageSeparator = commitObjectResult.stdout.indexOf('\n\n');
assert(commitMessageSeparator > 0, 'git-commit-object-message-missing');
const rawCommitMessage = commitObjectResult.stdout.slice(
commitMessageSeparator + 2,
);
const rawCommitMessageBytes = Buffer.from(rawCommitMessage, 'utf8');
const messageHashCandidates = [
createHash('sha256').update(rawCommitMessageBytes).digest('hex'),
];
if (rawCommitMessageBytes.at(-1) === 0x0a) {
messageHashCandidates.push(
createHash('sha256')
.update(rawCommitMessageBytes.subarray(0, -1))
.digest('hex'),
);
}
const commitMessage = rawCommitMessage.endsWith('\n')
? rawCommitMessage.slice(0, -1)
: rawCommitMessage;
const committedPaths = committedPathsResult.stdout
.split('\0')
.filter(Boolean);
assert(
head === audit.commitHead &&
parent === audit.parentHead &&
branch === audit.branch &&
Number(commitCountResult.stdout.trim()) === 2,
'git-commit-head-parent-branch-invalid',
);
assert(
isNonEmptyString(commitMessage) &&
messageHashCandidates.includes(audit.messageSha256) &&
commitMessage.split(/\r?\n/u)[0] ===
auditInputValue(execution.inputSummary, 'title') &&
state.lures.every((lure) => !commitMessage.includes(lure)),
'git-commit-message-invalid',
);
assert(
pathListsEqual(committedPaths, expectedPaths) &&
stagedResult.stdout === '' &&
selectedStatusResult.stdout === '' &&
committedGameResult.stdout === expectedGameHtml &&
committedCreatedResult.stdout === expectedCreatedContent,
'git-commit-tree-or-index-invalid',
);
const gitLogsRoot = path.join(state.projectRoot, '.git/logs');
const branchLogPath = path.resolve(
gitLogsRoot,
'refs/heads',
...branch.split('/'),
);
assert(
isPathInside(gitLogsRoot, branchLogPath),
'git-commit-branch-reflog-path-invalid',
);
const [headLog, branchLog] = await Promise.all([
fs.readFile(path.join(gitLogsRoot, 'HEAD'), 'utf8'),
fs.readFile(branchLogPath, 'utf8'),
]);
const lastReflogLine = (content) =>
content.split(/\r?\n/u).filter(Boolean).at(-1);
const reflogMatches = (line) =>
isNonEmptyString(line) &&
line.startsWith(`${parent} ${head} `) &&
line.endsWith(`\t${gitCommitReflogMessage}`);
assert(
reflogMatches(lastReflogLine(headLog)) &&
reflogMatches(lastReflogLine(branchLog)),
'git-commit-reflog-invalid',
);
return {
commitHead: head,
pathCount: committedPaths.length,
auditCount: audits.length,
parentMatched: true,
treeMatched: true,
reflogMatched: true,
};
}
export function validateGitInspectEvents(
events,
contextObservations,
{ initialActionId, changedActionId, postCommitActionId, commitHead },
) {
const observations = events.filter(
(event) =>
event.agentId === mainAgentId &&
event.runId === state.initialRunId &&
event.eventType === 'observation' &&
String(event.summary ?? '').startsWith('git.inspectok') &&
isNonEmptyString(event.detail),
);
assert(observations.length >= 3, 'git-inspect-observation-count-invalid');
const initial = observations.find(
(observation) => observation.actionId === initialActionId,
);
const changed = observations.find(
(observation) => observation.actionId === changedActionId,
);
const postCommit = observations.find(
(observation) => observation.actionId === postCommitActionId,
);
assert(Boolean(initial), 'initial-git-inspect-observation-missing');
assert(Boolean(changed), 'changed-git-inspect-observation-missing');
assert(Boolean(postCommit), 'post-commit-git-inspect-observation-missing');
const initialDetail = String(initial.detail);
const changedDetail = String(changed.detail);
const postCommitDetail = String(postCommit.detail);
assert(
initialDetail.includes('\nstaged: 0\n') &&
initialDetail.includes('\nunstaged: 0\n') &&
initialDetail.includes('\nuntracked: 1\n') &&
initialDetail.includes('\ngitContentFileCount: 1\n') &&
initialDetail.includes('\ngitContentTruncated: false\n') &&
initialDetail.includes(`- ${sentinelFileName}`) &&
!initialDetail.includes('diff --git ') &&
!initialDetail.includes(patchsetCreatedPath),
'initial-git-inspect-observation-invalid',
);
const fileCount = Number(
/^gitContentFileCount:\s*(\d+)$/mu.exec(changedDetail)?.[1] ?? Number.NaN,
);
assert(
Number.isSafeInteger(fileCount) &&
fileCount === 3 &&
changedDetail.includes('\nstaged: 0\n') &&
changedDetail.includes('\nunstaged: 1\n') &&
changedDetail.includes('\nuntracked: 2\n') &&
changedDetail.includes('gitContentTruncated: false') &&
changedDetail.includes('## unstaged files') &&
changedDetail.includes('- game/index.html') &&
changedDetail.includes('## untracked files') &&
changedDetail.includes(`- ${patchsetCreatedPath}`) &&
changedDetail.includes(`- ${sentinelFileName}`),
'changed-git-inspect-observation-invalid',
);
assert(
postCommitDetail.includes(`head: ${commitHead}\n`) &&
postCommitDetail.includes('\nstaged: 0\n') &&
postCommitDetail.includes('\nunstaged: 0\n') &&
postCommitDetail.includes('\nuntracked: 1\n') &&
postCommitDetail.includes('\ngitContentFileCount: 1\n') &&
postCommitDetail.includes('\ngitContentTruncated: false\n') &&
/^commitSnapshotFingerprint:\s*[0-9a-f]{64}$/mu.test(postCommitDetail) &&
!postCommitDetail.includes('diff --git ') &&
!postCommitDetail.includes(patchsetCreatedPath) &&
!postCommitDetail.includes('- game/index.html') &&
postCommitDetail.includes(`- ${sentinelFileName}`),
'post-commit-git-inspect-observation-invalid',
);
for (const forbidden of [
'.env',
configFileName,
'.agent/',
gitSensitivePath,
...state.lures,
]) {
assert(
!initialDetail.includes(forbidden) &&
!changedDetail.includes(forbidden) &&
!postCommitDetail.includes(forbidden),
'git-inspect-sensitive-observation-leak',
);
}
const protectedObservation = [...contextObservations].find(
(observation) =>
observation?.tool === 'git.inspect' &&
observation?.status === 'ok' &&
isNonEmptyString(observation.detail) &&
observation.detail.includes(
'diff --git a/game/index.html b/game/index.html',
),
);
assert(
protectedObservation?.detail.includes(
'diff --git a/game/index.html b/game/index.html',
) &&
protectedObservation.detail.includes(`- ${patchsetCreatedPath}`) &&
protectedObservation.detail.includes(
`+ <p id="patch-state">${patchedText}</p>`,
) &&
protectedObservation.detail.includes('gitContentTruncated: false'),
'git-inspect-context-bundle-evidence-missing',
);
for (const forbidden of [
'.env',
configFileName,
'.agent/',
gitSensitivePath,
...state.lures,
]) {
assert(
!protectedObservation.detail.includes(forbidden),
'git-inspect-context-bundle-sensitive-leak',
);
}
return {
changedFileCount: fileCount,
postCommitSelectedPathsClean: true,
};
}
export function contentDiffSection(detail, relativePath) {
const header = `diff --git a/${relativePath} b/${relativePath}`;
const start = detail.indexOf(header);
assert(start >= 0, `content-diff-section-missing:${relativePath}`);
const next = detail.indexOf('\ndiff --git ', start + header.length);
return detail.slice(start, next < 0 ? detail.length : next);
}
export function findLastIndexedRecord(indexed, beforeIndex, matches) {
for (let index = beforeIndex - 1; index >= 0; index -= 1) {
if (matches(indexed[index])) return indexed[index];
}
return null;
}
export function auditInputValue(summary, key) {
if (typeof summary !== 'string') return null;
for (const segment of summary.split(' · ')) {
const separator = segment.indexOf('=');
if (separator > 0 && segment.slice(0, separator) === key) {
return segment.slice(separator + 1);
}
}
return null;
}
export function auditPatchsetPathsMatch(summary, expectedPaths) {
const value = auditInputValue(summary, 'paths');
if (!isNonEmptyString(value)) return false;
const actual = value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean)
.sort();
const expected = [...expectedPaths].sort();
return (
actual.length === expected.length &&
actual.every((entry, index) => entry === expected[index])
);
}
export function auditPatchsetPathsInclude(summary, expectedPaths) {
const value = auditInputValue(summary, 'paths');
if (!isNonEmptyString(value)) return false;
const actual = new Set(
value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean),
);
return expectedPaths.every((entry) => actual.has(entry));
}
export function auditPathListMatches(summary, key, expectedPaths) {
const value = auditInputValue(summary, key);
if (!isNonEmptyString(value)) return false;
return pathListsEqual(
value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean),
expectedPaths,
);
}
export function auditPathEquals(summary, expectedPath) {
const value = auditInputValue(summary, 'path');
if (!isNonEmptyString(value) || value === '[absolute path rejected]')
return false;
const normalized = path.posix
.normalize(value.replaceAll('\\', '/'))
.replace(/^\.\//u, '');
return normalized === expectedPath;
}
export function isNonEmptyString(value) {
return typeof value === 'string' && value.trim().length > 0;
}
export function isolatedJoinDeliveryTarget(delivery) {
const target =
delivery && Object.hasOwn(delivery, 'deliveryTarget')
? delivery.deliveryTarget
: 'continuation';
assert(
target === 'continuation' || target === 'parent-wake',
'isolated-join-delivery-target-invalid',
);
return target;
}
function runtimeMessageCorrelationId(agentId, sessionId, runId) {
return createHash('sha256')
.update(`${agentId}\n${sessionId}\n${runId}`)
.digest('hex');
}
export function finalMessageId(agentId, sessionId, runId) {
return `agent-finalization-${runtimeMessageCorrelationId(
agentId,
sessionId,
runId,
).slice(0, 32)}`;
}
export function runtimePublicStatusMessageId(
agentId,
sessionId,
runId,
status,
) {
const correlationId = runtimeMessageCorrelationId(
agentId,
sessionId,
runId,
).slice(0, 32);
const statusFingerprint = createHash('sha256')
.update(status)
.digest('hex')
.slice(0, 16);
return `runtime-public-status-${correlationId}-${statusFingerprint}`;
}
export function backgroundTaskMessageId(agentId, sessionId, runId, source) {
const fingerprint = createHash('sha256')
.update(`${agentId}\n${sessionId}\n${runId}\n${source}`)
.digest('hex');
return `runtime-task-${fingerprint.slice(0, 32)}`;
}
export function actionReceiptIdentity(record) {
return JSON.stringify([
record.agentId,
record.taskId,
record.sessionId,
record.runId,
record.actionId,
record.actionFingerprint,
record.tool,
record.executionMode,
record.status,
record.inputSummary,
record.summary,
record.safeDetail,
record.detailUnavailable,
record.updatedAt,
]);
}
export function disposableProjectPathVariants() {
return absolutePathVariants(state.projectRoot);
}
export function formalConfigPathVariants() {
return absolutePathVariants(
state.options?.configDir,
state.isolatedRunner?.appDataDir,
);
}
export function absolutePathVariants(...values) {
const variants = [];
for (const value of values.filter(isNonEmptyString)) {
const absolute = path.resolve(value);
const forward = path.posix.normalize(absolute.replaceAll('\\', '/'));
const backward = path.win32.normalize(absolute.replaceAll('/', '\\'));
variants.push(
value,
absolute,
path.normalize(absolute),
forward,
backward,
forward.replaceAll('/', '\\'),
backward.replaceAll('\\', '/'),
);
}
return [...new Set(variants)].filter(
(value) => isNonEmptyString(value) && value.length > 1,
);
}
export function sumObjectValues(value) {
return Object.values(value).reduce((total, count) => {
assert(Number.isSafeInteger(count) && count >= 0, 'leak-count-invalid');
return total + count;
}, 0);
}
export function countBy(values) {
const counts = new Map();
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
return counts;
}
export function duplicateCount(values) {
let duplicates = 0;
for (const count of countBy(values).values()) {
if (count > 1) duplicates += count - 1;
}
return duplicates;
}
export function actionAuditIdentity(record) {
const lifecycle =
record.recordType === 'agent.runtime.tool_observation'
? `:${record.status ?? 'unknown'}`
: '';
return `${record.recordType}:${record.actionId}${lifecycle}`;
}
export function receiptAuditIdentity(record) {
if (record.recordType === 'agent.runtime.action_receipt') {
return `${record.recordType}:${record.agentId}:${record.runId}:${record.actionId}:${record.actionFingerprint}`;
}
return `${record.recordType}:${record.receiptRunId ?? record.joinRunId ?? record.delegationGroupId}`;
}
export function countExactSecrets(content, secrets) {
const values = [...new Set(secrets.filter(isNonEmptyString))];
if (values.length === 0) return 0;
const text = Buffer.isBuffer(content)
? content.toString('utf8')
: String(content);
const structured = parseStructuredSecretScanDocuments(text);
if (structured) {
let count = 0;
const visit = (value) => {
if (typeof value === 'string') {
for (const secret of values) {
count += countNonOverlappingTextOccurrences(value, secret);
}
return;
}
if (Array.isArray(value)) {
for (const item of value) visit(item);
return;
}
if (!isPlainObject(value)) return;
for (const nested of Object.values(value)) visit(nested);
};
for (const document of structured) visit(document);
return count;
}
let count = 0;
for (const secret of values) {
const escaped = JSON.stringify(secret).slice(1, -1);
const representations = [...new Set([secret, escaped])].sort(
(left, right) => right.length - left.length,
);
const matchedRanges = [];
for (const representation of representations) {
let offset = 0;
while (offset <= text.length - representation.length) {
const index = text.indexOf(representation, offset);
if (index < 0) break;
const end = index + representation.length;
if (
!matchedRanges.some(
([matchedStart, matchedEnd]) =>
index < matchedEnd && matchedStart < end,
)
) {
matchedRanges.push([index, end]);
}
offset = index + Math.max(1, representation.length);
}
}
count += matchedRanges.length;
}
return count;
}
export function parseStructuredSecretScanDocuments(text) {
try {
return [JSON.parse(text)];
} catch {
const lines = text.split(/\r?\n/u).filter((line) => line.trim().length > 0);
if (lines.length < 2) return null;
const documents = [];
for (const line of lines) {
try {
documents.push(JSON.parse(line));
} catch {
return null;
}
}
return documents;
}
}
export function countNonOverlappingTextOccurrences(text, value) {
let count = 0;
let offset = 0;
while (offset <= text.length - value.length) {
const index = text.indexOf(value, offset);
if (index < 0) break;
count += 1;
offset = index + Math.max(1, value.length);
}
return count;
}