Skip to content

fix(reward)!: refuse the certificate claims this version cannot check - #40

Merged
abrichr merged 1 commit into
mainfrom
claude/certificate-narrowing
Sep 3, 2026
Merged

fix(reward)!: refuse the certificate claims this version cannot check#40
abrichr merged 1 commit into
mainfrom
claude/certificate-narrowing

Conversation

@abrichr

@abrichr abrichr commented Sep 3, 2026

Copy link
Copy Markdown
Member

HOLD. Do not merge without the founder naming the version. A merge to main here runs semantic-release and publishes to PyPI in the same action, and this change is breaking. The register carries it as B66. Sequencing against the openadapt-flow reward worker work is part of that call.

What this refuses

A reward certificate was trusted on its shape. Four properties it asserted were never checked, anywhere, by anything. Each of the four reproduced against published openadapt-evals 0.97.0 and openadapt-types 0.17.0, with no worker, no oracle, and no read:

Reproduction Before After
issue_certificate(issuer="organization", calibration_scope="production") receipt reads certified: true, calibration_scope: production, production_certified: true production is not a member of RewardCalibrationScopeV1; organization is not a member of RewardCertificateIssuerV1
certificate with measured epsilon 0.248885 against a contract demanding 0.05 satisfies() returns False and the receipt still reads certified score() returns certified false and names the shortfall
certificate whose reward_contract_digest is another contract's certified score() refuses it
grep -i revok across the reward code in types, evals and flow no matches, and the certificate docstring described revocation as a mechanism the issuer checks the docstring says expiry is the only withdrawal mechanism

The production scope was reachable because the validator refused exactly one pair, self_signed + production, and issuer is an unauthenticated string. RewardCertificateV1.satisfies had one caller in the repository and it was a test.

The shape

Narrowing, not cryptography. There is no key registry here and this change does not build one. It makes the type refuse what nothing can verify.

  • RewardCalibrationScopeV1 keeps SYNTHETIC. RewardCertificateIssuerV1 keeps SELF_SIGNED. Both stay enums rather than becoming Literal, so the import surface is unchanged and adding a member back once a registry exists is a widening, not a break.
  • RewardEvidenceReceiptV1.production_certified is gone. With one scope it could only return False, and the name promised a distinction the type cannot draw.
  • score() takes contract as a required keyword and drops scoring. It reads the scalar from contract.scoring and certifies only a certificate that names this contract by digest and clears contract.certificate_policy. Requiring the contract is what stops a caller certifying against a policy nobody read; an optional argument would have left the old path open.
  • RewardScoreV1 gains certification_refusals, so a false certified says why instead of being a bare boolean.
  • RewardCertificateV1.unmet(policy) lists each shortfall and satisfies is now not unmet(...). It is live on the certification path.
  • RewardEvidenceReceiptV1.certification_refusals(contract, certificate) lets a reader who holds both recheck a receipt. The receipt carries digests, so it cannot check its own flag during validation, and this is the honest way to say so.
  • The docstrings drop the revocation sentence and state that signature is checked for encoding and length only.

What breaks

This is breaking. major_on_zero = false, so semantic-release reads the ! as a minor bump to 0.18.0.

  • score() callers. The reward worker in openadapt-flow calls it with scoring=self.contract.scoring and already holds the contract, so its fix is one line.
  • Readers of production_certified. Two assertions in openadapt-flow's tests/test_reward_worker.py.
  • Anything constructing a production-scope certificate. Nothing does, in any of the three repositories or in the Prime Intellect environment.

openadapt-flow pins openadapt-types>=0.17.0,<0.18.0, so this release does not reach it until someone widens that bound. openadapt-evals pinned an unbounded floor; a companion PR there bounds it to <0.18.0 for the same reason.

Hold

Do not merge until the founder names the version. Merging this repository's main publishes to PyPI, and the narrowing is breaking, so the release level is his call and not this session's. Merging also wants sequencing against the openadapt-flow worker work already in flight.

Opened by an agent session, not the founder.

🤖 Generated with Claude Code

A reward certificate was trusted on its shape. Four properties it asserted
were never checked anywhere:

* `calibration_scope: production`. The validator refused only the pair
  `self_signed` + `production`, so `issuer="organization"` bought a receipt
  reading `certified: true, calibration_scope: production,
  production_certified: true` with no worker, no oracle, and no read.
* `issuer: organization`. There is no issuer key registry, so nothing can
  resolve `issuer_key_id` to a key anyone trusts.
* The contract's own `certificate_policy`. `RewardCertificateV1.satisfies`
  had exactly one caller, a test. A certificate measured at epsilon 0.248885
  against a contract demanding 0.05 still produced `certified`.
* Revocation. `grep -i revok` over the reward code in types, evals, and flow
  returns nothing, and the certificate docstring described revocation as an
  existing mechanism checked by the issuer.

The narrowing:

* `RewardCalibrationScopeV1` keeps `SYNTHETIC` only, and
  `RewardCertificateIssuerV1` keeps `SELF_SIGNED` only. Both stay enums, so
  adding a member back once a registry exists is not a breaking change.
* `RewardEvidenceReceiptV1.production_certified` is gone. With one scope it
  could only ever return False, and its name promised a distinction the type
  cannot draw.
* `score()` takes the contract as a required keyword and drops `scoring`. It
  reads the scalar from `contract.scoring` and certifies only a certificate
  that names this contract by digest and clears
  `contract.certificate_policy`. `RewardScoreV1` gains
  `certification_refusals`, so a false `certified` says why.
* `RewardCertificateV1.unmet(policy)` lists each shortfall; `satisfies` is
  now `not unmet(...)` and is live code on the certification path.
* `RewardEvidenceReceiptV1.certification_refusals(contract, certificate)`
  lets a reader who holds both recheck a receipt's flag. The receipt carries
  digests, so it cannot check itself during validation.
* The docstrings drop the revocation sentence and say plainly that
  `signature` is checked for encoding and length only.

Breaking for `score()` callers and for anything that reads
`production_certified` or constructs a production-scope certificate. Nothing
outside tests constructs one today.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrichr
abrichr force-pushed the claude/certificate-narrowing branch from 17c6457 to 250ecd1 Compare September 3, 2026 21:33
@abrichr
abrichr merged commit b7b40f3 into main Sep 3, 2026
1 check passed
@abrichr
abrichr deleted the claude/certificate-narrowing branch September 3, 2026 21:37
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