Skip to content

feat(spec): evidence levels -- submitted / device_reported / inspected_output (must-close 5) - #345

Merged
LamaSu merged 32 commits into
masterfrom
feat/evidence-levels
Oct 4, 2026
Merged

LamaSu merged 32 commits into
masterfrom
feat/evidence-levels

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

What this closes (technical pack §3, must-close 5)

It keeps three kinds of evidence apart:

Level Meaning
submitted The device took the work: a spooler queued it, an API answered 202, a machine loaded the program, a carrier picked up the parcel.
device_reported The device that did the work reports it finished, with its own completion record or its own measurement.
inspected_output The output was observed or measured by something other than the device that produced it.

PR #343 makes pcc-node report acceptance as execution_progress at level submitted. This PR gives every consumer the one classification, so none of them can read an accepted-only job as a completed one.

Rules

  • Every member of EVIDENCE_EVENT_TYPES is ruled on, including those that prove no outcome level. A test fails if a new vocabulary type arrives without a ruling.
    • submitted: gcode_received, gcode_loaded, method_loaded, execution_progress, courier_pickup_confirmed
    • device_reported: execution_completed, digital_task_completed, batch_session_completed, courier_delivery_confirmed
    • inspection types: cv_inspection_result, photo_comparison_result, instrument_result, batch_sample_result
      • These count as inspected_output only when the events name the executing devices and the observer is not one of them.
      • Otherwise they are device_reported: a device measuring its own output, or an observation whose independence can't be shown.
  • printer_job_verified proves no level. It is a log-stream summary with no success field (evidence vocabulary ruling, 2026-09-07).
  • These prove no level until composition rules them in for a CSD: custody, capture-protocol and touchstone events.
  • Fabricated events (isFabricated) prove no level, and a fabricated execution event can't make an inspection look independent.
  • An event with no device attribution proves no level.
  • Events the gateway stamps itself prove no level (GATEWAY_STAMPED_DEVICE_ID, "gateway", @ba31ea65). PUT /api/jobs/:jobId/complete writes its own execution_completed and the caller's evidenceEvents, of any type, under that id; no device reported them. They also name no executing device, so the stamp can't make a device's inspection of its own output look independent.
  • deriveContradictions(events) (@cb81284f) is the one public contradiction rule the oracle signs rejects on (J4): completion-and-failure and completion-and-failed-inspection, with fabricated events ignored. Event types decide there, so a gateway-stamped completion still counts: a contradiction can only refuse.
  • A level is strength, not verdict. A failed inspection is still inspected_output. Failure and contradiction stay with the committed program (execution_failed absence) and the measurement profile's policy.

Consumers

Tests

  • 20 new; spec 817/817; spec tsc clean.
  • @ba31ea65: spec 826/826; spec tsc clean. Dropping the gateway clause fails both new level tests.
  • Six mutants, each turning at least one test red: independence, the size guard, submitted membership, fabricated filter, attribution, fabricated executors.

