Repository navigation
Conversation
…nalMilestonePackageV2 carries Item 8 cannot mint a package without evidence.evidenceBlockHash, and no public code produced it: the encoding existed only as the evidence lane's mirror script on #270 (not merged). This ports it to @pcc/spec: computeEvidenceBlockHash over the six roots, plus computeUnitContextDigest, computeSettlementUnitId (the escrow's frozen derivation), computeSessionKeyAuthDigest, computeAttestationSetRoot and taggedDigestToBytes32. It reuses computeWorkProductHash and computeVerificationProgramHash for the other two roots. Byte-exact against the pinned goldens: domain 0xf15817db..., the escrow's settlement unit id 0x4453a3d2... (also the integrated settlement vector's), and the mirror's evidenceBlockHash 0x4605a6e9... from the mirror's roots. Finding in the evidence lane's own golden: the mirror derived kernelSignedEventsRoot over "0x"-prefixed event hashes, but the kernel signs hashBundle over "sha256:"-prefixed ones, so the mirror's root (0xcc6a7e95...) is not the signed bundleHash (0x24bb6410...), and a block built that way could never match a genuinely signed bundle. The producer takes the root from the signed bundleHash; for the golden inputs the block is 0x854079f7.... A context whose milestoneIndex or stepId does not derive its settlementUnitId is refused, and hex inputs must be 0x + lowercase hex of exact width. Tests: 15 new; spec 812/812; tsc clean. Eight mutants (unit coherence, hex case, address case, role order, job binding, quorum binding, version, leading-zero decimals) each turn a test red. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…rted from #270) R19 group B: the one #270 mirror whose home is this PR. Two schema-valid events (ISO-8601 timestamps) go through the production hashEvent and hashBundle, then taggedDigestToBytes32, and reproduce the golden that #270's kernel-signed-events-root-golden-vector.cjs pinned for the oracle's EvidenceBlockV2 builder: 0x4e0af964e4e066717998ed7a49bf7c874023bd402b825da22b4dabd70fb6f9fe, with both per-event hashes pinned too. Properties pinned alongside it: - the root does not depend on event order; - an event's id and hash are outside its preimage; - the root moves for a 0x-tagged inner preimage, a simulated source, and pass:true in place of pass:1. The existing fixture keeps the v2 mirror's inputs, whose Unix-second timestamps are not schema-valid. Its block golden 0x854079f7 is unchanged. spec 816/816, tsc clean. Mutants (hashBundle without the sort, id or hash inside the event preimage) are each killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
A unit's evidence is one kernel-signed bundle that holds every outcome-bearing event, so the events root it commits leaves nothing out. Same rule as the LO-EV-9 header (bus #3543). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fied bundle (E7 F1) hashBundle trusts each carried event.hash, so two bundles with different payloads and the same carried hashes shared one root, and nothing refused an empty bundle or a bundleHash that did not match its events. Add computeKernelSignedEventsRoot(bundle): snapshot the events once (a JSON round trip of each event), recompute every event hash with hashEvent and require it to equal the carried one, recompute hashBundle over the snapshot and require it to equal bundle.bundleHash, refuse an empty list, and return the bytes32 root. Every failure is an EvidenceBlockInputError naming the field. The module header now states the boundary this function does not cover (kernel signature and session-key delegation: the LO-EV-1 verifier #338; one finalized bundle per settlement unit: a stateful record at the gateway/VCR boundary; parallel, partial, superseded or unfinalized bundles: the oracle), and computeEvidenceBlockHash is documented as the low-level mirror-exact function whose roots must come from the verified derivations. The pinned goldens are unchanged: the suite still reproduces 0x4605a6e9..., 0x854079f7... and the production-form root 0x4e0af964... byte for byte. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…re hashing (E7 F2)
The only attestation validation was the format of each hash, so a quorum
such as {roleId "", minPositive 2, total 1, minScore NaN, hashes [H, H]}, an
empty role set, an empty role, and two roles with one roleId all hashed.
Duplicates stayed cryptographically bound, so a downstream evaluator that
counts entries could have its quorum inflated.
computeAttestationRoleDigest and computeAttestationSetRoot now validate each
role and copy it into frozen plain data (every field and array element read
once), then hash only that copy. Rules, from the E7 design and the #270
mirror inventory:
- job and roleId: 1-128 printable ASCII characters, no whitespace
- the set has at least one role (the mirror pins no empty set) and roleIds
are distinct
- a role has at least one attestation hash
- minPositive and total are safe integers, 1 <= minPositive <= total
- minScore is a safe integer in [0, 100] (golden 80, scores 92 and 88; the
production type is int 0..100)
- attestation hashes are 0x + 64 lowercase hex, distinct, and no more than
total (golden: 2 of 3)
Negative zero is refused for every integer. Duplicates are refused, never
silently de-duplicated. Every refusal is an EvidenceBlockInputError naming the
field.
The pinned goldens are unchanged: the golden role set still hashes to the
same root and the block goldens 0x4605a6e9... and 0x854079f7... still hold.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…rization (E7 F3)
Validation and hashing read the same live objects more than once, so a getter
or Proxy could pass validation with one answer and be committed with another:
attestationHashes was read for validation and again for hashing, and the
session authorization went live into canonicalize, which reads every property
in filter and again in map. The authorization the consumer then evaluated could
differ from the one hashed.
Roles: computeAttestationRoleDigest and computeAttestationSetRoot hash only the
frozen snapshot taken by the F2 validation, in which every field and every array
element is read once. A getter's second answer is never consulted. This commit
adds the tests that pin that behaviour (getters on every field, on an array
element, and a Proxy over the roles array).
Session authorization: add sessionKeyAuthSnapshot(auth) returning { value,
digest }. value is a deep-frozen plain copy of exactly the fields
SessionKeyAuthorization declares (nested scope and arrays included), read once
through property descriptors so an accessor is never invoked. An unknown own
key, a symbol key, an accessor, a non-enumerable field, a non-plain object or
any Proxy is refused. digest is sha256(canonicalize(value)) over the frozen
copy. computeSessionKeyAuthDigest(auth) returns snapshot.digest. Consumers
evaluate value, never the object they passed in. A compile-time guard fails the
build if SessionKeyAuthorization gains a field this snapshot does not list.
The name snapshotSessionKeyAuthorization is not used (it belongs to #438).
The pinned goldens are unchanged; the golden authorization digests exactly as
sha256(canonicalize(auth)) as the #270 mirror defines it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… has one digest (E7 F4) SessionKeyAuthorization.publicKey permits an optional 0x prefix and the gateway intake accepts uppercase, and the digest hashed the raw spelling, so one Ed25519 key had several digests (0x vs bare, upper vs lower). sessionKeyAuthSnapshot now pins publicKey to 64 and parentSignature to 128 lowercase hex characters with no 0x prefix, by REJECTION, never normalization. That is the golden's form and the only form every in-repo producer emits (kernel-sdk job-handler.ts:357,361 and gateway identity-session.ts:119,123, both via a lowercase bare toHex), so no producer is refused and neither the oracle mirror nor the goldens change. parentSignature is pinned the same way as an in-spirit extension of the design (same defect class, same producer forms); it is one line to drop. Cross-form vectors: 0x-prefixed, uppercase, mixed-case, padded and wrong-length spellings are refused; the producers' exact hex is accepted. NOT done (STOP condition from the design): the scope arrays allowedActions and contractIds are not required to be strictly ascending and duplicate-free. The design asks for that only if every producer already complies, and gateway/src/routes/identity-session.ts:99-124 does not: it returns the caller's arrays verbatim through SessionKeyService.issueSessionKey (verifier/src/workflow/ephemeral-identity.ts:156-160), by design (the parent signature covers a sorted copy; ephemeral-identity.test.ts:1184-1232). The module header states the arrays are committed in the order given, and a test.todo marks the open item. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
Number.isSafeInteger(-0) is true and BigInt(-0) is 0n, so -0 and 0 reached the same ABI word although the module pins one spelling of each input. uintWord now refuses a number for which Object.is(value, -0) holds, with a field-named EvidenceBlockInputError. Zero stays valid as 0, 0n and "0", and still yields the same word. The pinned goldens are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ot in the verdict) DROPPABLE: this commit is independent of F1-F5 and is not in the E7 design. computeUnitContextDigest read each context field to check that settlementUnitId derives from them and then read them all again to hash, the same live-object double read as F3. A getter could answer the check coherently and be committed with different values. The function now copies each field once and uses the copies for both the derivation check and the hash. A non-object context is now an EvidenceBlockInputError instead of a TypeError. Reproduced first against 2fa5af8: every field was read twice and the digest differed from the coherent context's. The pinned goldens are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…header (E7 F5) The header still described integers as safe integers without saying that -0 is refused since F5. Also wraps one over-long declaration. No behaviour change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…E7 tests A mutation pass over the new rules left three mutants alive and exposed guards with no direct test: - accessor refusal: the tests asserted only the field name, but an accepted accessor is refused downstream under the same field for required keys, so the mutant passed. They now assert the message, and a new case covers an accessor on the optional derivationPath, which a lenient reader would have dropped as "absent" without any refusal. - required fields: the missing-field test now asserts "is required". - array length: a Proxy over an array passes Array.isArray and NaN < 1 is false, so without the explicit safe-integer check an array with a lying length is read as empty and an EMPTY bundle, role set or hash list would be accepted. New tests cover bundle events, roles and attestationHashes with length NaN, -1, 1.5, "2", undefined and Infinity. No source change; the pinned goldens are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
, so the event snapshot reuses it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… return the snapshot with the root (E7b) E7b (HIGH, cross-family verdict on #361 at 53e347d): snapshotEvent used a JSON.stringify/parse round trip, so computeKernelSignedEventsRoot verified and committed a projection of the evidence, not the evidence. Reproduced first at 53e347d with a scratch test (8 of 8 cases failed): a {value: NaN} event with the carried hash and bundleHash of a {value: null} event was accepted with the same root; so were a top-level toJSON() returning the hashed event, an accessor (its getter ran), a class-instance payload, a non-enumerable or prototype toJSON, Infinity, an undefined array element and a hole. - snapshotEvent takes canonicalSnapshot (#359, merged in 456f076): own property descriptors only, so no getter or toJSON runs; it refuses accessors, non-enumerable and symbol keys, NaN and Infinity, bigint, functions, undefined array elements and holes, cycles and non-plain objects. A NonCanonicalValueError becomes an EvidenceBlockInputError whose field is the event index plus the member path (events[1].payload.value); the reason is bounded and has no control characters. - The snapshot's own value is what hashEvent checks and what is returned, deep-frozen. - computeKernelSignedEventsRoot returns { root, events } (KernelSignedEventsSnapshot). events is exactly the data whose hashes were recomputed and checked; consumers evaluate it, never the object they passed in. The return type changes; the only callers at this head are the tests, updated here. - An undefined OBJECT member stays omitted, as canonicalize omits it everywhere (the gateway's carrier events leave optional fields undefined and hash them that way); it is absent from the hash and from the returned events alike. Refusing it would reject honest bundles and need a second walk over the input. - Existing tests follow the new return and the accessor case, which is now refused at events[0].hash with the getter never invoked. The pinned goldens are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…mitted and returned as one snapshot (E7b)
Fifteen tests for the E7b fix, one per case of the verdict plus the contract of the
returned value:
- (a) the reviewer's repro: a {value: NaN} event with the same carried hash and
bundleHash as a {value: null} event is refused, naming events[0].payload.value;
Infinity, an undefined array element, a hole, a bigint, a function, a symbol,
an unsafe integer and a symbol key are refused at their member before any hash is
compared; a hole or undefined entry in the events array is refused at its index.
- (b) a toJSON on the event (own, non-enumerable, on the prototype) or on a payload
is refused and never called.
- (c) an accessor on a payload member, an array element or source is refused and its
getter never runs.
- (d) a class instance, Date, Map, Set, RegExp, typed array, Error and an object with
a substituted prototype are refused, wherever they appear.
- (e) an undefined array element is refused. An undefined OBJECT member is omitted
from the hash and from the returned events alike, as canonicalize omits it
everywhere; pinned so a change of that policy is noticed.
- (f) { root, events }: events equal the submitted events in the order given, are
copies, are frozen at every depth, do not change when the input is mutated
afterwards, still re-verify against every carried hash and the root, and have no
prototype. Frozen, structuredClone and null-prototype spellings of the same data
give the same root and events.
- (g) a Proxy event is read once through its reflection traps (no [[Get]]), so a
trap that answers differently on a second read cannot split the hashed value from
the returned one.
- A refusal's field and message carry no control characters and are bounded, so a
hostile key cannot inject log lines.
Reproduced first at 53e347d: the scratch version of cases (a) to (f) failed 8 of 8.
The pinned goldens are untouched.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… the E7b mutation run The mutation run over the E7b fix left one mutant alive: dropping the Array.isArray check in snapshotEvent. An array snapshots as valid JSON, so without the check it reaches the hash comparison and is refused at events[0].hash instead of at events[0] as "expected an event object", and no test pinned which. The non-object table now includes [] and [1, 2]. No source change; the pinned goldens are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…order (E7 F4, gateway #4670)
The F4 residual was blocked because SessionKeyService.issueSessionKey passed the
caller's scope arrays through in the caller's order, so one set of permissions could
have several digests and a producer existed that would not comply. The gateway
agreed (bus #4670) to build scope.allowedActions and scope.contractIds as
[...new Set(xs)].sort() before signing, so every producer emits the canonical form.
sessionKeyAuthSnapshot now pins both arrays by rejection: each must be an array of
strings in STRICTLY ascending UTF-16 code-unit order (each element < the next with
plain JS string comparison, so no duplicates). An unsorted or duplicated array is
refused at the first element that breaks the order, with the field path
(sessionKeyAuthorization.scope.contractIds[1]); nothing is sorted or de-duplicated.
An empty array is allowed. Each array is still read once, through descriptors, and
the order is judged on the copy that is committed. readStringArray becomes
readCanonicalStringSet.
The F4 it.todo is replaced by tests: unsorted refused, duplicated refused (adjacent
or not), sorted and de-duplicated accepted and committed as given, empty accepted,
the order is code-unit order (not locale, not code point: "Z" before "a", an astral
character before U+FF5E), and of the 192 arrays over {a, b, c} that hold all three
permissions exactly one (the canonical form) is accepted, so two inputs differing only
in order or duplicates can no longer both produce a digest. One existing test pushed
"added-after-hashing" after "unit-golden"; it now pushes a value that sorts after, so
it still tests that later changes to the original do not reach the snapshot.
The module header's F4 note now says the arrays are pinned and producers emit the
canonical form. The pinned goldens are untouched: their scope arrays have one element.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ect member is omitted (E7b) Comments and one assertion only, no behaviour change. acd5b7f gave, in its message and in the module header, a reason that is wrong: that the gateway's carrier events leave optional fields undefined and that refusing an undefined object member would therefore reject honest bundles. Checked afterwards against the producers: carrier.ts takes those fields from TrackerWebhookEvent, which types them string | null, and lob.ts includes trackingNumber only when present "so canonical hashing omits it rather than hashing null". No producer in this tree leaves an undefined member, so that reason is withdrawn. (acd5b7f is unpushed history and is not rewritten; this commit is the correction.) The behaviour stands on these grounds instead: - canonicalize, and so hashEvent and verifyEventHash, omit an undefined OBJECT member everywhere in the repo, and #359 pins that (canonical.test.ts:240-241); - JSON transport drops it too, so the oracle's reconstruction sees the same event; - it is absent from the hashed text and from the returned events alike, so what a consumer evaluates is what was hashed; - refusing it would take a second read of the input, against the lane's decision to reuse canonicalSnapshot and not write another walker, and would make this step stricter than verifyEventHash, which accepts the same event. The (e) test now also asserts that verifyEventHash accepts the event with the undefined member, which is the premise of the last point. If the lane wants such an event refused, the single-read way is a strict option in canonicalSnapshot (#359); this test would then flip. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… state the Proxy read-once guarantee (E7b) - printable() now cuts the text to its limit BEFORE it scrubs control characters, so a very long hostile key cannot make the scrub itself expensive. The limit is applied once, so there is a single cap. - computeKernelSignedEventsRoot's doc states in one place that a Proxy is not refused but read once, through its reflection traps, so what is hashed is what is returned and evaluated, however the traps answer a later read. - A new test pins that a value nested 100,000 levels deep, as a payload member or as the event itself, is refused as an EvidenceBlockInputError at events[0], never a RangeError. No other behaviour change; the pinned goldens are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
LamaSu
changed the base branch from
master
to
fix/spec-canonicalize-refuse-non-json
October 1, 2026 10:05
…ore reading it, use captured intrinsics (E7c) E7c (HIGH, cross-family verdict on 55721c3): computeKernelSignedEventsRoot read bundle.events and bundle.bundleHash with a [[Get]] (so a caller getter ran) and accepted Proxy events (so their traps ran inside canonicalSnapshot). That code can replace Object.freeze with the identity function before deepFreeze() and restore it with queueMicrotask, so the returned events stay mutable and the events a consumer evaluates can differ from the events that were committed. Reproduced at 55721c3 for all six shapes: events getter, events-array Proxy, Proxy nested in a payload, Proxy bundle, bundleHash getter, and a caller that replaces Object.freeze across the call. Design: refuse every code-running input BEFORE touching it, and use only captured intrinsics afterwards. - Capture Object.freeze, Array.isArray, Reflect.ownKeys, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Number.isSafeInteger and util.types.isProxy when the module loads. They replace every use of the globals after input is touched: deepFreeze, the result freezes, and the session and role snapshots (key enumeration, freezes, array and integer checks). - The bundle: refuse a Proxy; read events and bundleHash only from their own descriptors. An accessor ("a getter on the bundle runs code"), a missing property and an inherited one are refused. - The events array: refuse a Proxy (checked before Array.isArray, which throws on a revoked one); read length and each index from own data descriptors, so a hole or an accessor element is refused. - assertNoCodeRunningInput runs on each event before canonicalSnapshot: iterative, captured functions only, a WeakSet skips a node already walked. It refuses a Proxy at any depth and an accessor anywhere, and refuses (as canonicalize does) a node whose prototype is not plain before enumerating it, so a large typed array or Buffer stays an O(1) refusal instead of an ownKeys over every element. JSON-type rules stay in canonicalSnapshot, which now runs on a graph proven free of Proxies and accessors. - The events array is frozen before hashBundle receives it. Also found while capturing the intrinsics: isPlainObject asked an object's prototype for its own prototype, which runs a Proxy prototype's getPrototypeOf trap in sessionKeyAuthSnapshot. A Proxy prototype, and a revoked Proxy, are now refused unasked. Roles are unchanged apart from the captured intrinsics. Tests. Three existing tests pinned the behavior this removes and now expect REFUSAL, with no trap or getter allowed to run: - "reads the bundle's events and bundleHash exactly once each" expressly accepted and invoked bundle getters (the reviewer's evidence-block.test.ts 465-479); it is now "refuses a getter on the bundle ... and never runs it", and the reviewer's exact reproduction (a) sits beside it; - "reads each element of the events array once" accepted a Proxy events array; - "(g) reads a Proxy event once, through its reflection traps" accepted a Proxy event. New: (b) events-array Proxy, (c) a Proxy at every depth and as a prototype, (d) bundle Proxy, (e) bundleHash getter, (f) Object.freeze replaced across the whole call (events and session snapshot) still returns deeply frozen data, plus an accessor-before-JSON-defect precedence pin, a 50,000-deep Proxy, the non-plain-before-enumeration cases, and a spy test showing the module calls none of the replaceable intrinsics at call time. canonical.ts and types/evidence.ts are untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ype-chain Proxies, found by the E7c mutation run The mutation run over the E7c fix killed every required mutant (bundle read with [[Get]], isProxy pre-walk dropped, global Object.freeze in deepFreeze, accessor refusal dropped in the pre-walk) and one survived: judging a descriptor as data with `"value" in descriptor` instead of by the fields it owns. That reads an inherited `value` from a polluted Object.prototype, so an accessor on the bundle or in an event would be taken for a data property holding the polluter's value. A new test pollutes Object.prototype.value for the synchronous part of the call and requires both an accessor on the bundle and an accessor in an event to be refused unrun; the survivor now fails it. Also pinned: a Proxy that sits in the prototype chain of the bundle, of the events array or of an event is never asked anything (identity comparison of the prototype only, no trap), whether the call is accepted or refused. Test-only: no production change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…pe the test Proxy helper (E7c) Comments and test typing only; no behavior change. The header and the capture block said the module captures "everything it calls on caller data". That overstates it: the roles, the session fields and the unit context still use Set, Map, Object.is and BigInt, which see only primitives after the objects have been inspected. What is captured is what inspects and freezes caller-supplied objects (Object.freeze, Array.isArray, Reflect.ownKeys, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Number.isSafeInteger, util.types.isProxy), and that is what the text now says. recordingHandler in the test file is generic over the Proxy target, so the file type-checks under the temporary tests tsconfig (the package tsconfig excludes __tests__): no error in evidence-block.test.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…y point, read fields from own descriptors, encode without mutable globals (E7c round 2) Round 1 closed the events bundle. The other entry points still read their input with a [[Get]] and looked Buffer.from, Number, BigInt, regular expressions, Array.prototype.sort and Hash.prototype.update up at call time. Reproduced at ad2359e: a unit, unit-context, role, role-set or roots getter that swaps Buffer.from or Array.prototype.sort runs, the call succeeds and the digest changes (u1, r1, r2, b1); a context getter makes a forged settlementUnitId derive (u2b); a Proxy at any of the six inputs runs its traps (p1-p6); an accessor inherited from a polluted Object.prototype is read for a missing field (h1, h2). The session snapshot already refused (controls s1-s3). Every exported function that takes an object or an array now runs the SAME guard first, assertNoCodeRunningInput (a Proxy at any depth, an accessor anywhere, an object that is not plain), through one helper, admit(): computeSettlementUnitId, computeUnitContextDigest, computeAttestationRoleDigest, computeAttestationSetRoot, computeEvidenceBlockHash, sessionKeyAuthSnapshot (and so computeSessionKeyAuthDigest). Each field is then read from its own data descriptor (fieldOf, ownDataValue), never with a [[Get]], so no getter runs and a field the object does not own is missing whatever Object.prototype says. A non-object input is a typed refusal (it was a TypeError for a unit and for the roots). "Plain" is now one predicate for every input: prototype null or a prototype-less object that is no Proxy (any realm), so a class instance, Map, typed array, Error or a Proxy prototype is refused before it is enumerated; canonicalize stays the strict judge of an event. After the guard only captured references and pure loops are used: Number, BigInt, the Uint8Array constructor, Hash.prototype.update and digest, Set and String.prototype.charCodeAt and slice are captured at load; hex, bytes, ABI words and the field checks are char-code and byte loops (no Buffer, no regular expression, no Uint8Array.from or .set, no Object.is, no Map); sorting is a heapsort over a copy and arrays are built with Reflect.defineProperty, so no Array.prototype method is looked up. @noble/hashes and node:crypto internals are not capturable and are not claimed. Tests. Eight existing tests pinned the behavior this removes and now expect REFUSAL, with no getter or trap allowed to run: the five "roles are read once" tests (getter answers, accessor element, scalar getters, Proxy roles array), "every field is read once" for the unit context, and the session accessor test (its message is now the guard's); one F2 test sees a Set as attestationHashes refused as non-plain instead of "expected an array". New, for each of the seven entry points: honest input accepted (plain or null-prototype), a Proxy as the whole input (live and revoked) and at every member, a getter at every member, a getter that swaps Buffer.from, Array.prototype.sort and Object.freeze (none is touched), a missing field never supplied by a polluted Object.prototype, an unrelated member ignored but a Proxy or getter there refused, a class instance, Map or Proxy prototype refused; plus a test that every digest is unchanged while about 130 Buffer, Array, Set, Map, RegExp, String, Object, Reflect, JSON, Function.prototype and Hash methods throw when called and Number and BigInt cannot be called, a 2,800-case comparison of every validator against the regular expression it replaced, and a shuffled-input test of the new sort. The golden literals are untouched and byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…un (decimal strings, the events call, an accessor under a polluted value) The round-2 mutation run killed every guard mutant (one per entry point) and every shared-piece mutant, and left one survivor in the pure encoding layer: isDecimal judged by a regular expression instead of a char-code loop. The replaced-globals test only passed bigint and number chain ids, so isDecimal (string spellings only) was never called inside its window. It now also passes decimal-string unit and context spellings, and a second window covers the synchronous part of computeKernelSignedEventsRoot (admission, the walk, the snapshot and its freezes) with Array, Set, Map, RegExp, String, Object, Reflect, JSON and Hash methods throwing, which also kills an events path that goes back to events.push. Also pinned, per entry point: an accessor member is still refused while Object.prototype carries a `value` (judging a descriptor with `"value" in descriptor` instead of by the fields it owns would read the polluter's value); this kills that mutant at all seven entry points, where only the round-1 bundle test did before. Equivalent, left alone: reading an array's `length` with a [[Get]] instead of from its descriptor (E2). A real array's length is an own non-configurable data property, and the only exotic array, a Proxy, is refused before that line. Test-only: no production change. 179 tests in the file, 1162 in the package. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… static node:util import, and fail closed without it build-dashboard has failed on this PR since 53e347d: apps/dashboard bundles @pcc/spec's evidence barrel with Vite, the dashboard aliases node:crypto to a browser shim but has nothing for node:util, and evidence-block.ts imported { types } from "node:util" (the only spec module that does) to get util.types.isProxy: packages/spec/dist/evidence/evidence-block.js (143:9): "types" is not exported by "__vite-browser-external" Reproduced locally at 814f694 (spec, contract-builder and ui built with tsc, then the dashboard's own tsc -b && vite build): same error, same line. The static import is gone. The module now takes isProxy ONCE, when it loads, from process.getBuiltinModule("node:util").types.isProxy (Node >= 20.16 and >= 22.3), with the other captured intrinsics, so a util.types.isProxy that is replaced after load still changes nothing (the two existing tests that replace it pass unchanged). If the runtime cannot give it (an older Node, a browser, a getBuiltinModule that is missing, throws, or answers with something that is not a function) the module still LOADS: nothing is checked at import time. Every Proxy test in the module goes through one function, isProxy(), which THROWS EvidenceBlockInputError (field "runtime"): evidence-block needs Node's util.types.isProxy (Node >= 20.16 / 22.3) to refuse code-running input; it is unavailable in this runtime An unknown is never "no": there is no fallback to skipping the Proxy check, and the throw comes before any caller object is read, so no trap runs. All eight entry points that take an object or an array (the seven synchronous ones, and computeKernelSignedEventsRoot, which rejects) refuse; taggedDigestToBytes32 and the exported constants take no object and work everywhere. node:crypto is untouched (the dashboard aliases it); the dashboard config and every other package are untouched; there is no new export. Tests (evidence-block.test.ts, +5: 184 in the file, 1167 in the package). Each loads a FRESH copy of the real module while process.getBuiltinModule is absent or wrong in each of six shapes and node:util throws if anything imports it, so there is no production override or seam. They assert: the import succeeds and the constants and taggedDigestToBytes32 work; every one of the eight entry points refuses an honest input with the typed error; a Proxy input is refused the same way with no trap run; the table covers every function the module exports; and, as a control, a host that does give the function gets the same module accepting honest input, refusing a Proxy as a Proxy, and asked for node:util exactly once, at load. The existing Proxy-refusal tests pass unchanged on Node 22. Verified: the dashboard build now exits 0 (3890 modules). The evidence-block bundle, built with the dashboard's node:crypto alias, loads in a bare VM context with no process, Buffer or require; its constants match the goldens and every guarded entry point refuses with the typed error without running a trap. tsc --noEmit is clean and the goldens check passes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…bservable assertion, not a vacuous one The control test in 9e370bb ended with expect(asked).toEqual(["node:util"]), meant to pin that the module takes util.types.isProxy from the host once, when it loads, and never again. It could not fail: loadWith has put the real process.getBuiltinModule back by the time the module is used, so the recorder was no longer installed and nothing was ever recorded at call time. The mutation run found it: a module that resolves the host function at CALL time (M5) was killed by the two older "replaced util.types.isProxy" tests and by two of the new ones, but not by this test. The recorder is now installed again while the module is used, and the test asserts that the host was asked exactly once during the load (empty after that, across the honest calls, the checks against the other copy of the module, and the Proxy calls). Under M5 it now fails with the host asked 99 times; on the real module it passes. Test-only: no production change. 184 tests in the file, 1167 in the package. Mutants run against the committed source for this change, each restored with git checkout: M1 reinstate the static named import of node:util: the dashboard build fails ("types" is not exported by "__vite-browser-external"), and all five new tests fail (the node:util mock throws on import); the 179 older tests pass. M2 remove the fail-closed throw (return false): the two refusal tests fail (an honest input returns the golden digest); the 179 older tests pass. M3 drop the typeof-function check on the host value: killed by the refusal test. M4 rethrow from the catch around getBuiltinModule: killed by the load and refusal tests. M7 a throwing getBuiltinModule falls back to a no-op Proxy test: killed by the refusal test. M5 resolve the host function at call time: five tests, two of them old. M8 add an exported function that the table does not list: killed by the coverage test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…AttestationSetRoot to ratified D4 keccak/abi formula #361 implemented an older sha256/JCS role-tree formula bound to the job, and refused an empty role set and an empty role. The ratified D4 formula (oracle #1030, #1289) is keccak/abi and bound to the funded program instead, and its empty case is defined (oracle #4836): a role with no attestations yet and a funded program naming no roles are both valid. - AttestationQuorumRole gains `signers: { kind: "registry"; registryId; snapshotHash }` (D4: registry-snapshot role policy, not inline signers). - computeAttestationRoleDigest/computeAttestationSetRoot now take `fundedProgramHash: Bytes32Hex` instead of a free-form `job: string`, and compute keccak256(abi.encode(ROLE_DOMAIN, K(roleId), roleSignersDigest, uint32 minPositive, uint32 total, uint32 minScore, fundedProgramHash, sortedAttestationHashes)) and keccak256(abi.encode(ATTSET_DOMAIN, fundedProgramHash, sortedRoleDigests)) respectively, byte-exact against the evidence lane's golden attestation-set-root-golden-vector.cjs (#270 @ c56bc14). - Kept every existing refusal (duplicate roleIds, duplicate attestation hashes within a role, invalid quorum, a malformed fundedProgramHash/snapshotHash, a non-token registryId) plus a non-"registry" signers kind and uint32 range on minPositive/total. Dropped only the empty-set and empty-role refusals. - Added `keccakAbiWithTrailingArray`, matching Solidity's abi.encode layout for a parameter list whose only dynamic type is a trailing bytes32[] (offset, then length, then elements), built from the module's existing captured-intrinsic helpers (appendTo, uintWord, keccakWords): no Array.prototype method is looked up. - Captured `TextEncoder`'s constructor and `.encode` at module load (keccakUtf8 is now called per attestation call, not only once for the three existing domain constants, so its UTF-8 encoding needed the same discipline as the hash methods already have). - No code-running-input or captured-intrinsics regression: the module's admit/ assertNoCodeRunningInput guard, own-data-descriptor reads and captured intrinsics are unchanged in discipline, just extended to the new `signers` field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…shared block golden, extend realm-patch coverage Realigns the test file with #361's D4 realignment (see the paired fix(spec) commit): - The top-level `roles`/`attJob` fixture IS now the evidence lane's pinned golden (attestation-set-root-golden-vector.cjs on #270 @ c56bc14): A1/A2 and fundedProgramHash copied verbatim from its printed output, roleId "inspector" with a registry-snapshot signers policy. - New "E7 D4" describe block pins: the inspector roleDigest and attestationSetRoot goldens byte-exact; the two empty-set goldens (oracle #4836); an empty role (no attestations yet) is valid; negatives (3)-(9) from the spec (snapshotHash, minPositive, roleId, duplicate roleId/hash, non-registry kind, uint32 overflow). - The shared EvidenceBlockV2 block golden (goldenRoots(), 0x4605a6e9.../0x854079f7...) now pins the OLD pre-D4 sample's attestationSetRoot as an opaque PRE_D4_SAMPLE_ATTESTATION_SET_ROOT constant (recorded before any change: 0x606f17fcfd5dabd1746cd8ec406636b3d1bae2e80f310bd6dee221a756c024b2) instead of calling computeAttestationSetRoot, so it does not move. - E7 F2 rewritten for D4: signers added to every role fixture; dropped the two empty-set/empty-role refusal assertions (now valid, per brief); added refusals for signers.kind, signers.registryId, signers.snapshotHash and a malformed fundedProgramHash in place of the old job-token checks; added uint32-overflow cases for minPositive/total. - E7 F3, the E7c round-2 generic entry table, the "judges every spelling" fuzz table, the non-integer-array-length block and the host-realm module-reload table all extend to the new `signers` field and the renamed `attFundedProgramHash` parameter. - Fixed two now-stale realm-patch assertions exposed by the D4 switch to keccak (@noble/hashes calls Number.isSafeInteger internally, same documented caveat as the unit-context checks; TextEncoder.prototype.encode is now called per attestation call, not only at module load for the three pre-existing domain constants). Suite: 1176/1176 passing (45 files), tsc --noEmit clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
#359 merged master in (6f513fe; A05e CONFIRMED). The only conflict here was packages/spec/src/evidence/index.ts. Both exports are kept: master's signing-preimage.js, then #361's evidence-block.js. Spec: 54 files, 1472 tests pass; tsc --noEmit is clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…fied D3 two-value keccak/abi formula computeSessionKeyAuthDigest was the OLD mirror form, 0x + sha256(canonicalize(snapshot of SessionKeyAuthorization)). Replace it with the RATIFIED D3 form the oracle's production EvidenceBlockV2 builder uses (oracle #1030, Evidence Commitment Profile v1 S3), byte-exact with the evidence lane's golden sessionkey-grant-golden-vector.cjs on #270: sessionKeyGrantHash = keccak256(GRANT_DOMAIN || canonicalSessionKeyBytes(sessionKey)) sessionKeyAuthDigest = keccak256(abi.encode(AUTH_DOMAIN, sessionKeyGrantHash, bytes parentSignature, bytes32 parentPublicKeyRaw32, uint8 scheme, uint32 keyVersion)) canonicalSessionKeyBytes is utf8(JSON.stringify(body)), body in the EXPLICIT field order the golden's production re-derivation (ephemeral-identity.ts) uses: sessionId, parentAgentId, publicKey, issuedAt, expiresAt, scope{allowedActions, contractIds, maxSignatures}, [derivationPath]. parentSignature is EXCLUDED from the grant (it is the signature over that body) and BOUND into the auth digest instead, alongside the parent's raw32 public key, the scheme and the key version -- none of which live on SessionKeyAuthorization itself, so the smallest honest API adds a separate `context` argument (SessionKeyAuthDigestContext: parentPublicKey, keyVersion, scheme) rather than widening the authorization type. New exports: - SessionKeyAuthDigestContext, SessionKeyScheme - computeSessionKeyGrantHash(auth): the grant hash alone, independently useful since a verifier checks the parentSignature against it before it ever sees a context. - computeSessionKeyAuthDigest(auth, context): now two-argument; auth is admitted and snapshotted exactly as before (sessionKeyAuthSnapshot is unchanged -- its own `.digest` stays the superseded mirror form); context is admitted the same way every object input is (E7c). New module-private helpers, all pure loops/captured intrinsics (no Array.prototype method, no Buffer, no ambient lookup at call time), matching the module's existing discipline: - `jsonStringify` captured at load (alongside TextEncoderConstructor/textEncoderEncode/UINT32_MAX, moved earlier in the file so the new session code can reference them in reading order). - sessionKeyGrantBody / sessionKeyGrantHashOf / keccakDomainPrefixedBytes: the grant hash is keccak256(domainWord || data) over a VARIABLE-length byte string with no padding -- literal concatenation, not abi.encode (keccakWords/keccakAbiWithTrailingArray only ever concatenate whole 32-byte words). - paddedDataWords / keccakAbiSessionKeyAuth: abi.encode(bytes32, bytes32, bytes, bytes32, uint8, uint32) for the one dynamic `bytes` parameter (parentSignature) this formula ever encodes -- the six-slot head holds an offset word at slot 2, the tail holds the length then the 32-byte-padded data. The superseded mirror-form fixtures (old sessionKeyAuthDigest 0x73b60d4d..., block goldens 0x4605a6e9.../0x854079f7...) are preserved via an opaque constant in the test file, unaffected by this change (test commit follows). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…oduction-aggregate acceptance test, update every call site for the new context argument - "E7 D3": the evidence lane's own golden sample (sessionkey-grant-golden-vector.cjs on #270 @ 973fdeb), copied verbatim: sessionKeyGrantHash 0x9b0a9a62..., sessionKeyAuthDigest 0xaccbbe5a... (keyVersion 1, scheme ed25519). Negatives: a mutated parentSignature changes the auth digest but not the grant hash (excluded from the grant, bound in the auth); keyVersion 2 changes the digest; a mutated session body changes the grant (and so the auth digest). Malformed context: a non-raw32 parentPublicKey, a keyVersion outside uint32, an unknown scheme, a non-object context, and a Proxy/accessor inside it -- all refused with EvidenceBlockInputError, the accessor case never invoking the getter. - New top-level describe: the six production roots (unitContextDigest, kernelSignedEventsRoot via computeKernelSignedEventsRoot over the production ISO-8601 event set, sessionKeyAuthDigest via the D3 golden sample, attestationSetRoot via D4, workProductRoot, programHash -- #361's own functions wherever #361 computes them) give computeEvidenceBlockHash(...) === 0xcb30733c2904e714a4ef89a387b75bdcfa07aff5a4ea7482b55404a1e24e396c, the oracle's pinned production aggregate (bus #1069, #5772). - goldenRoots() now feeds sessionKeyAuthDigest from a new opaque constant, PRE_D3_SAMPLE_SESSION_KEY_AUTH_DIGEST = the exact pre-change computeSessionKeyAuthDigest(sessionKeyAuth) output (0x73b60d4d..., recorded via a scratch vitest file against the pre-change code, run once, then deleted, not committed), so the two superseded mirror-form block goldens (0x4605a6e9... / 0x854079f7...) are unchanged and still pass -- same method D4 used for PRE_D4_SAMPLE_ATTESTATION_SET_ROOT. - New top-level fixture `sessionAuthContext` (a fixed, arbitrary SessionKeyAuthDigestContext) pairs with the existing `sessionKeyAuth` sample everywhere it is reused: the E7c round-2 entry table, the globals-saboteur "derives every digest" test, the per-character regex-fuzz table, the session-authorization and E7 F4 describe blocks, and the host-realm module-reload table (which also gains a `computeSessionKeyGrantHash` entry, required by its own exhaustive "covers every function the module exports" assertion). - Digest-VALUE assertions that compared computeSessionKeyAuthDigest to sha(canonicalize(...)) (the old mirror form) are repointed: sessionKeyAuthSnapshot's OWN `.digest` field is unchanged and still asserted equal to sha(canonicalize(...)) wherever that was the thing under test; every computeSessionKeyAuthDigest assertion now either compares against a golden value pinned from the D3 vector, or proves internal consistency (same content via two separate objects gives the same digest; a mutated/rotated input gives a different one) -- never a parallel hand-rolled reimplementation of the D3 formula. Suite: 198/198 passing in evidence-block.test.ts (193 pre-change + 5 new); 1477/1477 across the whole spec package. tsc --noEmit: exit 0, zero diagnostics (the package tsconfig excludes src/__tests__/**, so this does not typecheck the test file; vitest's esbuild transform runs it untyped, which is why every call site still needed fixing by hand). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ter load changes a session-key hash (E7f MEDIUM) E7f (astra on 2550254, SHIP-WITH-FIXES) found that the captured JSON.stringify still asks every object and array of the D3 grant body for `toJSON`, through the prototype chain. Reproduced, by a different route than the review's: - The review's own repro (an own toJSON on Object.prototype or Array.prototype) does NOT change a hash at 2550254. canonicalize's polluted-prototype guard (util/canonical.ts, from #359) refuses the session snapshot first, and the hook never runs. - That guard reads only those two prototypes' own properties. A toJSON on an object inserted between Array.prototype and Object.prototype passes it. At 2550254 the hook runs (4 calls), the grant hash moves from 0x9b0a9a62... to 0xad76b713..., and the auth digest from 0xaccbbe5a... to 0x904a9e10.... Fix: - sessionKeyGrantBody builds the body and its scope with no prototype. - Both scope arrays are copied into arrays with no prototype (prototypeFreeArray: elements are defined, never assigned). - JSON.stringify's toJSON lookup now ends at each value itself. - The bytes are unchanged: the D3 goldens and the production aggregate 0xcb30733c... still pass. New test: three installs after load, each as a method and as a counting getter, over every synchronous entry point: - an own toJSON on Object.prototype or Array.prototype; - a toJSON on an object inserted under Array.prototype; - an own toJSON on String.prototype or Number.prototype. The hook is never looked up or run. The session-key entry points are refused in the first case (the guard) and unchanged in the other two. spec: 54 files, 1478 tests; tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ath, so no inherited getter supplies one (E7g MEDIUM) E7g (astra on 605eb92, NOT CONFIRMED) found one more route for E7f's property. The snapshot leaves an absent derivationPath out, and it has Object.prototype. sessionKeyGrantBody read value.derivationPath with an ordinary [[Get]], so an absent field walked to Object.prototype, where a getter placed after load could supply a path that the grant then committed. Reproduced at 605eb92 with the review's own steps: the fixture has no own derivationPath, and Object.prototype.derivationPath is a getter returning "m/injected". Results: - the getter ran 4 times; - the grant hash moved from 0x9b0a9a62... to 0xa92a2f16...; - the auth digest moved from 0xaccbbe5a... to 0x60264b41.... Fix: sessionKeyGrantBody takes derivationPath through ownDataValue (own descriptors only). It is present only as the validated string; ABSENT leaves the key out. Every other field it reads is always present on the snapshot. Tests: - the review's reproduction, one to one: the getter never runs, and both hashes are unchanged; - the general property: an Object.prototype accessor (logging get and set) for EVERY key the module uses. Those are every own key of every input, the descriptor keys, the result keys and "length". Every synchronous entry point runs. At 605eb92 the log is exactly "get derivationPath" x4, so no other key reaches a prototype. Now the log is empty, and every answer is unchanged. The E7f inputs and runner are hoisted to the describe block and shared. spec: 54 files, 1480 tests; tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… end pre-stage; index.ts keeps both exports Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
….ts keeps every export, plus the explicit Bytes32Hex re-export (TS2308 with #496) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ch was first fast-forwarded to its E7i-confirmed end pre-stage (0259682) Clean merge, no conflicts: #361's own files (evidence-block.ts, its test) and every module they import are byte-identical to 0259682; evidence/index.ts takes master's kernel-pull-capture export beside #361's evidence-block export. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116fiVRS9gDE1HrCECyjVR9
…use keyVersion 0 (oracle D3 review F1, F3) The Opus 5.5 review of oracle D3 (pcc-oracle #23 @2cb5095f) found two LOWs that are partly evidence's: - F1: no vector computed outside the oracle pinned the ORDER of the uint8 scheme slot and the uint32 keyVersion slot. At keyVersion 1 both slots hold 1, so #361's own goldens had the same blind spot: an encoder that swapped the two slots passed all 201 tests. The new test pins keyVersion 2 (0x82c74db8...026f) and 2^32-1 (0x7afa19ea...2a4a). Both values were computed outside this module by two encoders that share no code: ethers 6.16.0's AbiCoder (the D3 golden generator's authV2) and a pure-Python Keccak-256 over a hand-written 288-byte layout (bus #7155). Negative control: swapping the two words in computeSessionKeyAuthDigest fails only this test; the received value is F1's predicted 0xefbcc851...8b2c. - F3: keyVersion >= 1 as a SHARED rule. computeSessionKeyAuthDigest now refuses 0 (context.keyVersion), matching the oracle's step-c range [1, 2^32-1] and evidence's ruling that versions start at 1 (bus #6776). Evidence Commitment Profile v1 section 3 carries the amendment. Negative control: a minimum of 0 fails only the refusal test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0116fiVRS9gDE1HrCECyjVR9
LamaSu
changed the base branch from
fix/spec-canonicalize-refuse-non-json
to
master
October 7, 2026 00:18
LamaSu
had a problem deploying
to
trusted-checks
October 7, 2026 00:19 — with
GitHub Actions
Failure
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Must-close 8 needs
evidence.evidenceBlockHashto mint a FinalMilestonePackageV2 (#358), and nothing in public PCC produced it. The encoding existed only as the evidence lane's mirror script on #270, which is not merged. This ports it to@pcc/spec.New functions:
computeEvidenceBlockHashcomputeUnitContextDigest, which refuses a context whosemilestoneIndexorstepIddoesn't derive itssettlementUnitIdcomputeSettlementUnitId, the escrow's frozen derivationcomputeSessionKeyAuthDigestcomputeAttestationSetRoot, which binds role, quorum and jobtaggedDigestToBytes32It reuses
computeWorkProductHashandcomputeVerificationProgramHash.Byte-exact
These reproduce the pinned goldens:
0xf15817db…;0x4453a3d2…, the same one the integrated settlement vector uses;evidenceBlockHash0x4605a6e9…, from the mirror's six roots.Finding in the evidence lane's own golden
The mirror derived
kernelSignedEventsRootover0x-prefixed event hashes. The kernel signshashBundleoversha256:-prefixed ones. So the mirror's root (0xcc6a7e95…) is not the signedbundleHash(0x24bb6410…), and a block built that way could never match a genuinely signed bundle.This producer takes the root from the signed
bundleHash. For the same golden inputs, the block is0x854079f7…, and both facts are pinned in tests. The oracle has been asked which derivation it reconstructs.Input forms
Every hex value must be
0x+ lowercase hex of its exact width. An EIP-55 escrow address must be lowercased by the caller; this is finding F3 from #358, decided here. Integers may be a bigint, a safe integer or a decimal string without leading zeros.Tests
tscclean.Base: master.
🤖 Generated with Claude Code
https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS