Skip to content

fix(kernel): store exactly the plain JSON evidence its hash commits to, copied at the call (pack 265 MEDIUM) - #578

Draft
LamaSu wants to merge 32 commits into
masterfrom
fix/n123-emitter-detached-input
Draft

LamaSu wants to merge 32 commits into
masterfrom
fix/n123-emitter-detached-input

Conversation

@LamaSu

@LamaSu LamaSu commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Pack 265's MEDIUM row (steward #6465), stacked on #573. Merge after #573: this branch carries #573's commits (head 95b48e8), and its own delta is the last commit.

Problem

addEvent hashed and stored the adapter's own nested objects. It copied the payload only shallowly, so a change the adapter made after the call reached the stored event, which then no longer matched its own hash. Verification refused that event, so it failed closed. Pack 265 scoped the follow-up as: detach and validate the whole hash-covered input before hashing (source included), and refuse shared memory and non-JSON values.

Fix (packages/kernel/src/evidence-emitter.ts)

  • A copy at the call. addEvent takes its own deep copy (structuredClone) of exactly the four fields the hash covers: type, timestamp, source and payload. It hashes and stores that copy, and hands callers copies from addEvent and getEvents.

  • Hashed fields only. A field outside those four is no longer stored, since the hash never covered it.

  • Plain JSON only. The copy must be plain JSON data, checked before the event takes a place. Canonical JSON has no faithful form for anything else:

    • it hashes a Date, a Map, a Set, a RegExp or an Error as {};
    • it hashes a typed array as an object of its indices;
    • it hashes shared memory as its bytes of the moment (structuredClone keeps a SharedArrayBuffer shared);
    • it hashes an undefined element as null, and an array without its named members;
    • it writes a hole, NaN and Infinity as text no JSON parser reads, and JSON carries them as null.

    The check refuses the values fix(spec): canonicalize refuses values that have no JSON form #359's canonicalize refuses, including D5's integers outside the safe range, which the oracle and VCR refuse to hash.

Evidence

🤖 Generated with Claude Code

LamaSu added 7 commits October 3, 2026 19:07
addEvent awaited each event's SHA-256 digest before storing it, so overlapping
calls on one step stored in digest-finish order: a slow digest put an event
after later ones. master's runPrintJob makes such calls (printer-job.ts:196),
so a print's signed bundle could list progress before started, and the IPP
binding test flaked (#474, #456, #6307).

Each step now keeps a chain: an event is hashed at its call, as before, and
stored only after the step's earlier events are stored or have failed. The
hashes still run concurrently, a refused event still rejects at once and
takes no place, a failed hash rejects only its own call, and steps never wait
on each other.

Tests: refvertical's reproduction (#6343) and four gated cases in
evidence-emitter-order.test.ts. The IPP binding test drops #571's ordered
emitter (carried here) and gains a production-path case: runPrintJob with a
plain emitter and a slow execution_started digest keeps the printer's order,
in the signed bundle too.
…he printer line's hunk

The printer line (#521 to #541) rewrites the abort case right after where it was, so the two
changes touched and conflicted. Moved unchanged.
…k 259)

Three HIGH findings on #573 (astra pack 259). Each was reproduced at dce82e8,
and on master's emitter, before this change:

- finalizeBundle hashed the live events array, awaited the signer, then copied
  the array, so an add stored meanwhile appeared in bundle.events without being
  covered by bundleHash. It now waits for every add called before it, refuses
  a step that lost an accepted add, and hashes and returns ONE snapshot. An add
  called later is stored after the bundle and never enters it.
- registerStep replaced a step while adds were pending, and those adds then
  resolved into a record nothing reads. It now refuses while adds are pending.
  A step whose adds have settled is still replaced, so a later run starts a
  fresh record. An add pending when its step is cleaned up now fails instead of
  storing.
- Steps were keyed by the joined string job:step, so ("a:b", "c") and
  ("a", "b:c") shared one record and one chain. They are now two map levels.

Tests: seven lifecycle cases in evidence-emitter-order.test.ts, six of which
failed before the fix. Mutations: 15 of 15 killed. Kernel: 1140 of 1140.
…s (pack 261)

Pack 261 found pack 259's HIGH 1 still open. The snapshot copied only the
array, so its event objects were the stored ones that callers still hold:
addEvent returns them and getEvents hands them out. A change made through such
a reference while the bundle was signed reached the bundle. Reproduced at
239b944: the bundle's event was the caller's object, and after its hash was
changed during signing, verifyBundleHash returned false.

The snapshot is now a structuredClone of the stored events, so the bundle holds
copies no caller can reach. A new test changes the caller's event (its hash and
payload) while the signer is gated. The bundle keeps the original, and both its
bundle hash and its event hash verify.

