Skip to content

fix(schema): resync the packaged copy, and make the next drift fail - #136

Merged
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/schema-copies-drift
Aug 9, 2026
Merged

fix(schema): resync the packaged copy, and make the next drift fail#136
imran-siddique merged 1 commit into
agentrust-io:mainfrom
lywinged:fix/schema-copies-drift

Conversation

@lywinged

@lywinged lywinged commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Small one. 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, because both files are individually valid and each is exercised on its own.

The visible consequence

subject. The root file and models.py both accept a DID subject; the packaged copy still required ^spiffe://.

>>> from agentrust_trace import validate_json
>>> rec = json.loads(Path("examples/intel-tdx.json").read_text())
>>> rec["subject"] = "did:web:example.org:agent:payments-processor"
>>> validate_json(rec)
ValidationError: 'did:web:example.org:agent:payments-processor' does not match '^spiffe://'

The same record passes schema/trace-claim.json and TrustRecord.model_validate. So a subject form the specification permits was rejected by the reference validator, and the two places a reader would check to find out why both said it was fine.

What is resynced

Was Now
subject.pattern / .description packaged: ^spiffe:// ^(spiffe://|did:), matching the root file and models.py
build_provenance.slsa_level.description packaged had lost the sentence explaining Level 0 restored; Level 0 is what minimum: 0 exists for
$id root file: trace-v0.1.json trace-v0.2.json, taken from the packaged copy

The $id goes the other way from the other two, because neither file was uniformly right: the root file's $id still named v0.1 on a schema whose eat_profile const is v0.2. 0.5.1 moved that URL's domain and left its version segment.

The guard

tests/test_validate.py now 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 change fails both tests; I checked that rather than assuming it.

Compared as parsed JSON rather than byte for byte on purpose: the two files differ in line endings by long-standing accident — the root file is CRLF, the packaged one LF — and that changes nothing about how either validates a record. A byte comparison would fail today for a reason that does not matter and would push someone toward reformatting a 428-line file.

Also in here

One changelog entry moved. The verify_record() profile-cutover enforcement shipped in 0.6.0, but I filed its entry under 0.5.1 in #125, next to the cutover declaration it implements. That left 0.5.1 with two ### Fixed blocks and 0.6.0 with no entry for its only behaviour change. Moved to 0.6.0 with its two internal cross-references corrected, since they pointed at neighbours it no longer has. My mistake, and this change edits the same file, so it seemed better than a second PR for one paragraph.

Checks

205 passed, 1 skipped. ruff check src tests and mypy src/agentrust_trace clean. No normative text, no schema semantics beyond the three resynced fields, no record field added or removed.

`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 agentrust-io#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.
@github-actions

github-actions Bot commented Aug 9, 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 9, 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.

Verified rather than taken on trust: rebased onto current main (which now carries the origin block and its if/then cross-field rule) and ran the suite. 205 passed, including your new drift test, so the two schema copies are genuinely in sync after both changes land.

The subject finding is the real one. The root file and models.py both accept a DID, the packaged copy required ^spiffe://, and validate_json() loads the packaged copy — so the SDK rejected a subject form the spec permits, and nothing failed because each file is individually valid. That is the same shape of defect as two schema copies drifting anywhere else: no test compared them, so the drift was invisible.

Comparing parsed JSON rather than bytes is the right call given the CRLF/LF difference, and saying so in the docstring means nobody 'fixes' it back to a byte comparison later.

The misfiled 0.6.0 changelog entry is a good catch alongside it.

@imran-siddique
imran-siddique merged commit b2dc00d into agentrust-io:main Aug 9, 2026
6 of 7 checks passed
@lywinged

lywinged commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator Author

One residue from this, which merging cannot reach: the backfilled release notes for v0.5.1 and v0.6.0 were generated from CHANGELOG.md before this change, so they still carry the old placement. v0.5.1's body has the verify_record() cutover paragraph; v0.6.0's does not. A release body is a snapshot rather than a view of the file, so the fix here does not propagate to either page.

Impact is small: 0.5.1 is superseded twice over, and the wrong answer only reaches someone reading that page to find out which version carries the cutover enforcement. Recording it rather than proposing a schedule -- it was my misfiling in #125, and I have no way to edit a release body.

If it is worth doing, it is one paragraph moved between two pages: the one beginning "verify_record() now enforces the profile cutover", which on main now sits under [0.6.0] with its two cross-references corrected.

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/schema-copies-drift branch August 18, 2026 05:01
imran-siddique added a commit to agentrust-io/agentrust-io.github.io that referenced this pull request Aug 20, 2026
The file served at https://agentrust-io.com/schema/trace-v0.2.json is the one an
implementation fetches, because that is the $id every TRACE record carries. It
was published once and never updated, and had fallen four commits behind the
normative copy in trace-spec.

The drift was not cosmetic. The published copy was missing the allOf constraint
that stops a record assembled from another party's evidence from claiming a
hardware root, missing the origin property that constraint reads, and it
required `transparency` where the specification does not. A validator using the
canonical URL was therefore rejecting records the specification allows, at
exactly the trust levels that have no transparency anchor.

Resync it, and publish the two revocation schemas, which declare an
agentrust-io.com $id and returned 404.

trace-spec already guards its packaged SDK copy against its normative copy,
after the same class of drift (agentrust-io/trace-spec#136). The published copy
was a third copy that no check covered. Add that check here, where the artifact
is served: every file under schema/ must match its source in trace-spec and must
declare the $id it is actually served at. It runs on changes and daily, since
upstream can move without anything happening in this repository.

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