mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-17 23:28:04 +02:00
* feat: consolidate offline eval and operator workflows
Compose the retained framework, operator skill, roadmap and cleanup ranges on current main. Preserve current release dependencies and keep candidate execution disabled pending OS containment. Repair draft/DOCX behavior, obligation uniqueness, trusted send and audience guidance, runner provenance and eval diagnostics.
Source-PR: 2930 0abe3727d2b500c6e4830bdeb47ed67cae3f4785
Source-PR: 2931 992b49c44ed872def49675b791168b8fcd091df6
Source-PR: 2932 4a193dd13041cb7a6bebf4d2e910a0cd32bcc797
Source-PR: 2933 59cdfe500a91949ba1415f1edd7279620f21e804
Source-Base: ca185ef5f7
* fix: repair foundation CI and update js-yaml
* fix: reconcile pending-delete capsule locks after close
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
181 lines
7.7 KiB
JavaScript
181 lines
7.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Offline-verifiable capsule receipts.
|
|
*
|
|
* Framework 5 of the eval-harness set (verifiable receipts, local only).
|
|
* A receipt names the capsule root hash, entry count, schema version, the
|
|
* artifact digest under evaluation, and the gate receipt digest. It can be
|
|
* verified on a machine that never sees the source store as long as it has
|
|
* the exported bundle. The signature field is a detached interface: callers
|
|
* pass a signer/verifier pair; nothing here generates or stores keys.
|
|
*
|
|
* Signatures prove who vouched for the bytes, not that the run was correct.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { isDeepStrictEqual } = require('util');
|
|
|
|
const { canonicalJson, hashValue, sha256Hex } = require('./canonical');
|
|
const capsule = require('./capsule');
|
|
const envelope = require('./envelope');
|
|
|
|
const RECEIPT_SCHEMA = 'capsule-receipt/v1';
|
|
|
|
function digestFile(filePath) {
|
|
return sha256Hex(fs.readFileSync(filePath));
|
|
}
|
|
|
|
/**
|
|
* Build a receipt and persist its verified projection in the capsule directory.
|
|
* options: { artifact_path | artifact_digest, gate_receipt (object), signer(fn) }
|
|
*/
|
|
function buildReceipt(capsuleDir, options = {}) {
|
|
if (options.artifact_digest !== undefined && options.artifact_digest !== null
|
|
&& (typeof options.artifact_digest !== 'string' || !envelope.HASH_PATTERN.test(options.artifact_digest))) {
|
|
throw new capsule.CapsuleError('receipt.schema_invalid', 'artifact_digest must be a SHA-256 digest or null');
|
|
}
|
|
const artifactDigest = options.artifact_digest
|
|
|| (options.artifact_path ? digestFile(options.artifact_path) : null);
|
|
const projection = capsule.writeProjection(capsuleDir);
|
|
const receipt = {
|
|
schema: RECEIPT_SCHEMA,
|
|
envelope_schema: envelope.SCHEMA_VERSION,
|
|
capsule_id: projection.capsule_id,
|
|
run_id: projection.run_id,
|
|
capsule_root: projection.root_hash,
|
|
entry_count: projection.entry_count,
|
|
journal_sha256: projection.journal_sha256,
|
|
projection_hash: projection.projection_hash,
|
|
artifact_digest: artifactDigest,
|
|
gate_receipt_digest: options.gate_receipt ? hashValue(options.gate_receipt) : null,
|
|
gate_verdict: options.gate_receipt ? options.gate_receipt.verdict || null : null,
|
|
created_at: (options.clock ? options.clock() : new Date()).toISOString(),
|
|
signature: null,
|
|
};
|
|
const receiptHash = hashValue(receipt);
|
|
return {
|
|
...receipt,
|
|
receipt_hash: receiptHash,
|
|
signature: typeof options.signer === 'function' ? options.signer(receiptHash) : null,
|
|
};
|
|
}
|
|
|
|
function validReceiptSchema(receipt) {
|
|
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)
|
|
|| receipt.schema !== RECEIPT_SCHEMA || receipt.envelope_schema !== envelope.SCHEMA_VERSION
|
|
|| !Number.isSafeInteger(receipt.entry_count) || receipt.entry_count < 0) return false;
|
|
for (const field of ['run_id', 'capsule_id']) {
|
|
if (typeof receipt[field] !== 'string' || !envelope.ID_PATTERN.test(receipt[field])) return false;
|
|
}
|
|
for (const field of ['capsule_root', 'journal_sha256', 'projection_hash', 'receipt_hash']) {
|
|
if (typeof receipt[field] !== 'string' || !envelope.HASH_PATTERN.test(receipt[field])) return false;
|
|
}
|
|
for (const field of ['artifact_digest', 'gate_receipt_digest']) {
|
|
if (receipt[field] !== null && (typeof receipt[field] !== 'string' || !envelope.HASH_PATTERN.test(receipt[field]))) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** Read and compare the supplied projection without writing or regenerating it. */
|
|
function projectionMatches(dir, expected, receipt) {
|
|
try {
|
|
const bytes = fs.readFileSync(path.join(path.resolve(dir), capsule.PROJECTION_FILE));
|
|
const raw = bytes.toString('utf8');
|
|
if (!bytes.equals(Buffer.from(raw, 'utf8'))) return false;
|
|
const stored = JSON.parse(raw);
|
|
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return false;
|
|
const { projection_hash: claimed, ...body } = stored;
|
|
return hashValue(body) === claimed && claimed === receipt.projection_hash
|
|
&& isDeepStrictEqual(stored, expected);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify a receipt against a capsule directory (or exported bundle).
|
|
* Returns { ok, check, reason }. `check` names the first failing check:
|
|
* schema, receipt_hash, signature, journal_present, journal_integrity,
|
|
* truncation, stale_checkpoint, capsule_root, metadata, projection, artifact, gate_receipt.
|
|
*/
|
|
function verifyReceipt(receipt, capsuleDir, options = {}) {
|
|
const fail = (check, reason) => ({ ok: false, check, reason });
|
|
if (!validReceiptSchema(receipt)) {
|
|
return fail('schema', 'receipt schema, count, identity or digest fields are invalid');
|
|
}
|
|
const { receipt_hash: claimedHash, signature, ...unsigned } = receipt;
|
|
const recomputed = hashValue({ ...unsigned, signature: null });
|
|
if (recomputed !== claimedHash) {
|
|
return fail('receipt_hash', 'receipt content does not match receipt_hash');
|
|
}
|
|
if (typeof options.verifier === 'function') {
|
|
if (!signature) {
|
|
return fail('signature', 'receipt is unsigned but a verifier was supplied');
|
|
}
|
|
if (!options.verifier(claimedHash, signature)) {
|
|
return fail('signature', 'signature does not verify for this receipt_hash');
|
|
}
|
|
}
|
|
const journalPath = path.join(path.resolve(capsuleDir), capsule.JOURNAL_FILE);
|
|
if (!fs.existsSync(journalPath)) {
|
|
return fail('journal_present', 'journal.ndjson missing from capsule directory');
|
|
}
|
|
const state = capsule.readCapsule(capsuleDir);
|
|
if (!state.ok) {
|
|
const check = state.code.startsWith('capsule.metadata_') ? 'metadata' : 'journal_integrity';
|
|
return fail(check, `${state.reason} (entry ${state.failed_at})`);
|
|
}
|
|
if (state.entries.length < receipt.entry_count) {
|
|
return fail('truncation', `journal has ${state.entries.length} entries, receipt names ${receipt.entry_count}`);
|
|
}
|
|
const rootAtReceipt = receipt.entry_count === 0
|
|
? envelope.GENESIS_HASH
|
|
: state.entries[receipt.entry_count - 1].entry_hash;
|
|
if (rootAtReceipt !== receipt.capsule_root) {
|
|
return fail('capsule_root', 'journal prefix does not reproduce the receipt capsule_root');
|
|
}
|
|
if (state.entries.length > receipt.entry_count) {
|
|
return fail('stale_checkpoint', `journal advanced to ${state.entries.length} entries after the receipt (prefix verified)`);
|
|
}
|
|
if (receipt.journal_sha256 !== state.journal_sha256) {
|
|
return fail('journal_integrity', 'journal bytes differ from receipt journal_sha256');
|
|
}
|
|
if (receipt.run_id !== state.meta.run_id || receipt.capsule_id !== state.meta.capsule_id) {
|
|
return fail('metadata', 'receipt identity differs from the verified capsule');
|
|
}
|
|
if (!projectionMatches(capsuleDir, state.projection, receipt)) {
|
|
return fail('projection', 'projection is missing, unreadable, corrupt or differs from the verified capsule and receipt');
|
|
}
|
|
if (options.artifact_path) {
|
|
let digest;
|
|
try { digest = digestFile(options.artifact_path); } catch {
|
|
return fail('artifact', 'artifact could not be read');
|
|
}
|
|
if (digest !== receipt.artifact_digest) {
|
|
return fail('artifact', 'artifact digest does not match receipt');
|
|
}
|
|
} else if (options.artifact_digest && options.artifact_digest !== receipt.artifact_digest) {
|
|
return fail('artifact', 'artifact digest does not match receipt');
|
|
}
|
|
if (options.gate_receipt && hashValue(options.gate_receipt) !== receipt.gate_receipt_digest) {
|
|
return fail('gate_receipt', 'gate receipt digest does not match receipt');
|
|
}
|
|
return { ok: true, check: null, reason: 'receipt verified' };
|
|
}
|
|
|
|
function writeReceipt(receipt, filePath) {
|
|
fs.mkdirSync(path.dirname(path.resolve(filePath)), { recursive: true });
|
|
fs.writeFileSync(filePath, canonicalJson(receipt) + '\n', 'utf8');
|
|
return path.resolve(filePath);
|
|
}
|
|
|
|
module.exports = {
|
|
RECEIPT_SCHEMA,
|
|
buildReceipt,
|
|
verifyReceipt,
|
|
writeReceipt,
|
|
digestFile,
|
|
};
|