Based on master (no dependency on #338 / #341).

🤖 Generated with Claude Code

https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS

LamaSu and others added 2 commits September 24, 2026 02:44
…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
…eported or inspected_output

Must-close 5 keeps three kinds of evidence apart: the device TOOK the work
(submitted), the device that did the work reports it finished
(device_reported), and the output was observed or measured by something else
(inspected_output). pcc-node now reports acceptance as execution_progress at
level submitted (PR #343); this gives every consumer one classification so
none of them can read an accepted-only job as a completed one.

evidenceLevelOf / evidenceLevelOfBundle rule on every member of
EVIDENCE_EVENT_TYPES, including the ones that prove no outcome level, and a
test fails if a new vocabulary type arrives without a ruling. An inspection
counts as inspected_output only when the events name the executing devices
and the observer is not one of them; a device measuring its own output, or an
observation whose independence cannot be shown, is device_reported.
Fabricated events (isFabricated) and events with no device attribution prove
no level, and a fabricated execution event cannot make an inspection look
independent. printer_job_verified proves no level: it is a log-stream summary
with no success field (evidence vocabulary ruling, 2026-09-07).

A level is strength, not verdict: a failed inspection is still
inspected_output. Failure and contradiction stay with the committed program
and the measurement profile's policy. The level names match
MeasurementProfile AcceptanceLevel (#336), which can alias EvidenceLevel.

Tests: 20 new; spec 817/817; spec tsc clean. Six mutants (independence,
the size guard, submitted membership, fabricated filter, attribution,
fabricated executors) each turn at least one test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu and others added 8 commits September 24, 2026 13:58
# 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
…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
…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
…he oracle signs rejects on (J4)

The oracle signed reject verdicts on a producer-asserted contradiction flag
it never derived: the same class as signing an underived claim. Ruling
(#3240): it signs reject only for a contradiction it derives from
authenticated events, and an underived one is refused unsigned. The derivable
rule must be public, so the oracle and profile admission read one predicate.

- deriveContradictions(events) returns, in a fixed order:
  - "completion-and-failure": a device-reported completion and an
    execution_failed in the same set;
  - "completion-and-failed-inspection": a completion and an inspection
    reporting its own negative verdict.
- inspectionFailed(event): an INSPECTION_EVENT_TYPES event whose
  payload.passed is present and not true. This is the rule sensors adopted
  in profile admission (#363); it now lives next to the inspection types.
- A failure without a completion is a device failure, not a contradiction.
- Fabricated events prove no contradiction, just as they prove no level.

Tests: evidence-level 26/26 (6 new); spec 823/823; tsc clean. 5 mutants
killed: fabricated counted, completion gate, absent verdict, non-inspection
verdict, failure kind.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
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>
PUT /api/jobs/:jobId/complete writes the gateway's own execution_completed
and the caller's evidenceEvents, of any type, under source.deviceId
"gateway". No device reported them. Until now evidenceLevelOf read that
stamp as a device: a gateway completion counted as device_reported, and it
named "gateway" as an executing device, so a printer inspecting its own
output could read as inspected_output.

GATEWAY_STAMPED_DEVICE_ID is not a device attribution, so those events prove
no level and name no executor. deriveContradictions is unchanged (the oracle
mirrors it, J4): types decide there, and a contradiction can only refuse.

Found while checking readmodels' #441 against the evidence vocabulary; #441
will take evidenceLevel from evidenceLevelOfBundle.

spec 826/826, tsc clean. Mutant (drop the clause) fails both new tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Oct 1, 2026
…ontradictions and the gateway-stamp rule

#349 was cut from #345 before cb81284 (deriveContradictions, J4) and
ba31ea6 (gateway-stamped events prove no level). Merged, not rebased,
so nothing is force-pushed. No conflicts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
LamaSu and others added 10 commits October 1, 2026 01:17
…main bundles with closed inspection verdicts (E5 F1-F4)

Cross-family review E5 on #345 returned DO-NOT-SHIP. F1-F4 all reproduced
against ba31ea6 and are fixed here. F5 is a documented scope rule, not code.

- F1: replace the event-list API (executingDeviceIds, evidenceLevelOf,
  evidenceLevelOfBundle) with evidenceLevelOfBundles(bundles, context) and
  deriveContradictions(bundles). Independence is judged between
  AUTHENTICATED trust domains (operator principals), never declared
  source.deviceId strings. inspected_output needs a non-empty assigned
  executorTrustDomains, a known executor set, an inspector bundle with a
  trust domain, and that domain outside the executor set. Malformed
  trust domains throw the new EvidenceLevelInputError. The gateway stamp is
  compared after an ASCII trim and ASCII lowercase.
- F2+F3: one closed verdict per inspection type (inspectionVerdict: pass,
  fail, none, malformed). Only pass and fail prove a level. In
  contradictions fail and malformed count as failed, none does not.
  inspectionFailed stays as a wrapper. Pinned fields: instrument_result
  `pass` (boolean), batch_sample_result `status` ("PASS"/"FAIL"),
  photo_comparison_result none (no producer anywhere).
- F4: fabrication is bundle-wide (bundleHasFabricatedEvents). A fabricated
  bundle proves no level, is ignored by deriveContradictions, and still
  contributes its trust domain (or UNKNOWN) to the executor set.
- F5: the per-settlement-unit scope rule is written into the module header
  and the deriveContradictions doc.

OPEN, not guessed: cv_inspection_result is NOT pinned. The ruling pins
`pass`, but the real producers emit `passed` (kernel photo-camera-adapter,
mock-camera, onboard-kit camera templates) while types/dpp.ts reads `pass`.
Until the contract owner rules, either spelling is malformed (no level,
fails closed in contradictions). See the E5 triage file.

The old evidence-level.test.ts exercises the removed API and is rewritten
in the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ndle API and pin E5 F1-F5

Rewrites evidence-level.test.ts for evidenceLevelOfBundles,
deriveContradictions and inspectionVerdict (the event-list API is gone).
The vocabulary partition test is kept.

- F1: declared-id attacks ("printer-1 ", "PRINTER-1", a second id, a decoy
  executor) are not independent; the id is never consulted; an unknown
  executor domain, an empty or absent executorTrustDomains, an inspector
  bundle with no trust domain, and a fabricated execution bundle's domain
  all block independence; malformed trust domains and bundle shapes throw
  EvidenceLevelInputError; the gateway stamp folds ASCII case and
  whitespace only (NBSP, U+2028, U+FEFF, full-width are real attributions).
- F2/F3: each pinned type has pass, fail, none and malformed rows
  (instrument_result `pass`, batch_sample_result `status`, photo_comparison_result
  unpinned); none and malformed prove no level; in contradictions
  malformed fails closed and none does not; non-plain payloads, inherited
  keys and accessors are malformed.
- F4: a bundle with any fabricated event proves no level and is ignored
  by deriveContradictions.
- F5: one test pins the per-settlement-unit scope rule.
- cv_inspection_result: OPEN. Tests cover only what holds under any ruling
  (empty payload is none, either spelling is malformed until pinned) and
  an it.todo marks the decision.

114 single-site mutants of evidence-level.ts (trust-domain comparison,
unknown/empty executor blocks, bundle-wide fabrication, each pinned field,
malformed/none handling, the gateway ASCII fold, the principal pattern,
input validation, list membership) each turn at least one test red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…the evidence-level contract header

The header listed the verdict-pinning rule but not which types are pinned.
Say it where a reader of the contract will see it: instrument_result and
batch_sample_result are pinned, photo_comparison_result has no producer, and
cv_inspection_result is NOT pinned yet (the producers emit `passed`,
types/dpp.ts reads `pass`), so it proves no level. Comment-only change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…`passed` (E5 round 2, D1)

Lane decision D1. Every real producer (kernel PhotoCameraAdapter and
MockCameraAdapter, the onboard-kit camera templates) and every reader
except types/dpp.ts (the baseline module, the oracle J4 mirror, sensors
profile-admission) use `passed`. types/dpp.ts:462 reads `pass`, which no
producer writes; that reader bug is routed to its owner and is not
touched here.

- cv_inspection_result is pinned to `passed`, a boolean: true is pass,
  false is fail, any other present value is malformed, and a payload that
  carries only `pass` is malformed (pass is not the pinned field).
- The OPEN it.todo becomes real rows: pass, fail, none, malformed (string,
  number, null, undefined, array, object), the `pass` spelling, and a valid
  `passed` beside a stray `pass` (the pinned field decides).
- cv now proves inspected_output through an independent trust domain, and a
  failed cv inspection is a "fail" in contradictions, not a malformed one.
- The contract header and the PINNED_VERDICTS comment state the pin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…per-event facts (E5 round 2, D2)

Lane decision D2. prepareBundles validated events[j] but the fabrication
check, the execution scan, the level pass and deriveContradictions each
re-read the live array and each event's type/source/payload, so a Proxy
array or an accessor that answers differently on each read could make two
passes disagree about one event.

- prepareBundles now reads bundles.length and events.length once, each
  element once, and each event's type, source and payload once, then
  source.deviceId, source.simulated and payload.mock once each, and builds
  ONE frozen facts record per event: type, deviceAttributed (with the
  gateway fold), fabricated (the canonical isFabricated predicate over the
  values already read) and verdict (the inspection verdict logic run on the
  payload already read). A bundle's fabricated flag is "any event
  fabricated"; holdsExecutionEvent comes from the facts.
- Levels and deriveContradictions use ONLY the facts.
- The verdict logic is now a function of a payload already read
  (verdictOfPayload): one prototype read, one snapshot of the own key
  names, one descriptor read of the pinned field, never a getter call.
- The public inspectionVerdict(event) keeps reading type and payload once
  each. Lengths must be non-negative safe integers or the input is refused.
- Tests: getters and Proxy arrays that answer differently on each read
  (the first read of type says execution_completed, later reads say
  execution_failed) cannot split the contradiction rule from the level;
  read counters prove one read per field; a Proxy payload shows one
  prototype, key and descriptor read; fabrication equals isFabricated.
  8 of the 12 new tests fail against the previous module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…vel the bundle API lacked (E5 round 2, D3)

Lane decision D3. sensors profile-admission.ts:650 needs the level of each
event and evidenceLevelOfBundle's replacement returned only the maximum.

- evidenceLevelsOfEvents(bundles, context?) returns a frozen array, in
  input order, of frozen { bundleIndex, eventIndex, level } records, one
  per event. It is computed from the same per-event facts and rules: every
  event of a fabricated bundle is null, an inspection is inspected_output
  only through an independent authenticated trust domain, and so on.
- evidenceLevelOfBundles is now the maximum over evidenceLevelsOfEvents, so
  there is ONE implementation of the level rules and the two cannot
  disagree.
- Exported through the evidence barrel (export *); the barrel comment names
  it.
- Tests: record shape, frozenness, order and indices, each class of event,
  fabricated bundles, per-bundle independence, empty input, input
  validation and a single read, the barrel export, and the agreement
  between the two functions over 1536 combinations of trust domains,
  assignments, verdicts and fabrication.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… after an ASCII fold (E5 round 2, D4)

Lane decision D4. Verdict-key detection was an exact-spelling closed set, so
`Pass`, ` passed` or `Status` read as "no verdict" and a failure could hide
under a spelling the module did not recognise.

- Detection of verdict-like keys, and of the pinned field's presence, now
  folds ASCII case and trims ASCII whitespace on the payload's OWN key names
  (enumerable or not; symbols ignored). `Passed`, ` passed`, `PASS` and
  `Status` all count as present.
- A folded match of the pinned field that is not the EXACT key is
  malformed: `{Passed:true}` on cv_inspection_result is malformed, not a
  pass, and so is `{passed:true, Passed:false}` (two readings). Another
  verdict-looking key beside a valid exact pinned field is still ignored:
  the pinned field decides.
- The fold is ASCII only (A-Z lowered, space/TAB/LF/VT/FF/CR trimmed), never
  toLowerCase: a Unicode lookalike such as the Kelvin sign in `oK` (which
  Unicode lowercasing turns into "ok") or a full-width `pass` does not match.
  A key longer than any verdict name after the trim is skipped without
  building a folded copy, so the fold stays linear.
- The gateway-stamp comparison now shares the same asciiFold helper (same
  behaviour, one implementation).
- Still one read: a snapshot of the own key names, one prototype read, one
  descriptor read of the exact pinned field.
- Tests: every verdict name in eight ASCII spellings, per-type pinned
  spellings (with and without the exact key beside them), conflicts, stray
  keys beside a valid pinned field, Unicode lookalikes, symbol and
  non-enumerable keys, 200k-character padded keys, and the level and
  contradiction consequences. 9 of the 10 new tests fail against the
  previous module.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
… after D2-D4 (E5 round 2)

The round-2 mutation pass over the committed D4 head (171 mutants) left
three survivors that were real test gaps:

- G21: a fractional `length` was only refused because an index happened to
  be missing. The test now uses a Proxy that answers every index with a
  valid event, so only the length can refuse 0.5 and 1.5.
- H16: the prototype-read count was only proved on the Object.prototype
  branch of the plain-object check. The trap test now also runs a
  null-prototype payload, which reaches the second branch, and asserts one
  getPrototypeOf either way.
- H20: a lying length on `executorTrustDomains` was untested. A Proxy
  that answers length 1 then 0 must still yield the assigned executor, and
  the log must show one length read and one element read.

Tests only; no source change. The remaining non-kills are the two mutants
that cannot be observed through the public API (internal records not
frozen; the redundant accessor check) and Reflect.ownKeys, whose symbol
keys fold to the empty string and match nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…nt verdict-looking key beside a valid pinned field is ignored

D4 makes a non-exact spelling of the pinned field malformed (even beside
the exact key). The converse is part of the contract too and was only in
the verdictOfPayload comment: a DIFFERENT verdict-looking key beside a
valid exact pinned field (for example `{pass: true, Status: "FAIL"}` on
instrument_result) is ignored, because the pinned field decides. Comment-only
change; the mutation pass ran on c4c5e66.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…s malformed

Lane decision on fixer-juliet's round-2 interpretation: with the exact
pinned field valid, another verdict-looking key (folded) was ignored, so
cv {passed:true, pass:false} read as a pass here while types/dpp.ts,
which reads pass for cv, reads FAIL. Two readers, two answers from one
payload. Such a payload now carries two claims and fails closed: no
level, and a failed inspection in deriveContradictions. No real producer
emits a second verdict key (fixer-juliet's producer inventory: the
cameras emit passed plus confidence/findings/hashes/model).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
LamaSu and others added 2 commits October 1, 2026 02:08
…rier legs included (E5b)

Cross-family round E5b on 9c86a0f, the one open HIGH: the courier events
(pickup is submitted, delivery is device-reported) were missing from
EXECUTION_EVENT_TYPES. So a courier operator's own instrument_result,
signed in its domain beside its courier_delivery_confirmed, was
inspected_output when it was not the assigned executor. Reproduced first
at 9c86a0f: the reviewer's case and a pickup variant both returned
inspected_output.

- EXECUTION_EVENT_TYPES adds both courier events, plus the executing
  party's own records: workflow_step_completed, printer_log_captured,
  printer_job_verified, process_log_summary, log_hash_chain_entry.
- New NON_EXECUTOR_EVENT_TYPES: the inspections, plus the telemetry,
  captures, custody, integrity and device-lifecycle records an
  independent observer may emit. The two lists partition the vocabulary,
  and a test fails if a new type is not ruled on.
- The executor set is the union of EXECUTION_EVENT_TYPES with every
  submitted and device-reported type, so "took or finished the work"
  identifies an executor even if a list is edited carelessly.

spec 922; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…an executor (E5c)

Cross-family round E5c on bb20068, the one open HIGH:
custody_handoff_confirmed was ruled non-executor, yet its only producer is
the print-and-mail driver doing the seal and drop-off
(gateway print-and-mail-handoff.ts). So the driver's domain could self-inspect
to inspected_output. Reproduced first: the reviewer's case returned
inspected_output.

Rather than move one type, every non-executor ruling was audited against
its producers in this repo:
- custody_handoff_confirmed and photo_captured have one producer, the driver
  handoff, so they identify an executor;
- custody_sealed and custody_handoff_initiated have no producer, but sealing
  and handing off are a leg's work, so they identify an executor too
  (fail closed; a future inspector-receipt flow needs its own type);
- camera_snapshot stays non-executor: independent inspection cameras emit it,
  and the driver flow always also emits custody_handoff_confirmed.
The producer audit is written into the NON_EXECUTOR_EVENT_TYPES doc. Tests:
the reviewer's case, the photo and the other custody events, and an
independent camera_snapshot. spec 925; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
@LamaSu
LamaSu marked this pull request as ready for review October 1, 2026 19:11
@LamaSu LamaSu closed this Oct 1, 2026
@LamaSu LamaSu reopened this Oct 1, 2026
@LamaSu LamaSu closed this Oct 3, 2026
@LamaSu LamaSu reopened this Oct 3, 2026
LamaSu and others added 8 commits October 2, 2026 19:28
…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
… 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
…r master moved

#345 (must-close 5, evidence levels) was SHIP at 8c5d68c. It went
CONFLICTING once master merged #338's signing-preimage export, and it dropped
off MERGE NOW.

The only conflict was packages/spec/src/evidence/index.ts. Both exports are
kept: master's `export * from "./signing-preimage.js"`, then #345's
`export * from "./evidence-level.js"`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
…erge-up after #341 merges

The steward's merge-tree simulation (#5833): #345 conflicts with #341 in
packages/spec/src/evidence/index.ts. Both add an `export *` at the same
place.

Resolution: both exports are kept. #341's subject-binding.js comes first,
then #345's evidence-level.js.

Spec: 51 files, 1339 tests pass; tsc is clean.

This is a pre-stage branch. #345's own branch stays frozen at ad3d4a5 until
#341 merges; then this merge becomes #345's merge-up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VGNHoFFhbAdeNBc4BWigst
@LamaSu
LamaSu merged commit 37adc7b into master Oct 4, 2026
9 checks passed
LamaSu added a commit that referenced this pull request Oct 4, 2026
… 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
LamaSu added a commit that referenced this pull request Oct 4, 2026
The one conflict was #345's comment in evidence/index.ts; master's comment is kept,
with #363's profile-admission export. profile-admission.ts still calls #345's
removed device-id API (evidenceLevelOf, evidenceLevelOfBundle, executingDeviceIds),
so it does not compile at this commit. The next commit migrates it to the
trust-domain rule (steward #6478).
LamaSu added a commit that referenced this pull request Oct 4, 2026
…ne rule (N24, steward #6478)

#345 replaced the device-id independence rule that admission called
(evidenceLevelOf, evidenceLevelOfBundle, executingDeviceIds) with one rule over
AUTHENTICATED bundles: independence between trust domains, the rule the oracle
signs on. Admission now uses it. It does not keep a private copy of the old rule.

- New input executorTrustDomains: the operators the deal assigned, from the
  accepted deal (as subject comes from the job record). It must be a list of
  operator principal ids; absent or malformed rejects as input-unreadable.
- The signature leg answers { trustDomain }: the verified signer's operator
  principal, or null when the registry names none. false, a throw, a bare true,
  a malformed principal or an accessor fails it (unauthenticated-bundle).
- Levels come from evidenceLevelsOfEvents and contradictions from
  deriveContradictions, both over the authenticated bundles. An event present in
  several bundles counts at the LOWEST level any copy gets (steward #6478), and
  reached is the highest of those.
- An inspection counts only with a pass under evidence's pinned verdict field
  (inspectionVerdict), not payload.passed: instrument_result is `pass`.

Tests: the pilot world has two operators, the assigned executor (A) and an
independent inspector (B). New cases cover the lowest level across copies, the
leg's answer, the executor-domain checks, assignment of the inspector's own
operator, and contradiction across domains. 116 of 116. Mutations: 10 of 11
killed; the survivor ("null is lowest") is equivalent here, because a null copy
needs a fabricated bundle, which admission refuses first (simulationProhibited
is always true). Spec suite: 2529 of 2529.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…6478) into #384, which follows #363 (a plain merge, no edits)
LamaSu added a commit that referenced this pull request Oct 4, 2026
…to the realm-hardened admission

#363 moved profile admission to #345's one rule (steward #6478). The deal's
executorTrustDomains are an input. The signature leg answers { trustDomain }.
Levels and contradictions run over authenticated bundles, each event counts at
its LOWEST level, and inspections pass by the pinned verdict field
(inspectionVerdict). #519 had made admission hold under post-load realm
mutation. This merge carries the migration into #519's hardened file, so both
hold.

Conflicts and how each was resolved:
- evidence-level.ts: #577's file (426be44), the realm port of #345's final
  file. It already freezes EVIDENCE_LEVELS, #363's only change to that file,
  and master has not touched the file since #577 forked. E5's
  evidence-level.test.ts is #577's too, for the two expectations #577 changed.
- profile-admission.ts: #519's hardened file with the migration applied in
  its style:
  - executorTrustDomains is read as data with the other fields (INPUT_FIELDS,
    DATA_FIELDS, plainDataCopy) and checked by isOperatorPrincipalId. That is
    a code-unit predicate equal to principal-id.ts parseOperatorPrincipalId,
    with no RegExp; it is exported, and a test holds the two equal on edge
    cases and 20,000 near-misses.
  - The signature leg's answer is read once from its own data (signerOf) into
    a frozen null-prototype record. A proxy, an own then, an accessor or a
    malformed domain fails it.
  - A native promise is followed by the then captured at load
    (followedPromise, new in util/primordials.ts), and its value is read
    inside the handler.
  - A promised answer must have no prototype (signedBy, exported). Resolving
    an ordinary object looks then up on Object.prototype, where code running
    after load could substitute the answer. A synchronous plain answer is
    still accepted, since nothing resolves it.
  - Levels and contradictions run over null-prototype AuthenticatedBundles.
    The lowest level per event hash is kept in a null-prototype record.
    reached is the highest of those.
- Tests:
  - #363's two-operator world, with the auto-merged helper's duplicated
    executorTrustDomains removed.
  - #519's pack-187 inputs now carry executorTrustDomains, and the
    binding-lookup test uses the two-bundle pilot.
  - New cases cover the signature leg under a then getter planted on
    Object.prototype, signedBy, a promised ordinary record, a thenable or
    proxy answer, and the executors refused at the input boundary (no leg
    runs).
- The realm harness:
  - It moves to the two-operator world (the printer in A's bundle, every
    other device in B's) and the new leg contract.
  - It gains cases for the new leg (a signedBy async leg, a promised ordinary
    record, a bare true, a null domain, the executor's own camera, no or
    malformed executors, a thenable).
  - Its evidence-level items are dropped: #577's harness
    (evidence-level-realm.ts) holds the levels under realm mutation.

Spec 2676 of 2676, admission 249 of 249, tsc clean.
Mutations: 19 of 20 killed. The survivor, "null copy not lowest", is
equivalent, as in pack 271: copies of one event differ in level only when a
bundle is fabricated, and admission refuses fabricated events first.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…urrences, as #345 does (steward #6623, evidence #6559)

The duplicates ruling is option (a): admission keeps no policy of its own,
and takes #345's per-occurrence semantics. Each occurrence is levelled by
its own bundle, and an event counts at the highest. This reverses the
"lowest copy" rule of steward #6478. With one domain per signer, only a
copy in a truly independent signer's bundle can lift an event.

- reached is now #345's own evidenceLevelOfBundles over the authenticated
  bundles (the maximum over every occurrence).
- The per-event level the observation loop counts is the max of
  evidenceLevelsOfEvents over the event's occurrences (higherLevel; null
  is the lowest).

Tests:
- The duplicate case flips: the camera's inspection in both the executor's
  and the independent inspector's bundle now admits at inspected_output.
  With only the executor's copy it is device_reported.
- A parity test holds admission's reached equal to evidenceLevelOfBundles,
  and the counted level equal to the max of evidenceLevelsOfEvents, on the
  duplicate case.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…ng witness grant is named (steward #6694)

Two additions to round 13's grants:
- A key that signs as the subject's executor is the executor's own, never an
  independent inspector. #345 builds its executor set from the deal's
  executorTrustDomains plus the operators of bundles holding execution events.
  So the kernel's own key, whose operator the deal left out, could sign an
  inspections-only bundle that counted as independent: #345 on the deal's
  executors alone says inspected_output (now a test). Admission now passes #345
  the executor set completed by roles: every executor-role bundle's operator
  joins it, and an executor key whose operator the registry does not name
  leaves no inspection independent. A deal that names no executor still shows
  no independence, as before.
- When the profile requires inspected_output and no pinned row grants a witness
  for the subject, the shortfall is no-witness-authorized (under onMissingData),
  not level-not-reached. No registry record assigns witnesses yet (N132), so
  inspected_output through a witness waits for it visibly (steward #6694).

Admission 134/134, spec 2563/2563, tsc clean; 8 of 8 mutations for this change
killed, and round 13's set re-run.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…ned admission

#363's pinned key registry (astra pack 275) and the steward's max-over-occurrences
ruling (#6623), ported into #519's admission:
- registryKeys and pinnedRegistryDigest are read as data at the input boundary and
  walked for code; rows go into null-prototype records, checked by isRegistryKey
  (a code-unit predicate equal to ^0x[0-9a-f]{64}$), unique by key and by signer id;
- the registry digest is the captured SHA-256 over canonicalize({domain, keys});
- each bundle's Ed25519 signature is checked here, synchronously, through
  node:crypto's verify as captured at load, with its key options in a
  null-prototype record. No promise is on the signature path: Web Crypto resolves
  importKey with a CryptoKey object, and that resolution looks `then` up on
  Object.prototype (the realm harness reproduced a forged admit through an
  awaited helper first);
- the signer leg, its snapshot, isSignerId and followedPromise are gone;
- the per-event level is the max over occurrences, and reached is #345's.
The realm harness moves to the registry (seeded keys, registry cases, node:crypto
and options-pollution scenarios); the intrinsics test checks isRegistryKey.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…ned admission

#363's signer grants (astra packs 281, 285, 287; steward DECISIONS 00:26 and
#6694), ported into #519's admission:
- registry rows carry grants { role: executor | witness, kernelId, jobId? },
  and session-key rows { publicKey, delegatedBy, authorization } are rooted in a
  registered row. They are read through own descriptors and checked with
  Reflect.ownKeys as captured at load, and become frozen null-prototype
  records. Grants are sorted by a JSON-quoted identity that orders as #363's
  does, so the v2 digest is byte-identical;
- the delegation's root signature is checked synchronously through node:crypto's
  verify, captured at load, over the LO-EV-1 delegation preimage rebuilt here:
  - a fixed key order;
  - strings quoted by JSON.stringify as captured at load;
  - safe-integer numbers;
  - lists sorted by code unit;
  - lowercase key hex;
  - UTF-8 encoded code unit by code unit;
- authorization after binding: a grant must name the subject, a witness signs
  inspections only, and a session key counts within its scope and window. The
  window uses Date.parse as captured at load and floors whole seconds by
  arithmetic;
- the executor set for #345's levels is completed by executor-role operators,
  and no-witness-authorized names an inspected_output shortfall with no witness
  granted;
- computeRegistryDigest reads a plain-data copy.
The realm harness gets the round-13 cases with seeded session keys and new
scenarios: grant-field pollution, plus JSON.stringify and Date.parse
(IDENTICAL-strict), Math.floor and toLowerCase. New tests show the rebuilt
delegation bytes equal LO-EV-1's: unicode, a lone surrogate, unsorted lists,
either-case hex, a derivation path. Pack 287's LOW (a test comment that did not
match its assertion) is reworded here as the tracked follow-up.
LamaSu added a commit that referenced this pull request Oct 6, 2026
fix(spec): the evidence levels hold under post-load realm mutation (#345's final file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant