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>
252 lines
9.1 KiB
JavaScript
252 lines
9.1 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* capsule-envelope/v1: the portable record contract for one journal entry.
|
|
*
|
|
* Framework 1 of the eval-harness set (telemetry and capsule contract).
|
|
* The envelope is deliberately small. It carries identity, lineage, effect
|
|
* class, a hash link to its predecessor, and an allowlisted payload. Raw
|
|
* secrets, credentials, and unrestricted reasoning text never enter the
|
|
* default envelope: the payload passes through a default-deny property
|
|
* allowlist and a secret canary scan before it is written.
|
|
*/
|
|
|
|
const { hashValue } = require('./canonical');
|
|
|
|
const SCHEMA_VERSION = 'capsule-envelope/v1';
|
|
|
|
/** The five append-only lineages a capsule records. */
|
|
const LINEAGES = Object.freeze(['plan', 'attempt', 'interaction', 'environment', 'strategy']);
|
|
|
|
/**
|
|
* Side-effect classes, ordered from pure to irreversible.
|
|
* SE0 read-only evaluation. SE1 reversible local writes inside a capsule root.
|
|
* SE2 sandboxed process or filesystem mutation, no live network writes.
|
|
* SE3 append-only remote evidence publication. SE4 economic or external effects.
|
|
*/
|
|
const EFFECT_CLASSES = Object.freeze(['SE0', 'SE1', 'SE2', 'SE3', 'SE4']);
|
|
|
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
const HASH_PATTERN = /^[0-9a-f]{64}$/;
|
|
const GENESIS_HASH = '0'.repeat(64);
|
|
|
|
/** Scalar types mirror schemas/capsule-envelope.schema.json. */
|
|
const PAYLOAD_TYPES = Object.freeze({
|
|
task_id: 'string',
|
|
task_family: 'string',
|
|
tool: 'string',
|
|
tool_call_id: 'string',
|
|
args_hash: 'string',
|
|
response_hash: 'string',
|
|
status: 'string',
|
|
exit_code: 'integer|null',
|
|
duration_ms: 'number',
|
|
tokens_in: 'integer',
|
|
tokens_out: 'integer',
|
|
cost_usd: 'number',
|
|
model: 'string',
|
|
message: 'string',
|
|
note: 'string',
|
|
decision: 'string',
|
|
reason: 'string',
|
|
score: 'number',
|
|
passed: 'integer',
|
|
failed: 'integer',
|
|
total: 'integer',
|
|
variant: 'string',
|
|
digest: 'string',
|
|
path: 'string',
|
|
fixture_key: 'string',
|
|
stage: 'string',
|
|
verdict: 'string',
|
|
hits: 'integer',
|
|
branch_id: 'string',
|
|
parent_branch_id: 'string',
|
|
summary: 'string',
|
|
});
|
|
const DEFAULT_PAYLOAD_ALLOWLIST = Object.freeze(Object.keys(PAYLOAD_TYPES));
|
|
const ENVELOPE_FIELDS = new Set([
|
|
'schema', 'run_id', 'capsule_id', 'seq', 'ts', 'lineage', 'kind',
|
|
'effect_class', 'harness_version', 'task_family', 'parent_hash', 'entry_hash', 'payload',
|
|
]);
|
|
|
|
/**
|
|
* Secret and credential canaries. A match anywhere in a payload string is
|
|
* a hard refusal: the entry is not written and the caller sees which
|
|
* canary fired. Patterns are intentionally broad and cheap.
|
|
*/
|
|
const SECRET_CANARIES = Object.freeze([
|
|
{ name: 'private_key_block', pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
{ name: 'aws_access_key', pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
{ name: 'openai_style_key', pattern: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
|
|
{ name: 'github_token', pattern: /\bgh[pousr]_[A-Za-z0-9]{30,}\b/ },
|
|
{ name: 'slack_token', pattern: /\bxox[abpr]-[A-Za-z0-9-]{10,}\b/ },
|
|
{ name: 'stripe_key', pattern: /\b[sr]k_(?:live|test)_[A-Za-z0-9]{16,}\b/ },
|
|
{ name: 'bearer_header', pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{20,}/ },
|
|
{ name: 'jwt', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
{ name: 'env_assignment', pattern: /\b(?:API_KEY|SECRET|TOKEN|PASSWORD|PASSWD)\s*=\s*\S{8,}/i },
|
|
]);
|
|
|
|
function scanForCanaries(value, findings = [], trail = '$') {
|
|
if (typeof value === 'string') {
|
|
for (const canary of SECRET_CANARIES) {
|
|
if (canary.pattern.test(value)) {
|
|
findings.push({ canary: canary.name, path: trail });
|
|
}
|
|
}
|
|
return findings;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value.forEach((item, index) => scanForCanaries(item, findings, `${trail}[${index}]`));
|
|
return findings;
|
|
}
|
|
if (value && typeof value === 'object') {
|
|
for (const key of Object.keys(value)) {
|
|
scanForCanaries(value[key], findings, `${trail}.${key}`);
|
|
}
|
|
}
|
|
return findings;
|
|
}
|
|
|
|
function isPlainObject(value) {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
const prototype = Object.getPrototypeOf(value);
|
|
return prototype === Object.prototype || prototype === null;
|
|
}
|
|
|
|
/** Inspect descriptors before reading values; this is not a boundary for proxies. */
|
|
function dataObjectErrors(value, label) {
|
|
if (!isPlainObject(value)) return [`${label} must be a plain data object`];
|
|
const errors = [];
|
|
for (const key of Reflect.ownKeys(value)) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
if (typeof key !== 'string' || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
errors.push(`${label} must contain only enumerable string data properties`);
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
function matchesPayloadType(value, type) {
|
|
if (type === 'string') return typeof value === 'string';
|
|
if (type === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
if (type === 'integer|null' && value === null) return true;
|
|
return typeof value === 'number' && Number.isInteger(value);
|
|
}
|
|
|
|
/**
|
|
* Return { payload, dropped, findings, errors } without coercing retained fields.
|
|
* Custom allowlists only narrow v1. Invalid data is never scanned or hashed.
|
|
*/
|
|
function redactPayload(payload, options = {}) {
|
|
const errors = dataObjectErrors(payload, 'payload');
|
|
if (errors.length) return { payload: {}, dropped: [], findings: [], errors };
|
|
const allowlist = new Set(options.allowlist || DEFAULT_PAYLOAD_ALLOWLIST);
|
|
const kept = {};
|
|
const dropped = [];
|
|
for (const key of Object.keys(payload)) {
|
|
if (!Object.hasOwn(PAYLOAD_TYPES, key) || !allowlist.has(key)) {
|
|
dropped.push(key);
|
|
} else if (!matchesPayloadType(payload[key], PAYLOAD_TYPES[key])) {
|
|
errors.push(`payload field ${key} must have type ${PAYLOAD_TYPES[key]}`);
|
|
} else {
|
|
kept[key] = payload[key];
|
|
}
|
|
}
|
|
const findings = errors.length ? [] : scanForCanaries(kept);
|
|
return { payload: kept, dropped: dropped.sort(), findings, errors };
|
|
}
|
|
|
|
/**
|
|
* Validate one envelope. Returns an array of error strings; empty means valid.
|
|
* The check is structural and independent of the journal it came from.
|
|
* Hash-link correctness is verified by the capsule reader, not here.
|
|
*/
|
|
function validateEnvelope(entry) {
|
|
const errors = dataObjectErrors(entry, 'envelope');
|
|
if (errors.length) return errors;
|
|
if (Object.keys(entry).some(key => !ENVELOPE_FIELDS.has(key))) {
|
|
errors.push('envelope has unknown top-level fields');
|
|
}
|
|
if ([...ENVELOPE_FIELDS].some(key => !Object.hasOwn(entry, key))) {
|
|
errors.push('envelope is missing required own fields');
|
|
}
|
|
if (errors.length) return errors;
|
|
if (entry.schema !== SCHEMA_VERSION) {
|
|
errors.push(`schema must be ${SCHEMA_VERSION}`);
|
|
}
|
|
for (const field of ['run_id', 'capsule_id']) {
|
|
if (typeof entry[field] !== 'string' || !ID_PATTERN.test(entry[field])) {
|
|
errors.push(`${field} must match ${ID_PATTERN}`);
|
|
}
|
|
}
|
|
if (!Number.isInteger(entry.seq) || entry.seq < 0) {
|
|
errors.push('seq must be a non-negative integer');
|
|
}
|
|
if (typeof entry.ts !== 'string' || Number.isNaN(Date.parse(entry.ts))) {
|
|
errors.push('ts must be an ISO-8601 timestamp');
|
|
}
|
|
if (!LINEAGES.includes(entry.lineage)) {
|
|
errors.push(`lineage must be one of ${LINEAGES.join(', ')}`);
|
|
}
|
|
if (typeof entry.kind !== 'string' || !/^[a-z][a-z0-9_.-]{0,63}$/.test(entry.kind)) {
|
|
errors.push('kind must be a short lowercase identifier');
|
|
}
|
|
if (!EFFECT_CLASSES.includes(entry.effect_class)) {
|
|
errors.push(`effect_class must be one of ${EFFECT_CLASSES.join(', ')}`);
|
|
}
|
|
if (typeof entry.harness_version !== 'string' || entry.harness_version.length === 0) {
|
|
errors.push('harness_version must be a non-empty string');
|
|
}
|
|
if (typeof entry.task_family !== 'string' || entry.task_family.length === 0) {
|
|
errors.push('task_family must be a non-empty string');
|
|
}
|
|
if (typeof entry.parent_hash !== 'string' || !HASH_PATTERN.test(entry.parent_hash)) {
|
|
errors.push('parent_hash must be a 64-char hex sha256');
|
|
}
|
|
if (typeof entry.entry_hash !== 'string' || !HASH_PATTERN.test(entry.entry_hash)) {
|
|
errors.push('entry_hash must be a 64-char hex sha256');
|
|
}
|
|
if (!isPlainObject(entry.payload)) {
|
|
errors.push('payload must be an object');
|
|
} else {
|
|
const { dropped, findings, errors: payloadErrors } = redactPayload(entry.payload);
|
|
errors.push(...payloadErrors);
|
|
if (dropped.length > 0) {
|
|
errors.push(`payload has non-allowlisted keys: ${dropped.join(', ')}`);
|
|
}
|
|
for (const finding of findings) {
|
|
errors.push(`payload tripped secret canary ${finding.canary} at ${finding.path}`);
|
|
}
|
|
}
|
|
if (errors.length === 0) {
|
|
const expected = computeEntryHash(entry);
|
|
if (expected !== entry.entry_hash) {
|
|
errors.push('entry_hash does not match entry content');
|
|
}
|
|
}
|
|
return errors;
|
|
}
|
|
|
|
/** The hash covers every field except entry_hash itself. */
|
|
function computeEntryHash(entry) {
|
|
const { entry_hash: _ignored, ...rest } = entry;
|
|
return hashValue(rest);
|
|
}
|
|
|
|
module.exports = {
|
|
SCHEMA_VERSION,
|
|
LINEAGES,
|
|
EFFECT_CLASSES,
|
|
GENESIS_HASH,
|
|
DEFAULT_PAYLOAD_ALLOWLIST,
|
|
SECRET_CANARIES,
|
|
ID_PATTERN,
|
|
HASH_PATTERN,
|
|
redactPayload,
|
|
scanForCanaries,
|
|
validateEnvelope,
|
|
computeEntryHash,
|
|
effectRank: (effectClass) => EFFECT_CLASSES.indexOf(effectClass),
|
|
};
|