2618 lines
89 KiB
JavaScript
2618 lines
89 KiB
JavaScript
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_')
|
||
);
|
||
}
|
||
|
||
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_history:ok') &&
|
||
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.inspect:ok') &&
|
||
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;
|
||
}
|