UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

174 lines • 8.23 kB
/** * Strict saved claim-verification protocol. Role: validate and detach a recorded * check, never reconstruct evidence or rerun its checker. Storage authenticity * and caller authorization are the surrounding checkpoint owner's responsibility. */ import { copyJson, ownData } from '../../../lib/boundedJson.js'; import { canonicalJson } from '../../../lib/canonicalJson.js'; import { decodePresentIdentityField, encodeIdentityField, } from '../../../memory/identity/encode.js'; import { comparableValueOf, participates } from '../../../integrity/assertion/types.js'; import { BINDING_LIMITS, boundedText } from '../../../integrity/claim-binding/scope.js'; import { validateCoverage } from '../../../integrity/claim-binding/admit.js'; import { readResultIdentity } from '../../../integrity/claim-binding/capture.js'; import { periodProblem } from '../../../semantics/coverage/period.js'; import { STANDING_VALUES } from './types.js'; import { VERIFICATION_UNVERIFIED_REASONS } from './verificationTypes.js'; /** Reuse receipt bounds; declared values have the compact binding-request cap. */ export const VERIFICATION_LIMITS = Object.freeze({ rowBytes: BINDING_LIMITS.resultBytes, valueBytes: BINDING_LIMITS.scopeBytes, identityText: BINDING_LIMITS.resultIdText + 'claim-verification'.length - 'claim-result'.length, }); /** A runtime-reserved claim occurrence cannot collide with a result occurrence. */ export function claimIdentityOf(raw) { const identity = ownData(raw, ['namespace', 'runId', 'ordinal']); if (!boundedText(identity.namespace) || !boundedText(identity.runId) || typeof identity.ordinal !== 'number' || !Number.isSafeInteger(identity.ordinal) || identity.ordinal < 1) throw new TypeError('Invalid runtime claim identity.'); return `claim-verification/${encodeIdentityField(identity.namespace)}/${encodeIdentityField(identity.runId)}/${identity.ordinal}`; } /** Canonical claim tuple reader, shared with checkpoint lineage/counter checks. */ export function readClaimIdentity(value) { if (typeof value !== 'string' || value.length > VERIFICATION_LIMITS.identityText) throw new TypeError('Invalid saved claim identity.'); const parts = value.split('/'); if (parts.length !== 4 || parts[0] !== 'claim-verification') throw new TypeError('Invalid saved claim identity.'); const identity = { namespace: decodePresentIdentityField(parts[1]), runId: decodePresentIdentityField(parts[2]), ordinal: Number(parts[3]), }; if (value !== claimIdentityOf(identity)) throw new TypeError('Noncanonical saved claim identity.'); return Object.freeze(identity); } function integer(value, minimum) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum; } function eligibleAssertion(raw) { const assertion = ownData(raw, ['subject', 'predicate', 'value', 'stratum', 'provenance']); const subject = ownData(assertion.subject, ['kind', 'id']); const comparable = comparableValueOf(assertion.value); if (subject.kind !== 'entity' || !boundedText(subject.id) || !boundedText(assertion.predicate) || assertion.stratum !== 'asserted' || typeof assertion.provenance !== 'string' || !Object.hasOwn(assertion, 'value') || !participates(assertion.value) || comparable === null || comparable === undefined || comparable === 'unknown') throw new TypeError('Invalid eligible assertion.'); return assertion; } function validateResolved(row, claimIdentity) { const resolved = ownData(row.resolved, [ 'resultId', 'rule', 'scope', 'coverage', 'period', 'evidence', 'claim', ]); const rule = ownData(resolved.rule, ['id', 'version']); const scope = ownData(copyJson(resolved.scope, BINDING_LIMITS.scopeBytes, true)); const resultIdentity = readResultIdentity(resolved.resultId); if (resultIdentity.namespace !== claimIdentity.namespace || resolved.resultId !== row.declaredResult || !boundedText(rule.id) || rule.id !== row.declaredRule || !boundedText(rule.version) || Object.keys(scope).length === 0 || Object.keys(scope).length > BINDING_LIMITS.dimensions || !Object.keys(scope).every(boundedText)) throw new TypeError('Invalid verification association.'); validateCoverage(resolved.coverage); if (Object.hasOwn(resolved, 'period') && periodProblem(resolved.period, 'camel') !== undefined) throw new TypeError('Invalid verification period.'); const evidence = eligibleAssertion(resolved.evidence); const claim = eligibleAssertion(resolved.claim); if (canonicalJson(evidence.subject) !== canonicalJson(claim.subject) || evidence.predicate !== claim.predicate || evidence.provenance !== `result ${resolved.resultId}` || claim.provenance !== `rule ${rule.id}` || canonicalJson(claim.value) !== canonicalJson(row.declaredValue)) throw new TypeError('Invalid verification pair association.'); } /** Validate the recorded protocol without evaluating whether its value agrees. */ export function readClaimVerificationRow(raw) { const detached = copyJson(raw, VERIFICATION_LIMITS.rowBytes, true); const row = ownData(detached, [ 'kind', 'claimId', 'declaredOn', 'standing', 'iteration', 'turn', 'checker', 'declaredResult', 'declaredRule', 'declaredValue', 'outcome', 'reason', 'resolved', ]); const checker = ownData(row.checker, ['id', 'version']); const claimIdentity = readClaimIdentity(row.claimId); if (row.kind !== 'claim-verification' || typeof row.standing !== 'string' || !STANDING_VALUES.includes(row.standing) || !integer(row.iteration, 0) || (Object.hasOwn(row, 'turn') && !integer(row.turn, 1)) || checker.id !== 'claim-pair' || checker.version !== 1 || !Object.hasOwn(row, 'declaredValue') || (Object.hasOwn(row, 'declaredResult') && (typeof row.declaredResult !== 'string' || row.declaredResult.length > BINDING_LIMITS.resultIdText)) || (Object.hasOwn(row, 'declaredRule') && !boundedText(row.declaredRule))) throw new TypeError('Invalid claim-verification row.'); copyJson(row.declaredValue, VERIFICATION_LIMITS.valueBytes, true); if (row.declaredOn !== 'answer') { const at = ownData(row.declaredOn, ['toolCallId']); if (!boundedText(at.toolCallId)) throw new TypeError('Invalid claim declaration location.'); } if (row.outcome === 'unverified') { if (typeof row.reason !== 'string' || !VERIFICATION_UNVERIFIED_REASONS.includes(row.reason) || Object.hasOwn(row, 'resolved') || (row.standing === 'noise' && row.reason !== 'ineligible-noise') || (row.reason === 'ineligible-noise' && row.standing !== 'noise') || (row.reason === 'missing-check' && Object.hasOwn(row, 'declaredRule')) || (!['missing-check', 'malformed-check', 'ineligible-noise'].includes(row.reason) && (!Object.hasOwn(row, 'declaredRule') || (row.reason !== 'missing-result' && !Object.hasOwn(row, 'declaredResult'))))) throw new TypeError('Invalid unverified claim row.'); } else { if (row.standing === 'noise' || !((row.outcome === 'supported' && (row.reason === 'no-conflict' || row.reason === 'numeric-leniency')) || (row.outcome === 'contradicted' && row.reason === 'conflict'))) throw new TypeError('Invalid compared claim row.'); validateResolved(row, claimIdentity); } return detached; } /** Non-throwing checkpoint union guard; admission uses the same strict reader. */ export function isClaimVerificationRow(raw) { try { readClaimVerificationRow(raw); return true; } catch { return false; } } //# sourceMappingURL=verificationRow.js.map