diff --git a/CHANGELOG.md b/CHANGELOG.md index 6434d61..c23e230 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ ### Added +- **Closed domain-separated identity digest authority.** The P03 identity + module now owns one digest authority behind every RunIdentityV1 surface. + `IDENTITY_LABELS` is a frozen null-prototype closed registry of the + established `run-manifest.v1`, `assignment-prompt.v1`, and + `child-envelope.v1` spellings plus the ratified R1 surfaces + `run-identity.v1`, `child-identity.v1`, `resolution-snapshot.v1`, + `resolved-lane-binding.v1`, `workspace-anchor.v1`, + `workspace-identity.v1`, `dispatch-attempt.v1`, + `provider-operation.v1`, `provider-run-identity.v1`, + `request-idempotency.v1`, `provider-capability.v1`, + `evidence-bundle.v1`, `verification-policy.v1`, + `verification-command-descriptor.v1`, + `verification-executable-closure.v1`, + `verification-command-plan.v1`, and + `verification-execution-receipt.v1`. There is no runtime registration: + every digest path resolves its label through the registry, so arbitrary or + unregistered labels fail with a stable `unknown_label` error before any + byte is read. New generic `identityDigestV1(label, parts)` accepts only + exact registry constants, requires ordinary Node Buffer parts (Proxies, + custom prototypes, typed-array views, DataViews, ArrayBuffers, + SharedArrayBuffers, growable ArrayBuffers, streams, getter-bearing values, + and coercion hooks are rejected without ever running caller code), reads + lengths and viewed backing stores through trusted `%TypedArray%` internal + slots so spoofed `length` properties cannot lie and + `Buffer.from(SharedArrayBuffer)` or growable-ArrayBuffer parts are + rejected before any byte is read, snapshots bytes so later mutation cannot + drift a digest, enforces exported caps + `MAX_IDENTITY_DIGEST_PARTS = 16` and + `MAX_IDENTITY_DIGEST_INPUT_BYTES = 4_194_304` with stable + `parts_exceeded`/`unbounded_input` errors in fixed validation order, and + returns the shared deeply frozen detached descriptor over the unchanged + length-framed domain/version/label/parts layout. Absent and explicit-false + diagnostic partial authorization keep identical identity bytes; exact true + remains distinct. All existing manifest, assignment-prompt, child-envelope, + and prompt-golden digests keep their exact bytes; adversarial coverage + lives in `test/v3-identity-digest-authority.test.mjs`. - **ProfileV1 whole-catalog snapshot port.** Adds the additive `loadProfileCatalogSnapshot(options)` API beside `loadProfiles`/`findProfile`: one read of both catalogs closes the merged result into a single detached, @@ -84,6 +120,35 @@ ### Fixed +- **Identity digest authority hardens detached backing, hostile labels, + prototype brands, captured intrinsics, and parts-container bounds.** + `identityDigestV1` and every dedicated RunIdentityV1 surface now normalize + a failed trusted typed-array internal-slot read or byte snapshot — most + importantly an otherwise ordinary Buffer whose ArrayBuffer backing store + was detached out from under it — to one stable typed `invalid_object` + error with a constant content-free path (`parts`) and message, so no + native TypeError leaks and nothing is hashed; ordinary, pooled, subarray, + empty, and caller-ArrayBuffer-backed Buffers stay accepted and byte exact. + Digest labels now pass an O(1) code-unit type/length preflight (exported + bound `MAX_IDENTITY_LABEL_CODE_UNITS = 64`) before the closed-registry + lookup, so overlength, control-bearing, or secret-bearing labels are never + hashed, scanned, truncated, or reflected: every unknown label fails with + one constant content-free `unknown_label` diagnostic. The exact + `MAX_IDENTITY_DIGEST_PARTS = 16` cap is enforced before any indexed + descriptor is captured. Active and revoked Proxies are rejected first; the + container must be an exact ordinary array whose intrinsic length is read + O(1), and each bounded indexed data descriptor is captured exactly once + through precomputed numeric keys. Extra string or symbol decorations are + never enumerated or hashed, so even massively decorated under-cap arrays + cannot drive unbounded work or change a digest. Buffer authority now + requires the trusted Uint8Array internal brand plus exact + `Buffer.prototype`, preventing prototype-spoofed non-byte typed arrays and + prototype traps from swapping a validated part. Buffer/Uint8Array, + `writeUInt32BE`, hash `update`/`digest`, JSON `stringify`, `String`, and + reflection/collection/typed-array intrinsics are captured at clean module + import. Existing deterministic ordering for normal arrays, total-byte/ + part-index errors, digest framing, and all manifest, assignment-prompt, + child-envelope, run-identity, and prompt-golden digests are unchanged. - **ProfileV1 accepts a bounded model beside every provider under one shared grammar.** Profile definitions now validate `model` beside any of the four exact providers (`grok`, `cursor-local`, `cursor-cloud`, `dsh`) whenever it diff --git a/plugins/codex-co-engineer/mcp/v3/identity.mjs b/plugins/codex-co-engineer/mcp/v3/identity.mjs index c36d63f..73f2b3a 100644 --- a/plugins/codex-co-engineer/mcp/v3/identity.mjs +++ b/plugins/codex-co-engineer/mcp/v3/identity.mjs @@ -16,6 +16,27 @@ // explicit per-input label, and each input part behind 4-byte big-endian // length prefixes. Different surfaces can never collide, and no input // can be concatenated ambiguously or left unbounded. +// - Closed label registry: the only digest labels are the centrally +// ratified constants in IDENTITY_LABELS. No runtime registration exists, +// and every digest path resolves its label through that closed registry, +// so callers can never hash under an arbitrary or unregistered label. +// An O(1) code-unit type/length preflight runs before registry lookup, +// so hostile labels are never hashed, scanned, truncated, or reflected: +// every unknown or overlength label fails with one constant content-free +// diagnostic. +// - Bounded hostile-container preflight: the parts container is resolved +// as a concrete array with its intrinsic length read in O(1) before any +// traversal. At most sixteen indexed data descriptors are captured once +// through precomputed numeric keys; irrelevant extra properties are never +// enumerated, so huge, sparse, decorated, or proxied containers cannot +// drive unbounded work or alter a digest. +// - Detached-backing normalization: every trusted typed-array internal-slot +// read and the byte snapshot/copy normalize failure to one stable typed +// RunContractV1Error with a constant content-free message and path, so a +// Buffer whose ArrayBuffer backing store was detached can never leak the +// platform's native TypeError and nothing is hashed. SharedArrayBuffer +// and growable ArrayBuffer backing stores are rejected before any byte +// is read. // - Opaque prompt content: user prompts are hashed as exact UTF-8 bytes // bound to their run and assignment identity. They are never trimmed, // Unicode-normalized, re-encoded, or otherwise interpreted; two prompts @@ -35,12 +56,18 @@ // Canonicalization walks a fully validated detached snapshot with captured // private reflection; it never consults caller-mutable Object.keys. +import { Buffer as NodeBuffer } from 'node:buffer'; import { createHash, timingSafeEqual } from 'node:crypto'; +import { types } from 'node:util'; import { capturedCreate, + capturedDescriptor, capturedFreeze, + capturedGetPrototypeOf, capturedHasOwn, + capturedIsArray, + capturedJoin, capturedObjectIs, capturedOwnKeys, capturedTest, @@ -73,20 +100,112 @@ export const IDENTITY_LABEL_PATTERN = new RegExp( PRIVATE_IDENTITY_LABEL_PATTERN.source, PRIVATE_IDENTITY_LABEL_PATTERN.flags, ); -export const IDENTITY_LABELS = Object.freeze({ +// Closed central label registry. Every entry is a centrally ratified digest +// label owned by one identity surface; the set is closed at module load and +// can never grow at runtime. Labels follow the repository namespace +// convention: the `codex-co-engineer` namespace is carried by +// IDENTITY_DOMAIN, and each label spells `.v1`. Dedicated +// digest functions exist only for the three established RunIdentityV1 +// surfaces; the remaining labels are reserved for their owning contracts +// and have no digest function in this module. +const OBJECT_ASSIGN = Object.assign; +export const IDENTITY_LABELS = capturedFreeze(OBJECT_ASSIGN(capturedCreate(null), { RUN_MANIFEST: 'run-manifest.v1', ASSIGNMENT_PROMPT: 'assignment-prompt.v1', CHILD_ENVELOPE: 'child-envelope.v1', -}); + RUN_IDENTITY: 'run-identity.v1', + CHILD_IDENTITY: 'child-identity.v1', + RESOLUTION_SNAPSHOT: 'resolution-snapshot.v1', + RESOLVED_LANE_BINDING: 'resolved-lane-binding.v1', + WORKSPACE_ANCHOR: 'workspace-anchor.v1', + WORKSPACE_IDENTITY: 'workspace-identity.v1', + DISPATCH_ATTEMPT: 'dispatch-attempt.v1', + PROVIDER_OPERATION: 'provider-operation.v1', + PROVIDER_RUN_IDENTITY: 'provider-run-identity.v1', + REQUEST_IDEMPOTENCY: 'request-idempotency.v1', + PROVIDER_CAPABILITY: 'provider-capability.v1', + EVIDENCE_BUNDLE: 'evidence-bundle.v1', + VERIFICATION_POLICY: 'verification-policy.v1', + VERIFICATION_COMMAND_DESCRIPTOR: 'verification-command-descriptor.v1', + VERIFICATION_EXECUTABLE_CLOSURE: 'verification-executable-closure.v1', + VERIFICATION_COMMAND_PLAN: 'verification-command-plan.v1', + VERIFICATION_EXECUTION_RECEIPT: 'verification-execution-receipt.v1', +})); + +export const MAX_IDENTITY_DIGEST_PARTS = 16; +export const MAX_IDENTITY_DIGEST_INPUT_BYTES = 4_194_304; +export const MAX_IDENTITY_LABEL_CODE_UNITS = 64; const MAX_FRAMED_INPUT_BYTES = 0xffffffff; +const DIGEST_HEX_PATTERN = /^[0-9a-f]{64}$/u; + +// Capture every mutable intrinsic used after validation once, at clean +// import, before caller code can replace globals. Grammar already captured +// the shared reflection leaf; Buffer, typed-array, hash, JSON, and +// collection seams used by the digest authority are captured here. +const ARRAY_PROTOTYPE = Array.prototype; +const ARRAY_PUSH = ARRAY_PROTOTYPE.push; +const BUFFER_ALLOC = NodeBuffer.alloc.bind(NodeBuffer); +const BUFFER_FROM = NodeBuffer.from.bind(NodeBuffer); +const BUFFER_PROTOTYPE = NodeBuffer.prototype; +const BUFFER_WRITE_UINT32BE = BUFFER_PROTOTYPE.writeUInt32BE; +const CREATE_HASH = createHash; +const HASH_PROTOTYPE = capturedGetPrototypeOf(CREATE_HASH(DIGEST_ALGORITHM)); +const HASH_UPDATE = HASH_PROTOTYPE.update; +const HASH_DIGEST = HASH_PROTOTYPE.digest; +const IS_SAFE_INTEGER = Number.isSafeInteger; +const JSON_STRINGIFY = JSON.stringify; +const OBJECT_VALUES = Object.values; +const REFLECT_APPLY = Reflect.apply; +const SET_CTOR = Set; +const SET_HAS = SET_CTOR.prototype.has; +const STRING = String; +const TIMING_SAFE_EQUAL = timingSafeEqual; +const UINT8_ARRAY = Uint8Array; +const UINT8_ARRAY_PROTOTYPE = UINT8_ARRAY.prototype; +const ARRAY_BUFFER_PROTOTYPE = ArrayBuffer.prototype; +const IS_ARRAY_BUFFER = types.isArrayBuffer; +const IS_PROXY = types.isProxy; +const IS_SHARED_ARRAY_BUFFER = types.isSharedArrayBuffer; +const IS_UINT8_ARRAY = types.isUint8Array; + +const REGISTERED_IDENTITY_LABELS = new SET_CTOR(OBJECT_VALUES(IDENTITY_LABELS)); + +const IDENTITY_PART_INDEX_KEYS = capturedFreeze([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, +]); +const IDENTITY_PART_PATHS = capturedFreeze([ + 'parts[0]', 'parts[1]', 'parts[2]', 'parts[3]', + 'parts[4]', 'parts[5]', 'parts[6]', 'parts[7]', + 'parts[8]', 'parts[9]', 'parts[10]', 'parts[11]', + 'parts[12]', 'parts[13]', 'parts[14]', 'parts[15]', +]); + +const UNKNOWN_IDENTITY_LABEL_MESSAGE = + 'Identity label is not a ratified identity label; pass an IDENTITY_LABELS constant.'; +const UNREADABLE_PART_CODE = 'invalid_object'; +const UNREADABLE_PART_PATH = 'parts'; +const UNREADABLE_PART_MESSAGE = + 'A digest part could not be snapshotted through trusted typed-array internal slots.'; +const ORDINARY_BUFFER_PART_MESSAGE = + 'must be an ordinary Node Buffer; plain objects, arrays, strings, numbers, typed-array views, DataViews, ArrayBuffers, SharedArrayBuffers, streams, and getter-bearing values are not byte parts.'; + +const TYPED_ARRAY_PROTOTYPE = capturedGetPrototypeOf(UINT8_ARRAY_PROTOTYPE); +const TYPED_ARRAY_LENGTH_GETTER = capturedDescriptor(TYPED_ARRAY_PROTOTYPE, 'length')?.get; +const TYPED_ARRAY_BUFFER_GETTER = capturedDescriptor(TYPED_ARRAY_PROTOTYPE, 'buffer')?.get; +const ARRAY_BUFFER_RESIZABLE_GETTER = capturedDescriptor(ARRAY_BUFFER_PROTOTYPE, 'resizable')?.get; +const ARRAY_BUFFER_DETACHED_GETTER = capturedDescriptor(ARRAY_BUFFER_PROTOTYPE, 'detached')?.get; function fail(code, path, message) { throw new RunContractV1Error(code, path, message); } +function failUnreadablePart() { + fail(UNREADABLE_PART_CODE, UNREADABLE_PART_PATH, UNREADABLE_PART_MESSAGE); +} + function truncateForMessage(value) { - const text = String(value); + const text = STRING(value); return text.length > 48 ? `${text.slice(0, 45)}...` : text; } @@ -101,15 +220,10 @@ function assertWellFormedText(value, path) { } } -function toCanonicalBytes(value, path) { - assertWellFormedText(value, path); - return Buffer.from(value, 'utf8'); -} - function exactJsonEqual(left, right) { if (capturedObjectIs(left, right)) return true; - if (Array.isArray(left)) { - if (!Array.isArray(right) || left.length !== right.length) return false; + if (capturedIsArray(left)) { + if (!capturedIsArray(right) || left.length !== right.length) return false; for (let index = 0; index < left.length; index += 1) { if (!exactJsonEqual(left[index], right[index])) return false; } @@ -118,8 +232,15 @@ function exactJsonEqual(left, right) { if (isPlainObject(left) && isPlainObject(right)) { const leftKeys = sortedCapturedKeys(left); const rightKeys = sortedCapturedKeys(right); - return leftKeys.length === rightKeys.length - && leftKeys.every((key) => capturedHasOwn(right, key) && exactJsonEqual(left[key], right[key])); + if (leftKeys.length !== rightKeys.length) return false; + for (let index = 0; index < leftKeys.length; index += 1) { + const key = leftKeys[index]; + if (key !== rightKeys[index] || !capturedHasOwn(right, key) + || !exactJsonEqual(left[key], right[key])) { + return false; + } + } + return true; } return false; } @@ -132,7 +253,7 @@ export function canonicalJsonStringify(value) { assertManifestComplexity(value); const parts = []; emitCanonical(parts, value, '$', 0); - return parts.join(''); + return capturedJoin(parts, ''); } function emitCanonical(parts, value, path, depth) { @@ -140,78 +261,230 @@ function emitCanonical(parts, value, path, depth) { fail('depth_exceeded', path, `${path} exceeds the maximum canonical depth of ${MAX_MANIFEST_DEPTH}.`); } if (value === null) { - parts.push('null'); + ARRAY_PUSH.call(parts, 'null'); return; } const kind = typeof value; if (kind === 'string') { assertWellFormedText(value, path); - parts.push(JSON.stringify(value)); + ARRAY_PUSH.call(parts, JSON_STRINGIFY(value)); return; } if (kind === 'number') { - if (!Number.isSafeInteger(value)) { + if (!IS_SAFE_INTEGER(value)) { fail('invalid_type', path, `${path} is ${truncateForMessage(value)}; canonical JSON numbers must be safe integers.`); } - parts.push(String(value)); + ARRAY_PUSH.call(parts, JSON_STRINGIFY(value)); return; } if (kind === 'boolean') { - parts.push(value ? 'true' : 'false'); + ARRAY_PUSH.call(parts, value ? 'true' : 'false'); return; } if (kind !== 'object') { fail('invalid_type', path, `${path} is not a canonical JSON value (received ${kind === 'undefined' ? 'undefined' : kind}).`); } - if (Array.isArray(value)) { + if (capturedIsArray(value)) { assertDenseJsonArray(value, path); - parts.push('['); + ARRAY_PUSH.call(parts, '['); for (let index = 0; index < value.length; index += 1) { - if (index > 0) parts.push(','); + if (index > 0) ARRAY_PUSH.call(parts, ','); emitCanonical(parts, value[index], `${path}[${index}]`, depth + 1); } - parts.push(']'); + ARRAY_PUSH.call(parts, ']'); return; } const keys = sortedCapturedKeys(value); - parts.push('{'); + ARRAY_PUSH.call(parts, '{'); for (let index = 0; index < keys.length; index += 1) { - if (index > 0) parts.push(','); + if (index > 0) ARRAY_PUSH.call(parts, ','); assertWellFormedText(keys[index], `${path}.`); - parts.push(JSON.stringify(keys[index]), ':'); + ARRAY_PUSH.call(parts, JSON_STRINGIFY(keys[index])); + ARRAY_PUSH.call(parts, ':'); emitCanonical(parts, value[keys[index]], `${path}.${keys[index]}`, depth + 1); } - parts.push('}'); + ARRAY_PUSH.call(parts, '}'); +} + +function typedArrayLength(bytes) { + if (typeof TYPED_ARRAY_LENGTH_GETTER !== 'function') failUnreadablePart(); + try { + return REFLECT_APPLY(TYPED_ARRAY_LENGTH_GETTER, bytes, []); + } catch { + failUnreadablePart(); + } } function framedUpdate(hash, bytes, path) { - if (bytes.length > MAX_FRAMED_INPUT_BYTES) { + const length = typedArrayLength(bytes); + if (length > MAX_FRAMED_INPUT_BYTES) { fail('unbounded_input', path, `${path} exceeds the ${MAX_FRAMED_INPUT_BYTES}-byte framed-input bound.`); } - const prefix = Buffer.alloc(4); - prefix.writeUInt32BE(bytes.length, 0); - hash.update(prefix); - hash.update(bytes); + const prefix = BUFFER_ALLOC(4); + BUFFER_WRITE_UINT32BE.call(prefix, length, 0); + HASH_UPDATE.call(hash, prefix); + HASH_UPDATE.call(hash, bytes); +} + +function resolveRegisteredIdentityLabel(value) { + if (typeof value !== 'string') { + fail('invalid_format', 'label', 'Identity label must be an exact IDENTITY_LABELS constant string.'); + } + if (value.length > MAX_IDENTITY_LABEL_CODE_UNITS + || !REFLECT_APPLY(SET_HAS, REGISTERED_IDENTITY_LABELS, [value])) { + fail('unknown_label', 'label', UNKNOWN_IDENTITY_LABEL_MESSAGE); + } + return value; +} + +function intrinsicArrayLength(value) { + let descriptor; + try { + descriptor = capturedDescriptor(value, 'length'); + } catch { + return null; + } + if (!descriptor || !capturedHasOwn(descriptor, 'value')) return null; + const length = descriptor.value; + if (typeof length !== 'number' || !IS_SAFE_INTEGER(length) || length < 0) return null; + return length; +} + +function captureBoundedIdentityParts(value) { + if (IS_PROXY(value)) { + fail('invalid_array', 'parts', 'parts must be a concrete JSON array, not a Proxy.'); + } + if (!capturedIsArray(value)) fail('invalid_type', 'parts', 'parts must be an array.'); + let prototype; + try { + prototype = capturedGetPrototypeOf(value); + } catch { + fail('invalid_array', 'parts', 'parts prototype could not be inspected safely.'); + } + if (prototype !== ARRAY_PROTOTYPE) { + fail('invalid_array', 'parts', 'parts must use the exact Array.prototype.'); + } + const intrinsicLength = intrinsicArrayLength(value); + if (intrinsicLength === null) { + fail('invalid_array', 'parts', 'parts must expose an ordinary array length.'); + } + if (intrinsicLength > MAX_IDENTITY_DIGEST_PARTS) { + fail('parts_exceeded', 'parts', + `Identity digest accepts at most ${MAX_IDENTITY_DIGEST_PARTS} parts; received ${intrinsicLength}.`); + } + const captured = capturedCreate(null); + captured.length = intrinsicLength; + for (let index = 0; index < intrinsicLength; index += 1) { + const key = IDENTITY_PART_INDEX_KEYS[index]; + let descriptor; + try { + descriptor = capturedDescriptor(value, key); + } catch { + fail('invalid_array', IDENTITY_PART_PATHS[index], + `${IDENTITY_PART_PATHS[index]} could not be inspected safely.`); + } + if (!descriptor) { + fail('invalid_array', 'parts', 'parts must be dense; sparse arrays are not digest part lists.'); + } + if (!descriptor.enumerable || !capturedHasOwn(descriptor, 'value')) { + fail('invalid_array', IDENTITY_PART_PATHS[index], + `${IDENTITY_PART_PATHS[index]} must be an enumerable data element.`); + } + captured[key] = descriptor.value; + } + return captured; +} + +function assertExactBufferPart(part, path) { + if ((typeof part !== 'object' && typeof part !== 'function') || part === null) { + fail('invalid_type', path, `${path} ${ORDINARY_BUFFER_PART_MESSAGE}`); + } + if (IS_PROXY(part)) { + fail('invalid_object', path, `${path} must be an ordinary Buffer, not a Proxy.`); + } + const hasUint8ArrayBrand = IS_UINT8_ARRAY(part); + let prototype; + try { + prototype = capturedGetPrototypeOf(part); + } catch { + fail('invalid_object', path, `${path} prototype could not be inspected safely.`); + } + if (!hasUint8ArrayBrand) { + if (prototype === BUFFER_PROTOTYPE) { + fail('invalid_object', path, `${path} has Buffer.prototype without the Uint8Array byte brand.`); + } + fail('invalid_type', path, `${path} ${ORDINARY_BUFFER_PART_MESSAGE}`); + } + if (prototype !== BUFFER_PROTOTYPE) { + if (prototype === UINT8_ARRAY_PROTOTYPE) { + fail('invalid_type', path, `${path} ${ORDINARY_BUFFER_PART_MESSAGE}`); + } + fail('invalid_object', path, `${path} must use the exact Buffer.prototype.`); + } + if (typeof TYPED_ARRAY_BUFFER_GETTER !== 'function') { + fail('invalid_object', path, `${path} backing store could not be read through trusted typed-array slots.`); + } + let viewed; + try { + viewed = REFLECT_APPLY(TYPED_ARRAY_BUFFER_GETTER, part, []); + } catch { + failUnreadablePart(); + } + if (IS_SHARED_ARRAY_BUFFER(viewed) || !IS_ARRAY_BUFFER(viewed)) { + fail('invalid_object', path, + `${path} must be backed by an ordinary ArrayBuffer; SharedArrayBuffer-backed Buffers cannot yield an immutable digest snapshot.`); + } + try { + if (typeof ARRAY_BUFFER_RESIZABLE_GETTER === 'function' + && REFLECT_APPLY(ARRAY_BUFFER_RESIZABLE_GETTER, viewed, []) === true) { + fail('invalid_object', path, + `${path} must be backed by a fixed-length ArrayBuffer; growable ArrayBuffer-backed Buffers cannot yield an immutable digest snapshot.`); + } + if (typeof ARRAY_BUFFER_DETACHED_GETTER === 'function' + && REFLECT_APPLY(ARRAY_BUFFER_DETACHED_GETTER, viewed, []) === true) { + failUnreadablePart(); + } + } catch (error) { + if (error instanceof RunContractV1Error) throw error; + failUnreadablePart(); + } + return typedArrayLength(part); } function identityDigestHex(label, parts) { - if (typeof label !== 'string' || !capturedTest(PRIVATE_IDENTITY_LABEL_PATTERN, label)) { - fail('invalid_format', 'label', `Identity label must match ${PRIVATE_IDENTITY_LABEL_PATTERN.source}.`); - } - const hash = createHash(DIGEST_ALGORITHM); - framedUpdate(hash, Buffer.from(IDENTITY_DOMAIN, 'utf8'), 'identity_domain'); - const version = Buffer.alloc(4); - version.writeUInt32BE(IDENTITY_VERSION, 0); - hash.update(version); - framedUpdate(hash, Buffer.from(label, 'utf8'), 'identity_label'); + const registeredLabel = resolveRegisteredIdentityLabel(label); + const capturedParts = captureBoundedIdentityParts(parts); + const snapshots = capturedCreate(null); let inputBytes = 0; - for (const part of parts) { - framedUpdate(hash, part, 'identity_input'); - inputBytes += part.length; + for (let index = 0; index < capturedParts.length; index += 1) { + const key = IDENTITY_PART_INDEX_KEYS[index]; + const path = IDENTITY_PART_PATHS[index]; + const length = assertExactBufferPart(capturedParts[key], path); + inputBytes += length; + if (inputBytes > MAX_IDENTITY_DIGEST_INPUT_BYTES) { + fail('unbounded_input', 'parts', + `Identity digest input exceeds the ${MAX_IDENTITY_DIGEST_INPUT_BYTES}-byte total bound at ${path}.`); + } + try { + const snapshot = new UINT8_ARRAY(capturedParts[key]); + if (typedArrayLength(snapshot) !== length) failUnreadablePart(); + snapshots[key] = snapshot; + } catch { + failUnreadablePart(); + } } - return { digest: hash.digest('hex'), input_bytes: inputBytes }; + const hash = CREATE_HASH(DIGEST_ALGORITHM); + framedUpdate(hash, BUFFER_FROM(IDENTITY_DOMAIN, 'utf8'), 'identity_domain'); + const version = BUFFER_ALLOC(4); + BUFFER_WRITE_UINT32BE.call(version, IDENTITY_VERSION, 0); + HASH_UPDATE.call(hash, version); + framedUpdate(hash, BUFFER_FROM(registeredLabel, 'utf8'), 'identity_label'); + for (let index = 0; index < capturedParts.length; index += 1) { + framedUpdate(hash, snapshots[IDENTITY_PART_INDEX_KEYS[index]], 'identity_input'); + } + return { digest: HASH_DIGEST.call(hash, 'hex'), input_bytes: inputBytes }; } function digestDescriptor(label, parts) { @@ -226,16 +499,10 @@ function digestDescriptor(label, parts) { }); } -// Identity form of one complete run manifest: the fully validated frozen -// parse with one normalization applied. An own -// return_contract.allow_diagnostic_partial_candidate of exactly false is -// omitted so its canonical bytes equal the absent form byte for byte; any -// other validated shape (including explicit true) passes through untouched. -// The snapshot is already validated plain data — own enumerable data -// properties only, no accessors, symbols, or exotic prototypes — so this -// rebuild reads no caller-executable surface and dispatches no traps. -// Keys are taken from captured private reflection, never caller-mutable -// Object.keys. +export function identityDigestV1(label, parts) { + return digestDescriptor(label, parts); +} + function projectReturnContractForIdentity(contract) { const projected = capturedCreate(null); for (const key of RETURN_CONTRACT_REQUIRED_KEYS) { @@ -256,7 +523,9 @@ function manifestIdentityForm(manifest) { return snapshot; } const projected = capturedCreate(null); - for (const key of capturedOwnKeys(snapshot)) { + const keys = capturedOwnKeys(snapshot); + for (let index = 0; index < keys.length; index += 1) { + const key = keys[index]; if (typeof key !== 'string') continue; projected[key] = key === 'return_contract' ? projectReturnContractForIdentity(contract) @@ -265,60 +534,54 @@ function manifestIdentityForm(manifest) { return capturedFreeze(projected); } -// Canonical validated form of one complete run manifest (P02 normalized: -// absent and explicit-false diagnostic partial authorization project to -// identical canonical bytes). export function runManifestCanonicalJsonV1(manifest) { return canonicalJsonStringify(manifestIdentityForm(manifest)); } -// Stable digest of one complete run manifest over its canonical JSON form. export function runManifestDigestV1(manifest) { const canonical = runManifestCanonicalJsonV1(manifest); - return digestDescriptor(IDENTITY_LABELS.RUN_MANIFEST, [Buffer.from(canonical, 'utf8')]); + return digestDescriptor(IDENTITY_LABELS.RUN_MANIFEST, [BUFFER_FROM(canonical, 'utf8')]); } -// Constant-time digest verification against a previously recorded hex value. -// Returns false (never throws) for malformed expectations. export function verifyRunManifestDigestV1(manifest, expectedDigestHex) { if (typeof expectedDigestHex !== 'string' || expectedDigestHex.length !== DIGEST_HEX_LENGTH - || !capturedTest(/^[0-9a-f]{64}$/u, expectedDigestHex)) { + || !capturedTest(DIGEST_HEX_PATTERN, expectedDigestHex)) { return false; } const actual = runManifestDigestV1(manifest).digest; - return timingSafeEqual(Buffer.from(actual, 'hex'), Buffer.from(expectedDigestHex, 'hex')); + return TIMING_SAFE_EQUAL(BUFFER_FROM(actual, 'hex'), BUFFER_FROM(expectedDigestHex, 'hex')); } function promptDigestFromSnapshot(snapshot, assignmentId) { if (typeof assignmentId !== 'string') { fail('invalid_type', 'assignment_id', 'assignment_id must be the exact declared identifier string.'); } - const assignment = snapshot.assignments.find((entry) => entry.assignment_id === assignmentId); + const assignments = snapshot.assignments; + let assignment; + for (let index = 0; index < assignments.length; index += 1) { + if (assignments[index].assignment_id === assignmentId) { + assignment = assignments[index]; + break; + } + } if (!assignment) { fail('unknown_assignment_id', 'assignments', `No assignment "${truncateForMessage(assignmentId)}" is declared by run "${snapshot.run_id}".`); } - const parts = [ - Buffer.from(snapshot.run_id, 'utf8'), - Buffer.from(assignmentId, 'utf8'), - // The prompt itself stays opaque: exact validated UTF-8 bytes only. - Buffer.from(assignment.prompt, 'utf8'), - ]; - return digestDescriptor(IDENTITY_LABELS.ASSIGNMENT_PROMPT, parts); + return digestDescriptor(IDENTITY_LABELS.ASSIGNMENT_PROMPT, [ + BUFFER_FROM(snapshot.run_id, 'utf8'), + BUFFER_FROM(assignmentId, 'utf8'), + BUFFER_FROM(assignment.prompt, 'utf8'), + ]); } -// Stable per-child prompt digest bound to its run and assignment identity. export function assignmentPromptDigestV1(manifest, assignmentId) { return promptDigestFromSnapshot(parseRunManifestV1(manifest), assignmentId); } -// Stable digest of one compiled child envelope over its canonical form. export function childEnvelopeDigestV1(envelope) { if (!isPlainObject(envelope)) fail('invalid_type', 'envelope', 'A child envelope must be a JSON object.'); - // Inspect the complete direct-JavaScript data graph before reading even one - // caller property. This rejects accessors, symbols, hidden keys, exotic - // prototypes, sparse arrays, aliases, and cycles without invoking getters. assertManifestComplexity(envelope); if (envelope.schema !== CHILD_ENVELOPE_SCHEMA_ID) { fail('invalid_format', 'envelope.schema', `Envelope schema must be exactly "${CHILD_ENVELOPE_SCHEMA_ID}".`); @@ -336,31 +599,25 @@ export function childEnvelopeDigestV1(envelope) { fail('invalid_format', 'envelope.envelope_byte_length', 'envelope_byte_length must be present and equal the UTF-8 byte length of envelope_text.'); } - // The structured form is never trusted on its own: envelope_text is parsed - // strictly, the supplied envelope must exactly match the complete parsed - // canonical shape (closed keys, IDs, nested acceptance, framing offsets, - // byte length), and the digest is then taken over that validated parsed - // form. Exact structural comparison distinguishes negative zero from zero - // before canonical JSON intentionally normalizes that spelling, while - // remaining indifferent to caller key order. identity.mjs -> prompt-compiler.mjs is - // the only direction of this dependency; no import cycle exists. const parsed = parseChildEnvelopeV1(envelope.envelope_text); const canonical = canonicalJsonStringify(parsed); if (!exactJsonEqual(envelope, parsed)) { fail('envelope_shape_mismatch', 'envelope', 'Supplied child envelope does not exactly match the strict parse of its own envelope_text.'); } - return digestDescriptor(IDENTITY_LABELS.CHILD_ENVELOPE, [Buffer.from(canonical, 'utf8')]); + return digestDescriptor(IDENTITY_LABELS.CHILD_ENVELOPE, [BUFFER_FROM(canonical, 'utf8')]); } -// One bounded, order-stable identity record for a run: the manifest digest -// plus every child prompt digest in manifest order. export function describeRunIdentityV1(manifest) { const snapshot = parseRunManifestV1(manifest); - const promptDigests = snapshot.assignments.map((assignment) => capturedFreeze({ - assignment_id: assignment.assignment_id, - digest: promptDigestFromSnapshot(snapshot, assignment.assignment_id).digest, - })); + const promptDigests = []; + for (let index = 0; index < snapshot.assignments.length; index += 1) { + const assignment = snapshot.assignments[index]; + promptDigests[index] = capturedFreeze({ + assignment_id: assignment.assignment_id, + digest: promptDigestFromSnapshot(snapshot, assignment.assignment_id).digest, + }); + } return capturedFreeze({ run_id: snapshot.run_id, assignment_count: snapshot.assignments.length, diff --git a/plugins/codex-co-engineer/test/v3-identity-digest-authority.test.mjs b/plugins/codex-co-engineer/test/v3-identity-digest-authority.test.mjs new file mode 100644 index 0000000..772cd70 --- /dev/null +++ b/plugins/codex-co-engineer/test/v3-identity-digest-authority.test.mjs @@ -0,0 +1,1077 @@ +// P03 adversarial suite for the closed domain-separated digest authority in +// identity.mjs: closed label registry (no arbitrary or unregistered labels), +// exact ordinary-Buffer parts with zero-trap rejection, exact 16-part and +// 4 MiB total caps, unambiguous length framing, mutation-proof snapshots, +// stable typed errors, and byte-exact golden stability for every existing +// RunIdentityV1 surface. + +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { PassThrough } from 'node:stream'; +import test from 'node:test'; + +import { + MAX_IDENTITY_DIGEST_INPUT_BYTES, + MAX_IDENTITY_DIGEST_PARTS, + MAX_IDENTITY_LABEL_CODE_UNITS, + IDENTITY_DOMAIN, + IDENTITY_LABELS, + IDENTITY_VERSION, + assignmentPromptDigestV1, + canonicalJsonStringify, + childEnvelopeDigestV1, + describeRunIdentityV1, + identityDigestV1, + runManifestCanonicalJsonV1, + runManifestDigestV1, + verifyRunManifestDigestV1, +} from '../mcp/v3/identity.mjs'; +import { + RunContractV1Error, +} from '../mcp/v3/run-manifest.mjs'; +import { parseRunManifestV1 } from '../mcp/v3/run-policy.mjs'; +import { + compileChildEnvelopeV1, + parseChildEnvelopeV1, +} from '../mcp/v3/prompt-compiler.mjs'; + +const BASE_SHA = 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0'; +const POLICY = Object.freeze({ + max_concurrency: 8, + require_same_base: true, + require_disjoint_writer_scopes: true, + allow_post_dispatch_fallback: false, + allow_merge: false, + allow_create_pr: false, + attention_mode: 'aggregate', + completion_mode: 'all_settled_then_verify', +}); + +function writerAssignment(id, overrides = {}) { + return { + assignment_id: id, + role: 'implement', + access: 'writer', + prompt: `Prompt for ${id}.`, + execution: { provider: 'dsh', model: 'stealth/ox-alpha' }, + write_scope: ['src/**'], + acceptance: [{ command_id: 'unit-tests', timeout_ms: 600_000 }], + expected_duration_ms: 1_200_000, + required_evidence: ['provider_report'], + ...overrides, + }; +} + +function goldenManifest() { + return { + schema: 'codex-co-engineer.run.v1', + run_id: 'golden-run-fixture', + repository: { path: '/run-fixtures/repository', base_sha: BASE_SHA }, + objective: 'Pin P03 identity goldens.', + assignments: [ + writerAssignment('backend-writer', { prompt: 'Writer prompt \u{1F98A} \u00e9.\n' }), + writerAssignment('frontend-writer', { + write_scope: ['web/**'], + execution: { profile: 'fast-implementer' }, + prompt: 'Reviewer prompt.', + }), + ], + policy: { ...POLICY }, + return_contract: { mode: 'verified_decision', include_artifact_refs: true }, + }; +} + +function errorOf(action) { + try { + action(); + } catch (error) { + assert.ok(error instanceof RunContractV1Error, `expected RunContractV1Error, got ${error}`); + return error; + } + assert.fail('expected the action to throw RunContractV1Error'); +} + +// Independent re-derivation of the documented framing: 4-byte big-endian +// length prefixes over the domain, the big-endian identity version, the +// label, and every part, hashed with SHA-256. +function expectedFramedDigestHex(label, byteParts) { + const chunks = []; + const pushLength = (length) => { + const prefix = Buffer.alloc(4); + prefix.writeUInt32BE(length, 0); + chunks.push(prefix); + }; + const domain = Buffer.from(IDENTITY_DOMAIN, 'utf8'); + pushLength(domain.length); + chunks.push(domain); + const version = Buffer.alloc(4); + version.writeUInt32BE(IDENTITY_VERSION, 0); + chunks.push(version); + const labelBytes = Buffer.from(label, 'utf8'); + pushLength(labelBytes.length); + chunks.push(labelBytes); + for (const part of byteParts) { + pushLength(part.length); + chunks.push(part); + } + return createHash('sha256').update(Buffer.concat(chunks)).digest('hex'); +} + +// --- Closed central label registry ------------------------------------------ + +test('the registry is closed, frozen, and exactly the ratified label set', () => { + assert.deepEqual(Object.assign({}, IDENTITY_LABELS), { + RUN_MANIFEST: 'run-manifest.v1', + ASSIGNMENT_PROMPT: 'assignment-prompt.v1', + CHILD_ENVELOPE: 'child-envelope.v1', + RUN_IDENTITY: 'run-identity.v1', + CHILD_IDENTITY: 'child-identity.v1', + RESOLUTION_SNAPSHOT: 'resolution-snapshot.v1', + RESOLVED_LANE_BINDING: 'resolved-lane-binding.v1', + WORKSPACE_ANCHOR: 'workspace-anchor.v1', + WORKSPACE_IDENTITY: 'workspace-identity.v1', + DISPATCH_ATTEMPT: 'dispatch-attempt.v1', + PROVIDER_OPERATION: 'provider-operation.v1', + PROVIDER_RUN_IDENTITY: 'provider-run-identity.v1', + REQUEST_IDEMPOTENCY: 'request-idempotency.v1', + PROVIDER_CAPABILITY: 'provider-capability.v1', + EVIDENCE_BUNDLE: 'evidence-bundle.v1', + VERIFICATION_POLICY: 'verification-policy.v1', + VERIFICATION_COMMAND_DESCRIPTOR: 'verification-command-descriptor.v1', + VERIFICATION_EXECUTABLE_CLOSURE: 'verification-executable-closure.v1', + VERIFICATION_COMMAND_PLAN: 'verification-command-plan.v1', + VERIFICATION_EXECUTION_RECEIPT: 'verification-execution-receipt.v1', + }); + const values = Object.values(IDENTITY_LABELS); + assert.equal(new Set(values).size, values.length, 'registry labels must be unique'); + assert.ok(Object.isFrozen(IDENTITY_LABELS)); + assert.equal(Object.getPrototypeOf(IDENTITY_LABELS), null); + assert.equal(MAX_IDENTITY_DIGEST_PARTS, 16); + assert.equal(MAX_IDENTITY_DIGEST_INPUT_BYTES, 4_194_304); + assert.equal(typeof MAX_IDENTITY_DIGEST_PARTS, 'number'); + assert.equal(typeof MAX_IDENTITY_DIGEST_INPUT_BYTES, 'number'); +}); + +test('every registered label digests through the shared framing', () => { + const payload = Buffer.from('registry payload'); + for (const [key, label] of Object.entries(IDENTITY_LABELS)) { + const descriptor = identityDigestV1(label, [payload]); + assert.equal(descriptor.label, label, `${key} must round-trip its label`); + assert.equal(descriptor.domain, IDENTITY_DOMAIN); + assert.equal(descriptor.version, IDENTITY_VERSION); + assert.equal(descriptor.digest, expectedFramedDigestHex(label, [payload])); + assert.match(descriptor.digest, /^[0-9a-f]{64}$/u); + } +}); + +test('unregistered raw labels are rejected without coercion', () => { + const nearMisses = [ + // Near-miss spellings of ratified labels. + 'run-manifest.v2', 'run-manifest', 'run-manifest.v10', 'manifest.v1', + 'RUN-MANIFEST.V1', 'Run-Manifest.v1', 'run_manifest.v1', 'run-manifest..v1', + 'assignment-prompt.V1', 'child-envelope.v1 ', ' child-envelope.v1', + 'child-envelope.v1\n', 'evidence-bundle.v1.', 'artifact-id', 'artifact-id.v1', + 'resolved-plan.v1', 'request-idempotency.v1x', 'verification-policy.v1\t', + 'provider-capability.v11', 'candidate-composition.v1', 'run-decision.v1', + 'run-identity.v2', 'child-identity', 'resolution-snapshot.v01', + // Key names are not labels. + 'RUN_MANIFEST', 'EVIDENCE_BUNDLE', 'RUN_IDENTITY', 'RUN_DECISION', + // The codex-co-engineer namespace lives in the domain, not in the label. + 'codex-co-engineer.run-decision.v1', 'codex-co-engineer.identity.v1', + // Plausible but never ratified. + 'rogue.v1', 'attacker-controlled.v1', 'toString', 'constructor', + 'hasOwnProperty', 'label.v1', + ]; + for (const label of nearMisses) { + const error = errorOf(() => identityDigestV1(label, [Buffer.from('x')])); + assert.equal(error.code, 'unknown_label', label); + assert.equal(error.path, 'label'); + assert.ok(error.message.length < 200, `unbounded diagnostic for ${JSON.stringify(label)}`); + } + const nonStrings = [undefined, null, 123, 4_194_304, true, false, BigInt(1), + Symbol('label'), {}, ['run-identity.v1'], new String('run-identity.v1'), + () => 'run-identity.v1']; + for (const label of nonStrings) { + const error = errorOf(() => identityDigestV1(label, [Buffer.from('x')])); + assert.equal(error.code, 'invalid_format', String(label)); + assert.equal(error.path, 'label'); + } + // A String object with an exact ratified spelling is still not a constant. + const boxed = errorOf(() => identityDigestV1(new String(IDENTITY_LABELS.RUN_IDENTITY), [])); + assert.equal(boxed.code, 'invalid_format'); +}); + +test('the registry cannot be extended or spoofed at runtime', () => { + assert.throws(() => { 'use strict'; IDENTITY_LABELS.ROGUE = 'rogue.v1'; }, TypeError); + assert.throws(() => { + Object.defineProperty(IDENTITY_LABELS, 'ROGUE', { value: 'rogue.v1', enumerable: true }); + }, TypeError); + // Registry membership was fixed at module load; a derived object changes nothing. + const spoofed = Object.create(IDENTITY_LABELS, { ROGUE: { value: 'rogue.v1', enumerable: true } }); + const error = errorOf(() => identityDigestV1(spoofed.ROGUE, [Buffer.from('x')])); + assert.equal(error.code, 'unknown_label'); + // A frozen array copy of the values is equally unable to admit new labels. + const frozen = Object.freeze([...Object.values(IDENTITY_LABELS), 'rogue.v1']); + assert.equal(errorOf(() => identityDigestV1(frozen.at(-1), [])).code, 'unknown_label'); +}); + +// --- Exact Buffer parts, zero-trap rejection, no coercion -------------------- + +test('only ordinary Buffer instances are accepted as parts', () => { + const accepted = [ + ['empty buffer', Buffer.alloc(0)], + ['ascii buffer', Buffer.from('abc')], + ['binary buffer', Buffer.from([0, 255, 127, 1])], + ['subarray view', Buffer.from('abcdefgh').subarray(2, 5)], + ['shared pool buffer', Buffer.allocUnsafe(8)], + ['buffer over caller ArrayBuffer', Buffer.from(new ArrayBuffer(4))], + ]; + for (const [name, part] of accepted) { + const descriptor = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [part]); + assert.equal(descriptor.input_bytes, part.length, name); + } +}); + +test('Buffer-backed SharedArrayBuffer parts are rejected before any byte snapshot', () => { + // An ordinary Buffer over shared storage passes every prior ordinary-Buffer + // check while its bytes stay concurrently mutable from outside this agent, + // so accepting it would permit a torn digest snapshot. + const shared = Buffer.from(new SharedArrayBuffer(8)); + assert.equal(Buffer.isBuffer(shared), true); + assert.equal(Object.getPrototypeOf(shared), Buffer.prototype); + shared.write('drift'); + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [shared])); + assert.equal(error.code, 'invalid_object'); + assert.equal(error.path, 'parts[0]'); + assert.match(error.message, /SharedArrayBuffer/u); + assert.ok(error.message.length < 200); + + // Rejection stays index-exact in mixed containers: the shared part is named + // and never hashed even after an ordinary leading part validates. + const mixed = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, + [Buffer.from('ok'), shared])); + assert.equal(mixed.code, 'invalid_object'); + assert.equal(mixed.path, 'parts[1]'); + + // Ordinary, pooled, subarray, empty, and caller-ArrayBuffer-backed Buffers + // keep their exact acceptance and framing bytes. + const accepted = [ + ['ordinary', Buffer.from('abc')], + ['pooled', Buffer.allocUnsafe(4)], + ['subarray view', Buffer.from('abcdefgh').subarray(2, 5)], + ['empty', Buffer.alloc(0)], + ['caller ArrayBuffer-backed', Buffer.from(new ArrayBuffer(4))], + ]; + for (const [name, part] of accepted) { + const descriptor = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [part]); + assert.equal(descriptor.input_bytes, part.length, name); + assert.equal(descriptor.digest, expectedFramedDigestHex(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [part]), name); + } +}); + +test('non-Buffer byte shapes are rejected with stable typed errors', () => { + const cases = [ + ['plain object', {}], + ['length-shaped object', { length: 4 }], + ['index-shaped object', { 0: 104, 1: 105, length: 2 }], + ['isBuffer-spoofed object', { _isBuffer: true }], + ['Uint8Array view', new Uint8Array(8)], + ['Uint8Array over SharedArrayBuffer', new Uint8Array(new SharedArrayBuffer(8))], + ['Float64Array view', new Float64Array(2)], + ['DataView view', new DataView(new ArrayBuffer(8))], + ['ArrayBuffer', new ArrayBuffer(8)], + ['SharedArrayBuffer', new SharedArrayBuffer(8)], + ['string', 'bytes'], + ['number', 104], + ['bigint', BigInt(8)], + ['boolean', true], + ['null', null], + ['undefined', undefined], + ['symbol', Symbol('bytes')], + ['array of byte numbers', [104, 105]], + ['nested parts array', [[Buffer.from('x')]]], + ['function', () => {}], + ['Date', new Date()], + ['Map', new Map([[0, Buffer.from('x')]])], + ['Set', new Set([Buffer.from('x')])], + ['RegExp', /bytes/u], + ['Error', new Error('bytes')], + ['Promise', Promise.resolve()], + ['web ReadableStream', new ReadableStream()], + ['node PassThrough stream', new PassThrough()], + ['Buffer-returning Number wrapper', Object(4)], + ]; + for (const [name, part] of cases) { + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part])); + assert.equal(error.code, 'invalid_type', name); + assert.equal(error.path, 'parts[0]', name); + } + // A brand-less object wearing Buffer.prototype passes the Uint8Array + // instance chain but fails the exact-prototype check. + const branded = errorOf(() => + identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [Object.create(Buffer.prototype)])); + assert.equal(branded.code, 'invalid_object'); +}); + +test('custom prototypes and Proxies never reach byte reads', () => { + // Live Proxy over a real Buffer: rejected before any trap dispatch. + const buffer = Buffer.from('proxied'); + const trapCounts = { get: 0, has: 0, set: 0, ownKeys: 0, getOwnPropertyDescriptor: 0, getPrototypeOf: 0 }; + const counted = new Proxy(buffer, { + get: (target, key, receiver) => (trapCounts.get += 1, Reflect.get(target, key, receiver)), + has: (target, key) => (trapCounts.has += 1, Reflect.has(target, key)), + set: (target, key, value) => (trapCounts.set += 1, Reflect.set(target, key, value)), + ownKeys: (target) => (trapCounts.ownKeys += 1, Reflect.ownKeys(target)), + getOwnPropertyDescriptor: (target, key) => ( + trapCounts.getOwnPropertyDescriptor += 1, Reflect.getOwnPropertyDescriptor(target, key)), + getPrototypeOf: (target) => (trapCounts.getPrototypeOf += 1, Reflect.getPrototypeOf(target)), + }); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [counted])).code, + 'invalid_object'); + assert.deepEqual(trapCounts, { get: 0, has: 0, set: 0, ownKeys: 0, getOwnPropertyDescriptor: 0, getPrototypeOf: 0 }); + + // Revoked Proxy: rejected fail-closed instead of surfacing a TypeError. + const { proxy, revoke } = Proxy.revocable(buffer, {}); + revoke(); + const revokedError = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [proxy])); + assert.equal(revokedError.code, 'invalid_object'); + + // Proxy containers are equally rejected before any element is touched. + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, + new Proxy([buffer], {}))).code, 'invalid_array'); + const { proxy: revokedParts, revoke: revokeParts } = Proxy.revocable([buffer], {}); + revokeParts(); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, revokedParts)).code, + 'invalid_array'); +}); + +test('Buffer prototype proxies cannot execute traps or swap the validated part', () => { + const original = Buffer.from('x'); + const replacement = Buffer.alloc(MAX_IDENTITY_DIGEST_INPUT_BYTES + 1, 0x61); + const parts = [original]; + let prototypeTrapRuns = 0; + const hostilePrototype = new Proxy(Buffer.prototype, { + getPrototypeOf() { + prototypeTrapRuns += 1; + parts[0] = replacement; + Object.setPrototypeOf(original, Buffer.prototype); + return Buffer.prototype; + }, + }); + Object.setPrototypeOf(original, hostilePrototype); + + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, parts)); + assert.equal(error.code, 'invalid_object'); + assert.equal(error.path, 'parts[0]'); + assert.equal(prototypeTrapRuns, 0); + assert.equal(parts[0], original); + assert.doesNotMatch(error.message, /replacement|prototype trap/iu); +}); + +test('non-byte typed arrays cannot acquire Buffer authority by prototype spoofing', () => { + for (const value of [new Uint16Array([0x6162]), new Float64Array([42])]) { + Object.setPrototypeOf(value, Buffer.prototype); + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [value])); + assert.equal(error.code, 'invalid_object'); + assert.equal(error.path, 'parts[0]'); + } +}); + +test('digest copying uses module-captured Buffer and Uint8Array intrinsics', () => { + const nativeBuffer = globalThis.Buffer; + const nativeUint8Array = globalThis.Uint8Array; + const part = nativeBuffer.from('captured'); + const expected = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part]); + let hostileRuns = 0; + try { + globalThis.Buffer = { + isBuffer() { + hostileRuns += 1; + throw new Error('hostile Buffer.isBuffer ran'); + }, + }; + globalThis.Uint8Array = class HostileUint8Array { + constructor() { + hostileRuns += 1; + throw new Error('hostile Uint8Array constructor ran'); + } + }; + const actual = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part]); + assert.deepEqual(actual, expected); + } finally { + globalThis.Buffer = nativeBuffer; + globalThis.Uint8Array = nativeUint8Array; + } + assert.equal(hostileRuns, 0); +}); + +test('hostile getters and coercion hooks never run', () => { + let getterReads = 0; + const getterPart = { + get length() { + getterReads += 1; + return 4; + }, + get 0() { + getterReads += 1; + return 104; + }, + }; + const coercionCounts = { valueOf: 0, toString: 0, toPrimitive: 0 }; + const coercingPart = { + valueOf() { + coercionCounts.valueOf += 1; + return Buffer.from('spoofed'); + }, + toString() { + coercionCounts.toString += 1; + return Buffer.from('spoofed'); + }, + [Symbol.toPrimitive]() { + coercionCounts.toPrimitive += 1; + return Buffer.from('spoofed'); + }, + }; + const throwingPart = { + get poisoned() { + throw new Error('getter must never run'); + }, + }; + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [getterPart])).code, + 'invalid_type'); + assert.equal(getterReads, 0); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [coercingPart])).code, + 'invalid_type'); + assert.deepEqual(coercionCounts, { valueOf: 0, toString: 0, toPrimitive: 0 }); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [throwingPart])).code, + 'invalid_type'); +}); + +test('lengths are read through trusted internal slots, never spoofable properties', () => { + // Spoofed long length must not trip the cap or lie in input_bytes. + const long = Buffer.from('abcd'); + Object.defineProperty(long, 'length', { value: MAX_IDENTITY_DIGEST_INPUT_BYTES + 1 }); + const longDescriptor = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [long]); + assert.equal(longDescriptor.input_bytes, 4); + assert.equal(longDescriptor.digest, + identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [Buffer.from('abcd')]).digest); + // Spoofed short length must not hide real bytes from the digest. + const short = Buffer.from('abcd'); + Object.defineProperty(short, 'length', { value: 1 }); + const shortDescriptor = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [short]); + assert.equal(shortDescriptor.input_bytes, 4); + assert.equal(shortDescriptor.digest, + identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [Buffer.from('abcd')]).digest); + // Shrinking a container's length property truncates the array (JS + // semantics), so a spoofed length can never smuggle hidden parts past the + // cap: only the elements the container honestly exposes are ever hashed. + const parts = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS + 1 }, () => Buffer.from('x')); + Object.defineProperty(parts, 'length', { value: 1 }); + const truncated = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, parts); + assert.equal(truncated.input_bytes, 1); + assert.equal(truncated.digest, + identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [Buffer.from('x')]).digest); + // A non-writable, non-configurable length spoof attempt is a plain TypeError + // from the platform, never a digest over spoofed bounds. + const frozenParts = Object.freeze(Array.from({ length: 2 }, () => Buffer.from('x'))); + assert.throws(() => Object.defineProperty(frozenParts, 'length', { value: 99 }), TypeError); + assert.equal(identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, frozenParts).input_bytes, 2); +}); + +// --- Hostile parts containers ------------------------------------------------ + +test('parts containers must be bounded ordinary arrays with dense indexed data slots', () => { + const part = Buffer.from('x'); + const containers = [ + ['undefined', undefined], + ['null', null], + ['number', 4], + ['string', 'x'], + ['plain object', {}], + ['length-shaped object', { length: 1, 0: part }], + ['Map', new Map()], + ['Set', new Set([part])], + ['Array subclass', (() => { + class Parts extends Array {} + return Parts.of(part); + })()], + ['sparse array', (() => { + const sparse = new Array(2); + sparse[1] = part; + return sparse; + })()], + ]; + for (const [name, container] of containers) { + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, container)); + assert.ok(['invalid_type', 'invalid_array'].includes(error.code), `${name}: ${error.code}`); + assert.equal(error.path, 'parts', name); + } + // Data-only hardening is fine: frozen arrays remain valid. + const descriptor = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, Object.freeze([part])); + assert.equal(descriptor.input_bytes, 1); +}); + +test('irrelevant array decorations are not enumerated or included in the digest', () => { + const part = Buffer.from('x'); + const decorated = [part]; + let getterRuns = 0; + Object.defineProperty(decorated, 'poison', { + enumerable: true, + get() { + getterRuns += 1; + throw new Error('decoration getter ran'); + }, + }); + Object.defineProperty(decorated, Symbol('poison'), { + enumerable: true, + get() { + getterRuns += 1; + throw new Error('symbol decoration getter ran'); + }, + }); + for (let index = 0; index < 50_000; index += 1) { + decorated[`extra_${index}`] = index; + } + const nativeOwnKeys = Reflect.ownKeys; + let ownKeysCalls = 0; + Reflect.ownKeys = (...args) => { + ownKeysCalls += 1; + return nativeOwnKeys(...args); + }; + let actual; + try { + actual = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, decorated); + } finally { + Reflect.ownKeys = nativeOwnKeys; + } + assert.equal(getterRuns, 0); + assert.equal(ownKeysCalls, 0); + assert.deepEqual(actual, identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part])); +}); + +// --- Detached backing, hostile labels, and bounded containers (P03R4) -------- + +const UNREADABLE_PART_DIAGNOSTIC = + 'A digest part could not be snapshotted through trusted typed-array internal slots.'; +const UNKNOWN_LABEL_DIAGNOSTIC = + 'Identity label is not a ratified identity label; pass an IDENTITY_LABELS constant.'; + +function detachBackingStore(backing) { + const view = Buffer.from(backing); + structuredClone(backing, { transfer: [backing] }); + assert.equal(backing.detached, true); + return view; +} + +test('detached-backing Buffers fail with one stable content-free typed error', () => { + const detached = detachBackingStore(new ArrayBuffer(8)); + detached.write('drift'); + // Every structural ordinary-Buffer check still passes; only the trusted + // snapshot can discover the detachment. + assert.equal(Buffer.isBuffer(detached), true); + assert.equal(Object.getPrototypeOf(detached), Buffer.prototype); + + const single = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [detached])); + assert.equal(single.code, 'invalid_object'); + assert.equal(single.path, 'parts'); + assert.equal(single.message, UNREADABLE_PART_DIAGNOSTIC); + assert.ok(single instanceof RunContractV1Error); + // No platform TypeError text and no reflected caller content escapes. + assert.ok(!single.message.includes('Cannot perform Construct')); + assert.ok(!single.message.includes('drift')); + + // The diagnostic is one stable error everywhere: mixed containers, other + // surfaces, repeated calls, and zero-length detached stores all agree on + // the same constant code, path, and message. + const mixed = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, + [Buffer.from('ok'), detached])); + assert.deepEqual( + { code: mixed.code, path: mixed.path, message: mixed.message }, + { code: single.code, path: single.path, message: single.message }, + ); + const zeroView = detachBackingStore(new ArrayBuffer(0)); + const zero = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, [zeroView])); + assert.deepEqual( + { code: zero.code, path: zero.path, message: zero.message }, + { code: single.code, path: single.path, message: single.message }, + ); + assert.equal( + errorOf(() => identityDigestV1(IDENTITY_LABELS.PROVIDER_CAPABILITY, [detached])).message, + UNREADABLE_PART_DIAGNOSTIC, + ); + + // Ordinary, pooled, subarray, empty, and caller-ArrayBuffer-backed Buffers + // remain accepted and byte exact. + const accepted = [ + Buffer.from('abc'), + Buffer.allocUnsafe(4), + Buffer.from('abcdefgh').subarray(2, 5), + Buffer.alloc(0), + Buffer.from(new ArrayBuffer(4)), + ]; + for (const part of accepted) { + const descriptor = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [part]); + assert.equal(descriptor.digest, expectedFramedDigestHex(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [part])); + } +}); + +test('hostile digest labels are stopped by the O(1) content-free preflight', () => { + const hostiles = [ + // Overlength with embedded credential-shaped material. + `sk-live-${'A'.repeat(4096)}-9f27c4d8e5b2SECRET`, + `${'token '.repeat(1024)}ghp_0123456789abcdefghijklmnopqrstuvwxyz`, + `${'x'.repeat(MAX_IDENTITY_LABEL_CODE_UNITS + 1)}\u0000secret`, + // Control-bearing shapes. + 'run-manifest.v1\u0000\u0007\n\r\t', + '\u0000'.repeat(MAX_IDENTITY_LABEL_CODE_UNITS), + // Exact preflight boundaries: at the bound and one over it. + 'b'.repeat(MAX_IDENTITY_LABEL_CODE_UNITS), + 'c'.repeat(MAX_IDENTITY_LABEL_CODE_UNITS + 1), + // Ordinary unknown labels receive the identical constant diagnostic. + 'rogue.v1', 'run-manifest.v2', '', + ]; + for (const label of hostiles) { + const error = errorOf(() => identityDigestV1(label, [Buffer.from('x')])); + assert.equal(error.code, 'unknown_label'); + assert.equal(error.path, 'label'); + assert.equal(error.message, UNKNOWN_LABEL_DIAGNOSTIC); + if (label.length > 0) { + assert.ok(!error.message.includes(label)); + assert.ok(!UNKNOWN_LABEL_DIAGNOSTIC.includes(label.slice(0, 64))); + } + } + assert.equal(MAX_IDENTITY_LABEL_CODE_UNITS, 64); + // Registered constants still resolve through the same entry point. + assert.equal(identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, []).label, 'run-identity.v1'); +}); + +test('active and revoked proxy containers are rejected before cap and traversal', () => { + const overCap = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS + 1 }, () => Buffer.from('x')); + const trapCounts = { + get: 0, has: 0, set: 0, ownKeys: 0, getOwnPropertyDescriptor: 0, getPrototypeOf: 0, + }; + const counted = new Proxy(overCap, { + get: (target, key, receiver) => (trapCounts.get += 1, Reflect.get(target, key, receiver)), + has: (target, key) => (trapCounts.has += 1, Reflect.has(target, key)), + set: (target, key, value) => (trapCounts.set += 1, Reflect.set(target, key, value)), + ownKeys: (target) => (trapCounts.ownKeys += 1, Reflect.ownKeys(target)), + getOwnPropertyDescriptor: (target, key) => ( + trapCounts.getOwnPropertyDescriptor += 1, Reflect.getOwnPropertyDescriptor(target, key)), + getPrototypeOf: (target) => (trapCounts.getPrototypeOf += 1, Reflect.getPrototypeOf(target)), + }); + // An over-cap active Proxy is rejected as a Proxy first, never as a count + // error, and no trap ever runs. + const active = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, counted)); + assert.equal(active.code, 'invalid_array'); + assert.equal(active.path, 'parts'); + assert.deepEqual(trapCounts, { + get: 0, has: 0, set: 0, ownKeys: 0, getOwnPropertyDescriptor: 0, getPrototypeOf: 0, + }); + + // A revoked Proxy must not leak the platform IsArray TypeError even though + // its target would also exceed the cap; under-cap revoked Proxies fail + // identically. + const { proxy, revoke } = Proxy.revocable(overCap, {}); + revoke(); + const revoked = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, proxy)); + assert.equal(revoked.code, 'invalid_array'); + assert.equal(revoked.path, 'parts'); + assert.ok(revoked instanceof RunContractV1Error); + const { proxy: small, revoke: revokeSmall } = Proxy.revocable([Buffer.from('x')], {}); + revokeSmall(); + const smallError = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, small)); + assert.deepEqual( + { code: smallError.code, path: smallError.path }, + { code: revoked.code, path: revoked.path }, + ); +}); + +test('over-cap containers fail on the exact cap before any deep traversal', () => { + // Sparse over-cap: the huge length is judged O(1); holes are never walked. + const sparse = []; + sparse.length = 50_000; + sparse[49_999] = Buffer.from('x'); + const sparseError = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, sparse)); + assert.equal(sparseError.code, 'parts_exceeded'); + assert.equal(sparseError.path, 'parts'); + assert.match(sparseError.message, /at most 16 parts/u); + assert.match(sparseError.message, /received 50000/u); + + // Huge length-only container: rejected without materializing anything. + const hugeError = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, new Array(2 ** 28))); + assert.equal(hugeError.code, 'parts_exceeded'); + + // Dense over-cap of hostile non-Buffer elements: the cap wins before any + // element type validation runs. + const hostileDense = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS + 1 }, () => null); + assert.equal( + errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, hostileDense)).code, + 'parts_exceeded', + ); + + // Decorated over-cap: extra and symbol properties cannot turn the cap + // error into a density error, because the cap is enforced first. + const decorated = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS + 1 }, () => Buffer.from('x')); + decorated.extra = 'decoration'; + decorated[Symbol('extra')] = true; + assert.equal( + errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, decorated)).code, + 'parts_exceeded', + ); + + // Under-cap decorations are outside the bounded semantic surface and do + // not affect the indexed part list or its digest. + const underCapDecorated = [Buffer.from('x')]; + underCapDecorated.extra = true; + assert.deepEqual( + identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, underCapDecorated), + identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, [Buffer.from('x')]), + ); +}); + +test('the exact 16/17 part boundary is exact under hostile shapes', () => { + const oneByte = Buffer.from('x'); + const atCap = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS }, () => oneByte); + const atCapDescriptor = identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, atCap); + assert.equal(atCapDescriptor.input_bytes, MAX_IDENTITY_DIGEST_PARTS); + assert.equal(atCapDescriptor.digest, expectedFramedDigestHex(IDENTITY_LABELS.RUN_IDENTITY, atCap)); + + const detached = detachBackingStore(new ArrayBuffer(4)); + // At 16 parts the cap passes, so the detached part is discovered by the + // trusted snapshot and normalizes to the stable detached diagnostic. + const sixteenWithDetached = errorOf(() => identityDigestV1( + IDENTITY_LABELS.RUN_IDENTITY, [...atCap.slice(0, -1), detached])); + assert.equal(sixteenWithDetached.code, 'invalid_object'); + assert.equal(sixteenWithDetached.path, 'parts'); + assert.equal(sixteenWithDetached.message, UNREADABLE_PART_DIAGNOSTIC); + + // At 17 parts the cap fires before any element is read, even when a later + // element is detached or hostile. + const seventeen = [...atCap, oneByte]; + assert.equal( + errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, seventeen)).code, + 'parts_exceeded', + ); + const seventeenWithDetached = [...atCap, detached]; + assert.equal( + errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, seventeenWithDetached)).code, + 'parts_exceeded', + ); + + // Under the cap, accessor elements still fail the exact existing density + // check at the exact part index, and the getter never runs. + let getterRuns = 0; + const accessor = atCap.slice(); + Object.defineProperty(accessor, 7, { + enumerable: true, + configurable: true, + get() { + getterRuns += 1; + return oneByte; + }, + }); + const accessorError = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, accessor)); + assert.equal(accessorError.code, 'invalid_array'); + assert.equal(accessorError.path, 'parts[7]'); + assert.equal(getterRuns, 0); +}); + +// --- Exact caps and deterministic first errors ------------------------------- + +test('the part-count cap is exact and fails closed', () => { + const oneByte = Buffer.from('x'); + const atCap = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS }, () => Buffer.from('x')); + const overCap = [...atCap, oneByte]; + const descriptor = identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, atCap); + assert.equal(descriptor.input_bytes, MAX_IDENTITY_DIGEST_PARTS); + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, overCap)); + assert.equal(error.code, 'parts_exceeded'); + assert.equal(error.path, 'parts'); + assert.match(error.message, /at most 16 parts/u); + // Count is checked before element contents: a hostile element cannot change + // which error an oversized container produces. + const hostileOverCap = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS + 1 }, (_, index) => ( + index === 3 ? 'not-a-buffer' : Buffer.from('x'))); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, hostileOverCap)).code, + 'parts_exceeded'); +}); + +test('the total-input cap is exact and fails closed', () => { + const partBytes = MAX_IDENTITY_DIGEST_INPUT_BYTES / MAX_IDENTITY_DIGEST_PARTS; + const exact = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS }, () => Buffer.alloc(partBytes)); + const descriptor = identityDigestV1(IDENTITY_LABELS.PROVIDER_CAPABILITY, exact); + assert.equal(descriptor.input_bytes, MAX_IDENTITY_DIGEST_INPUT_BYTES); + assert.equal(descriptor.digest, + expectedFramedDigestHex(IDENTITY_LABELS.PROVIDER_CAPABILITY, exact)); + + const oneOver = [...exact.slice(0, -1), Buffer.alloc(partBytes + 1)]; + const overError = errorOf(() => identityDigestV1(IDENTITY_LABELS.PROVIDER_CAPABILITY, oneOver)); + assert.equal(overError.code, 'unbounded_input'); + assert.match(overError.message, /4194304-byte total bound at parts\[15\]/u); + + const singleOver = [Buffer.alloc(MAX_IDENTITY_DIGEST_INPUT_BYTES + 1)]; + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.PROVIDER_CAPABILITY, singleOver)).code, + 'unbounded_input'); + // Bytes are checked in index order: the first crossing part is reported. + const half = Buffer.alloc(MAX_IDENTITY_DIGEST_INPUT_BYTES / 2); + const crossError = errorOf(() => identityDigestV1( + IDENTITY_LABELS.PROVIDER_CAPABILITY, [half, Buffer.alloc((MAX_IDENTITY_DIGEST_INPUT_BYTES / 2) + 1)])); + assert.equal(crossError.code, 'unbounded_input'); + assert.match(crossError.message, /parts\[1\]/u); + // Byte accumulation precedes any later element's type validation. + const firstCrossing = errorOf(() => identityDigestV1( + IDENTITY_LABELS.PROVIDER_CAPABILITY, + [Buffer.alloc(MAX_IDENTITY_DIGEST_INPUT_BYTES + 1), 'hostile-later'])); + assert.equal(firstCrossing.code, 'unbounded_input'); + assert.match(firstCrossing.message, /parts\[0\]/u); + const exactlyAtCapThenHostile = errorOf(() => identityDigestV1( + IDENTITY_LABELS.PROVIDER_CAPABILITY, [Buffer.alloc(MAX_IDENTITY_DIGEST_INPUT_BYTES), 'hostile-later'])); + assert.equal(exactlyAtCapThenHostile.code, 'invalid_type'); +}); + +test('validation order is fixed: label, container, count, then parts in index order', () => { + assert.equal(errorOf(() => identityDigestV1('rogue.v1', 'nope')).code, 'unknown_label'); + assert.equal(errorOf(() => identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, 'nope')).code, 'invalid_type'); + assert.equal(errorOf(() => identityDigestV1( + IDENTITY_LABELS.RUN_IDENTITY, Array.from({ length: 17 }, () => 'hostile'))).code, 'parts_exceeded'); + assert.equal(errorOf(() => identityDigestV1( + IDENTITY_LABELS.RUN_IDENTITY, [Buffer.from('ok'), 'hostile'])).code, 'invalid_type'); + // An empty parts list is valid data, not an error. + const empty = identityDigestV1(IDENTITY_LABELS.RUN_IDENTITY, []); + assert.equal(empty.input_bytes, 0); +}); + +test('empty parts lists and empty parts are distinct, stable frames', () => { + const noParts = identityDigestV1(IDENTITY_LABELS.VERIFICATION_POLICY, []); + const emptyPart = identityDigestV1(IDENTITY_LABELS.VERIFICATION_POLICY, [Buffer.alloc(0)]); + assert.equal(noParts.input_bytes, 0); + assert.equal(emptyPart.input_bytes, 0); + assert.notEqual(noParts.digest, emptyPart.digest); + assert.equal(noParts.digest, expectedFramedDigestHex(IDENTITY_LABELS.VERIFICATION_POLICY, [])); + assert.equal(emptyPart.digest, + expectedFramedDigestHex(IDENTITY_LABELS.VERIFICATION_POLICY, [Buffer.alloc(0)])); + assert.equal(identityDigestV1(IDENTITY_LABELS.VERIFICATION_POLICY, []).digest, noParts.digest); + const sixteenEmpty = Array.from({ length: MAX_IDENTITY_DIGEST_PARTS }, () => Buffer.alloc(0)); + const sixteenDescriptor = identityDigestV1(IDENTITY_LABELS.VERIFICATION_POLICY, sixteenEmpty); + assert.equal(sixteenDescriptor.input_bytes, 0); + assert.notEqual(sixteenDescriptor.digest, noParts.digest); +}); + +// --- Unambiguous framing and domain separation ------------------------------- + +test('framing makes every part splitting a unique preimage', () => { + const splittings = [ + [Buffer.from('abc')], + [Buffer.from('ab'), Buffer.from('c')], + [Buffer.from('a'), Buffer.from('bc')], + [Buffer.from('a'), Buffer.from('b'), Buffer.from('c')], + [Buffer.from(''), Buffer.from('abc')], + [Buffer.from('abc'), Buffer.from('')], + [Buffer.from('a'), Buffer.from(''), Buffer.from('bc')], + [Buffer.from(''), Buffer.from(''), Buffer.from('abc')], + ]; + const digests = new Set(); + for (const splitting of splittings) { + const descriptor = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, splitting); + digests.add(descriptor.digest); + assert.equal(descriptor.digest, expectedFramedDigestHex(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, splitting)); + } + assert.equal(digests.size, splittings.length, 'distinct splittings must never share a digest'); + + // The naive concatenation these splittings collapse into. + const naive = (parts) => createHash('sha256').update(Buffer.concat(parts)).digest('hex'); + assert.equal(naive([Buffer.from('ab'), Buffer.from('c')]), naive([Buffer.from('a'), Buffer.from('bc')])); + assert.notEqual( + identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [Buffer.from('ab'), Buffer.from('c')]).digest, + identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [Buffer.from('a'), Buffer.from('bc')]).digest, + ); +}); + +test('identical bytes under different labels never share a digest', () => { + const payload = Buffer.from('same bytes'); + const labels = Object.values(IDENTITY_LABELS); + const digests = new Set(); + for (const label of labels) { + const descriptor = identityDigestV1(label, [payload]); + assert.equal(descriptor.digest, expectedFramedDigestHex(label, [payload])); + assert.ok(!digests.has(descriptor.digest), `${label} collided with another label`); + digests.add(descriptor.digest); + } + assert.equal(digests.size, labels.length); +}); + +// --- Mutation-proof, detached descriptors ------------------------------------ + +test('later caller mutation cannot drift an already-taken digest', () => { + const mutable = Buffer.from('one'); + const before = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [mutable]); + mutable.write('two'); + const after = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [mutable]); + assert.notEqual(before.digest, after.digest); + assert.equal(before.digest, identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [Buffer.from('one')]).digest); + assert.equal(after.digest, identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [Buffer.from('two')]).digest); + + // Subarray views stay pinned to the bytes they exposed at digest time. + const parent = Buffer.from('0123456789'); + const view = parent.subarray(2, 6); + const viewDigest = identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [view]).digest; + parent.fill('x'); + assert.equal(identityDigestV1(IDENTITY_LABELS.REQUEST_IDEMPOTENCY, [Buffer.from('2345')]).digest, viewDigest); +}); + +test('descriptors are deeply frozen, primitive-only, and detached', () => { + const descriptor = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [Buffer.from('detach')]); + assert.ok(Object.isFrozen(descriptor)); + assert.deepEqual([...Object.keys(descriptor)].sort(), + ['algorithm', 'digest', 'domain', 'input_bytes', 'label', 'version']); + assert.equal(descriptor.algorithm, 'sha256'); + assert.deepEqual(JSON.parse(JSON.stringify(descriptor)), { ...descriptor }); + assert.throws(() => { 'use strict'; descriptor.digest = '00'; }, TypeError); + assert.equal(descriptor.input_bytes, 6); +}); + +// --- Golden stability for the existing RunIdentityV1 surfaces ---------------- + +test('manifest, prompt, envelope, and run-identity goldens keep their exact bytes', () => { + const manifest = goldenManifest(); + const envelope = compileChildEnvelopeV1(manifest, 'backend-writer'); + // Pinned against the accepted product-foundation identity bytes for this + // fixture; the authority refactor must not move one byte. + assert.deepEqual(runManifestDigestV1(manifest), { + algorithm: 'sha256', + domain: 'codex-co-engineer.identity.v1', + version: 1, + label: 'run-manifest.v1', + input_bytes: 1178, + digest: '636efc2ef2d5496c273cffccb05540343ec4468755903eae0a22f320abd65522', + }); + assert.deepEqual(assignmentPromptDigestV1(manifest, 'backend-writer'), { + algorithm: 'sha256', + domain: 'codex-co-engineer.identity.v1', + version: 1, + label: 'assignment-prompt.v1', + input_bytes: 55, + digest: 'cf3673907687460df58957149361aaa4ffb63ba31d15f32ca40a4f63c59fcc88', + }); + assert.deepEqual(assignmentPromptDigestV1(manifest, 'frontend-writer'), { + algorithm: 'sha256', + domain: 'codex-co-engineer.identity.v1', + version: 1, + label: 'assignment-prompt.v1', + input_bytes: 49, + digest: 'f64f9226311397247d575d4544a400ad00da7281d3504f4cba4383d87f2e0ccb', + }); + assert.deepEqual(childEnvelopeDigestV1(envelope), { + algorithm: 'sha256', + domain: 'codex-co-engineer.identity.v1', + version: 1, + label: 'child-envelope.v1', + input_bytes: 1519, + digest: '61359b14dcaf7dcf5015ea1242ecdbd49540f125a75942f5bd31527ee4b85f30', + }); + const identity = describeRunIdentityV1(manifest); + assert.deepEqual(identity.assignment_prompt_digests.map((entry) => entry.digest), [ + 'cf3673907687460df58957149361aaa4ffb63ba31d15f32ca40a4f63c59fcc88', + 'f64f9226311397247d575d4544a400ad00da7281d3504f4cba4383d87f2e0ccb', + ]); + assert.equal(identity.manifest_digest.digest, + '636efc2ef2d5496c273cffccb05540343ec4468755903eae0a22f320abd65522'); + assert.ok(verifyRunManifestDigestV1(manifest, identity.manifest_digest.digest)); +}); + +test('the generic helper shares one framing with every dedicated surface', () => { + const manifest = goldenManifest(); + const envelope = compileChildEnvelopeV1(manifest, 'backend-writer'); + const canonicalManifest = runManifestCanonicalJsonV1(manifest); + assert.deepEqual( + identityDigestV1(IDENTITY_LABELS.RUN_MANIFEST, [Buffer.from(canonicalManifest, 'utf8')]), + runManifestDigestV1(manifest), + ); + const assignment = parseRunManifestV1(manifest).assignments[0]; + assert.deepEqual( + identityDigestV1(IDENTITY_LABELS.ASSIGNMENT_PROMPT, [ + Buffer.from(manifest.run_id, 'utf8'), + Buffer.from(assignment.assignment_id, 'utf8'), + Buffer.from(assignment.prompt, 'utf8'), + ]), + assignmentPromptDigestV1(manifest, assignment.assignment_id), + ); + assert.deepEqual( + identityDigestV1(IDENTITY_LABELS.CHILD_ENVELOPE, [ + Buffer.from(canonicalJsonStringify(parseChildEnvelopeV1(envelope.envelope_text)), 'utf8'), + ]), + childEnvelopeDigestV1(envelope), + ); +}); + +test('growable ArrayBuffer-backed Buffers are rejected before any byte snapshot', () => { + const growable = new ArrayBuffer(8, { maxByteLength: 16 }); + const part = Buffer.from(growable); + assert.equal(Buffer.isBuffer(part), true); + assert.equal(Object.getPrototypeOf(part), Buffer.prototype); + part.write('drift'); + const error = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part])); + assert.equal(error.code, 'invalid_object'); + assert.equal(error.path, 'parts[0]'); + assert.match(error.message, /growable/u); + assert.ok(error.message.length < 200); + const mixed = errorOf(() => identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, + [Buffer.from('ok'), part])); + assert.equal(mixed.code, 'invalid_object'); + assert.equal(mixed.path, 'parts[1]'); + const fixed = Buffer.from(new ArrayBuffer(8)); + const descriptor = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [fixed]); + assert.equal(descriptor.digest, expectedFramedDigestHex(IDENTITY_LABELS.EVIDENCE_BUNDLE, [fixed])); +}); + +test('digest framing captures writeUInt32BE, hash, JSON, and String at import', () => { + const part = Buffer.from('captured-seams'); + const second = Buffer.from('two'); + const expectedOne = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part]); + const expectedTwo = expectedFramedDigestHex(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part, second]); + const expectedJson = '{"a":"x","z":1}'; + const nativeWrite = Buffer.prototype.writeUInt32BE; + const hashPrototype = Object.getPrototypeOf(createHash('sha256')); + const nativeUpdate = hashPrototype.update; + const nativeDigest = hashPrototype.digest; + const nativeStringify = JSON.stringify; + const nativeString = globalThis.String; + let hostileRuns = 0; + const boom = () => { + hostileRuns += 1; + throw new Error('hostile intrinsic ran'); + }; + try { + Buffer.prototype.writeUInt32BE = function writeUInt32BEHostile() { return boom(); }; + hashPrototype.update = function updateHostile() { return boom(); }; + hashPrototype.digest = function digestHostile() { return boom(); }; + JSON.stringify = function stringifyHostile() { return boom(); }; + globalThis.String = function StringHostile() { return boom(); }; + const actual = identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part, second]); + assert.equal(actual.digest, expectedTwo); + assert.equal(canonicalJsonStringify({ z: 1, a: 'x' }), expectedJson); + } finally { + Buffer.prototype.writeUInt32BE = nativeWrite; + hashPrototype.update = nativeUpdate; + hashPrototype.digest = nativeDigest; + JSON.stringify = nativeStringify; + globalThis.String = nativeString; + } + assert.equal(hostileRuns, 0); + assert.deepEqual(identityDigestV1(IDENTITY_LABELS.EVIDENCE_BUNDLE, [part]), expectedOne); +}); + +test('unratified historical labels never become registry members', () => { + for (const label of [ + 'resolved-plan.v1', 'artifact-id.v1', 'candidate-composition.v1', 'run-decision.v1', + ]) { + const error = errorOf(() => identityDigestV1(label, [Buffer.from('x')])); + assert.equal(error.code, 'unknown_label'); + assert.equal(error.path, 'label'); + assert.equal(error.message, UNKNOWN_LABEL_DIAGNOSTIC); + } + assert.equal(Object.hasOwn(IDENTITY_LABELS, 'RESOLVED_PLAN'), false); + assert.equal(Object.hasOwn(IDENTITY_LABELS, 'ARTIFACT_ID'), false); + assert.equal(Object.hasOwn(IDENTITY_LABELS, 'CANDIDATE_COMPOSITION'), false); + assert.equal(Object.hasOwn(IDENTITY_LABELS, 'RUN_DECISION'), false); +});