fix(schema): resync the packaged copy, and make the next drift fail - #136
Conversation
`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.
|
❔ Contributor Check: UNKNOWN
Automated check by AgenTrust Contributor Check. |
imran-siddique
left a comment
There was a problem hiding this comment.
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.
|
One residue from this, which merging cannot reach: the backfilled release notes for v0.5.1 and v0.6.0 were generated from 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 " |
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>
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>
Small one.
validate_json()loadssrc/agentrust_trace/schema/trace-v0.2.json; the spec, README and CONTRIBUTING all point a reader atschema/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 andmodels.pyboth accept a DID subject; the packaged copy still required^spiffe://.The same record passes
schema/trace-claim.jsonandTrustRecord.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
subject.pattern/.description^spiffe://^(spiffe://|did:), matching the root file andmodels.pybuild_provenance.slsa_level.descriptionminimum: 0exists for$idtrace-v0.1.jsontrace-v0.2.json, taken from the packaged copyThe
$idgoes the other way from the other two, because neither file was uniformly right: the root file's$idstill named v0.1 on a schema whoseeat_profileconst is v0.2. 0.5.1 moved that URL's domain and left its version segment.The guard
tests/test_validate.pynow compares the two as parsed JSON on every run, plus a regression test that both subject forms passiter_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### Fixedblocks 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 testsandmypy src/agentrust_traceclean. No normative text, no schema semantics beyond the three resynced fields, no record field added or removed.