Skip to content

feat(spec): committed programs per CSD tier and the pre-funding assurance gate (must-close 6) - #349

Draft
LamaSu wants to merge 11 commits into
feat/evidence-levelsfrom
feat/committed-program-assurance
Draft

LamaSu wants to merge 11 commits into
feat/evidence-levelsfrom
feat/committed-program-assurance

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Update 2026-09-24 @074e3231: the gate refuses tiers that cannot be verified end to end (board N19)


What this closes (technical pack §3, must-close 6; the resolver half of LO-CO-3)

Non-zero assurance must bind the exact committed program for its CSD tier before funding, and that program must release on what the tier promises. Today the v3 composition commitment (#327) pins a verificationProgramHash supplied by whoever builds the plan. Nothing resolves it from the CSD tier or checks the program against the tier.

Contents

  • Committed-stage shape. This is the shape the oracle hashes and runs: string predicates over raw event types. It is hashed with computeVerificationProgramHash's preimage.

  • checkCommittedProgramForTier(program, csdTier) requires:

    The v3 program that released on printer_job_verified fails three of these. The function also returns unenforcedTierEvidence.

  • resolveAcceptedProgram(csd, tierKey) gives one committed program per (CSD, non-zero tier). Finding none, or two, fails closed. Only print-and-mail tier2 has one; tier1 and tier3 have no committed program.

  • assertAcceptedProgramForTier is the pre-funding gate:

    • the committed hash must equal the resolved program's hash exactly, so a tier upgrade or downgrade without its own program is refused;
    • the registry entry must hash to its program;
    • the program must pass the tier check;
    • tier 0 takes no program.

Finding for evidence, oracle and composition

Against the real CSD, tier2 promises printer_log_captured, photo_captured and photo_anti_spoof_check, but the v4 program requires none of them. They are enforced only if the oracle verifies those primitives itself, which public PCC can't see. The question has been put to the oracle. Separately, the public vocabulary marks every tier-1+ print-and-mail primitive except receipt.kernel_signed as verifierStatus: "stub".

Not in this PR

  • Wiring the gate into composition commitment and funding. That needs a named call site in composition, escrow or gateway, and the routing question is with the steward.
  • Moving the registry into the CSD schema, which would change CSD identity hashes.

Tests

  • 23 new; spec 840/840; tsc clean.
  • Nine mutants, each turning a test red: exact hash, supporting evidence, no-level events, not-simulated, tier-zero program, registry hash, tier check at the gate, ambiguous registry, failure pairing.

Stacked on #345.

🤖 Generated with Claude Code

https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS

…ance gate

Must-close 6: non-zero assurance binds the exact committed program for its
CSD tier before funding, and that program must release on what the tier
promises. The v3 composition commitment (#327) pins a verificationProgramHash
supplied by whoever builds the plan; nothing resolved it from the CSD tier or
checked the program against the tier. This is the resolver half of
composition's LO-CO-3.

- The committed-stage shape the oracle hashes and runs (string predicates
  over raw event types), hashed through computeVerificationProgramHash's
  preimage. The document-print-and-mail v4 programs are ported from the
  evidence golden on #270 (not merged) and reproduce their pins byte-exact
  (independence 0xd229c8da..., honest-asymmetry 0x6e00cad1...); the same
  formula reproduces the oracle-confirmed v3 pin 0xe1cac435....
- checkCommittedProgramForTier: every positive stage names an event the tier
  binds through a non-supporting primitive and that proves an outcome level;
  some stage proves device_reported or stronger; not-simulated is present;
  execution_completed is paired with execution_failed absent. The v3 program,
  which released on printer_job_verified, fails three of these. Tier evidence
  no stage requires is returned as unenforcedTierEvidence.
- resolveAcceptedProgram: one committed program per (CSD, non-zero tier) from
  COMMITTED_PROGRAM_REGISTRY; none or two fails closed. Only print-and-mail
  tier2 has one today; tier1 and tier3 have no committed program.
- assertAcceptedProgramForTier, the gate: the committed hash must equal the
  resolved program's hash exactly (tier upgrade or downgrade without its own
  program is refused), the registry entry must hash to its program, and the
  program must pass the tier check. Tier 0 takes no program.

Finding: against the real CSD, tier2 promises printer_log_captured,
photo_captured and photo_anti_spoof_check, and the v4 program requires none
of them. They are enforced only if the oracle verifies those primitives
itself; that is not visible from public PCC.

Tests: 23 new; spec 840/840; tsc clean. Nine mutants (exact hash, supporting
evidence, no-level events, not-simulated, tier-zero program, registry hash,
tier check at the gate, ambiguous registry, failure pairing) each turn a test
red.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
… be verified end to end (N19)

The oracle reads a funded tier's evidence two ways. One is event presence
under the CSD's ladder (#383). The other is a registered verifier per listed
primitive, and a stub verifier fails closed. A tier whose primitives nobody
can verify was still fundable here, as long as its committed program fit.
That sold assurance the oracle cannot check, and the tier evidence no stage
enforces weakened the promise without anyone seeing it.

assertAcceptedProgramForTier now takes the funded CSD's whole evidence map
in place of one tier. It refuses with tier-not-eligible, and the reasons,
unless tiers 0..T are eligible under computeCsdEligibility's oracle-enforcing
mode (requireImplementedVerifier). A funded tier absent from the CSD is
tier-not-in-csd. The program check runs against the CSD's own tier. The
primitive index is injectable, for tests and for when verifiers go live.

As authored, document-print-and-mail is eligible only at tier 0:
- tiers 1-3 lack ident.registered_key, which receipt.kernel_signed and
  machine.execution_log depend on;
- tier 2 has no Family-G primitive;
- six of its verifiers are stubs.
So the tier2 program (0xd229c8da) stays pinned but cannot be funded until
the CSD and its verifiers are fixed. This is N19's "or those tiers made
unfundable", derived instead of hand-listed.

API change for composition (#351/#356): pass
evidence: csd.evidence in place of tier: csd.evidence[tierKey].

Tests: committed-program.test.ts 31/31, including 8 new eligibility cases.
The existing program-leg negatives now fund a fixed CSD with live verifiers.
Spec suite green; tsc clean. 7 mutants killed: leg removed, report-only,
reasons scope, off-by-one, tier-in-CSD, missing-tier reason, caller index.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117ows6894R3n6YQXBCRahS
LamaSu and others added 9 commits September 28, 2026 20:35
…ust seal

Composition's accepted deal pins each registry a funded program's
verifiers check keys against (bus #3391, registry-pins-note.md). Its
compiler needs the (primitiveId, registryId) set server-side, so the gate
that accepts the (CSD, tier) now returns it: assertAcceptedProgramForTier
succeeds with { ok: true, registryPins }.

requiredRegistryPins(evidence, k) walks the funded CSD's tiers 0..k,
the same server-resolved map the gate checks (committed programs hold
stage predicates, not primitives). It takes every registry-backed
primitive (today ident.registered_key) with the registry its params
name. Pins are deduped on (primitiveId, registryId) and sorted by
primitiveId then registryId in code-unit order. A registry-backed
primitive that names no registry cannot be pinned, so the gate refuses
it as registry-not-named, before funding rather than at /settle.

Today's print-and-mail programs name no registry, so they return
registryPins []. The test fixture's fixed tiers now name
pcc.registry.kernel-signing-keys.v1 on ident.registered_key.
committed-program 33 (+2), spec 850, tsc clean. Five mutants are each
killed: a missing registry tolerated, no dedupe, no sort, only tier k
walked, and non-registry primitives counted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Composition's accepted-deal v3 seals each registry pin under the same id
rule as its other ids (ID_PATTERN: 1 to 128 printable ASCII characters).
requiredRegistryPins accepted any non-empty string, so a CSD tier naming,
for example, "pcc.registry.<U+212A>.v1" (the Kelvin sign) passed the gate
and would only fail later, at compile. It is now refused at the gate as
registry-not-named, before funding (evidence's v3 gate-pin ruling, #4568).

Reproduced first: the new test failed at 48a9587 (1 failed | 33 passed:
the Kelvin-sign id was accepted). Now 34/34; spec 851/851; tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ontradictions and the gateway-stamp rule

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…try or primitive-index override (E8 F4)

E8 F4 (MEDIUM): assertAcceptedProgramForTier accepted a registry and a primitive
index as 2nd and 3rd arguments, and the N19 leg only reads verifierStatus. A
caller of the public export could pass an index that marks every verifier "live"
and fund print-and-mail tier2. Reproduced at c56b0d7 through both the evidence
index and the package root.

assertAcceptedProgramForTier(input) now takes one argument and always resolves
against COMMITTED_PROGRAM_REGISTRY and the built-in primitive vocabulary. The
override-taking form is assertAcceptedProgramForTierWith(input, registry,
options): exported from committed-program.ts for tests, and not from
evidence/index.ts, which now lists its committed-program exports explicitly
instead of using export *. The package.json exports map exposes only ".",
"./schemas", "./identity" and "./tool-manifests", so a consumer cannot
deep-import the module; the doc says so.

Tests: the production gate has one parameter and ignores a smuggled registry and
a smuggled all-live index; neither the evidence index nor the package root
exposes the override form; the explicit export list loses no other export of the
module. Existing gate tests move to the test-only form. The pinned programs,
their hashes and COMMITTED_PROGRAM_REGISTRY are untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…so the checks and the pins cannot see different CSDs (E8 F3)

E8 F3 (HIGH): the gate read input.evidence once for the tier lookup, once for
eligibility and once for the pins. A getter or proxy could answer three times
with three maps: one that passes the program check, one the eligibility lint
accepts, and a last one with no registry-backed primitive. Reproduced at
c56b0d7 with such a getter: 3 reads, and the gate returned
{ok:true, registryPins:[]} for a CSD whose eligible map names the kernel
registry, so the deal would have sealed no registry pin.

The gate now reads input.csd, input.tierKey, input.committedProgramHash and
input.evidence exactly once each, at entry. evidence is materialized with
JSON.parse(JSON.stringify(evidence)) inside try/catch and must come out as a
plain non-null, non-array object. Tier lookup, eligibility, the program check
and pin extraction all use that one snapshot. Input that is not plain JSON data
(a throwing getter, a cycle, a BigInt, a non-object evidence, a non-object
input, a csd or tierKey that is not a string) returns the new refusal code
invalid-input instead of throwing. csd and tierKey must be primitive strings
because an object with a stateful toString would be a second channel for the
same race (it could pass as tier0 on one coercion and tier2 on the next).

Tests: all four fields are read exactly once on every path through the gate
(funded, tier0, no program, wrong hash); the three-map getter is evaluated on
its first answer only and equals the first map evaluated alone, pins included;
a nested getter, a proxy and a toJSON are each consulted once; the table of
non-JSON evidence and malformed inputs is refused as invalid-input; null-
prototype and class-instance evidence, which are plain data, still fund.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…fore it funds tier k (E8 F2)

E8 F2 (HIGH): computeCsdEligibility starts eligibleTier at 0 even when tier0 is
absent (original design in 1c475e9: tier 0 is the always-listable floor), then
advances through tier1 and tier2. The gate trusted that result, so a CSD with no
tier0 could be funded at tier2. Reproduced at c56b0d7: delete tiers.tier0, gate
tier2, and the gate returned ok:true with the kernel registry pin. The same
happened with tier0 present but null, an array, a string or a number.

In the gate, after the program hash check and before eligibility, require the
exact keys tier0 ... tier<k> as own keys of the snapshot, each a non-null,
non-array object. A missing key returns {ok:false, code:"tier-not-in-csd",
reasons:[...]}, one reason per missing tier, in order. The funded key must also
be exactly tier<k> (a key like tier02, which tierNumber reads as 2, would
otherwise make the program check and the eligibility check look at different
tiers). eligibility.ts is not touched: its other callers are reported in the
triage file.

Tier 0 itself still needs no declaration to be funded. It is the permissionless
floor and a CSD with no evidence block is tier-0 only by the eligibility rule,
so requiring tier0 there would close the on-ramp. This is a decision for the
lane to confirm; a test pins it.

Tests: delete tier0 (the reviewer's repro) with the lint's own eligibleTier as a
positive control; tier0 and tier1 as null, array, string and number; several
missing tiers named in order; an inherited tier0 through Object.prototype is not
declared; a non-exact funded key is refused; only tiers 0..k are required, so
tier3 may be absent. Two existing tests change: a missing tier1 is now
tier-not-in-csd with its reason instead of tier-not-eligible, and the missing
funded tier gains its reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qb6kQhDYRwUDd6AVes3Fwx
…ted deal's digest grammar (E8 F5)

E8 F5 (MEDIUM): the gate lowercased the whole committed hash before comparing, so
HASH.toUpperCase() ("0XD229...") passed the gate, while the accepted deal's
parser (DIGEST_PATTERN in csd/composition-commitment.ts: a literal lowercase 0x
and 64 hex digits) refuses it. Reproduced at c56b0d7. It did not create two
funded commitments, because the deal refuses 0X, but it made pre-funding
acceptance inconsistent with what the deal would accept.

DIGEST_PATTERN is exported, so the gate imports it instead of mirroring it: the
committed hash must match it first, and only then is it compared after
lowercasing the hex digits alone. A hash that does not match the grammar is a
program-hash-mismatch, the same code as any other wrong hash.

Tests: a table of 16 inputs (the pinned hash, uppercase and mixed-case digits,
0X with lowercase and uppercase digits, no prefix, 63 and 65 digits, a non-hex
digit, a fullwidth letter, leading and trailing space, a trailing newline, an
embedded NUL, the empty string, another valid digest) where the gate accepts
exactly when the accepted deal's own validatePlan accepts the hash and
deriveCapabilityContractRoot canonicalizes it to the pinned hash's root; the
reviewer's HASH.toUpperCase() row; non-string hashes, including objects that
coerce to the hash, are a mismatch.

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

E8 F1 (HIGH): the tier check never looked at allowedProvenance. A program whose
"event-present-independent" mail stage allowed only ["operator_self_report"],
registered for tier2 under its own hash, produced no violations and the gate
funded it. The word "independent" was established by a predicate label and an
unchecked string allowlist the program's author controls. Reproduced at c56b0d7
for the reviewer's repro and for seven variants (absent, empty, duplicate,
superset, and stray provenance on an event-present, an event-absent and a
not-simulated stage): every one gave no violation and a funded tier2.

New evidence-owned data: INDEPENDENT_PROVENANCE, a frozen null-prototype map
holding exactly { courier_pickup_confirmed: ["independent_carrier_scan"] }, the
only independent provenance the pinned programs use. A label counts as
independent only if the evidence lane lists it there; the doc says the oracle
must still authenticate the label's source when it evaluates the stage.

checkCommittedProgramForTier now adds:
- independence-not-established (stageId, eventType) unless an
  event-present-independent stage has an allowedProvenance that is a non-empty
  array of distinct strings, each listed in INDEPENDENT_PROVENANCE for the
  stage's event type (own-property lookup; an indexed loop so a sparse array's
  hole is not a label);
- provenance-on-dependent-stage (stageId) for any other predicate that carries
  allowedProvenance, since the field means nothing there.
An unknown predicate, or an independent stage with no usable event type, keeps
its existing single violation. INDEPENDENT_PROVENANCE is exported from
evidence/index.ts.

The pinned data is untouched: PRINT_AND_MAIL_INDEPENDENCE_PROGRAM and
PRINT_AND_MAIL_HONEST_ASYMMETRY_PROGRAM, their hashes (0xd229c8da..., 0x6e00cad1...)
and COMMITTED_PROGRAM_REGISTRY. Both programs still pass the tier check against
the real tier2; a test pins the literal hashes and the registry.

Tests: the reviewer's repro through the check and through the gate (registered
via the test-only registry override); twelve attack variants, each refused with
exactly one violation by the check and by the gate; event-specificity (the
carrier label does not make execution_completed independent); stray provenance
on three predicates, including empty and null, and on the honest-asymmetry
mail stage; an explicit undefined is not stray; violations keep stage order.

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

Found while reviewing fixer-lima's E8 round: tierNumber used /^tier(\d+)$/,
so "tier00" parsed as tier 0. resolveAcceptedProgram then returned the
tier-zero result, and the gate funded it with tier0's pins before its
exact-key check ran. tierNumber now accepts 0 or a decimal without a
leading zero; a non-canonical key finds no committed program and is
refused. Same class as E8 F5 (one accepted spelling per input).

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

This branch has not been deployed

No deployments
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