Skip to content

schema: put the references block in the schema and the model - #199

Open
lywinged wants to merge 1 commit into
agentrust-io:mainfrom
lywinged:schema/references-block
Open

schema: put the references block in the schema and the model#199
lywinged wants to merge 1 commit into
agentrust-io:mainfrom
lywinged:schema/references-block

Conversation

@lywinged

Copy link
Copy Markdown
Collaborator

Spec section 3.1.2 landed in #198 as text. The schema was not part of it, and text alone did not make the field usable:

  • schema/trace-claim.json sets additionalProperties: false at the top level, so references is rejected outright.
  • TrustRecord is extra="forbid", so the reference model rejects it too.

examples/tpm2.json validates as committed. Add one references entry and it fails against main with Additional properties are not allowed ('references' was unexpected). Both artifacts a producer would validate against reject a record the specification permits, and nothing failed, because nothing connects the field table in §3.1 to the schema.

This adds the block to both, plus the reference model, docs/schema.md, and a test file. Additions only — 163 inserted, 0 deleted across the six existing files.

The array shape

references is an array of entries. That is the reading the text supports rather than a decision made here: §3.1.2 says "a references entry", "an entry in references", and "MUST omit the entry", and its field table describes the fields of an entry rather than of the block. If you meant a single object, this is the PR to say so on and it is a small change.

What is tested, and what cannot be

Of the four rules in §3.1.2, two are properties of a record:

  • Rule 1references MUST NOT affect runtime.platform. A hardware record that adds references and no origin keeps the platform it earned. The converse is tested too: adding references must not relax the origin if/then, or "does not lower assurance" could be satisfied by a schema that had stopped enforcing anything on that record.
  • Rule 2 — the signature MUST cover references. Nine tamper cases (each member, an entry appended, an entry removed, order reversed, the block removed) each fail verification.

Rules 3 and 4 bind verifiers rather than records, and no schema can express either. The schema description says that rather than implying it enforces them — a description that recites verifier obligations reads like enforcement to whoever is looking at the file to find out what is checked. They belong in the conformance suite, and I will open that separately against agentrust-trace-tests unless you would rather it went elsewhere.

The two choices I would rather you looked at than accepted

  1. minItems: 1. The text does not say the array must be non-empty. I read it out of rule 4 — omit the entry rather than emit an empty one — and applied the same logic to the block. An empty array is a record that says it points at something and does not. Easy to drop if you disagree.
  2. rel is a closed enum. §3.1.2 says "Registered rel values", which implies a registry that can grow, and closing it means a new value needs a schema change. origin.kind is closed for the same stated reason — a verifier keying on the value — so I matched it. Also worth naming: this is the shape of decision the registry question in §7 would settle properly.

resolver is constrained on presence and not on value. Rule 4 requires a producer that cannot name a resolver to omit the entry, and whether an identifier is self-asserted is not decidable from the record, so a pattern here would be theatre.

retention

Validated as an ISO 8601 duration by a pattern written out in alternation rather than with a negative lookahead. That is not style: pydantic's default regex engine is Rust's, which has no look-around, so a lookahead form builds fine in JSON Schema and raises at import in the model — the two files would have to disagree. Written this way, one string serves both, and a test asserts they are byte-equal.

It accepts P30D, P1Y6M15D, P1Y15D, P2W, PT1H30M15S, P1DT12H. It rejects P and PT (no components), P1DT (a time designator with no time), P1H (hours outside the time part), P1D1Y (out of order), P1Y2W and P2WT12H (the week form does not combine). It also rejects fractional components such as PT0.5H, which ISO 8601 permits on the smallest component; say the word if a retention period needs them.

I deliberately did not add "format": "duration". iter_errors runs with a FormatChecker attached, and format: duration asserts only when an optional dependency is installed — so the same record would validate differently on two machines, and the field would look checked where it was not.

Drift

Both schema copies change together. test_validate.py already fails when src/agentrust_trace/schema/trace-v0.2.json drifts from the normative file, and the CHANGELOG records the last time that drift shipped and cost a valid did: subject. Two new guards in the same spirit: the retention and digest pattern strings must be byte-equal between the model and the schema, and the registered rel set must be the same in both. A case table only catches a divergence it happens to sample; byte equality does not depend on sampling. docs/schema.md gets the field, a table and a section, guarded the way the build_provenance table already is.

Verification

604 tests pass, ruff check clean on src and tests. Every guard was checked by reverting the thing it guards and confirming it fails:

put back to main result, out of the 108 tests in the new file
the schema block, both copies 58 failed
the model field and its export 22 failed
docs/schema.md 2 failed
the strict duration pattern, replaced with a naive one 10 failed
the duration pattern made to differ between model and schema the parity guard failed
one rel value spelled differently in the model the parity guard failed
a docs/schema.md row edited three separate ways the doc guard failed each time
the baseline example broken instead of the block 39 failed — the rejection cases are attributable to references and not to something else going wrong

That last row is the one I care about most. A table of "this must be rejected" cases passes trivially if the record was already invalid, so each rejection asserts the error path actually starts at references.

Contribution basis

No sponsoring organization: under CONTRIBUTING this is a schema change tracking an already-merged spec change, plus tests and docs. It adds no normative rule of its own. The uppercase keywords in the schema description are quotations of §3.1.2 and name it in the same sentence; docs/schema.md states the same two rules in lowercase and attributes them, which is what test_requirement_keywords_are_attributable.py asks of a file under docs/ (that guard does not read schema/, so the schema description follows the same rule by intent rather than by enforcement).

Spec section 3.1.2 landed in agentrust-io#198 as text. Text alone did not make the field
usable: schema/trace-claim.json sets additionalProperties: false at the top
level and TrustRecord is extra="forbid", so a record carrying references was
rejected by both artifacts a producer validates against. examples/tpm2.json
validates as committed and fails with "Additional properties are not allowed
('references' was unexpected)" the moment an entry is added.

The block is an array. That is the reading the text supports rather than a
decision made here: 3.1.2 says "a references entry", "an entry in references",
and "omit the entry", and its field table describes the fields of an entry
rather than of the block.

Two of the four rules in 3.1.2 are properties of a record and are tested:
rule 1, a record carrying references and no origin keeps the hardware
runtime.platform it earned; rule 2, the signature covers the block, so
rewriting any member of any entry in transit fails verification. Rules 3 and 4
bind verifiers rather than records — no schema can express them — and belong to
the conformance suite. The schema description says so rather than implying it
enforces them.

resolver is constrained on presence and not on value. Rule 4 requires a
producer that cannot name a resolver to omit the entry, and whether an
identifier is self-asserted is not decidable from the record.

retention is validated as an ISO 8601 duration by a pattern written out in
alternation rather than with a negative lookahead, so the same string serves
both files: pydantic's default regex engine has no look-around, and a lookahead
form would build in JSON Schema and raise in the model, forcing the two apart.
A test asserts the two pattern strings are byte-equal, and that the rel sets
match, because a case table only catches a divergence it happens to sample.

Both schema copies change together; tests/test_validate.py already fails on
drift between them. docs/schema.md gets the field and a table, guarded the same
way as the build_provenance table.

604 tests pass. Each guard was checked by reverting what it guards.

Signed-off-by: Louielunz <48041247+lywinged@users.noreply.github.com>
@lywinged
lywinged requested a review from a team as a code owner August 22, 2026 01:26
@github-actions

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

1 participant