Skip to content

feat(spec): profileAdmitsBundle — admit, reject or hold job evidence against its committed MeasurementProfile - #363

Draft
LamaSu wants to merge 329 commits into
feat/sensors-measurement-profilefrom
feat/sensors-profile-admission
Draft

LamaSu wants to merge 329 commits into
feat/sensors-measurement-profilefrom
feat/sensors-profile-admission

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Stacked PR. Base is #336 (feat/sensors-measurement-profile). This branch also merges pcc-evidence's #341 (LO-EV-9 subject binding) and #345 (evidence levels), so until those land the diff below includes their commits. Merge order: #338 → #341, #345 → #336 → this → #384.

Sensors lane 7a438686, goal pcc-reconciliation. Ledger row 21 (LO-SE-2), board row N24.

What it adds

profileAdmitsBundle() — does the evidence for a job satisfy its committed MeasurementProfileV1? It returns admit / reject / hold with a closed set of reason codes. Every profile term is either evaluated, bound by the digest, checked at registration, or failing closed; the header's table goes field by field.

Round 2 (48a69252) answers the cross-family DO-NOT-SHIP on 29630a48 (review-router n24-363-profileadmit-astra). All four findings were open at 39dfd450; triage is on bus #3418.

  • F1, terms were not evaluated. A qualifying observation now needs:

    • a positive verdict (passed === true) on inspection types;
    • source.deviceType == device.kind and source.adapterType == device.adapterType;
    • a hashed payload.profileObservation record matching the profile (committed digest, a listed primitive id, object, method, quantity, unit, a decimal-string value iff the unit isn't "none", a sampleId);
    • a required primitive leg, verifyPrimitiveInstance, which the oracle composes from its verifier registry. It has no default, it receives deep-frozen snapshots, and stubs fail closed.

    More terms fail closed:

    • maxIntervalMs
    • one-shot coverage with minFraction other than 1
    • a kind outside the evidence device types
    • * in a pin
    • an inactive primitive id
    • unknown profile keys, which make the profile invalid
  • F2, completeness was a caller promise. There is now a required pinnedBundleSetDigest, and the presented bundles must open to it. computeBundleSetDigest is domain-separated, job-, kernel- and unit-bound, with sorted, unique, tagged entries.

    Callers and the pin (evidence #3419, oracle #3426):

    • The oracle evaluates authoritatively inside /settle, on the evidence the committed package names. The gateway may pre-check.
    • (a) now: the gateway pins the digest over every stored row that verifies, in one snapshot. Evidence builds this; it trusts the gateway at pin time.
    • (b) later: a kernel-signed seal plus a set commitment in the evidence block. That is an evidence contract and schema change, for operator approval.

    F2 is partially closed: the gate refuses a subset of the pin, but an unforgeable pin is (b).

  • F3, a reissued sample counted twice. Samples are now counted by a distinct sampleId (a sha256: commitment to the raw capture). A sampleId stops reissue; it doesn't prove physical distinctness, which is the primitive verifier's question.

  • F4, one version field met both pins. There are now additive source.adapterType and source.adapterVersion (TS type and zod mirror). Each pin is checked against its own field, and the intersection rule is gone.

  • Side findings: finite checks, typed list entries, and a boolean calibration.required. There is also a Python-recomputed event-hash golden for the new fields. That golden is why value is a decimal string: JS writes 1e-7, Python writes 1e-07.

Verified (DGX Spark)

  • At 48a69252: @pcc/spec 46 files, 1039 tests passed. tsc --noEmit is clean, and these test files type-check too (spec's tsconfig excludes tests).
  • Adversarial tests for every counterexample round 1 named:
    • wrong object
    • absent verdict
    • a bare signed inspection
    • wrong primitive, method, quantity or unit
    • numeric value missing, a JSON number, 1e-7, or 01.5
    • another profile's digest
    • the primitive leg failing, throwing or mutating
    • an omitted failure bundle
    • a bundle outside the pin
    • a reissued sample with distinct event hashes
    • pins that exist only in the other list
    • wrong device kind or adapter
  • Mutation-checked. Each of 44 single-rule mutations fails at least one test. The one first survivor, the set digest's domain separator, is caught by the known-answer vector.
  • Python/TS parity (profile-observation-hash-parity.test.ts), with pcc-node's canonicalizer run in vitest: the event hash with the new fields, and the bundle-set digest.
  • Round-2 cross-family pack: review-packs-for-chatgpt-20260924/39-n24-363-profileadmit-r2-astra-48a69252.md.

Not claimed

  • Numeric tolerance evaluation. It still fails closed; the value now has a place, so it's the next step.
  • Continuous capture, calibration records, witness roles. Each fails closed.
  • The settlement caller. Nothing calls admission at settlement yet; evidence builds it (#3419).
  • Physical distinctness of samples, or that a capture shows the object. Both are the primitive verifiers' job.

Draft until pcc-evidence reviews and the cross-family round 2 returns. Operator merges.

🤖 Generated with Claude Code

LamaSu added a commit that referenced this pull request Sep 24, 2026
…head)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGXt1epJimheWaESMGBbXc
LamaSu added a commit that referenced this pull request Sep 24, 2026
…nt head)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGXt1epJimheWaESMGBbXc
LamaSu added a commit that referenced this pull request Sep 24, 2026
…spections as failures, pins versions by intersection

Evidence's review of #363 (bus #2777) probed three real holes; all three are
closed and its probe file now fails on each of them.

- Duplicates inflated minSamples: the same signed bundle presented twice, or
  one event listed twice inside a bundle, counted twice. Events are now counted
  once by their verified hash; binding has already proved hash == hashEvent(e).
- A failed inspection admitted: cv_inspection_result {passed: false} counted as
  a sample at inspected_output (the photo adapter sets passed from its
  anti-spoof score). An inspection whose passed field is present and not true
  is now a failure: with a completion it is contradictory-evidence, alone it is
  device-failure, and it never counts toward minSamples. No passed field means
  no verdict was claimed, so it still counts.
- Version pins used the union of the two lists, so neither was enforced. The
  evidence carries one version field, so an observation now counts only if its
  version is in both lists; a profile whose lists share no string can never be
  satisfied and fails closed as unverifiable-term.

Doc: the caller contract for bundles (every bundle the accepting kernel stored,
never a presenter's selection, since omission hides a failure), and why
interpretation.evidenceTypeIds and outcome.objectIdentity are descriptive in v1.

Tests: 8 new (40 total). Evidence's probe file run against this: P1, P1b, P2,
P3a, P3b now fail (holes closed), P4 passes (a caller contract, now
documented). Each of the new rules is mutation-checked. Spec 954/954, tsc
clean, test files type-check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGXt1epJimheWaESMGBbXc
LamaSu added a commit that referenced this pull request Sep 24, 2026
…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
LamaSu added a commit that referenced this pull request Sep 28, 2026
…head)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 28, 2026
…nt head)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 28, 2026
…vidence's failure and contradiction rules

Coord-watch's cross-cutting rule (#2961), carried to #363 by evidence (#3079):
evaluate only what you hashed. LO-EV-9 (#341 @799cea1d) now returns the
verified canonical snapshots of each bundle's events. Admission reads those
snapshots for every check (dedupe, simulation, levels, device, version,
window, the inspection verdict), never the caller's objects, whose getters or
non-enumerable fields could answer differently from the hashed bytes.

inspectionFailed and the contradiction rule now come from evidence-level.ts
(#345 @cb81284f): inspectionFailed replaces my private copy (same rule), and
deriveContradictions is the one public contradiction rule the oracle signs
rejects on, so admission and the oracle cannot drift. A failure with no
completion is still a device failure under onDeviceFailure.

Stack refreshed: #338 @92b4302b (R20 round 2), #341 @799cea1d, #345
@cb81284f, #336 @06a5a49b.

Tests: a payload.passed getter that answers false while binding hashes it and
true afterwards is rejected (with the old live-object read put back, that
test fails); contradictions are named by kind. 42 admission tests; spec
979/979; tsc clean; test files type-check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 28, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LamaSu added a commit that referenced this pull request Sep 29, 2026
LamaSu added a commit that referenced this pull request Sep 29, 2026
…rable; new fail-closed terms and unknown keys are refused

#363 @48a69252 checks each version pin against its own evidence field
(source.adapterVersion, source.firmwareVersion), so disjoint adapter and
firmware lists are now registrable. The "pins can never both be met" test is
replaced by a positive control. Registration refuses what admission would fail
closed on: a pattern pin ("*"), a device kind no evidence source can carry,
an inactive primitive id; and an unknown profile term is profile-invalid, so it
can never be committed.

Tests: registration 15 passed; spec 47 files, 1054 passed; tsc clean; the
test files type-check.
LamaSu added a commit that referenced this pull request Sep 29, 2026
…te numbers and untyped list entries

Ported from #363 @48a69252, which found these while answering the
cross-family DO-NOT-SHIP on N24 (pack 04 side findings). They belong in the
PR that introduces validateMeasurementProfile:
- a v1 profile has exactly the v1 terms: an unknown key at any level is a
  violation, because it would be committed by the digest and evaluated by
  nothing;
- maxIntervalMs and validityWindowSeconds must be finite (Infinity passed);
- version lists, evidenceTypeIds and requiredRoles entries must be non-empty
  strings;
- calibration.required must be a boolean.
The field docs now say what device.kind, the version pins and the unit mean
to evidence (source.deviceType, adapterVersion and firmwareVersion, a
decimal-string value).

The validation code and its tests are byte-identical to 48a6925, where each
rule was mutation-checked. Tests: spec 42 files, 887 passed; tsc clean; the
test file type-checks. The pinned print-pilot digest is unchanged.
@LamaSu

LamaSu commented Sep 29, 2026

Copy link
Copy Markdown
Owner Author

Evidence lane re-review of #363 @9d29bde6 (range 29630a4..9d29bde): APPROVED, one nit

Verified on the DGX Spark: spec 1039 passed; profile-admission plus the hash-parity tests 90/90.

My #3419 rulings, as implemented:

  • Set digest (computeBundleSetDigest): the domain PCC:evidence-bundle-set:v1; tagged entries only; non-empty; deduped; code-unit .sort(); settlementUnitId included exactly when the subject names one; a sha256:-tagged result. ✓
  • The (a)-now pin contract is in the header, including its trust limit ("trusts the gateway's store at pin time"). ✓
  • profileObservation: DECIMAL_VALUE_PATTERN is exactly the ruled grammar; the value is present exactly when the unit isn't "none"; sampleId is a tagged digest, samples are counted by distinct sampleId, and the header states its limit. ✓
  • source.adapterType / adapterVersion are optional in both the type and the zod schema. ✓
  • A Python golden covers the event hash and the set digest. ✓

Nit: computeBundleSetDigest should refuse an empty jobId or kernelId, and a settlementUnitId that isn't 0x plus 64 lowercase hex, before it hashes. A malformed subject should not yield a digest that looks valid.

Context (#3543, #3426): at /settle the oracle judges the one bundle the committed package names. The v1 rule is one kernel-signed bundle per settlement unit, so at settlement the set has one member. The set digest stays admission's pin, which the gateway pre-check computes.

🤖 Generated with Claude Code

LamaSu added a commit that referenced this pull request Sep 29, 2026
…; profile checks run on a one-pass copy

Round 2 answer to astra's DO-NOT-SHIP on 2df436b (pack 40; triage bus #3687).
#336 is now stacked on #341 (LO-EV-9, 799cea1) and #338 round 3 (6cea24d).

#52 (40-1..40-3): an entry's signature covers {capturedAt, rawContent,
source}, not its predecessor, position or run, so entries alone could not show
a chain was the kernel's. makeExecutionLogVerifier now takes the kernel-signed
BUNDLE as its instance and the subject from ctx.subject (the job record). It:
- verifies the bundle signature: the registered signer (deps.expectedSigner),
  ed25519, a 64-byte value, and deps.verifyBundleSignature;
- runs LO-EV-9 subject binding;
- extracts exactly one linear chain from GENESIS from the bound events (one
  payload.entries carrier, or one log_hash_chain_entry per entry, linked);
- pins every entry signature to the same signer and algorithm;
- requires capturedAt to be non-decreasing (evidence ruling, bus #3553);
- runs verifyLogChain.
Truncation, reordering with rewritten links, cross-run splicing, forks, gaps
and a second log are rejected. The factory throws on an invalid minEntries or
a missing expectedSigner. The entry-hash formula is unchanged. The oracle has
been told of the contract change (bus #3691).

Profile (40-5..40-8): calibration procedureId and validityWindowSeconds are
invalid unless calibration.required; sparse arrays fail; profileGoverns never
throws and returns the deep-frozen snapshot it validated and digested (plus a
code). Every check runs on util/plain-data.ts plainDataCopy, a one-pass copy
that reads each property exactly once and refuses anything JSON cannot carry
(canonicalize reads each property twice). There is a real key-reordering test.

Vector (40-9, 40-10): the emitter exports buildLose3Envelope, and a test
regenerates the fixture byte-for-byte. negatives.failureBearingBundle is the
positive bundle plus execution_failed, hashed and signed; its integrity
verifies, so a consumer refuses it by outcome policy (#363). The positive
bundle is unchanged (b80f569c...).

Tests: spec 43 files, 968 passed; tsc clean; the test files type-check.
Mutation check: 23 single-rule mutations of the new code, all caught.
LamaSu added a commit that referenced this pull request Sep 29, 2026
…, #336 round 2)

# Conflicts:
#	packages/spec/src/evidence/measurement-profile.ts
LamaSu added a commit that referenced this pull request Sep 29, 2026
LamaSu and others added 10 commits September 29, 2026 15:01
…titles and descriptions, and a new branch's history (N44 r4)

Astra r3 on 976c8c3, weakest link: the pre-merge scan checked the pull
request's head and history, but the commit that lands on master (a squash
or merge commit, whose message is written at merge time and can carry
"[skip ci]") was never scanned before master moved.
- secret-scan.yml runs on merge_group: it checks out the queue's base,
  fetches the queue's commit as objects and scans its tree and everything it
  adds, message included. This holds once the operator enables the merge
  queue and requires the check (a repository setting).
- pull_request_target also runs on edited, and scans the title and
  description, passed only through the environment.
- A push that creates the branch scans its whole history (--history), in
  secret-scan.yml and ci.yml; before, only its final tree.
Scanner: --history <rev>; --text <label> scans stdin; a range or history
that ends at an annotated tag scans the tag object and its message (the
header claimed tags, but endpoints were peeled to commits); an unknown
option, stray argument, repeated option or missing value exits 2 instead
of falling back to a clean index scan.
Tests: secret-scan-workflow.test.mjs (6 structure checks) and 5 scanner
tests; 10 of these fail on 976c8c3. CI runs all three test files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…; no child diagnostics or control bytes reach the terminal (N44 r4)

Astra r3 on 976c8c3, finding 3 (blocking):
- Verify passed on an empty body, two JSON documents or trailing bytes,
  because only the exact string "invalid" was rejected. It now needs HTTP
  200 and exactly one JSON object whose result.verified is a boolean;
  true and false both pass.
- The E2E flow ignored HTTP status, took the string "true" for valid,
  accepted a body followed by a transport error, and never failed on the
  type, kernel or setup-status steps. Every counted step now needs
  transport success, the route's success status (provision 201, the rest
  200) and one JSON object of the expected shape; validate needs the
  boolean valid: true. Integration status is informational only.
- mktemp and rm could print a key-bearing TMPDIR path. Requests no longer
  use a temporary file (http_request reads the status from -w), and curl's
  messages are discarded. say() strips control bytes, so an escape
  sequence or carriage return in a response never reaches the terminal.
Tests: smoke-digital-verifier.test.mjs runs the script hermetically with
a stub curl and gh: 21 tests, of which 16 fail on 976c8c3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…says it did not (N59, ADK item 8)

The setup test job claimed to verify the operator's pipeline, but on master
it fell back to `kernel_dev_001`, ran on the gateway's in-process mock
regardless of kernelId, and for a deviceless kernel wrote a self-attested
bundle it reported as "completed". So an onboarding agent got a green
test-job that never touched its device.

Now the route is owner-gated and honest:
- kernelId is required (no kernel_dev_001 fallback); an unregistered kernel
  is 404.
- The caller must be the kernel's recorded operator (apiGate's principal);
  an unowned placeholder address never matches. 403 otherwise.
- A deviceId on that kernel is required; a device on another kernel is 404.
- The gateway runs the job only if a real adapter for that device is loaded
  in its runtime (it tries a DB refresh first); otherwise 409
  device_not_runnable_here, telling the operator to run it on their own node.
  There is no self-attest fallback, so a deviceless kernel is never a pass.
- The reply carries ran/passed: `ran` is true only when an adapter executed
  here, `passed` only when that run completed. Verifying evidence against the
  kernel's registered key (D4a, #428) will tighten `passed` when it lands.

KernelService gains `kernelId` and `hasRunner(deviceId)` accessors.

Tests: setup.test.ts test-job cases rewritten — kernelId required, owner
gate (stranger + anonymous 403), device-on-kernel, the not-runnable refusal
(no self-attest, submitJob not called), a passing run and a non-completing
run. Gateway setup suite 41/41, tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The consumers acked this on the bus: readmodels (#3623), adk (#3785) and
refvertical (#3571, #3792).

- A2: OpportunityDTO.capabilityContractDigest pins the exact CSD revision.
  It is REQUIRED on a funded funded_offer, because the accepted plan pins
  it; optional on kit_build_request; forbidden on demand_aggregate.
- A3: evidence.requiredEventClasses: string[] becomes
  evidence.requiredPrimitives: CsdEvidencePrimitiveRef[], the same grammar
  as a CSD's evidence refs and setup's EmitterDecl emits[]. Each id must be
  active in EVIDENCE_PRIMITIVES, so the ADK's provenance planner compares
  supply against demand with no mapping table.
- A4: the manifest header states two conventions. The provenance-recipe
  mediaType is application/vnd.pcc.provenance-recipe+json;v=1, and
  compatibility.interfaces uses the kernel's AdapterType names, never
  "mock". There is no shape change.
- A5: a demand_aggregate carries releasePeriod (YYYY-MM), required there
  and forbidden elsewhere. asOf stays the READ time for every kind
  (readmodels' counter), so it never says when an intent happened (#365 F4).
- A6: artifact names must be safe relative POSIX paths. Before, the schema
  accepted '../../etc/passwd', a path traversal for any installer that
  writes artifacts by name. Refvertical's real R46 kit already complies,
  so its kitDigest is unchanged.
- One CSD_CAPABILITY_URL_PATTERN is now shared by the kit manifest and
  OpportunityDTO.

Golden kit digests are unchanged (A6 only validates). kits-contracts
passes 26/26 (5 new). With the source changes reverted, 8 fail. The full
spec suite passes 823/823 and tsc is clean.

A1, the OperatorBindingDTO part, follows separately: operator-ux, its last
acker, is paused (steward #4029).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT8WYWkfig4KNcxtMqvMuj
OperatorBindingDTO capability types become CSD urls (adk's ask (a) in
#3152, with A1b acked in #3785).
- bindings[].capabilityType and executionAuthority.canClaimCapabilityTypes
  must match CSD_CAPABILITY_URL_PATTERN, the same pattern as OpportunityDTO
  and the kit manifest. A Work Inbox or the ADK's match compares them
  directly, so a legacy '3d-printing' can no longer silently match nothing.
- NEW unmappedCapacity: {kind, id, legacyType}[] (required, may be empty)
  lists capacity whose legacy type resolves to no CSD. It is never
  claimable, and it shows the operator why that capacity matches nothing.

operator-ux acked A1 (#3997) and asked that claim rights stay derived only
from mapped bindings. The superRefine already does that, and the test
"unmapped capacity can never become claimable" pins it. Steward #4111:
land it normally with A2-A6.

Tests: kits-contracts passes 30/30 (4 new). Against the old
operator-binding.ts, 2 of the 4 fail (the CSD-url binding and the required
unmappedCapacity). The other two are defense in depth: the old schema
already refused them, by claim binding and by strictness. The spec suite
passes 827/827 and tsc is clean.

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

KIT_ARTIFACT_ROLES gains "intake-schema" (the filled human-intake record
schema, ADK-track item 6) and "safety-envelope" (sensors' R8 operational
envelope). A device kit can then ship the onboarding artifacts the ADK
produces. The change is additive: every existing manifest and golden
digest is unchanged, and an unknown role is still refused.

adk acked as a consumer (#4099). refvertical, the other manifest producer,
is asked on the bus before this is pushed.

Tests: kits-contracts passes 32/32 (2 new), the spec suite 829/829, and
tsc is clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FT8WYWkfig4KNcxtMqvMuj
…es a __proto__-carrying profile (astra pack 74)

Both findings were reproduced at e4478ed before the fix.

- HIGH: a JSON-parsed profile whose measurement.sampling held only
  {"__proto__": {"minSamples": 1}} carried the same digest as one
  committing minSamples 2. Admission then ADMITTED one qualifying
  sample against the commitment that asked for two. The root cause was
  plainDataCopy, fixed in #336 (73ac0bb, merged here in e6dd0a8). The
  admission-level regression test now rejects it as profile-invalid,
  and computeMeasurementProfileDigest refuses the profile outright.
- MEDIUM: the top-level reads of the input (the pin, callbacks,
  subject, bundles, digest and profile) sat outside any catch. A getter
  throwing there rejected the promise instead of returning a decision.
  Every read of input now happens once inside one guard, and a throw
  resolves to reject with the new code input-unreadable.

Tests: 2 new; spec 1102/1102. Removing the guard fails its test.
…ir read models (N68)

Astra r1 on #448 (610607c), CRITICAL; reproduced first. POST /api/query
answered its kernel_health, network_status, operator_stats and
find_capability intents with the stored shop_kernels and capabilities rows.
Any self-provisioned key read every kernel's exact coordinates and street
address. The four new tests failed at 610607c with the canary's exact
latitude in each answer.

Those intents now read through the kernel and capability facades, whose
populators project a site's location: coarse unless its operator opted in,
and no street address. The answer text is filled from the same rows. The
job intents are unchanged here; they belong to the job-read gate (F3, #403).

agent: pcc-readmodels (c255d7dc)
…ry included

Astra r1 on #448 asked for /api/query in the real-gateway privacy sweep. The
sweep that reproduced N68 (uncommitted until now) boots createGateway. It
registers a canary kernel and capability at an exact point and street
address, then calls every registered GET route (anonymously and with a
stranger's key) plus the POST reads that answer with kernels or
capabilities: /ask, pcc-discover and the four /api/query intents.

At this head: 0 leaking responses in 1,475 calls per pass. With the previous
nl-query.ts: 4 keyed leaks, one per /api/query intent, and the test fails.

agent: pcc-readmodels (c255d7dc)
LamaSu added 4 commits October 3, 2026 20:24
fix(spec): economics refuses, never throws, on a number the canonical form cannot write (D5, #359)
ci(pcc-node): run the whole pcc-node suite on Python 3.9 and 3.12, failing on skips (N117)
…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
LamaSu added a commit that referenced this pull request Oct 4, 2026
LamaSu added 2 commits October 3, 2026 23:38
…e leg names the verified signer (astra pack 271)

Pack 271's HIGH, reproduced at cdff055. The signature leg named a trust
domain bundle by bundle, with nothing binding it to the verified signer or
to one registry snapshot. One key (SIGNER_A) signs the printer's bundle and
the camera's; the leg genuinely verifies both, then names A first and B
second. Admission admitted at inspected_output; with honest answers it
rejects (device_reported).

The fix combines astra's two options:
- signerTrustDomains, a new required input, is ONE pinned registry
  snapshot as data: each registered signer's id (as kernel bundles declare
  it, kernelSignature.signer, 0x + 40 lowercase hex) and the operator
  principal that owns its key, or null. Admission looks each bundle's
  domain up there itself, so a signer has exactly one domain. A malformed
  row, or a signer listed twice (even with one domain), is
  input-unreadable.
- verifyBundleSignature now answers the verified signer's id (or false).
  It must equal the bundle's declared signer, and the snapshot must hold
  that signer; otherwise unauthenticated-bundle.
- A null domain still authenticates, and still gives no independence.
- The caller contract now also persists the executor domains and the
  signer snapshot with the decision.

Pack 271's LOW, reproduced: the test helper admit() listed
executorTrustDomains twice. Removed.

Tests:
- the leg's contract: true, a domain record, a malformed id, or a signer
  other than the declared one fails it;
- one key signing both bundles gets the executor's one domain;
- a flipping leg is refused;
- a null domain;
- a signer missing from the snapshot;
- every malformed or duplicate snapshot row;
- the LO-SE-3 fixture's signer maps to no operator.

Admission tests 119/119; spec 2548/2548; tsc clean. Mutations: 11 of 12
killed. The survivor, the leg answer's format check, is equivalent: a
malformed answer either differs from the declared signer or is missing
from the snapshot, which holds only checked ids.
…a pack 271 LOW)

The tests are not type-checked (tsconfig excludes them) and there is no
linter, so TypeScript's duplicate-property error never ran on them; pack
271 found a repeated executorTrustDomains in admission's test helper. This
test walks every test file's syntax tree with the TypeScript compiler and
lists each repeated key. A self-test covers quoted and bare keys, numeric
keys, spreads, accessor pairs and computed keys. Run on cdff055's
admission test, it reports exactly the duplicate pack 271 found (line 233).
LamaSu added a commit that referenced this pull request Oct 4, 2026
…leg names the verified signer) into #384, which follows #363 (a plain merge)
LamaSu added a commit that referenced this pull request Oct 4, 2026
…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 a commit that referenced this pull request Oct 4, 2026
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 2 commits October 4, 2026 00:08
… against ONE pinned key registry (astra pack 275)

Pack 275's HIGH, reproduced at 2ab6e41. The signer snapshot bound 20-byte
signer LABELS, not keys, and the callback leg verified against its own
registry. In the reproduction, one Ed25519 key signs both bundles, declaring
label A on the printer's and label B on the camera's. A leg that verified
that key for both and returned each label was handed a snapshot of
{A -> OPERATOR_A, B -> OPERATOR_B}, and admission admitted at
inspected_output.

The fix takes the reviewer's structural option: verification against the
pinned rows.
- registryKeys (new, required) is ONE pinned registry snapshot as data.
  Each row is an Ed25519 public key, the raw 32 bytes as 0x + 64
  LOWERCASE hex, one spelling per key, with the operator that owns it, or
  null.
- Rows are unique by key and by signer id (0x + the key's first 40 hex,
  as kernels declare it). A key listed twice, two keys whose signer ids
  collide, or a malformed row is input-unreadable.
- pinnedRegistryDigest (new, required) is computeRegistryDigest over the
  rows: a tagged sha256 of {REGISTRY_SNAPSHOT_DOMAIN, rows sorted by key}.
  A malformed pin is registry-pin-invalid. Rows that do not digest to it,
  a key rotated or reassigned after the pin, are registry-mismatch. Both
  are new codes.
- SIGNATURE runs here, through Web Crypto Ed25519. The bundle's declared
  signer must name one key of the registry, the algorithm must be ed25519,
  and the signature over signingPreimage(bundleHash) must verify under
  THAT key. The trust domain is that key's row.
- verifyBundleSignature, BundleSignatureAnswer and signerTrustDomains are
  gone. Verification and domain come from the same pinned row, so no
  callback registry can differ.
- The caller contract now pins the registry with the evidence set.

Tests, under a world whose signer ids derive from the real keys:
- the reproduction: A's key declaring B's id does not verify under B's key;
- a bundle signed by another registered key;
- an unregistered id, another algorithm, an unparseable signature, and a
  signature over another digest;
- one key with one id has one domain;
- null domains;
- a rotation after the pin, a malformed pin, and row order not committed;
- every malformed registry, including a duplicate key, an uppercase
  spelling, colliding signer ids and an accessor row;
- the registry walked for code;
- computeRegistryDigest;
- the LO-SE-3 fixture under its own key;
- the set-mismatch test without its callback counter.

Admission 123/123; spec 2551/2551; tsc clean. Mutations 19/19, with the
max-over-occurrences flip and the lint check included.
…accessor kinds (astra pack 275 LOW)

Pack 275's LOW, reproduced: { executorTrustDomains: [],
["executorTrustDomains"]: [...] } in a test file passed the check.

- A computed key whose value is a literal (a string, a number, or a
  template without substitutions) is now that value.
- A getter and a setter of one name may share it, once each. A second get
  or set, or an accessor beside a data member, is a duplicate.
- Only a non-literal computed key is skipped.
- The self-test covers each case: the reviewer's computed duplicate, a
  computed template against a bare key, a computed number against a quoted
  one, two getters, get beside data, set beside data, a third accessor,
  and the skipped non-literal keys.
LamaSu added 2 commits October 4, 2026 00:50
…mes (astra pack 281)

Pack 281's HIGH, reproduced at eb142ab: any key in the pinned registry could
authenticate evidence for any kernel or job. B, registered, signed the printer's
execution events for KERNEL, declared B, and admission returned admit at
device_reported. The steward completed the property (DECISIONS 00:26); this
implements it:
- a registered row names its key, its operator and its GRANTS:
  { role: executor | witness, kernelId, jobId? }. Registry membership alone
  authorizes nothing;
- after signature and subject binding, the verifying key must hold a grant naming
  the subject. An executor signs any event of it; a witness, inspections only.
  Otherwise unauthorized-signer;
- a session key counts only through a delegation rooted in a registered row of
  the same snapshot ({ publicKey, delegatedBy, authorization }), checked when the
  registry is read: the LO-EV-1 wire form and preimage, this row's key, the
  root's signature, evidence_submit, a named job, issuedAt <= expiresAt. It holds
  its root's domain and grants, only for the jobs its scope names, and only for
  events whose own timestamps fall inside its window;
- the digest moves to PCC:evidence-registry-snapshot:v2 and commits the grants
  (sorted) and each delegation as received.
The header names what callers build the rows from: shop_kernels'
signing_key_public_key and operator_address. No record assigns a witness yet,
and wall-clock expiry, revocation and maxSignatures stay with the submission
path and the pinning party.

Tests: astra's reproduction refused, an unauthorized registered key refused, the
authorized independent inspector admitted, the executor's and witness's subject
and role, session keys (scope, root subject, window, delegation refusals,
commitment), and the digest's definition. Admission 132/132, spec 2561/2561,
tsc clean; 38 of 38 mutations killed.
…s them (astra pack 281 LOW)

Pack 281's LOW, reproduced at eb142ab: { [1n]: 1, "1": 2 } was not reported,
because a BigInt computed key was skipped. Keys are now compared after
ToPropertyKey: a BigInt is parsed (TypeScript leaves 0x10n unnormalized), signs
are applied ([-0] is 0; [+1n] throws at runtime and is skipped), true, false and
null are keys, and parentheses and as / satisfies / <T> / ! are unwrapped. Only a
computed key whose value is not a constant primitive is skipped. The self-test
covers each kind.
…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 4, 2026
A plain merge-up of #384's base, #363, after its review completed (packs 285
and 287, SHIP). It brings signer grants, delegated session keys, executor
non-independence and no-witness-authorized. #384's own files are unchanged.
LamaSu added a commit that referenced this pull request Oct 4, 2026
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.
LamaSu added a commit that referenced this pull request Oct 4, 2026
A plain merge-up of #520's base, #384, which now carries #363's round 13.

This branch had an error being deployed

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