fix(verify): enforce the profile cutover at verification time - #125
Merged
imran-siddique merged 1 commit intoAug 8, 2026
Merged
Conversation
The v0.2 changelog declares that a v0.2 verifier "requires the new URI and rejects the old one; it does not accept both", and spec/trace-v0.2.md section 2 makes the same requirement normative. verify_record() never read eat_profile: a record carrying the superseded v0.1 identifier, a future version, a foreign tag, or no profile at all verified exactly as a v0.2 record, provided its signature checked out. A valid signature over semantics this build does not implement is not evidence. The profile is now checked first, before any cryptographic work — safe, because the only action taken on the unauthenticated value is refusal, and a record that verifies has had its profile covered by the signature, which spans the whole record. Anything other than the v0.2 identifier raises ValueError; the superseded v0.1 identifier gets a message that says why. TRACE_PROFILE_V0_2 is exported so callers can pin the same constant the verifier accepts; the v0.1 tag is deliberately not exported. Same shape as the revocation fix (agentrust-io#76): an already-merged requirement the reference implementation did not carry out. Five tests, including one that proves the refusal does not depend on cryptographic work; neutralizing the check fails four of them. docs/verification.md step 4 notes the check is now built in. No normative text, schema, or record field changed. Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
Contributor
|
❔ Contributor Check: UNKNOWN
Automated check by AgenTrust Contributor Check. |
imran-siddique
approved these changes
Aug 8, 2026
imran-siddique
left a comment
Member
There was a problem hiding this comment.
Correct and well argued. The profile check belongs before any cryptographic work, and the docstring says why that ordering is safe: the only action taken on the unauthenticated value is refusal, and a record that verifies has had its profile covered by the signature.
Enforces what spec/trace-v0.2.md section 2 and the changelog already declared. Landing this before the LF move.
4 tasks
imran-siddique
pushed a commit
that referenced
this pull request
Aug 9, 2026
…136) `validate_json()` loads `src/agentrust_trace/schema/trace-v0.2.json`; the spec, README and CONTRIBUTING all point a reader at `schema/trace-claim.json`. The two had drifted in three places, so the schema someone reads was not the schema their record is checked against, and nothing failed: both files are individually valid JSON Schema and each test suite exercises only one of them. The visible consequence is `subject`. The root file and `models.py` both accept a DID subject; the packaged copy still required `^spiffe://`, so a subject form the specification permits was rejected by the reference validator. - `subject`: pattern and description resynced to the normative `^(spiffe://|did:)`. - `build_provenance.slsa_level`: description resynced; the packaged copy had lost the sentence explaining Level 0, which is the level `minimum: 0` exists for. - `$id`: the root file still said `trace-v0.1.json` on a schema whose `eat_profile` const is v0.2. Taken from the packaged copy rather than the other way round, since neither file was uniformly right. `tests/test_validate.py` compares the two as parsed JSON on every run, plus a regression test that both subject forms pass `iter_errors`. Deleting either half of the schema fix fails both. Compared parsed rather than byte for byte because the two files differ in line endings by long-standing accident, which changes nothing about how either validates a record. Also moved: the `verify_record()` profile-cutover entry shipped in 0.6.0 but its changelog entry was filed under 0.5.1, next to the cutover declaration it implements. That left 0.5.1 with two `### Fixed` blocks and 0.6.0 with no entry for a behaviour change. My mistake in #125; corrected here since this change edits the same file, with its two internal cross-references fixed to match where it now sits. 205 passed, 1 skipped. ruff and mypy clean.
lywinged
added a commit
to lywinged/trace-spec
that referenced
this pull request
Aug 10, 2026
Upstream merged agentrust-io#148, the last large piece this fork was carrying, which the maintainer rebased and opened himself under this fork's authorship. With agentrust-io#122, agentrust-io#125, agentrust-io#126, agentrust-io#136 and agentrust-io#137 already merged, most of the thirty-three commits here described work that now lives upstream with better provenance than this fork can give it: a PR number and a maintainer's review. `git rebase upstream/main` was tried first and abandoned. It stopped on the oldest commit in the set, a schema-and-version alignment whose content upstream has since taken, superseded and released three times over. Replaying thirty-three commits against fifteen of upstream's resolves early commits into shapes that no longer mean anything. So: main reset to upstream/main, fork-only material re-applied. The old history is tagged `archive/pre-576507b` and pushed rather than discarded. What is held here, and why it is held: - agentrust-io#117 gap disclosure: design note, two normative drafts, 18 vectors, generator - agentrust-io#116 verifier compatibility: 8 vectors, generator, normative draft - the normative crosswalk, mapping every RFC 2119 statement to whom it binds - `docs/conformance-method.md`, `coverage-report/` (historical), DECISIONS.md - the independent signature path and the package-consistency test Each is an unaccepted proposal or a method write-up, not unfinished work. Four files needed a real merge, and not in the same direction. `models.py` and `__init__.py` are upstream's plus this fork's two profile constants, because upstream had moved on with `origin` (agentrust-io#135) and the `declared` enforcement mode (agentrust-io#143) and a wholesale copy would have dropped both. `sign.py` and `test_sign.py` are this fork's `accepted_profiles` version, which supersedes the minimal cutover check upstream took from agentrust-io#125 -- a supersession CLAUDE.md predicted when agentrust-io#125 was offered. All four merged cleanly three-way against a817621, the last commit the two histories agree on. The crosswalk guard earned its place in the same run: upstream added two normative statements this fork had never seen, and `test_normative_crosswalk.py` failed until both had rows. A source-derived inventory noticing its subject moved is the property that document exists to have. 432 passed, 1 skipped. ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
lywinged
added a commit
to lywinged/trace-spec
that referenced
this pull request
Aug 24, 2026
Upstream merged agentrust-io#148, the last large piece this fork was carrying, which the maintainer rebased and opened himself under this fork's authorship. With agentrust-io#122, described work that now lives upstream with better provenance than this fork can give it: a PR number and a maintainer's review. `git rebase upstream/main` was tried first and abandoned. It stopped on the oldest commit in the set, a schema-and-version alignment whose content upstream has since taken, superseded and released three times over. Replaying thirty-three commits against fifteen of upstream's resolves early commits into shapes that no longer mean anything. So: main reset to upstream/main, fork-only material re-applied. The old history is tagged `archive/pre-576507b` and pushed rather than discarded. What is held here, and why it is held: - agentrust-io#117 gap disclosure: design note, two normative drafts, 18 vectors, generator - agentrust-io#116 verifier compatibility: 8 vectors, generator, normative draft - the normative crosswalk, mapping every RFC 2119 statement to whom it binds - `docs/conformance-method.md`, `coverage-report/` (historical), DECISIONS.md - the independent signature path and the package-consistency test Each is an unaccepted proposal or a method write-up, not unfinished work. Four files needed a real merge, and not in the same direction. `models.py` and `__init__.py` are upstream's plus this fork's two profile constants, because upstream had moved on with `origin` (agentrust-io#135) and the `declared` enforcement mode (agentrust-io#143) and a wholesale copy would have dropped both. `sign.py` and `test_sign.py` are this fork's `accepted_profiles` version, which supersedes the minimal cutover check upstream took from agentrust-io#125 -- a supersession CLAUDE.md predicted when agentrust-io#125 was offered. All four merged cleanly three-way against a817621, the last commit the two histories agree on. The crosswalk guard earned its place in the same run: upstream added two normative statements this fork had never seen, and `test_normative_crosswalk.py` failed until both had rows. A source-derived inventory noticing its subject moved is the property that document exists to have. 432 passed, 1 skipped. ruff and mypy clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: lywinged <48041247+lywinged@users.noreply.github.com>
lywinged
added a commit
to lywinged/trace-spec
that referenced
this pull request
Aug 24, 2026
…ded references Rebased onto upstream/main, 26 commits behind. Six files were flagged by the overlap check and three needed a real merge; the directions are the ones DECISIONS.md recorded on 2026-08-10, because the situation is the same one. models.py and __init__.py are upstream's plus this fork's two profile constants and the accepted_profiles machinery. sign.py merged cleanly as this fork's richer version, superseding the minimal cutover check upstream took from agentrust-io#125, which CLAUDE.md section 5 predicted when agentrust-io#125 was offered. The adequacy criteria are now upstream's, from agentrust-io#186, and the fork's own copy is dropped rather than merged: upstream's went through review, carries the completeness guard the fork's did not, and gained a delegation-link set from agentrust-io#184. Only the verifier-compatibility loader is re-applied, since that set is fork-only, along with its recorded shortfall. The pyproject pythonpath entry goes with it: it existed for a bare `from adequacy import` that upstream's version does not use. Upstream's models.py docstring says "TRACE v0.1 Trust Record" on a class whose eat_profile is Literal["...trace-v0.2"]. The fork's correction is kept. The crosswalk guard did what it exists to do. Upstream replaced "Verifiers MUST consult current revocation status" with a SCITT-anchored model and added the references block, and the source-derived inventory failed until nine rows moved. The four references rows are mostly absences: an assurance-neutral block staying neutral is discharged by nothing deriving a platform value from it, and two of the four MUST NOTs bind verifier behaviour that no record shape can express, which the schema description already says. The revocation rows are the ones worth reading. This library implements the fallback in 3.2.3 and not the primary rule: there is no last_valid_entry_id, no log_id and no receipt comparison in src/, so a record is rejected on a key match whether or not it carries an anchor. Section 3.2.3 names that behaviour itself for deployments with no receipts, so it is incomplete rather than wrong, and the row says which. 856 passed, 1 skipped; ruff and mypy clean. Each new row and the restored shortfall entry were checked by mutation: deleting a row, misspelling an anchor, and dropping a recorded boundary each turn the naming test red. Signed-off-by: lywinged <louie.lunz@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
verify_record()never readseat_profile. A record carrying the superseded v0.1 identifier, a future version, a foreign tag, or no profile at all verifies exactly as a v0.2 record, provided its signature checks out.The unreleased changelog already declares the intended behaviour — a v0.2 verifier "requires the new URI and rejects the old one; it does not accept both" — and
spec/trace-v0.2.md§2 makes the same requirement normative ("Cutover, not coexistence"). This PR makes the reference implementation carry it out: the profile is checked first, before any cryptographic work, and anything other than the v0.2 identifier raisesValueError. The v0.1 identifier gets a message that says why it is rejected. A missing profile is refused for the same reason a wrong one is: a verifier cannot supply the semantics by assumption, and a valid signature over semantics the build does not implement is not evidence.Same shape as the revocation fix (#76): an already-merged spec requirement the reference implementation did not enforce, needing no new normative text.
Scope, deliberately minimal
TRACE_PROFILE_V0_2is exported so callers can pin the constant the verifier accepts; the v0.1 tag is deliberately not exported. There is no configurable accepted-profiles set and no verification-statement return — #116 proposes designs in that space, and this PR takes no position on them. It only carries out the MUST that is already merged.Verification
ruff check src testsandmypy src/agentrust_tracecleandocs/verification.mdstep 4 (the manual profile assert) now notes the library does this itself