Mutations: 16 of 16 killed (a shallow snapshot is the new mutant).
Kernel: 1141 of 1141.
…resh full CI on a green master (steward #6465)
…o, copied at the call (pack 265 MEDIUM)

addEvent hashed and stored the adapter's own nested objects: it copied the
payload shallowly, so a change the adapter made after the call reached the
stored event, which then no longer matched its hash (fail closed: verification
refused it). Pack 265 kept that as a separate row (steward #6465), scoped as:
detach and validate the whole hash-covered input before hashing, source
included, and refuse shared memory and non-JSON values.

addEvent now takes its own deep copy (structuredClone) of exactly the four
fields the hash covers (type, timestamp, source, payload) at the call, hashes
and stores that copy, and hands callers copies from addEvent and getEvents. A
field outside those four is no longer stored, since the hash never covered it.

The copy alone does not make the hash faithful. Canonical JSON hashes a Date,
a Map, a Set, a RegExp or an Error as {}, a typed array as an object of its
indices, shared memory as its bytes of the moment (structuredClone keeps a
SharedArrayBuffer shared), a hole or an undefined element as null, and an
array without its named members, and JSON carries NaN and Infinity as null.
So the copy must be plain JSON data, checked before the event takes a place:
the values #359's canonicalize refuses, including D5's integers outside the
safe range, which the oracle and VCR refuse to hash.

Tests: the adapter changing its objects afterwards, callers changing returned
copies, bundle isolation, the plain JSON check (each refused kind, and plain
data accepted), and hashed fields only. Mutations 28 of 28.
…s comment (#578)

Master's canonicalize writes an array hole, NaN and Infinity as text no JSON
parser reads ([1,,3], NaN, Infinity), not as null; only an undefined element
becomes null (probed). The oracle and VCR claim is narrowed to what they
refuse: D5's integers outside the safe range. Comment only.
…ts tree passes the trusted secret scan (a plain merge)
LamaSu added 4 commits October 3, 2026 23:24
… clean head tree and a fresh full CI (steward #6548; a plain merge)
…y, with intrinsics captured at load, and refuses undefined (astra pack 273)

Pack 273's HIGH and MEDIUM, both reproduced at 1538d6f, where the emitter
is unchanged since 5bd1c1d:
- HIGH: addEvent ran adapter code (a payload getter) before its copy. In
  astra's recipe, the getter swaps globalThis.structuredClone for identity,
  the adapter keeps a reference into the stored event, and a later change
  reached it: the stored reading became 99, and the hash no longer
  verified. With Object.getPrototypeOf replaced, a Date passed the
  plain-JSON check.
- MEDIUM: {x: undefined} was stored with an own x, which the hash and JSON
  both drop; type, timestamp and source could be undefined.

Fix:
- addEvent first copies the four hashed fields (copyEventInput) through
  own property descriptors and node:util's trap-free proxy check, so no
  adapter code runs inside addEvent at all: no getter, setter or Proxy trap.
- copyPlain refuses an accessor, a proxy, undefined anywhere (type,
  timestamp and source included), a symbol-keyed, non-enumerable or
  __proto__ member, a non-plain object (class instances too), a named
  array member, a hole, a non-finite number, D5's unsafe integers, and
  nesting past 64.
- -0 is stored as 0, as JSON carries it.
- The copy is built by defining properties, so no setter on a prototype
  runs.
- Every intrinsic the boundary and the copies use is captured at load,
  structuredClone included, for addEvent's return, finalizeBundle's
  snapshot and getEvents.
- The job and unit binding read and write own members of the copy only.

Tests:
- astra's recipe: refused, and the getter never runs;
- an event getter and proxies: refused, with no trap run;
- seven intrinsics replaced during the call change no refusal;
- getEvents with structuredClone replaced still detaches;
- setters and a `get` planted on Object.prototype: no setter runs, and
  nothing is lost;
- a source scan for uncaptured intrinsic calls;
- undefined members, nested and at the root;
- refvertical's call-order test gives its events a source.

Kernel 1154/1154. Mutations: 40 of 40, pack 265's 16 included.
…tra pack 273)

The completion event carried summary: undefined whenever the sidecar sent
no summary. Neither the event hash nor JSON keeps an undefined member, and
the kernel's emitter now refuses one (pack 273's MEDIUM). The member is now
set only when there is a summary. barrier-attestation's end-to-end run was
the one test the stricter emitter failed; adapter-pylabrobot is 58/58.
…ray.prototype.push (astra pack 277)

Pack 277's HIGH, reproduced at 6779123. The copy was sound, but the store
still called the replaceable Array.prototype.push. A push replaced after
load (astra's recipe: keep every event handed to it, then delegate) was
handed the stored event, and could change it after it was hashed. In the
reproduction the stored pages became 99 and verifyEventHash failed. The
signed bundle's hash still verified, because hashBundle commits to the
event-hash strings, while its event did not verify.

- The element is now defined with the ObjectDefineProperty captured at
  load, through a null-prototype descriptor, so no push is called and
  nothing is handed out.
- addEvent's comment no longer claims that no adapter code runs anywhere
  in it. It says what is covered: the copy, and the store. It also names
  what is not: the emitter's own Map lookups, the store chain's Promise
  then, and @pcc/spec's sha256, which can break ordering or fail a hash
  but cannot reach a stored copy.

Tests: astra's reproduction, with the push replacement held across the
store chain. Nothing is handed to it; the stored event, its hash, and the
bundle (both its hash and its event) verify. The source scan also refuses
any method call on a step's stored array.

Kernel 1155/1155. Mutations: 41 of 41 killed (pack 265's 16, pack 273's
24, and the store through push).
LamaSu added 4 commits October 4, 2026 03:49
…d by a closed allowlist (steward #6651, #6668)

From addEvent through storage, getEvents, finalizeBundle and hashing, every call
in evidence-emitter.ts now goes through a binding captured when the module loads,
a function or class declared in the file, or a JS private member of the emitter;
finalizeBundle also calls the onBundle callbacks. The order test parses the file
with the TypeScript compiler and refuses anything else (member, computed,
parameter and global callees, for-of, spread, instanceof, `in`, unpinned awaits),
with a self-test on planted x.push(1), arr.map(f), this.foo(), map.get(k),
promise.then(f), regex.test(s), obj.method() and the implicit forms.

- Hashes: computed by the emitter, synchronously, with node:crypto's one-shot
  hash captured at load, byte for byte as spec's hashEvent and hashBundle. A
  captured crypto.subtle.digest is still forged by a replaced
  Promise.prototype.then (Node's async digest resolves through it), and
  createHash's wrapper by an accessor planted for its internal handle.
- Chain: no then anywhere; every await is on a promise given its own pinned
  constructor; links fulfil with undefined. lost is gone: an accepted add can
  fail only by its step's cleanup, which finalizeBundle already refuses.
- Map get/set/delete/size, WeakSet, Error, Date, String slice, Function bind,
  spec's canonicalize, ids.evidence/bundle, isFabricated,
  kernelPullCaptureIssue and Sentry.startSpan captured; RegExp replaced by a
  character check; registerStep copies the unit's two strings; state, signer
  and collaborators in JS private fields; storage methods bound when attached.
- Tests: Map, Promise then/constructor/species, crypto.subtle.digest and
  node:crypto hash/createHash/kHandle/TextEncoder, RegExp, ids, Sentry and
  storage methods, and instance-property writes, each replaced during the
  calls; astra's push repro kept; hash equality with spec; the
  Object.prototype.then residual pinned. N123 and pack-259 tests no longer
  hold the digest (it cannot be held now); the handoff test holds adds at
  addEvent's boundary, and the scope test's echoing device echoes on the next
  event-loop turn.
…not only the allowlist

- Map delete and size join get and set in the Map test: a second step is
  cleaned up while a size that says "empty" is in place, so an uncaptured size
  would drop the job's other step.
- Error, Error[Symbol.hasInstance], WeakSet add/has, Date and toISOString,
  String.prototype.slice, console.warn (taken when the emitter is built) and
  Function.prototype.bind (used when a storage service is attached) replaced
  around the calls: the refusal keeps the emitter's message and prototype, the
  test signature and createdAt are real, the archive still runs.
- checkTierRequirements with Array.prototype methods, its iterator, Set,
  Object.prototype.hasOwnProperty and spec's exported arrays changed answers as
  with them intact (non-camera events: spec's kernelPullCaptureIssue is a
  named residual).
…ts, so a live read changes the answer

The mutation check found that reading DEFAULT_TIER_REQUIREMENTS live (instead of
the copy taken at load) survived: the test emptied tier 1, which its events
already meet. Tier 2, which they do not meet, now asks for nothing while the
calls run, so a live read would answer met.
…s its step state by definition, checked default-deny (steward #6792)

The emitter now passes the default-deny check that #519's admission passes (steward
DECISIONS 01:30 and #6792, astra packs 289 and 291).

builtin-reads-check.ts is a copy of @pcc/spec's, identical below its header.
evidence-emitter-builtins.test.ts runs it over evidence-emitter.ts:
- the trusted hasOwn and ObjectCreate are the emitter's own, captured at load;
- the await wrapper is pinned();
- the self-test uses the same fixture lines as spec's.

The emitter's changes:
- Array and string elements are read through listAt and charAt. listAt is hasOwn
  over the hasOwnProperty captured at load; charAt is String.prototype.charAt
  captured at load. A hole or an index past the end never reaches a prototype.
- A step record's pending, stored and detached are set by defineField
  (ObjectDefineProperty with a null-prototype descriptor), never by [[Set]], so a
  setter planted on a prototype never runs.
- console is captured at load. Its warn is still bound when an emitter is built,
  the collaborator the order test replaces.
- The input check's refusal message is read as its own data, not through
  Error.prototype.
- The payload's commit pairs are records, read by property, instead of tuples
  read by index.

Evidence:
- The check reports nothing on evidence-emitter.ts.
- kernel: 50 files, 1169 tests pass.
- PyLabRobot adapter: 58/58.
- gateway's emitter test: 21/21.
- tsc is clean.
…und is no proof at the read (astra pack 293)

This syncs the kernel copy of the default-deny check with @pcc/spec's after astra pack 293
(#519 @7f3067e2). The canonical string-loop allowance compared identifiers by spelling and
allowed statements before the read, so it is removed. The copy is again identical below its
header.

The self-test's fixture lines match spec's:
- the allowed string loop uses charAt;
- canonicalLoop, shadowedRange, shadowedIndex and reassignedByCall are refused;
- every misclassified line is listed in one assertion.

The emitter already reads strings through charAt; the check still reports nothing there.
LamaSu added 2 commits October 4, 2026 04:20
… replaceable after load (DECISIONS 04:06)

Under DECISIONS 04:06, the default-deny check covers the trusted path's computed transitive closure. The
kernel's evidence emitter reaches spec's canonicalize, the primordials helpers it uses, and ids.

- primordials.ts:
  - listAt and charAt now sit beside hasOwn;
  - includesValue, mapList, joinStrings, sortedStrings, deepFreeze and stringSet read elements only through
    listAt, so a hole never continues to Array.prototype.
  These shared-region lines are byte-identical to #519's, so the two branches merge cleanly.
- canonical.ts: #519's version verbatim. It reads a key only through listAt and a hasOwn guard, and an
  element as the first statement of its guard.
- ids.ts: generateId uses Date.now, Math.random, Number.prototype.toString and String.prototype.substring
  as captured at load.

Behavior on data is unchanged. spec: 77 files and 2422 tests pass; tsc is clean.
…y.prototype.find skipped it (astra pack 299)

Astra pack 299, MEDIUM, reproduced at 6804552. A requirements list with a hole at 0 and a valid tier at 1
threw "Cannot read properties of undefined (reading 'tier')": listAt(requirements, r)! reached the hole.
checkTierRequirements now skips a hole with hasOwn.

The new test fails without the skip, with that TypeError. kernel: 50 files and 1170 tests pass; tsc is clean.
LamaSu added a commit that referenced this pull request Oct 4, 2026
…tive closure (DECISIONS 04:06)

Steward DECISIONS 04:06: the check's scope is the trusted entry points' transitive in-repo call closure,
computed from the TypeScript program, with no hand list. #519 adopts it before it merges; #578 uses the
same check, kept identical below the header.

- builtin-reads-check.ts:
  - trustedClosure computes every in-repo declaration the seed files reach. It follows ANY reference (a
    call, or a function passed, stored or captured as a value) through imports and re-exports, but not an
    import alone or a name used only in a type.
  - closureReads checks each reached declaration in place: what runs at load stays exempt.
  - A collaborator named with its reason is not entered, and the list is closed: an unused entry fails.
  - programOfFiles builds the multi-file self-test.
- The test seeds profile-admission.ts. The closure reaches 165 declarations: primordials 56,
  evidence-level 45, plain-data 25, measurement-profile 20, subject-binding 13, evidence 2,
  primitives, is-fabricated, canonical and admission 1 each. It reports none.
  - The reach is asserted, so it is not vacuous.
  - A two-file self-test shows the closure follows a call and a stored function, skips an import named
    only in a type, and stops at a named collaborator.
- primordials.ts: listAt and charAt move beside hasOwn, and every helper reads elements through listAt.
  These shared-region lines are byte-identical to #578's (75f1c81), so the branches merge cleanly.

Mutations:
- the closure following nothing: 2 tests fail;
- a live JSON lookup planted in ownPromise, which only the closure reaches: reported.

spec: 83 files and 2788 tests pass; tsc is clean.
LamaSu added 6 commits October 4, 2026 04:49
…oad (astra pack 299)

implementer-bravo

The kernel's tier gate captures kernelPullCaptureIssue at load and asks it
whether a camera event counts toward tier 2, but the function looked its
intrinsics up when it ran: with RegExp.prototype.test replaced after load,
a capture whose imageHash is "invalid" met tier 2 (reproduced at 6804552).

The function and every helper it calls now go only through bindings
captured when the module loads (spec's primordials plus a block of its own):
- the image hash is checked code unit by code unit (no RegExp);
- own data properties are copied onto null-prototype records, with keys in
  the dense array Reflect.ownKeys returns (no Map, no iterator protocol);
  every list is read by index, an element only where proven own;
- a descriptor is data only when it owns `value` (no `in`), so a `value`
  written on Object.prototype never makes an accessor look like data;
- the governed types are a copy taken at load (the export is mutable);
- isFabricated is handed the null-prototype copies, so a `simulated` or
  `mock` written on Object.prototype is not read as the event's own.

Same answers, reasons and order of checks for every input: the existing
kernel-pull-capture suite passes unchanged. The default-deny builtin-reads
check now reports nothing for the file (98 reports at 6804552).
kernel-pull-capture-intrinsics.test.ts replaces 31 intrinsics and writes 5
values after load, over 53 valid and invalid events, and requires the
untouched answers with no call of what was replaced.
… replaced after load (astra pack 299)

implementer-bravo

astra's reproduction at the emitter's tier gate, as a test: Tier 1 events
plus an LO-SE-1 camera_snapshot whose imageHash is "invalid" give the same
tier-2 answer (met false, the imageHash reason named) with
RegExp.prototype.test replaced after load. At 6804552 that answer was
{ met: true, missing: [] }.

Then each of 31 intrinsics replaced after load, and Object.prototype.value,
Array.prototype[0] and the exported KERNEL_PULL_CAPTURE_TYPES written, one
row each, over 24 camera events (valid captures and invalid ones, so a
replacement that forces either answer is caught): every answer equals the
untouched one and nothing replaced is called. Against spec built at
6804552, 27 of the 36 tests fail; all pass with the hardened function.

The spec test's iterator symbol is annotated as the well-known one, so
both new test files typecheck.
… simulated or mock at the camera gate

implementer-bravo

The mutation check showed two hardening points the kernel test could not
see: `"value" in descriptor` reintroduced for the event's own fields (no
fixture had an accessor at the event level), and isFabricated handed the
caller's event instead of the null-prototype copies (no row wrote
`simulated` or `mock` on Object.prototype). The spec test caught both.

Add a stored camera event whose `payload` is an accessor (defined after
stored(), whose spread would read it), and a describe that writes
Object.prototype.simulated and .mock after load. That describe's Tier 1
events and valid capture carry null-prototype sources and payloads,
because the emitter's own isFabricated call on the events it counts is a
separate path that reads through prototypes; the refused captures keep
ordinary objects, so an inherited `true` read as their own would change
their reason.
…by name, so every call it makes is seen (astra pack 303)

Under astra pack 303's rule, a call whose target is read back from a table of functions is a target the
default-deny check cannot see. The rewritten kernelPullCaptureIssue still ran each field's check that way.

The two check tables become:
- ordered key lists (CAPTURE_KEYS, then INSPECTION_KEYS);
- one fieldIssue(key, value, context) that holds each check in place, in a switch. An unknown key fails
  closed.

The order of checks and every reason string are unchanged. A differential fuzz against 6804552's
implementation found 2,000,000 of 2,000,000 answers identical (seeds 299 and 4207), and no getter ran
inside either check.
… as own data, so a prototype's simulated or mock changes nothing

Found by the implementer, then reproduced at the integrated head:
- With Object.prototype.mock written after load, a valid Tier-2 set drops from met to not met. It fails
  closed, but on a prototype's value.
- A getter planted on Object.prototype.simulated ran 4 times during one checkTierRequirements.

checkTierRequirements now hands spec's isFabricated a null-prototype view of the event's OWN
source.simulated and payload.mock, read through their descriptors, so no getter runs. An accessor or a
Proxy on the way fails closed: the event is taken as fabricated and never counts.

The test fails with the old IsFabricated(entry.event) call.
…h every call target seen or named (DECISIONS 04:06, astra packs 299 and 303)

The kernel copy of the default-deny check now matches @pcc/spec's (#519 @b61262ae), identical below its header.
- It computes the transitive in-repo closure (DECISIONS 04:06).
- A call target it cannot see fails closed unless its exact call site is named with a reason (astra pack 303).
- An instance field runs after load.
- Collaborator keys are root-relative and exact.

The test:
- runs the same fixture lines, and the same two closure self-tests, as spec's;
- seeds evidence-emitter.ts with @pcc/spec mapped to its source;
- asserts the reach includes kernelPullCaptureIssue, fieldIssue, isFabricated, canonicalize, generateId,
  sortedStrings and listAt;
- requires no reports.

The CLOSED collaborator list, each with its reason:
- the storage service's isReady and archiveBundle, which the closure reaches but does not enter;
- the call sites of this.#warn, this.#signFn, this.#storageIsReady and this.#storageArchive;
- Sentry's startSpan, captured at load;
- the onBundle listener.

Suites at this commit: spec 78 files and 2461 tests; kernel 51 files and 1212 tests; PyLabRobot 58/58;
tsc clean for both packages.
LamaSu added 3 commits October 4, 2026 05:49
… input as own data (astra pack 309)

checkTierRequirements read candidate.tier, event.type, requiredEventTypes,
minimumEvents and options.jobId through ordinary property access, so a write
on Object.prototype after load changed its answer: requirements [{}] met tier 2
with tier/requiredEventTypes/minimumEvents written there, and an
Object.prototype.type getter gave typeless events tier 1 (run 3 times).

Every field is now read through descriptors (ownField, ownElements,
ownGroups): no getter runs, nothing a prototype serves is read, and a Proxy,
an accessor or a field missing or of the wrong shape fails closed. The
caller's events stay unknown (kernelPullCaptureIssue and fabricated take
unknown); registerStep reads its unit the same way. isEvidenceInputError
answers a boolean and the storage service is held as unknown, so no caller
value takes a type it was never checked against.

Tests: both reproductions, a missing field not read from a prototype, and a
Proxy or accessor at each place in requirements, events, options and unit,
with zero traps and getters run. 6 of the 7 fail at 1a56cf2.
…n becomes typed only where its provenance is named (astra packs 307 and 309)

The kernel copy of the default-deny check takes #519's a416bbc body (pack
307: an opaque callee's function operands must be seen code; module
load-time statements join the closure) and adds the provenance rule:

- an entry point's parameter whose type may be an object is the caller's
  object, used only as unknown: tested, compared, handed to an own-data
  reader (OWN_DATA_READERS, by the path it was captured from) or to in-repo
  code at an unknown parameter. Never read, aliased, held typed or
  destructured where it is received;
- an unknown or any becomes an object type (as, a type predicate, an
  implicit any), and an assertion claims fields its operand lacks, only where
  CheckOptions.provenance names the place with where its own data comes
  from: a closed list, like the collaborators;
- an unknown is handed to code the check does not walk only if that code is
  an own-data reader.

The emitter names 4 conversions and 3 bind call sites (pack 307's rule). The
fixture gains 5 allowed and 12 refused lines; each of 7 single-point
mutations of the rule fails the test. Against 1a56cf2's emitter the rule
reports every read astra's HIGH used.
…id on a final line terminator (astra pack 309 LOW)

JavaScript's $ without the m flag matches only at the end of the input, so
the former RegExp refused a trailing LF, CR, CRLF, U+2028 and U+2029, as the
code-unit check does: the behavior is identical. The test pins that.
…ders include named in-repo captures (astra pack 309)

Closes a gap in the provenance rule: unknown narrowed by typeof to `object`
and then asserted to a readable shape (`(v as { tier: number }).tier`) was
not a conversion. An OPAQUE value (unknown, any, object, {}) taken as a
readable shape now is, and an opaque value handed to code the check does
not walk needs an own-data reader or a named call site.

Own-data readers also cover Object.keys, Reflect.apply of a reader
(ReflectApply(HasOwnProperty, o, [k])), the reject of an executor of the
Promise captured at load, and in-repo captures named in
CheckOptions.ownDataReaders (closed, like the collaborators).

The emitter's path names 4 more conversions (copyEventInput's two copies,
canonicalize's two record views), canonicalize's final String() call site,
and spec's nodeIsProxy as a reader. The fixture gains a named and an
unnamed reader, a narrowed-object assertion and an object handed to an
opaque callee. 10 single-point mutations of the rule each fail the test.
…ype minted only where a record is made (DECISIONS 05:05, astra pack 313)

Astra pack 313 (DO-NOT-SHIP on aad2aee): the provenance rule laundered
through types. A caller's object moved into a local unknown came back typed
through an unconstrained generic (cast<T>), `as never as T`, a closure, a
public method's this, or a collaborator's answer, and its fields were read
with a dot. Reproduced at 5e002ce. A static type is no evidence (DECISIONS
05:05), so the check now asks where the receiver came from.

The check (builtin-reads-check.ts):
- a dot read (or destructuring) needs an OWNED receiver: its type carries
  the brand `[OWNED]` (a `declare const OWNED: unique symbol` in a
  primordials file), or it is a const bound to a fresh ObjectCreate(null)
  record or to a literal that defines the field, or a property descriptor
  read where hasOwn proves the field its own;
- the brand is minted only on what the code makes (ObjectCreate(null), a
  literal defining every field, none optional, an array literal), or where
  CheckOptions.provenance names the snapshot;
- assertions to a type parameter or to never are refused;
- no owned type at the edge: an entry point's parameter, a callback opaque
  code calls, or what opaque code answers;
- an object handed to code the check does not walk needs an own-data
  reader, a slot-read built-in, or a named call site;
- writes by Object.defineProperty or a named writer land only on an owned
  snapshot, a fresh value, a private field or a promise, and an owned
  container is never widened where it is written.
The 309 caller-value and named-conversion rules are subsumed and removed.

The emitter: its step records, copied input, commit list, tier entries and
ownProperty reads are owned literals or null-prototype records; copyPlain
builds null-prototype records; every descriptor value read is proven own by
hasOwn; the console's warn is read as own data; stored hashes are kept beside
the events, so no stored event is read. spec's kernel-pull-capture owns its
Fields and check context, and spec's primordials declares the brand.

MEDIUM: setStorageService reads isReady and archiveBundle as the service's
OWN data (ownField): an accessor, an inherited method, one Object.prototype
serves, or a Proxy is refused, and no getter or trap runs.
EvidenceStorageService now defines both on each instance.
LOW: the options test now uses tier-2 camera evidence, where an unreadable
jobId must fail the tier.

Fixture: astra's forms each refused for their own reason; 14 single-point
mutations of the rule each fail the test. spec 2462, kernel 1222, PLR 58,
gateway 4803 (the same 5 jsdom files), tsc clean.

This branch had an error being deployed

1 failed deployment
trusted-checks — 923be7f9 Deployed Oct 4, 2026 by LamaSu via post-verdicts #139
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