Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

## [Unreleased]

### Fixed

- **The packaged schema had drifted from the normative one, and a DID subject was the casualty.** `validate_json()` loads `src/agentrust_trace/schema/trace-v0.2.json`, while the spec, README and CONTRIBUTING all point a reader at `schema/trace-claim.json`. The two disagreed in three places, so the schema someone reads was not the schema their record was checked against. The visible consequence: the root file and `models.py` both accept a `did:` subject, the packaged copy still required `^spiffe://`, so `agentrust_trace.validate_json()` rejected a subject form the specification permits. Also resynced: the `slsa_level` description, and the root file's `$id`, which still said `trace-v0.1.json` on a schema whose `eat_profile` const is v0.2.

`tests/test_validate.py` now compares the two as parsed JSON on every run, so the next drift fails instead of shipping. 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.

- **A 0.6.0 changelog entry was filed under 0.5.1.** The `verify_record()` profile-cutover enforcement shipped in 0.6.0; its entry landed in the 0.5.1 section, next to the cutover declaration it implements, which left 0.5.1 with two `### Fixed` blocks and 0.6.0 with no entry for a behaviour change. Moved, with its two internal cross-references corrected to match where it now sits.

## [0.7.0] — 2026-08-08

### Added
Expand Down Expand Up @@ -55,6 +63,8 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

- **The package description advertised TRACE v0.1.** The PyPI summary still named the superseded profile.

- **`verify_record()` now enforces the profile cutover this changelog already declares.** The 0.5.1 cutover entry states that a v0.2 verifier "requires the new URI and rejects the old one; it does not accept both" — but `verify_record()` never read `eat_profile`, so a record carrying the 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, so the profile is now checked first, before any cryptographic work: anything other than `TRACE_PROFILE_V0_2` (newly exported) raises `ValueError`, with a message that says why when the profile is the superseded v0.1 identifier. Same shape as the revocation enforcement in this release: an already-merged spec requirement (`spec/trace-v0.2.md` section 2) that the reference implementation did not carry out. `docs/verification.md` step 4 notes the check is now built in. No normative text, schema, or record field changed.

### Added

- **`TraceSandboxAdapter`: Trust Records from a sandboxed agent runtime.** A kernel sandbox confines one agent on one machine. It does not answer, on its own, which agent on which of two hundred machines took an action, what actually ran rather than what the policy said, or how to say either on a host with no secure hardware. The adapter builds a record from what such a runtime already has at session close: sandbox identity, image digest, the effective policy bundle bytes, and the decision log. No change to the runtime is required.
Expand Down Expand Up @@ -93,10 +103,6 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

- Other `agentrust.io` URLs moved to `agentrust-io.com`: the registry and verifier hosts in the AGT adapter and the schema `$id`.

### Fixed

- **`verify_record()` now enforces the profile cutover this changelog already declares.** The entry above states that a v0.2 verifier "requires the new URI and rejects the old one; it does not accept both" — but `verify_record()` never read `eat_profile`, so a record carrying the 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, so the profile is now checked first, before any cryptographic work: anything other than `TRACE_PROFILE_V0_2` (newly exported) raises `ValueError`, with a message that says why when the profile is the superseded v0.1 identifier. Same shape as the revocation fix above: an already-merged spec requirement (`spec/trace-v0.2.md` section 2) that the reference implementation did not carry out. `docs/verification.md` step 4 notes the check is now built in. No normative text, schema, or record field changed.

## [0.4.0]

### Added
Expand Down
2 changes: 1 addition & 1 deletion schema/trace-claim.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://agentrust-io.com/schema/trace-v0.1.json",
"$id": "https://agentrust-io.com/schema/trace-v0.2.json",
"title": "TRACE Trust Record",
"description": "A TRACE v0.2 Trust Record \u2014 hardware-attested governance evidence for an AI agent execution.",
"type": "object",
Expand Down
6 changes: 3 additions & 3 deletions src/agentrust_trace/schema/trace-v0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@
},
"subject": {
"type": "string",
"description": "Workload identity as a SPIFFE SVID URI.",
"pattern": "^spiffe://"
"description": "Workload identity as a SPIFFE SVID URI or DID URI.",
"pattern": "^(spiffe://|did:)"
},
"model": {
"type": "object",
Expand Down Expand Up @@ -250,7 +250,7 @@
"type": "integer",
"minimum": 0,
"maximum": 3,
"description": "SLSA Build Level achieved. Level 2 minimum for TRACE conformance; Level 3 for production mark."
"description": "SLSA Build Level achieved. Level 0 = software-only (development/staging); Level 2 minimum for TRACE conformance; Level 3 for production mark."
},
"builder": {
"type": "string",
Expand Down
45 changes: 45 additions & 0 deletions tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,48 @@ def test_origin_unknown_kind_fails_json_schema() -> None:
data["runtime"]["platform"] = "software-only"
data["origin"] = {"kind": "vendor-asserted", "producer": "vendor/1.0"}
assert iter_errors(data)


def test_packaged_schema_matches_the_normative_schema() -> None:
"""The packaged copy must say the same thing as ``schema/trace-claim.json``.

``validate_json`` loads the packaged copy, while the spec, README and
CONTRIBUTING all point a reader at the root file as the normative one. When the
two drift, the schema someone reads is not the schema their record is checked
against, and nothing fails: both files are individually valid.

Compared as parsed JSON rather than as bytes, because 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.
"""
repo_root = Path(__file__).parent.parent
normative = json.loads((repo_root / "schema" / "trace-claim.json").read_text(encoding="utf-8"))
packaged = json.loads(
(repo_root / "src" / "agentrust_trace" / "schema" / "trace-v0.2.json").read_text(
encoding="utf-8"
)
)
assert packaged == normative, (
"src/agentrust_trace/schema/trace-v0.2.json has drifted from the normative "
"schema/trace-claim.json"
)
assert SCHEMA == normative, "the schema loaded at runtime is not the normative one"


@pytest.mark.parametrize(
"subject",
[
"spiffe://trust.example.org/agent/payments-processor",
"did:web:example.org:agent:payments-processor",
],
)
def test_both_subject_forms_pass_validate_json(subject: str) -> None:
"""The subject pattern the model and the normative schema accept, checked at runtime.

This is the concrete shape the drift above took: the root file and ``models.py``
accepted a DID subject, the packaged copy still required ``spiffe://``, so a record
the specification permits was rejected by the reference validator.
"""
data = _load("intel-tdx.json")
data["subject"] = subject
assert iter_errors(data) == []
Loading