Skip to content

fix(verify): enforce the profile cutover at verification time - #125

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/enforce-profile-cutover
Aug 8, 2026
Merged

fix(verify): enforce the profile cutover at verification time#125
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/enforce-profile-cutover

Conversation

@lywinged

@lywinged lywinged commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

What

verify_record() never reads eat_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 raises ValueError. 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_2 is 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

  • 110 tests pass (105 before, plus 5 — including one proving the refusal does not depend on cryptographic work: a wrong-profile record with a garbage signature is refused for its profile, not its signature)
  • Neutralizing the check fails 4 of the 5
  • ruff check src tests and mypy src/agentrust_trace clean
  • Verified on Python 3.11 and 3.14
  • docs/verification.md step 4 (the manual profile assert) now notes the library does this itself
  • No normative text, schema, or record field changed
  • DCO signed

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>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Contributor Check: UNKNOWN

Check Result
Profile UNKNOWN
Credential LOW
Overall UNKNOWN

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:UNKNOWN Contributor check flagged UNKNOWN risk label Aug 7, 2026

@imran-siddique imran-siddique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@imran-siddique
imran-siddique merged commit d3e0a91 into agentrust-io:main Aug 8, 2026
1 of 2 checks passed
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
lywinged deleted the fix/enforce-profile-cutover branch August 18, 2026 05:01
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:UNKNOWN Contributor check flagged UNKNOWN risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants