Skip to content

fix(spec): profile registration holds under post-load realm mutation (#384 follow-up, steward #5186) - #520

Draft
LamaSu wants to merge 42 commits into
feat/sensors-profile-registration-checkfrom
feat/sensors-registration-hardening
Draft

LamaSu wants to merge 42 commits into
feat/sensors-profile-registration-checkfrom
feat/sensors-registration-hardening

Conversation

@LamaSu

@LamaSu LamaSu commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Follow-up to #384 (frozen at 64be17e), which it stacks on. It merges the #363 follow-up (feat/sensors-admission-hardening @6d401f3e, reviewed in its own PR). Its own change is one commit, ccdbc6f, for steward #5186: profile registration holds under post-load realm mutation.

Reproduced at 4a25a42 (#384, registration unmodified)

repro-384-at-4a25a424.txt: 4 failed / 36 passed; 30 violations.

  • A replaced push made 13 refused requests registrable.
  • A replaced array iterator, or its %ArrayIteratorPrototype%.next, dropped every unverifiable term (8 each).
  • map and JSON.stringify changed only the problems listed in a refusal.

The fix (ccdbc6f)

profile-registration.ts uses the intrinsics captured at load and index loops, the same discipline as admission. The child-process harness (__tests__/harness/registration-realm.ts) runs 38 scenarios, with 0 violations.

Checks (2453141)

  • spec 1641/1641, tsc --noEmit clean.
  • Mutations: 7/7 caught.

🤖 Generated with Claude Code

LamaSu added 23 commits October 3, 2026 04:35
…load realm mutation (steward #5186)

AGENT: implementer-admission-r9

#363 round 9: the realm-mutation class of astra packs 162-171, across
admission's trust path. Reproduced first at 45f2c1f with a patch harness:
51 of 88 scenarios changed an acceptance, a digest, a term list or a level
(e.g. Array.prototype.push replaced: every reject admitted; Object.prototype
.passed/.value/.profileObservation written: excluded observations counted;
DECIMAL_VALUE_PATTERN and TAGGED_DIGEST_PATTERN recompiled: a non-decimal
value and a non-digest sampleId admitted; "value" in descriptor with
Object.prototype.value written: an accessor pin and an accessor signature
leg read as data, a left-out failure bundle admitted; Promise.prototype.then
forging computeBundleSetDigest's and binding's returned promises; an
Object.prototype.then taking over admission's result; constructor+then
forging a leg's false). After: 0 of 88.

- profile-admission.ts and evidence-level.ts call only util/primordials.ts
  captures, plain loops and operators (source scans enforce it). Exported
  lists are frozen where defined; sets are stringSet records built at load,
  the vocabulary's active primitive ids included (EVIDENCE_PRIMITIVES defs are
  exported, mutable objects; getPrimitive answers from a Map).
- DECIMAL_VALUE_PATTERN becomes isDecimalValue, a code-unit predicate equal to
  the old pattern on edge cases and 25,000 generated strings; isTaggedSha256
  (primordials) replaces signing-preimage's RegExp here.
- codeInData and the input boundary use own-property descriptor reads
  (hasOwn), captured Reflect.ownKeys and an ancestor list: no in, for...of
  or Set. Everything evaluated is a null-prototype plain-data copy.
- computeBundleSetDigest hashes with node:crypto's SHA-256, methods captured
  at load (as measurement-profile does); the goldens are unchanged.
- The binding leg (subject-binding.ts, the evidence lane's) still hashes
  through ambient methods, so its answer is re-verified with the captured hash
  and own reads before anything is evaluated; the re-check only refuses more.
- Promises: awaited and returned promises get their own constructor and then
  (ownPromise); a leg's promise is followed through the then captured at load
  (fulfillsWithTrue); the result has a null prototype.
- evidence-level reads own data only; isFabricated is asked about a null-
  prototype view of the event's own source and payload.

Harness: profile-admission-intrinsics.test.ts runs every realm mutation in a
child realm (harness/admission-realm.ts via node --import tsx). Held in
vitest's own realm, a replaced Array.prototype[Symbol.iterator] stalls vitest
itself (a 300 ms timer test with no @pcc/spec code never completes), which
is what hung the first run; it was never our code.
…e and the delivered result, and serves an array hole

AGENT: implementer-admission-r9

Rows that exercise every line of the round-9 fix, so the mutation check can
kill a regression in each. All reproduce at 45f2c1f (53 of 91 scenarios
violate there; 0 here):
- Object.prototype.then turning binding's failure into ok with the presented
  events: evidence from another job, kernel, payload kernel, settlement unit,
  challenge or output admitted at 45f2c1f; the re-check refuses each now.
- Promise.prototype.constructor and then together: a sync leg's false, an
  async leg's false, the set digest, and the result read through .then.
- Array.prototype[1] holding a digest: computeBundleSetDigest read a hole.
- Object.prototype.value = true, and evidence-level accessors that must never
  run (a verdict, a simulated flag).
- The codeInData recipe must be identical, not merely a refusal: plainDataCopy
  would still refuse the accessor, so only the reason shows codeInData read
  the descriptor's own value.
- A cycle anywhere in the input resolves to a reject, never a rejected promise.
…ta hold, and legs that answer truthy but not true

AGENT: implementer-admission-r9

Two cases, so that the mutation check can kill a widened pin check (it would
turn the reject into a hold) and a leg answer read as truthy instead of
exactly true, synchronously or from a promise.
…(steward #5186)

AGENT: implementer-admission-r9

#384 round 9, after merging #363's hardening (feat/sensors-admission-
hardening). Reproduced first at 4a25a42 with a child-realm patch harness:
with Array.prototype.push replaced after load, every refused request (device
or capability mismatch, an unverifiable term, a client digest that is not the
profile's) became registrable; with the array iterator or
%ArrayIteratorPrototype%.next replaced, every unverifiable term was dropped
and the profile registered. Array.prototype.map and JSON.stringify changed
only the problems of a refusal.

checkProfileRegistration now calls only util/primordials.ts captures (append,
mapList, newList, JSONStringify), plain loops and operators; a source scan
enforces it. It reads its null-prototype plainDataCopy only, as before. The
header states the post-load property and the pre-load boundary.

Harness: profile-registration-intrinsics.test.ts and
harness/registration-realm.ts, one child realm per scenario (38 scenarios,
23 requests): every result identical, or a refusal (ok: false).
…w starts and between two window ends

AGENT: implementer-admission-r9

The mutation check found that swapping the capture window's bounds (opening
at the latest start, closing at the earliest end) survived: every window case
placed the observation outside both candidate bounds. These two admit cases
place it between them, so earliest-start and latest-end decide the outcome.
…prototype for an ordinary profile's

AGENT: implementer-admission-r9

The #384 mutation check found that an inherited read of measurement.tolerance
in unverifiableProfileTerms survived: registration and admission pass
null-prototype copies, and an inherited term only adds a refusal. The function
is exported and its header claims own-data reads, so this recipe holds it to
that claim: with Object.prototype.tolerance and maxIntervalMs written, the
terms of every ordinary-object profile are identical, not merely more.
…th a species pinned at load (astra pack 187)

Native then (and catch and finally, which go through it) builds the promise
it returns with SpeciesConstructor(promise), that is
promise.constructor[Symbol.species]. ownPromise pinned `constructor` to the
global Promise, whose species is a configurable accessor: code running after
load could point it at a constructor whose "promise" is a forged thenable,
and `await profileAdmitsBundle(signatureFails).then((x) => x)` received an
admit. Reproduced at 6d401f3 for profileAdmitsBundle, computeBundleSetDigest
(a rejected digest became a value too) and fulfillsWithTrue, through
then, then.then, then(undefined, r), catch and finally; and two siblings: a
derived promise awaited with Promise.prototype.then and .constructor
replaced, and a chain's second link with then replaced.

- ownPromise's own `constructor` is PINNED_CONSTRUCTOR: a frozen record with
  no prototype whose one property, [Symbol.species], is PinnedSpecies, a
  frozen constructor that makes a native promise with the Promise captured
  at load and passes it through ownPromise. Every derived promise is pinned
  too, at any depth. The record is shared and reachable as
  promise.constructor, so it is frozen: no caller can re-point it.
- awaitedHere keeps admission's await of the binding leg's answer on the
  engine's internal path (own constructor = %Promise%). An ownPromise now
  resolves `await` through its `then`, which would look `then` up on
  binding's ordinary-object answer.
- Tests: astra's recipe verbatim; every consumption shape; the shared
  constructor; the then-and-constructor sibling; a guard that admission's
  await adds no lookup on binding's answer. Realm harness: a
  Promise[Symbol.species] patch row, astra's recipe, and delivery rows in
  every scenario that use and await the promise then, catch and finally
  return (viaThen discarded it).
…ed at load (astra pack 187)

A caller's `promise.catch(...)` or `promise.finally(...)` looked the method
up on Promise.prototype, where code running after load can replace it with
one that answers anything. Checked with pack 187's species finding and
reproduced, at 6d401f3 and still after the species pin (296b0da):
admission's rejection delivered as an admit, the set digest as another
digest, and a rejected digest (the empty set) as a value, through either
method.

ownPromise now also gives the promise own, fixed `catch` and `finally`:
Promise.prototype's, as they were at load. The native catch calls the own
`then`, and the native finally builds its promises with the pinned species,
so both deliver what the promise settled with.

Tests: catch and finally replaced after load, in process; the realm
harness's Promise.prototype.catch and Promise.prototype.finally patch rows,
whose forgers run the original method and forge what its promise delivers.
The record's species was fixed twice: a non-writable, non-configurable
property, and the record frozen. Either alone holds it, so removing either
could not be caught by any test. Now the species is a plain own property
of the null-prototype record and the freeze is what fixes it; a test fails
without it (a caller holding any returned promise re-points the species).
…a caller's own callback result is outside the boundary
…which follows #384

A clean merge. spec green, tsc clean.
…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.
's final file)

Evidence #6440 asked sensors to own this port: #519's 550b212 pattern, applied to
#345's final evidence-level.ts. On master, code that runs after @pcc/spec loads
could change its answers. With Array.prototype.indexOf replaced,
meetsEvidenceLevel("submitted", "inspected_output") was true. The new harness,
run against master's file, changes answers in 31 of its 68 checks, escalations
included.

- It calls only intrinsics captured at load (util/primordials.ts, which is #519's
  reviewed version, verbatim), index loops and operators. No method looked up at
  call time, no iterator protocol, spread, in or RegExp, at load or at call time.
- The sets and the pinned-verdict table are frozen null-prototype records, and
  the exported lists are frozen.
- The principal-id RegExp is a code-unit predicate, held equal to the pattern.
- The pinned verdict field must hold an own value.
- Rule 7 still reads each field once, but only OWN properties count. An own
  accessor still runs once. payload.mock keeps isFabricated's read, which E5's
  trap test pins; written on Object.prototype it can only refuse.

Tests: E5's evidence-level.test.ts is unchanged, 128 of 128. The new
evidence-level-intrinsics.test.ts runs 61 child-realm scenarios over 141
answers, plus source scans, frozen exports, RegExp reachability and the
predicate's equivalence. Mutations: 26 of 26 killed. Spec: 2476 of 2476.
…own fields of objects only (pack 267)

Pack 267 (DO-NOT-SHIP) found 2 HIGH in #577. Both were reproduced at e13a395
by new realm scenarios; 6 scenarios changed answers.

- An inherited payload.mock was still read, through isFabricated, BEFORE
  source.deviceId. An Object.prototype.mock getter could write the event's
  deviceId and answer false, and an unattributed completion then proved
  device_reported (an up-tier). A throwing getter turned a classification into
  a refusal.
- A primitive source or payload was passed to isFabricated as it was. Its read
  boxed the primitive and consulted Number.prototype, String.prototype and
  Object.prototype: a written `simulated` made a bundle fabricated and erased its
  completion-and-failure contradiction.

isFabricated now receives null-prototype snapshots of source.simulated and
payload.mock. Each is read once as an OWN property, and is undefined for a
non-object. E5's trap test now expects the own-only read of mock
(getOwnPropertyDescriptor:mock, no get:mock). Its inherited-mock case is now an
explicit assertion: canonicalize hashes own fields only, so verified evidence
never carries an inherited marker.

The harness gains primitive-source and primitive-payload items and four
scenarios: a mock getter that writes deviceId, a throwing mock getter,
Number.prototype.simulated and String.prototype.mock. The test's mock exception
is gone, so every scenario must be identical. Mutations: 28 of 28 killed.
E5 128 of 128; realm tests 73.
The fullwidth and Arabic-Indic digits in the principal-id cases were literal
characters in the source. As escapes, a reviewer sees them for what they are.
No behaviour changes.
…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.
…519

#519 follows #577 for evidence-level.ts: the previous merge already took
#577's file and E5's two changed expectations. This merge brings #577's
history and its realm tests: evidence-level-intrinsics.test.ts and
harness/evidence-level-realm.ts.

One conflict, util/primordials.ts. #577's copy is #519's file verbatim
(3b27e8a's blob), so #519's side is the resolution: that file plus
followedPromise.

Spec 2749 of 2749 (81 files); tsc clean.
LamaSu added 3 commits October 3, 2026 23:55
…ardened admission

#363 now takes trust domains from ONE pinned signer snapshot
(signerTrustDomains, signer to operator, or null), and its leg answers the
VERIFIED signer's id. That id must equal the bundle's declared
kernelSignature.signer, so one key has one domain (astra pack 271). This
merge carries that into #519's hardened file.

profile-admission.ts was the one conflict, resolved by porting the fix in
#519's style:
- signerTrustDomains is read as data with the other fields (INPUT_FIELDS,
  DATA_FIELDS, the code walk, plainDataCopy). Its rows go into a frozen
  null-prototype record, checked by isSignerId (new: a code-unit predicate
  equal to ^0x[0-9a-f]{40}$, exported and tested on edge cases and 20,000
  near-misses) and isOperatorPrincipalId.
- A signer listed twice is refused.
- The leg's answer is a signer id, a string. A native promise is followed
  through the then captured at load (followedPromise), and a string
  resolves with no then lookup, so signedBy, signerOf and the
  null-prototype rule for promised records are gone.
- The answer must equal the declared signer, read from admission's
  null-prototype copy of the bundle, and the snapshot must hold it.

Tests:
- #363's new cases arrive through the merge.
- #519's signer describe now covers: a promised signer id followed; a
  thenable refused; signerTrustDomains refused at the input boundary
  before any leg runs; signerTrustDomains walked for code.
- The two pack-187 inputs get the snapshot.
- The realm harness takes the snapshot and the signer-id leg, with cases
  for: an async signer id; true; a domain record; another signer than
  declared; a null-domain snapshot (device_reported admits, inspected
  rejects); one key for both bundles; a missing signer; a duplicate row; a
  malformed row; a thenable.
- The intrinsics test adds isSignerId's agreement.

Spec 2755/2755 (82 files), admission 255/255, tsc clean. Mutations: 17 of
19 killed. Both survivors are equivalent:
- the leg answer's format check: a malformed answer either differs from
  the declared signer or is not in the snapshot, which holds only checked
  ids;
- "null copy not lowest", as in pack 271.
LamaSu added 2 commits October 4, 2026 00:27
…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.
…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 2 commits October 4, 2026 01:18
A plain merge-up. #519 now carries #363's rounds 12 and 13 (signer grants,
delegated session keys, executor non-independence, no-witness-authorized),
ported to the realm-hardened admission. #520's own files are unchanged.
A plain merge-up of #520's base, #384, which now carries #363's round 13.
LamaSu added 6 commits October 4, 2026 01:36
…rimordial, checked by type (astra pack 289)

Pack 289's HIGH, reproduced at dfb73b2. lowerHex() read bytes.length, the
accessor %TypedArray%.prototype inherits, so code that replaced it after load
could empty the publicKey in the LO-EV-1 delegation preimage. A root's signature
over a keyless delegation then authorized the row's full session key: a clean
realm rejected the input, and with the getter replaced it was admitted at
inspected_output.
- lowerHex iterates the caller's count (32 bytes for a key) and reads no length.
- A TYPE-AWARE closed allowlist (steward DECISIONS 01:30; the TypeScript
  checker): on a value whose static type is a built-in (typed arrays,
  ArrayBuffer, DataView, Map, Set, Promise, RegExp, Date, Error, functions,
  arrays, strings, numbers), only a numeric element access, or the own length of
  an array or a string, is allowed. Every other read or call fails. It covers
  profile-admission.ts and the functions admission calls (evidence-level,
  subject-binding, measurement-profile, plain-data, canonicalize, isFabricated).
  A self-test plants 14 violations. A diagnostics control rules out silent
  `any`, and the check reports the original bytes.length.
- The realm harness gets astra's attack as a case, a delegation whose root
  signed a keyless preimage, and a %TypedArray%.prototype length / byteLength /
  byteOffset scenario. That scenario and every other scenario added with the
  signature and registry rounds are now IDENTICAL-strict. An admission test is
  the clean-realm control: the keyless signature is refused.
…ver, which could be any built-in

A closed allowlist cannot pass what it cannot classify: a receiver whose type
the checker reports as `any` is refused like a built-in. The trusted path has
none; the self-test plants one.
…lement read is proven own (astra pack 291)

Astra pack 291 (SHIP-WITH-FIXES, two MEDIUMs on the checker) and steward #6792.

The check moves to builtin-reads-check.ts and walks EVERY node that runs after load on
admission's trusted path. A node passes only as one of the forms the file names, under
that form's condition; any other syntax is reported.

MEDIUM 1, numeric indexes:
- A numeric index passes unconditionally only on a typed array.
- An array, string or record element must be proven own, with nothing running between
  the check and the read: `hasOwn(x, k) ? x[k]`, `hasOwn(x, k) && x[k]`, or
  `if (hasOwn(x, k))` whose branch begins with the read. Other options are a string
  index inside a canonical `for (i < s.length; i++)` loop, or a key of a trusted
  `ObjectCreate(null)` record.
- An element key must be a string, number or symbol.
- The fixture refuses a hole, an out-of-range string index, an unbounded index, a guard
  with a call in between, and a guard on another receiver.
- A runtime control shows that `new Array(1)[0]` and `"ab"[5]` do read a written
  prototype, while listAt and charAt do not.
- A new identical-strict realm scenario writes Array.prototype[0..63] and
  String.prototype[0..255].

MEDIUM 2, closure:
- Default-deny covers object destructuring, `in`, a namespace import read by any
  spelling, and built-ins reached through a subclass, an intersection or a
  type-parameter constraint.
- hasOwn and ObjectCreate count only when declared in a primordials file, so a
  look-alike is refused.
- `x.length = n` is allowed only on an array, with a primitive value.

Production changes (mechanical; behavior unchanged for data):
- Array and string element reads go through listAt and charAt.
- readRegistry builds fresh null-prototype rows instead of writing into earlier ones.
- The error classes define `name` rather than assigning it.
- canonicalize reads an own key through hasOwn.
- This touches subject-binding.ts, measurement-profile.ts, evidence-level.ts,
  canonical.ts and plain-data.ts on admission's path.

Evidence:
- The checker reports nothing on the trusted modules.
- Six mutations of the new rules are each caught by their fixture line.
- spec: 83 files and 2788 tests pass; tsc is clean.
…d is no proof at the read (astra pack 293)

Astra pack 293 (SHIP-WITH-FIXES): MEDIUM 2 closed, MEDIUM 1 open on one allowance. The
canonical string-loop rule, provenInRange, compared identifiers by spelling. It also allowed
statements between the loop's bound and the read.

Reproduced at 96d4ce3, where each of these was unreported:
- astra's shadowedRange (`const s = ""` inside the loop);
- a shadowed index;
- a string reassigned through a call inside the loop.

At runtime, with String.prototype[0] written, the shadowed read returns the written value.

The fix is astra's first option: the allowance is removed. A string element is read only
through the captured charAt, and admission's path already does so (the check still reports
nothing there).

The self-test:
- its allowed string loop uses charAt;
- canonicalLoop, shadowedRange, shadowedIndex and reassignedByCall are refused lines;
- it now lists every misclassified line in one assertion. Run against the 96d4ce3 check,
  it names all four new negatives.

spec: 83 files and 2788 tests pass; tsc is clean.
…file admission captures at load

build-dashboard has failed on #519 since admission began taking node:crypto's one-shot verify at
load (astra pack 275):
  "verify" is not exported by "src/lib/node-crypto-shim.ts", imported by
  "../../packages/spec/dist/evidence/profile-admission.js"

Reproduced locally at 7f3067e with vite 5.4.21, using the dashboard's own aliases: spec's built
profile-admission.js was bundled through node-crypto-shim.ts and node-util-shim.ts and failed with
the same error.

The shim now exports `verify`, which throws if anything calls it:
- a browser has no synchronous Ed25519 check;
- the dashboard bundles admission but never admits a bundle;
- admission only captures the binding at load and never calls it there;
- so a signature is never taken as verified in a browser.

With the shim, the same vite build of all of spec's dist/index.js passes, with no missing export.
…IP, and the dashboard shim) into #520, which follows #519 (a plain merge)
LamaSu added 2 commits October 4, 2026 04:22
…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.
…ptures, and its trusted path is default-deny checked (astra pack 188)

Astra pack 188's tracked MEDIUM, reproduced at a3ee228: the registration harness's 38 scenarios lacked
Number.isInteger, Object.create, Object.defineProperty, Object.is, Object.prototype.hasOwnProperty and
Reflect.apply. Registration reaches all of them through validation and plainDataCopy.

- 19 recipes added (57 scenarios). Besides the six above: Date.parse, String.prototype.charAt,
  Set.prototype.add, Function.prototype.bind, the Promise prototype methods and Promise[Symbol.species],
  TypeError, Error, Proxy, structuredClone and Uint8Array.
- A fixed inventory assertion: a scenario exists for every callable intrinsic captured by the files the
  registration closure reaches. Against the old harness it fails and names the missing recipes.
- Registration's trusted path is checked by the computed closure (DECISIONS 04:06). It reaches
  plainDataCopy, validateMeasurementProfile, unverifiableProfileTerms, computeMeasurementProfileDigest and
  canonicalize.
- The closure found one read: terms[i] in checkProfileRegistration, now listAt. Put back, it is reported.

spec: 85 files and 2867 tests pass; tsc is clean.
LamaSu added 3 commits October 4, 2026 04:48
…nce field runs after load, collaborators are exact (astra pack 303)

Astra pack 303 (DO-NOT-SHIP) on the computed closure. All three findings were reproduced at dede88d.

HIGH 1: the closure dropped function values supplied at run time. An entry point's `cb()` was neither
reached nor reported, and admission's caller-supplied verifyPrimitiveInstance ran with zero
collaborators reported.

A call (and a `new`) now passes only when its TARGET is fixed, seen code:
- a function, method or class declared in the repository;
- a function written in place, or a const bound to one;
- a const computed at load, unless it was taken from third-party code;
- a parameter whose function is not an entry point (exported from a seed file, or a public member of
  an exported class), never escapes as a value, and gets seen code from every call in the scope;
- the resolve and reject of an executor passed to the Promise captured at load.

Anything else fails closed unless that exact call site is a named collaborator with its reason: a
callback, a private field set at run time, a const read from data, a let, an unresolved import or
third-party code. Admission names one: its PRIMITIVE leg, the caller's verifier, called only through
legAnswer. evidence-level's verdict table now holds a tag rather than a reader function, so
verdictOfPayload calls readBooleanVerdict or readPassFailVerdict by name (behavior unchanged).

HIGH 2: an instance field's initializer in a class evaluated at load was exempt. It now runs as code
after load; a computed name and a static initializer stay load-time.

MEDIUM: collaborator keys used base names, and reasons were never checked.
- Keys are now paths relative to CheckOptions.root, which is required.
- Every entry needs a non-empty reason and must match exactly one declaration or call site.
- When one declaration makes several calls of the same unseen callee, the call sites are numbered #n.
- Unexplained, stale and ambiguous entries are reported.

Tests:
- The fixture refuses six new forms (a caller's callback, a const from data, a declared value, a let,
  a private field, an instance field) and allows a higher-order helper, a static field and a named
  collaborator.
- A two-file self-test covers each finding.
- Mutations of each rule are caught: targets always fixed (3 tests fail), no field deferral (2),
  reasons unchecked (1), base-name keys (3).

spec: 83 files and 2789 tests pass; tsc is clean.
…rogram, and covers Array, Map, Promise, Set and Set.prototype.size (astra pack 305)

Astra pack 305 (SHIP-WITH-FIXES), its MEDIUM reproduced at d5c19b8. The hand-written inventory missed
captured callables: adding Array, Promise, Set and Set.prototype.size to it fails, naming all four.

The inventory is no longer written by hand. capturedIntrinsics() computes it:
- It covers the files the registration closure reaches.
- Every outermost property chain rooted at a global (default library or @types/node) in a top-level
  const initializer, whose value can be called or constructed, is a capture.
- So is every accessor captured through getOwnPropertyDescriptor(chain, "name").
- Roots resolve through top-level const aliases (FunctionPrototype is Function.prototype), and
  globalThis.X is X.
- A new capture therefore joins the inventory without anyone listing it.

What it found missing gets a recipe: Array, Map, Promise and Set (the globals replaced) and the
Set.prototype.size getter. 62 scenarios in all, each run identical or refused.

The test asserts the inventory holds pack 188's six and pack 305's four. Removing a recipe fails it,
naming the recipe.

Also: the registration closure check takes root (required since #519 @b61262ae) and names files from the
repo root.

spec: 85 files and 2873 tests pass; tsc is clean.

This branch had an error being deployed

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