Repository navigation
feat(evidence): LO-EV-9 bind device evidence to the accepted job and kernel before settlement - #341
Merged
Merged
Conversation
…kernel before settlement A signature over a bundle digest proves who signed which digest, not which job, node or output the digest is evidence for. The bundle-level jobId and kernelId are not inputs to hashBundle, and /complete never reopened the digest, so with the SEAM-2 gate open: - a genuine device bundle for job A, relayed again under job B, settled job B (same kernel, direct-signed); - a delegation scoped to several jobs let job A's bundle settle job B; - any other tagged digest the node key signs (a log-chain entryHash) passed the signature check as a bundle digest. @pcc/spec gains verifyEvidenceSubjectBinding (pcc.evidence.subject-binding.v1). It fails closed unless every event reproduces its own hash, the events reproduce the signed bundleHash, at least one event commits payload.jobId and every payload.jobId is the subject job, every source.kernelId and payload.kernelId is the kernel that accepted the job, and (when the subject names one) payload.outputHash matches. These are the fields kernel-sdk already hashes. resolveSettlementEvidence now requires a subject and the stored events for a device slot, checks the binding before the signature, and takes the delegation scope from the subject's job. /complete loads each device-signed row's events and tries every such row, so a replayed row stored first cannot hide the genuine one. The gate stays closed by default; the closed path is unchanged. Relayed rows only carry events once the operator relay stores them (#335, LO-GW-4b). Until then a relayed device row has no events and falls back to the gateway anchor. Tests: spec 843/843 (22 new), gateway full suite 2996 passed / 6 skipped (8 new resolver cases, 3 new /complete route cases with the gate forced open). Each binding leg was removed in turn and a test failed every time. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
This was referenced Sep 24, 2026
Merged
# Conflicts: # packages/gateway/src/__tests__/device-evidence-settlement.test.ts
…re-verified anchor Cross-family review of #341 (r1-341-binding-astra, DO-NOT-SHIP): the binding held in /complete, but /resume-settlement took the LAST evidence row for the job and drove the chain and the oracle with its digest, verifying nothing. Any row relayed after completion became the settlement evidence. - /resume-settlement settles only the evidence /complete pinned (job.evidenceBundleId) and re-verifies it before any settlement step (verifyPinnedSettlementEvidence): the row must belong to this job and its kernel; a device anchor must pass the subject binding and the registered-key signature again; a gateway anchor's bundleHash must recompute from its stored envelope (the bytes GET /api/evidence/:hash serves). Otherwise the claim is released and the route refuses with the reason. The chain receives the pinned digest as bytes32, as /complete's V3 path already does. - /complete no longer restates a device digest over the gateway's own events: the gateway record is stored under its own envelope hash, and when a verified device bundle anchors settlement the job pins that device row (its events open to its digest; its delegation is kept). The archive and the response report the anchor. - Recovery test fixtures pinned fake hashes ("sha256:trapped-evidence", a bare 0x); they now pin genuine gateway anchors. Merged #338 @897c7bae (R20 fixes) into this branch first. Tests: gateway full suite 3010 passed / 6 skipped / 0 failed (188 files; 12 new here); gateway tsc clean. Six mutants (latest row again, device re-check skipped, gateway recompute skipped, /complete restating the digest, job check removed, refusal ignored) each turn a test red. Still open from the review, with owners (not claimed here): delivered-output and assigned-device binding need authoritative execution state (gateway / composition); an acceptance nonce per execution (gateway); a persisted settlement intent for idempotent retries (escrow / gateway); comparing the pinned digest with evidence already on-chain (escrow read). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…anchor carries the canonical snapshots
Coord-watch's cross-cutting rule, from two independent verdicts: an unsigned
payload.jobId hidden as a non-enumerable property passed the job binding,
because hashing skipped it but the evaluator read it.
verifyEvidenceSubjectBinding now:
- hashes canonicalize({type, timestamp, source, payload}) itself;
- reads jobId, kernelId and outputHash from the parsed canonical text, as own
properties only, never from the live object;
- returns those snapshots (ok: true, events).
A non-enumerable, inherited or Object.prototype-polluted jobId, and a getter
that answers the hash with job A and the evaluator with job B, can no longer
bind a subject. Any event canonicalize cannot hash (a cycle today; more once
#359's strict tree lands) is malformed-event: the function still never
throws.
The gateway's resolveSettlementEvidence anchors on the binding's snapshots
instead of the caller's objects, so what is archived and evaluated
downstream is exactly what was hashed. The R1 pinned-evidence check uses the
same anchor verifier.
Tests:
- subject-binding 28/28: 5 new hashed-only cases plus snapshot return
- device-evidence-settlement 47/47: the anchor is the snapshot, independent
of later changes to the caller's copy
- spec 852/852
- gateway 3012 passed / 6 skipped, tsc clean; the one timeout in
completion-real-tier.test.ts under load passes 3/3 alone, three times
- 4 mutants killed: live objects evaluated, inherited reads,
canonicalize throw escaping, resolver returning caller objects
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…once, so one milestone's evidence cannot settle another The oracle's audit probe found that evidence signed for milestone 3 of a job settled milestone 4 of the same job. Kernel-signed events committed only payload.jobId; the unit and the challenge lived in the evidence block, which the producer assembles and the kernel never signs. - EvidenceSubject gains optional settlementUnitId and challengeNonce, each 0x + 64 lowercase hex and byte-equal to the settlement package's unitBinding.settlementUnitId and challengeBinding.nonce. They come from the unit record, never from the evidence. - When the subject names one, some event must commit it and every event that carries it must agree, the same rule as jobId and outputHash. The new reasons are unit-not-committed, unit-mismatch, challenge-not-committed and challenge-mismatch. A subject that names no unit behaves as before. - kernel-sdk's handler accepts settlementUnitId and challengeNonce on the job request (refusing either if malformed, 400) and commits them in the signed execution_started and execution_completed payloads. Jobs without them keep their bytes. - The gateway must forward both on dispatch, and pcc-node must commit them (adk). The oracle mirrors the check at /settle. Until then a unit-bound subject fails closed. Tests: - subject-binding 34/34 (6 new) - kernel-sdk 38/38 (3 new; the handler's evidence binds its unit and refuses another) - spec 858/858 - gateway 3013 passed / 6 skipped; gateway and kernel-sdk tsc clean - 6 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
…binding) into #358 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…ernel-sdk names the job on every event The oracle enforces per-event job binding at /settle: every event's payload.jobId must equal the settling job. kernel-sdk's gcode_hash_verified and workflow_step_completed carried no jobId, so every kernel-sdk bundle authenticated and then failed the job binding. The kernel signs event by event, and a session delegated for several jobs could not attribute a jobless event, so the producer moves (oracle #3101; my answer #3137). LO-EV-9 moves with it, so public and private binding agree. - LO-EV-9 rule 5 changes from "some event commits payload.jobId" to "EVERY event commits payload.jobId equal to the subject" (job-not-committed now names the event). - settlementUnitId and challengeNonce, when the subject names them, likewise scope EVERY event. outputHash stays "some event", since it lives on the completion. - kernel-sdk puts jobId, and the unit fields when given, on every event: the input commitment, every step completion, started and completed. - Other producers (pcc-node for adk; sensors' adapters) must follow. Until they do, their bundles fall back and fail closed. Tests: - subject-binding 36/36 (new: only some events naming the job; a unit carried by only some events) - kernel-sdk 38/38 (every event, step completions included, names the job and unit) - spec 860/860 - gateway 3013 passed / 6 skipped; gateway and kernel-sdk tsc clean - 4 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu
added a commit
that referenced
this pull request
Sep 24, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
…el bundles bind (LO-EV-9 per-event) Charter item 3, incumbents conform. After per-event job binding (0a4836a), no bundle from packages/kernel could bind: - EvidenceEmitter.addEvent knew the job but never committed it. - The digital kernels' input and step events had no jobId. - The IPP adapter put the PRINTER's own job number in payload.jobId, the field LO-EV-9 reserves for the PCC job. That evidence could never bind, even under the old "every present jobId must match" rule. - EvidenceEmitter commits payload.jobId on every event before hashing. When registerStep is given a settlement unit (0x hex32 unitId + nonce), it commits both on every event too. An adapter may pre-fill those fields, but a different value is refused, never overwritten. - IPP adapter: the printer-local number is now payload.ippJobId, at every evidence emit site (mock and real). printer-job reads printerJobId from it. Status responses are unchanged. - Accounting and procurement-RFQ kernels thread jobId into every step event and the input commitment. Tests: - evidence-emitter-binding 4/4: stamping, the unit, refusing another job or a malformed unit, and a real mock IPP print whose bundle binds {jobId, kernelId} - procurement 16/16: the bundle binds - kernel 868/868; tsc clean - spec 860; gateway 3013 passed / 6 skipped - 6 mutants killed Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
This was referenced Sep 24, 2026
Two gaps in the per-event unit binding, found while reviewing pcc-node's matching change (#420), where the same probes pass: - EvidenceEmitter kept a settlementUnitId or challengeNonce that an adapter pre-filled on a step registered without a unit, so a signed event could commit a unit the kernel was never given. Those fields are reserved for the binding; a unit-less step now refuses them. - kernel-sdk's job handler accepted a settlementUnitId without its challengeNonce, or the reverse. The oracle requires both on every event, so half a binding can never settle; it is now refused with 400 before anything executes. kernel-sdk 39/39, kernel 869/869, tsc clean for both. Four mutants (each guard dropped, the pair check narrowed to one direction, the nonce left unreserved) are each killed by one test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu
added a commit
that referenced
this pull request
Sep 28, 2026
…vidence's failure and contradiction rules Coord-watch's cross-cutting rule (#2961), carried to #363 by evidence (#3079): evaluate only what you hashed. LO-EV-9 (#341 @799cea1d) now returns the verified canonical snapshots of each bundle's events. Admission reads those snapshots for every check (dedupe, simulation, levels, device, version, window, the inspection verdict), never the caller's objects, whose getters or non-enumerable fields could answer differently from the hashed bytes. inspectionFailed and the contradiction rule now come from evidence-level.ts (#345 @cb81284f): inspectionFailed replaces my private copy (same rule), and deriveContradictions is the one public contradiction rule the oracle signs rejects on, so admission and the oracle cannot drift. A failure with no completion is still a device failure under onDeviceFailure. Stack refreshed: #338 @92b4302b (R20 round 2), #341 @799cea1d, #345 @cb81284f, #336 @06a5a49b. Tests: a payload.passed getter that answers false while binding hashes it and true afterwards is rejected (with the old live-object read put back, that test fails); contradictions are named by kind. 42 admission tests; spec 979/979; tsc clean; test files type-check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…ompleted commands are timed by the run's end Sensors' review of #417 (bus #3461, returns/pcc-sensors-review-417.md): - F1: every event now has an id, <opentronsRunId>:<commandId>, and a source.deviceType of "instrument", the value the kernel's Opentrons adapter reports (packages/kernel/src/opentrons/adapter.ts, on master and on #426). EvidenceEventSchema.safeParse passes on every event; on fbdab3d it failed with "id: Required" and "source.deviceType: Required". - F2, option (a): an entry is timed by the command's completedAt. A command that never completed is timed by run_ended_at, the run's own completedAt. run_ended_at is required in that case and may not be earlier than any completion, so capturedAt never runs backwards for that entry. createdAt stays in rawContent. Times are parsed as ISO-8601 with a zone, and a malformed time fails closed. - The TS goldens are recomputed with packages/spec's hashEvent and hashBundle. deviceType is part of the hashed source; id is not hashed, as in TS. Verified: 34 command-trace tests and 23 log-capture parity tests pass. With the TS spec (run through tsx), every event passes EvidenceEventSchema, Python and TS agree on hashEvent and hashBundle, and verifyEvidenceSubjectBinding (#341) returns ok. Against fbdab3d, 8 of the tests fail, each on the property it names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
… the enclosing bundle with subject binding
LamaSu
added a commit
that referenced
this pull request
Sep 29, 2026
…; profile checks run on a one-pass copy Round 2 answer to astra's DO-NOT-SHIP on 2df436b (pack 40; triage bus #3687). #336 is now stacked on #341 (LO-EV-9, 799cea1) and #338 round 3 (6cea24d). #52 (40-1..40-3): an entry's signature covers {capturedAt, rawContent, source}, not its predecessor, position or run, so entries alone could not show a chain was the kernel's. makeExecutionLogVerifier now takes the kernel-signed BUNDLE as its instance and the subject from ctx.subject (the job record). It: - verifies the bundle signature: the registered signer (deps.expectedSigner), ed25519, a 64-byte value, and deps.verifyBundleSignature; - runs LO-EV-9 subject binding; - extracts exactly one linear chain from GENESIS from the bound events (one payload.entries carrier, or one log_hash_chain_entry per entry, linked); - pins every entry signature to the same signer and algorithm; - requires capturedAt to be non-decreasing (evidence ruling, bus #3553); - runs verifyLogChain. Truncation, reordering with rewritten links, cross-run splicing, forks, gaps and a second log are rejected. The factory throws on an invalid minEntries or a missing expectedSigner. The entry-hash formula is unchanged. The oracle has been told of the contract change (bus #3691). Profile (40-5..40-8): calibration procedureId and validityWindowSeconds are invalid unless calibration.required; sparse arrays fail; profileGoverns never throws and returns the deep-frozen snapshot it validated and digested (plus a code). Every check runs on util/plain-data.ts plainDataCopy, a one-pass copy that reads each property exactly once and refuses anything JSON cannot carry (canonicalize reads each property twice). There is a real key-reordering test. Vector (40-9, 40-10): the emitter exports buildLose3Envelope, and a test regenerates the fixture byte-for-byte. negatives.failureBearingBundle is the positive bundle plus execution_failed, hashed and signed; its integrity verifies, so a consumer refuses it by outcome policy (#363). The positive bundle is unchanged (b80f569c...). Tests: spec 43 files, 968 passed; tsc clean; the test files type-check. Mutation check: 23 single-rule mutations of the new code, all caught.
LamaSu
added a commit
that referenced
this pull request
Oct 1, 2026
…the current subject binding 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
feat/evidence-loev1-byte-contract
to
master
October 3, 2026 02:13
…ets master Brings in #459 (the relay binds a signed document to its job and kernel) and the rest of master, so the subject binding is tested against current code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…LO-EV-9 #459's test (adk #4322, rule 5) presented a session bundle signed for job A as job B with no events or subject. Merged with #341 (LO-EV-9), the slot is refused earlier as missing-subject, so the test no longer exercised rule 5 (found by #341's first full CI on master: build-and-test, 1 failure). The test now binds the events to job B, the job being settled, so the subject binding passes, and signs them with a session key whose delegation names only job A. The scope check refuses it: contract_not_allowed. The whole-bundle replay (job A's bundle for job B) stays covered by the kernel-sdk subject test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
Owner
Author
|
Merge order (steward, bus #4856): this PR (#341) merges first, then #336 (MeasurementProfileV1, sensors), which carries this branch's commits and is now retargeted to master. 🤖 Generated with Claude Code |
… canonicalize (verbatim from 8dc6ef2) E11 (astra, DO-NOT-SHIP on #341 @51dbabd2) found that the subject binding reads caller objects live: an event.hash getter or Proxy answers the checks one way and the bundle hash another (F2), and a throwing getter or Proxy makes the verifier throw (F3). Sensors closed the same class on #336 with one plain-data copy at the boundary and intrinsics captured at load (astra packs 158-171, 171 SHIP). The steward suggested reusing it (#5232), and sensors reproduced realm-mutation forgeries against subject-binding.ts itself (#5381). These six files are byte-identical to #336 @8dc6ef2b, so #336's merge-up of #341 stays clean: - packages/spec/src/util/primordials.ts (new); - packages/spec/src/util/plain-data.ts (new): plainDataCopy, and isProxy from a static node:util import; - packages/spec/src/util/canonical.ts: canonicalize calls only captured intrinsics, with byte-identical output for JSON data; - apps/dashboard/src/lib/node-util-shim.ts (new) and the vite alias, so the dashboard build resolves node:util; - packages/spec/src/__tests__/plain-data-prototype.test.ts (new). The next commit routes verifyEvidenceSubjectBinding through them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…throws, and binds unit and challenge both ways (E11 F1-F3) astra's E11 on #341 @51dbabd2 was DO-NOT-SHIP. All three findings were reproduced at 51dbabd before any change: /mnt/sparkbulk/tmp/evidence-341-e11-repro-51dbabd2.txt has 3 of 3 failing. F1 (HIGH). A subject that names no settlement unit accepted evidence that commits one. The legacy /complete and /resume-settlement build exactly that subject ({jobId, kernelId}) and drive milestone 0, so evidence signed for U3/N3 could anchor them. Unit and challenge are now matched exactly in both directions: - named by the subject, every event commits the value; - not named, no event may commit one (unit-not-in-subject, challenge-not-in-subject). A consumer that cannot name the unit it settles therefore cannot accept unit-scoped evidence. The output stays one-way: it is content, not scope. F2 (HIGH). event.hash was read three times, so a getter or Proxy could answer the checks with B's hash and the bundle with A's. F3 (MEDIUM). Field reads sat outside the try, so a throwing getter or Proxy rejected the promise. For F2 and F3, every field is read once: - the input's three fields, the subject's five and each event's six through own data descriptors; - source and payload through #336's plainDataCopy. A Proxy, an accessor, a hole or non-JSON data is refused without being run. All checks and hashes then read the copies only, and the whole body is wrapped, so every input resolves to a refusal. Realm mutation (sensors' residual on this file, bus #5381). The checks call only intrinsics captured at load: - primordials; - canonicalize; - node:crypto's SHA-256, captured and synchronous, so nothing is awaited inside. There is no sort, iterator protocol, RegExp or JSON.parse. Results and copies have null prototypes and are frozen, so Object.prototype.then cannot rewrite an answer. Tests: - subject-binding.test.ts: the F1 unit test flips, plus 4 more F1 cases and 6 F2/F3 cases: astra's getter and Proxy reproductions, nested Proxy and accessor, throwing getters at every read, never-throws inputs, frozen null-prototype results. - subject-binding-realm.test.ts (new): 33 after-load patches x 9 cases leave every answer unchanged, and a source scan finds no ambient method, iterator, RegExp or await. - The scratch reproduction file is deleted. spec: 50 files, 1206 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
… patch held until the answer settles Each recipe forged an ok out of 51dbabd's binding leg. They were reproduced before the fix (/mnt/sparkbulk/tmp/evidence-341-realm-repro-51dbabd2.txt): - a targeted Array.prototype.sort answers job A's signed hashes for re-hashed job B events; - JSON.parse answers a snapshot that commits job B; - an Object.prototype.then getter forges the refusal into ok; - SubtleCrypto.prototype.digest hashes B's content as A's; - a replaced array iterator skips the unit and challenge checks. At this head all five refuse. Run against 51dbabd's subject-binding.ts and canonical.ts, all five fail (/mnt/sparkbulk/tmp/evidence-341-r4-recipes-vs-51dbabd2.txt). The patches are targeted, so vitest's own use of each intrinsic keeps working while the patch is held. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
… anchor /complete or /resume-settlement
/complete and /resume-settlement verify device evidence against
{ jobId, kernelId } and drive milestone 0 of the job's per-job escrow. A
bundle whose events commit settlement unit U3 and challenge N3 used to
anchor them (astra E11 F1).
LO-EV-9 now matches unit and challenge exactly in both directions, so these
routes refuse such evidence and settle on the gateway fallback. Recovery
refuses a unit-scoped row pinned before the fix.
device-evidence-settlement.test.ts:
- resolveSettlementEvidence on the exact /complete subject falls back with
unit-not-in-subject;
- a U4/N4 consumer gets unit-mismatch;
- the U3/N3 consumer anchors on the device (positive control);
- verifyPinnedSettlementEvidence refuses a pinned U3/N3 row.
paid-job-flow-evidence-binding.test.ts:
- /complete does not anchor or pin a U3/N3 bundle, while a unit-less bundle
from the same node still anchors;
- /resume-settlement answers 409 unit-not-in-subject for a unit-scoped pinned
row, and the job stays at evidence_submitted.
Comments at both subject sites say why they name no unit. No code change in
the gateway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…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
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…teward #5719) #341 carries #336's older commits: its port of the plain-data boundary, the load-time intrinsics and canonicalize is byte-identical to 8dc6ef2's (canonical.ts, plain-data.ts and primordials.ts hash the same on both sides). No conflict. Once #341 merges, master holds f249eb4, and #336's diff against it is #336's own changes again.
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
… after MERGE NOW's chain (steward #5952) MERGE NOW merges #336 (#35, carrying #341) before #359. The pre-stage 93dd671 (#359 + #341) then conflicted in spec/__tests__/canonical-intrinsics.test.ts: an add/add conflict, because #359 and #336 each added that file. The resolution is the sensors lane's own (bb8b8c3, evidence #5788), reused because neither side has changed the file since: - canonical-intrinsics.test.ts: #359's; - #336's tests move to canonical-legacy-bytes.test.ts and measurement-profile-source-scans.test.ts, verbatim from bb8b8c3; - canonical.ts, job-handler.ts and accounting-kernel.ts auto-merged to exactly bb8b8c3's resolution. spec build clean; spec 62 files, 1668 tests; kernel-sdk 54; kernel 883; gateway 207 files, 3735 tests; tsc clean (kernel, gateway). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…head of their merges (steward #5924) The one conflict, ipp-adapter.ts's real-mode polling: #502 moved the poll loop into poll() (the job is read once, a stopped or replaced job's answer is dropped, and the loop is tracked for quiesceEvidence), and #341 renamed the printer's job number in its execution_completed and execution_failed payloads to ippJobId, since payload.jobId is reserved for the PCC job (LO-EV-9). The resolution keeps poll() and applies both renames in it. Its execution_progress keeps payload.jobId exactly as #341 has it; that is a defect of its own, fixed in the next commit. #341's emitter commits the job's id into every recorded payload, and refuses an event that pre-fills another. #502's tests, written before it, are adapted: - R1's exact list of job B's payloads, and the shared-sensor summary, now carry the job's id. - Ten absences (not.toContainEqual of an exact object) could no longer fail: no recorded payload equals an object without the id. They now match by expect.objectContaining. Five mutants that let a late or another job's event be recorded fail 8 of them directly with objectContaining and none of them with exact objects; the other 2 sit behind an earlier hard assertion in the same test that the same mutants fail first. - pull-camera Fix A: a camera capture naming another job is now refused by the emitter before the tier check sees it, so the run fails as "could not be recorded", naming both jobs. The tier check's own refusal of such a capture keeps its direct test.
LamaSu
added a commit
that referenced
this pull request
Oct 3, 2026
…s ippJobId, so it is recorded (LO-EV-9) #341 reserved payload.jobId for the PCC job: the emitter commits it on every event and refuses an event that pre-fills another value. #341 renamed the IPP adapter's own job number to ippJobId on every event but one: the real-mode poll's execution_progress still sent it as jobId. The emitter refused that event, so a real print that reported progress failed: runPrintJob rejected at its Promise.all over the recorded events (printer-job.ts:254). (A JobRunner cannot start a real IPP print: its start carries no documentData.) Reproduced at 93f0852 by the new ipp-adapter-job-binding.test.ts, whose two real-mode completion cases fail there ("event payload.jobId 77 does not match the step's job-ipp-1"). It drives a fake IPP client in real mode, as adapter-quiesce-evidence does, and pins every emit site: a completed real print, an aborted one, and a mock one record all their events under the PCC job, with the printer's job as ippJobId. Reverting any one site's rename (progress, completed, failed, started, mock completed) fails it: 5/5.
LamaSu
marked this pull request as ready for review
October 3, 2026 22:25
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…336 The one conflict, spec/src/evidence/index.ts: both sides added an export (#363's profile-admission, master's principal-id). Both are kept. One test adapted to #341's hardened LO-EV-9 binding, which runs no caller code: in 'a getter that answered false to the hash cannot answer true to admission', the probe that counted the binding's reads of the getter now finds the binding refusing it (malformed-event at event 2) with 0 reads. The admission assertions are unchanged: an accessor is refused as input-unreadable before anything is read. spec 2301/2301, tsc clean.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…which follows #363 profile-admission.test.ts auto-merged with #363's probe adaptation to #341's binding. One more adaptation to #341: in 'admission's own await of binding's answer looks nothing up on it', the binding now answers with a frozen null-prototype object, so no inherited then is looked up at all (0, was 1 when its own promise resolved). The test asserts that, and pins why: the answer has no prototype and is frozen. A polluted Object.prototype.then cannot reach admission through it. spec green, tsc clean.
LamaSu
added a commit
that referenced
this pull request
Oct 4, 2026
…d (N79 round 8 merge-up) PR #462 conflicted with master in two places. The resolution keeps both sides' properties. - facades/job.facade.ts, updateStatus: master's N85(a) guard (#475) and N79's refund now compose in ONE transaction (escrow-refund.ts, writeJobStatusGuardedWithRefund). The guard decides whether a generic writer may write at all, and only a terminal write the guard allows gives the escrow back. Today that refund branch is unreachable: escrowForJob needs a session, and N85(a) treats any session as a settlement record. The composition keeps N79's property if N85(a) is ever relaxed. - routes/paid-job-flow.ts, resume-settlement: master's LO-EV-9 re-verification of the pinned evidence (#341) is the gate. On ANY refusal, N79's recovery runs: the prior status is restored (not hardcoded) and the escrow claim is handed back. The 409 keeps master's message and reason; its error is no_recorded_evidence_bundle (no pinned row) or pinned_evidence_unverified. Tests adjusted to master's semantics: - N79 tests that expected a generic writer (PATCH status, the operator relay) to finish or refund a paid job now expect N85(a)'s 409, with the escrow unchanged. - N79 resume fixtures use genuine gateway anchors (bundleHash is the canonical envelope hash), since LO-EV-9 re-verifies them. R4-H2's crank-hash assertion takes the chain form, the 0x hex of master's chainEvidenceHash. - Master's F1 (#475) configures its release address as the rowless default (ESCROW_CONTRACT_ADDRESS plus ESCROW_CONTRACT_VERSION=v1), which N79 round 8 requires for a job with no escrow row. No assertion changed. - New: n79-mergeup-guarded-refund.test.ts covers the composition: a refund on a guarded terminal write, none on a refusal, and both rolled back together. The resolution is reviewable as the diff from `git merge-tree --write-tree 6b54000 108c778` (tree e15941ad) to this commit.
LamaSu
added a commit
that referenced
this pull request
Oct 6, 2026
…ommitted-metadata fix(verifier): neither an event's unsigned id nor the events' order decides EvidenceVerifier's verdict; docs: they are uncommitted metadata (E11b/E11c, stacked on #341)
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.
Update 2026-09-24 @65485764: review rounds and the oracle's findings
Commits since the first cut (
4132c1ed):R1,
2a8f574e:/resume-settlementsettles only the pinned, re-verified anchor./completepins the device row itself.Merges of feat(spec): LO-EV-1 canonical signing byte contract + cross-language goldens #338:
da1268f0brought in R20;146df5dabrought in R20 round 2.Evaluate only what you hashed,
98352619(coord-watch's cross-cutting rule):jobId,kernelIdandoutputHashonly as own properties of the canonical snapshot of what was hashed, and returns those snapshots;jobId, and a getter answering A to the hash and B to the evaluator, cannot bind;canonicalizerefuses ismalformed-event, and the binding never throws.Milestone binding,
d3309de8(oracle audit #3006: milestone 3's evidence settled milestone 4):settlementUnitIdandchallengeNonce(0x hex32) must be committed by the events;Per-event binding,
0a4836a6(oracle #3101, which /settle already enforces):payload.jobId, and the unit fields when named;Incumbents conform,
65485764:EvidenceEmitterstamps the PCCjobId(and the unit and nonce when registered) on every event, and refuses a conflicting value.ippJobId, sincepayload.jobIdis reserved for the PCC job.Tests:
Review: coord-watch's Gate B slot takes this head (the range is
897c7bae..65485764).What this closes (technical pack §3, must-close 9)
A signature over a bundle digest proves who signed which digest. It does not say which job, node or output the digest is evidence for. The bundle-level
jobId/kernelIdare not inputs tohashBundle, and/completenever reopened the digest. So once the SEAM-2 gate opens, three replays settle money:jobIdis an unsigned label.contractIds.includes(B)passes.entryHash+ its per-entry signature, presented as a bundleEach negative test first shows the signature leg accepting the replay, then shows settlement refusing it.
The contract:
pcc.evidence.subject-binding.v1verifyEvidenceSubjectBinding({ bundleHash, events, subject })in@pcc/specfails closed unless:bundleHashis a canonical tagged digest (LO-EV-1);hash(hashEvent);bundleHash(hashBundle);payload.jobId, and everypayload.jobIdequals the subject job;source.kernelId, and everypayload.kernelId, equals the kernel that accepted the job;payload.outputHashis committed and matches.These are fields kernel-sdk already hashes, so its bundles conform unchanged. The positive control uses the real
createKernelHandler.Gateway wiring (gate still closed by default)
resolveSettlementEvidence:subjectand the storedeventsfor any device slot.subject.jobId.deviceBundles(every candidate). The first one that passes both checks anchors settlement. If none passes, the result falls back and reports the first failure./complete:findEventsByBundle.subject = { jobId, kernelId: job.kernelId }. This is the job record's kernel, not an id supplied by the relay.Dependency
Relayed device rows carry events only once the operator relay stores them (#335, LO-GW-4b). Until then, a relayed row has no events and falls back to the gateway anchor. That fails closed. This PR does not touch the relay route (#335 owns it).
Producer requirements this sets
source.kernelId, and at least one event needspayload.jobId.LogCaptureevents carry nosourcetoday.EvidenceEmitterbundles: these bind only if an adapter event commitspayload.jobId.Tests
/completeroute cases with the gate forced open. The route cases cover:tsc --noEmit: clean.Mutation checks: each leg was removed in turn, and every mutant turned at least one test red.
source.kernelIdcheck/completetries first row onlyNot in this PR
/complete: the gateway holds no digest of the delivered output there. The spec supportssubject.outputHashfor consumers that do hold one.stepId: not bound. kernel-sdk'spayload.stepIdnames manifest workflow steps, a different namespace fromjob.stepId.Stacked on #338 (LO-EV-1).
Update, 2026-10-03: round 4 (answering astra's E11 DO-NOT-SHIP on 51dbabd), head f249eb4
Every finding was reproduced at 51dbabd before any change (3 of 3 failing tests), and so were sensors' five realm recipes (bus #5381).
unit-not-in-subject,challenge-not-in-subject).event.hashgetter or Proxy): fixed.source/payloadare read throughplainDataCopy.primordials.tsandplainDataCopy, ported byte-identical from 8dc6ef2 (9214733), plus a captured synchronous node:crypto SHA-256.unit-not-in-subject,challenge-not-in-subjectandunsupported-runtime.SUBJECT_UNIT_FIELD_PATTERNis no longer exported (it had no users).E11b-binding-341-r4-f249eb48.md(PASTE ORDER 510), a full delta round.🤖 Generated with Claude Code
https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS