Repository navigation
feat(evidence): delegation-scope and event-time rules at settlement, one rule with the oracle - #438
Merged
Merged
Conversation
…one rule with the oracle The evidence rulings on the oracle's audit r3 questions (#3338 -> #3345, #3344, #3425 -> #3542), as public deterministic code, so the gateway and /settle apply one rule: - checkDelegationScope. scope.contractIds is a non-empty string list that names the settling job; an empty list is refused, never "any contract", and the stale doc comment in identity/ephemeral.ts is fixed. maxSignatures is a safe integer >= 1 and covers the bundle's session-signed events (the oracle's scope_signatures_exhausted). parentAgentId equals the funded operator's principal id when the consumer holds it. - parseEvidenceTimestamp and checkEventTimes. Event timestamps and a package's evidenceTimeBounds are RFC 3339 with an explicit offset (uppercase T and Z, years 1970-9999, at most 9 fraction digits, whole-second comparison). Every event lies inside the delegation window with 300 s of skew; bounds must satisfy start <= end and contain every event. - LO-EV-9 step 10: an optional subject.eventTimeWindow, evaluated on the hashed snapshots. The header also states the v1 rule of one kernel-signed bundle per settlement unit (#3543). - The gateway's delegation check now calls checkDelegationScope and counts the binding's verified events. It keeps the reason string contract_not_allowed for contract failures. Vectors in evidence/delegation-rules.vectors.json were computed independently (Python datetime): 18 timestamps, 12 scope cases, 10 time cases. spec 869 (+9), kernel-sdk 39, gateway 3015 passed and 6 skipped (+2), tsc clean for spec and gateway. Nine mutants are each killed: the 1970 floor, lowercase t/z, the empty-list check, the event-count check, inclusive skew edges, the bounds check, the parent check, LO-EV-9's window, and the gateway's event count. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, as the canonical golden has them Correction to ruling #3542 (bus #3567). The canonical integrated settlement-vector golden, which gives packageDigest 0xf78103a1 and matches the gateway fixture g2-settlement-vector-golden.json, carries evidenceTimeBounds as decimal strings of Unix seconds: start "1699999500", end "1700000000". The RFC 3339 body #3542 relied on is the D1 signature golden, which marks itself "not the schema". parseEvidenceTimeBound accepts exactly ^(0|[1-9][0-9]*)$ as a safe integer: no numbers, signs, leading zeros, fractions, exponents or RFC 3339. Event timestamps stay RFC 3339 (parseEvidenceTimestamp). The vectors replace the bounds cases (RFC 3339, leading zero, a JSON number and an above-max-safe bound are all refused) and add the canonical golden's bounds as an accepted case. The mutants that allow leading zeros or accept numbers are each killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…cimal Unix seconds, or that run backwards
evidenceTimeBounds.start and .end must be decimal strings of Unix
seconds, as the canonical settlement-vector golden carries them
("1699999500" / "1700000000"; ruling #3567). No sign, no leading zeros,
no fraction or exponent, no RFC 3339, no JSON number, and a safe
integer. start must not be after end. The strings are kept byte-for-byte,
so both goldens and the unit fixture digest exactly as before; the
golden's JCS, body hash and packageDigestV2 tests pass unchanged.
Previously any non-empty string was accepted.
The grammar is the same as spec parseEvidenceTimeBound (#438). gateway
3066 passed and 6 skipped (+1), tsc clean. The mutants that drop the
grammar check or the order check are each killed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…unt is refused (review E3) Cross-family review E3 on 4e57037, finding 2 (MEDIUM). Reproduced at 4e57037 with four tests, each returning { ok: true }: - a sparse contractIds list with the job at a filled index (`every` skips holes); - the job reachable only through an inherited index (`includes` reads the prototype); - sessionSignedEventCount -1 (a safe integer, and <= maxSignatures); - evidenceTimeBounds inherited from a prototype. checkDelegationScope now requires every index to be an own string and finds the job among own indices; the count must be a safe integer >= 0 (refused as scope-signatures-exhausted, no new reason code). checkEventTimes reads bounds.start and bounds.end as own properties only, as it already did for event timestamps. delegation-rules.vectors.json gains scope vector negative_event_count (45 vectors). The file re-serializes byte-identically, so no other byte moved. The oracle mirrors these rules, and its count check needs the >= 0. spec 874/874; tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and the receipt time (review E3) Cross-family review E3 on 4e57037, DO-NOT-SHIP. Reproduced at 4e57037, with the gate open, as tests that anchored or failed: - finding 1 (HIGH): a delegation whose parentAgentId is " attacker " anchored (checkDelegationScope got only { scope }, no operator); so did events an hour before issuedAt and events after the gateway received the bundle (LO-EV-9 got no eventTimeWindow); - finding 3 (MEDIUM): recovery of a bundle that was valid when received returned session_expired once the wall clock passed expiresAt. The device anchor now takes a trusted context: the funded operator's principal id (authoritative, never from the evidence) and the receipt time (the stored row's createdAt, the gateway's own clock). With both: - the whole delegation, parentAgentId included, goes to checkDelegationScope with operatorPrincipalId; - the events must lie in [issuedAt, min(expiresAt, receivedAt)] ± 300 s, on the hashed snapshots; - the session is judged valid as of receivedAt (currentTimestamp). Without both, session evidence fails closed (session-evidence-needs-trusted-context). /complete and recovery pass the receipt time but no operator: the gateway has no authoritative funded operator (kernel.operatorAddress is self-registered; J1 is the oracle's, at /settle). So session-signed evidence no longer anchors at the gateway, which is what the verdict requires until such a source exists. The gate stays closed by default, so production is unchanged. device-evidence-settlement 55/55 (6 new; 8 existing session tests now pass the trusted context and stamp their events inside the window); gateway tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and never throws; a zero-count vector (review E3b, M1 + M2) Cross-family review E3b on 01aff86, DO-NOT-SHIP. Both findings were reproduced as failing tests at 01aff86 before any source change. M1 (MEDIUM): checkDelegationScope read delegation.scope, scope.contractIds, scope.maxSignatures and delegation.parentAgentId through ordinary prototype-chain lookups. The review's Object.create example returned {ok:true}, and a throwing getter threw, against the documented "never throws". Each field (and the length and every index of contractIds) is now read ONCE as an OWN DATA property through an Object.getOwnPropertyDescriptor captured at module load: an inherited, accessor or missing field is refused with the existing reason for a missing one (malformed-delegation for scope and contractIds, max-signatures-invalid, parent-not-operator), a getter is never called, and the body is wrapped so nothing throws (a hostile proxy is malformed-delegation). No new reason code: the oracle's mirror reads the same way (JSON cannot encode these cases, so they stay JavaScript tests). M2 (MEDIUM): the lockstep vectors pinned equality, overflow and negative counts but no zero. Adds the scope vector zero_event_count (sessionSignedEventCount 0, maxSignatures 10 -> ok), 45 -> 46 vectors. The file is hand-kept (no generator; it is byte-identical to its own JSON.stringify(.., null, 2)), so it was written back through that and the diff is only the added vector. The oracle's mirror must replay it. delegation-rules.test.ts 14 -> 22 tests, spec 42 files 882/882, tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… slot and the authorization once, as an immutable snapshot (review E3b, H1) Cross-family review E3b on 01aff86, DO-NOT-SHIP (HIGH): trusted authorization and time data were read repeatedly from mutable objects, a check on one read and the use on another. Reproduced at 01aff86, gate open, both trusted values supplied, as tests whose slot or authorization answers differently on a given read; each anchored (source "device"): - sessionKeyAuthorization undefined on its first read (the window decision) and the real one afterwards: the event window was skipped for events an hour before issuedAt; - receivedAt a far-future value only on the read that builds the window's notAfter: events after the real receipt were accepted; - parentAgentId the label the principal signed (" attacker ") on the SessionKey read and the funded operator on the checkDelegationScope read. The same class, found by tracing the read order: the decision re-read bundleHash and kernelSignature (anchoring a digest that was never verified), the subject was read four times (the binding and the scope rule could judge different jobs), and recovery read the row's createdAt twice. A read-count test over proxies of the input, the slot and the authorization found 19 fields read 2 to 7 times. snapshotSessionKeyAuthorization(raw) reads every field ONCE through an Object.getOwnPropertyDescriptor captured at module load and accepts only OWN DATA properties of the right type (no accessor, nothing inherited, no missing required field; scope.allowedActions and contractIds are dense arrays copied index by index over own strings; derivationPath is an own string when present). It returns a deeply frozen plain copy, or null (malformed-session-authorization). Range rules (non-empty list, budget >= 1) stay with checkDelegationScope and keep their reasons. - verifyDeviceSignedEvidence reads each input field once at its top and uses only locals; the SessionKey, checkDelegationScope, the signature preimage and the session-validity check all use the one snapshot. - verifyDeviceAnchor reads each slot field once at its top, builds the window from the snapshot and the one receipt time, passes the same snapshot and receipt time on, and returns what it verified; the decision is built only from that (digest, a copy of the signature, events, the frozen snapshot), never from a fresh read of the candidate. - resolveSettlementEvidence reads the input's signer, operator and verifier once before the loop; verifyPinnedSettlementEvidence reads its input and row once. Every existing reason code is kept and the 55 existing tests pass unmodified. Reasons that change, all for input that was already refused: an accessor, inherited or non-integer field in an authorization is now malformed-session-authorization (a non-integer maxSignatures was max-signatures-invalid, a non-integer issuedAt or expiresAt session_key_malformed). A present authorization that cannot be read is still refused after the not-device-signed, signer, contract-id and trusted context checks, as before. device-evidence-settlement 55 -> 73 tests; gateway 188 files, 3039 passed (6 skipped); gateway tsc clean. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… check found it unpinned (review E3b, H1) Mutation check of the H1 fix (mutants applied to the working tree only): - verifyDeviceSignedEvidence handing checkDelegationScope the raw object survived while the only callers were the anchor, which already passes the snapshot. Adds two tests that call it directly: a read-count over proxies of its input, the signature and the authorization (each field exactly once), and the review's own scenario, a parentAgentId a proxy answers differently on its second read (refused as parent-not-operator). Both fail at 01aff86. - copying the subject once was unpinned: the read-count test now also counts the slot's subject and signature (a field read twice fails it). - a receipt time that is not a safe integer (fractional, NaN, infinite, beyond 2^53) was unpinned as "not trusted context": now tested. device-evidence-settlement 73 -> 76 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
LamaSu
added a commit
that referenced
this pull request
Oct 1, 2026
…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
…ported onto the hardened LO-EV-9 leg #341 round 4 (astra E11b SHIP, MERGE-READY) rewrote subject-binding.ts: - each field is read once, through own descriptors and plainDataCopy; - no caller code runs; - it never throws; - it uses only load-time intrinsics; - unit and challenge must match exactly, in both directions. #438 had added step 10 (an optional eventTimeWindow on the subject) to the older file. Conflicts: - subject-binding.ts: took round 4's file and re-added step 10 in its style: - eventTimeWindow is read once as own data and copied with plainDataCopy, and its bounds are safe integers checked with a captured Number.isInteger; - the window is checked on the verified copies after the unit and challenge checks, through checkEventTimes; - the reason and index are read as own properties, and the refusal is null-prototype; - #438's header text (step 10, one bundle per settlement unit) is kept. - device-evidence-settlement.ts: kept #438's destructured row fields and receivedAt, plus round 4's E11 F1 comment on the job-and-kernel subject. - device-evidence-settlement.test.ts: - the helper takes both #438's `at` and round 4's `unit`; - #341's isolated rule-5 negative gets the trusted context #438 requires for session evidence (operatorPrincipalId, receivedAt, events inside the window), so that only the scope can refuse it. It still expects contract_not_allowed. The next commit moves checkEventTimes's time path onto load-time intrinsics too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…ime intrinsics (no RegExp, no Date)
Step 10 of the binding leg calls checkEventTimes. Its parser used an RFC 3339
RegExp, Number(), Date.UTC with a Date round trip, Math.floor and a
call-time .call. Any of those replaced after load could change an answer,
which is the realm class astra and sensors closed for the rest of the leg
(E11b, bus #5381).
- parseEvidenceTimestamp now reads YYYY-MM-DDTHH:MM:SS[.f{1,9}](Z|±HH:MM)
code unit by code unit. It checks the date against the proleptic
Gregorian calendar and computes Unix seconds by integer arithmetic.
- parseEvidenceTimeBound is a code-unit decimal check with a safe-integer
bound.
- checkEventTimes reads the window, the bounds, the events and each
timestamp as own data through ownData.
- ownData uses primordials' hasOwn instead of a call-time `.call`.
- Behavior is unchanged. delegation-rules-time-parity.test.ts keeps the
replaced functions verbatim and runs both versions over a seeded corpus:
60,000 timestamps (boundaries, leap years, offsets, fractions, mutations;
thousands accepted and thousands refused) and 20,000 bounds. Zero
differences.
- #438's 46 delegation vectors still pass.
- subject-binding-realm.test.ts covers the time path:
- two windowed cases (inside, and a window that ends before the events);
- patches for Date.UTC, Date.prototype.getUTCFullYear, Math.floor,
Number, Number.isSafeInteger and Number.isInteger;
- a source scan of the time path in delegation-rules.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…for #438's merge-up after #345 merges In MERGE NOW's order, #345 (with #341) merges before #438. Folded after it, #438 conflicts in spec/src/evidence/index.ts: #345 adds `export * from "./evidence-level.js"` and #438 adds `export * from "./delegation-rules.js"` at the same spot. Both export lines are kept, in merge order. Nothing else changes in #438's files. spec tsc clean (no TS2308 between the two modules); spec 53 files, 1371 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…master (steward #6167) A plain merge: the tree is exactly git merge-tree of 885b444 and cd9d877, with no other change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
marked this pull request as ready for review
October 4, 2026 02:23
… a criss-cross (steward #6415) A plain merge, no edits: its tree equals git merge-tree of d94da4d and master. #438 carried #345's pre-staged head, and #345 merged as a merge commit, which left two merge bases (cd9d877, 03d4e46). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…rent master (steward wake-up, 23:2x) A plain merge, no edits: its tree equals git merge-tree of e2c67a3 and master. CI that ran on an older master isn't evidence: many PRs merged since. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
had a problem deploying
to
trusted-checks
October 4, 2026 07:03 — 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.
What
This PR publishes the evidence lane's rulings on the oracle's audit r3 questions as public, deterministic code, so the gateway and the oracle's
/settleapply one rule (lockstep).Rulings: #3338 → #3345, #3344, and #3425 → #3542. Full text:
returns/pcc-evidence-work/oracle-audit-r3-rulings.md./settlescope.contractIdsis a non-empty string list naming the settling job. An empty list is refused, never "any contract".checkDelegationScope; also fixes the stale comment inidentity/ephemeral.tsscope.maxSignaturesis a safe integer ≥ 1 and covers the bundle's session-signed events.checkDelegationScope(sessionSignedEventCount)scope_signatures_exhausted(#3344)parentAgentIdequals the funded operator's principal id, when the consumer holds it.checkDelegationScope(operatorPrincipalId)parent_not_operator(b5a458d)T/Z, years 1970–9999, at most 9 fraction digits, and comparisons in whole seconds.parseEvidenceTimestampevidenceTimeBoundsare decimal strings of Unix seconds (^(0|[1-9][0-9]*)$, a safe integer), as in the canonical settlement-vector golden (1699999500/1700000000). This corrects #3542 (bus #3567): the RFC 3339 body #3542 cited is the D1 signature golden, which marks itself "not the schema".parseEvidenceTimeBound[issuedAt, min(expiresAt, receivedAt)]± 300 s. Package bounds must satisfystart ≤ endand contain every event.checkEventTimes; LO-EV-9 step 10 (optionalsubject.eventTimeWindow)The gateway's delegation check (
device-evidence-settlement.ts) now callscheckDelegationScopeand counts the binding's verified events. It keeps the existing reason stringcontract_not_allowed.The LO-EV-9 header now states the v1 rule: one kernel-signed bundle per settlement unit (#3543).
Vectors
packages/spec/src/evidence/delegation-rules.vectors.jsonholds 18 timestamps, 12 scope cases and 14 time cases (including the canonical golden's bounds), computed independently with Pythondatetime. The oracle mirrors them.Tests (DGX Spark)
4e57037c. spec 870 (+10), kernel-sdk 39, gateway 3015 passed and 6 skipped (+2).tscis clean for spec and gateway.t/zaccepted;Stacking
Stacked on #341 (
feat/evidence-subject-binding@799cea1d). #358's mint guard will validate the bounds grammar once it merges this.Producer note: a delegation whose
maxSignaturesis below its bundle's event count no longer anchors. The oracle's example is the #1551 HP-printer delegation, withmaxSignatures1 and several events.Draft. Merging is the operator's call. Needs a cross-family review; it touches evidence and money-path verification.
🤖 Generated with Claude Code