Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ Format: [Semantic Versioning](https://semver.org/). Spec versions follow `MAJOR.

### Added

- **The `references` block is now in the schema and the model.** Spec section 3.1.2 landed the block in [#198](https://github.com/agentrust-io/trace-spec/pull/198) as text only, and text alone did not make it usable: `schema/trace-claim.json` sets `additionalProperties: false` at the top level and `TrustRecord` is `extra="forbid"`, so a record carrying the field the specification permits was rejected by both artifacts a producer validates against. `references` is an array of entries with required `rel` (`authorized-intent`, `approval-outcome`, `behavior-trace`), `id` and `resolver`, and optional `retention` (ISO 8601 duration) and `digest`.

Two of the four rules in section 3.1.2 are properties of a record and are tested here: a record carrying `references` and no `origin` keeps the hardware `runtime.platform` it earned, and the signature covers the block, so rewriting any member of any entry in transit fails verification. The other two — a verifier MUST NOT reject a record for an entry it cannot resolve, and MUST NOT treat a resolved entry as attested evidence — are verifier behaviour that no schema can express, and belong to the conformance suite.

`resolver` is constrained on presence and not on value: section 3.1.2 requires a producer that cannot name a resolver to omit the entry rather than emit a self-asserted one, and whether an identifier is self-asserted is not decidable from the record.

- **Record-signing key revocation is anchored to transparency-log entry ordering, not to `iat`.** New spec section 3.2.3 defines the `TraceRevocation/1.0` claim type: a record from a revoked key is valid if and only if its SCITT inclusion entry ID is at or below `last_valid_entry_id` on the log the statement names. The intuitive time-based rule cannot work, because a compromised record-signing key also signs the `iat` it would be judged against, so an attacker backdates the record and the rule passes. Entry IDs are monotonic and bound to the Merkle structure, so ordering survives the compromise a timestamp does not.

Distribution keeps section 3.3's no-callback property: statements are anchored in the same log as the records they govern, and verifiers cache a signed bundle carrying `valid_until`. An expired bundle is not a pass, and a verifier with none reports that it performed no revocation check rather than reporting an affirming appraisal. A revocation statement MUST be signed by a key above the revoked one in the section 3.2.1 hierarchy, or by a recovery key with an independent compromise domain, because a statement the compromised key could sign for itself is a tool for whoever stole it. Records with no usable inclusion entry ID fall back to binary revocation, which is the existing behaviour. Schemas: `schema/trace-revocation.json`, `schema/trace-revocation-bundle.json`. Resolves [#67](https://github.com/agentrust-io/trace-spec/issues/67).
Expand Down
17 changes: 17 additions & 0 deletions docs/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ JSON Schema for the TRACE v0.1 Trust Record. Source: [`schema/trace-claim.json`]
| `tool_transcript` | object | **yes** | Tool-call audit summary |
| `delegation` | object | no | A2A profile: link to the delegating hop's Trust Record |
| `origin` | object | no | Where the evidence came from, when that is not this runtime |
| `references` | array | no | Facts outside this record that it points at. Assurance-neutral |
| `build_provenance` | object | **yes** | Build-time artifact provenance |
| `appraisal` | object | **yes** | Verifier judgment |
| `transparency` | string | no | Registry or SCITT anchor for the record. Optional below Level 2, where an unanchored record has no receipt to name. Use `null`, never `""` |
Expand Down Expand Up @@ -99,6 +100,22 @@ It exists because `runtime.platform: "software-only"` is ambiguous on its own: i

A record whose `kind` is not `self` **must** carry `runtime.platform: "software-only"`. An importer holding someone else's log has no quote to present, so a hardware platform on such a record is untrue rather than stronger. Both the reference model and `schema/trace-claim.json` reject the combination.

## `references`

An array of pointers to facts held outside this record: an authorization decided before execution, a human approval, a behavioural trace. What the signature attests is that this record points there, not the truth of what it points at.

`origin` records where evidence *came from* and can lower assurance. `references` records what a record *points at* and cannot. Before the block existed, a record that needed to name something external had to use `origin` and take `runtime.platform: "software-only"` with it, which said something untrue about how the evidence was obtained.

| Field | Type | Required | Description |
|---|---|---|---|
| `rel` | string | **yes** | `authorized-intent`, `approval-outcome`, or `behavior-trace` |
| `id` | string | **yes** | Identifier of the referenced fact within the resolver's system |
| `resolver` | string | **yes** | Identifier of the party obliged to resolve `id` |
| `retention` | string | no | ISO 8601 duration the resolver undertakes to keep `id` resolvable. An undertaking only; nothing enforces it |
| `digest` | string | no | `sha256:` or `sha384:` digest of the referenced object, when the producer holds it at issue time |

Spec section 3.1.2 also binds verifiers: one **must not** reject a record because an entry cannot be resolved, and **must not** treat a resolved entry as attested evidence. A reference that could invalidate a record would hand whoever controls the target a way to invalidate evidence they do not hold. Both are verifier behaviour, so neither the schema nor the reference model can enforce them; they are conformance-suite rules. What the schema and the model do enforce is the shape, and that a producer who cannot name a `resolver` cannot emit an empty one.

## `build_provenance`

Build-time provenance binding the deployed artifact.
Expand Down
45 changes: 45 additions & 0 deletions schema/trace-claim.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,51 @@
},
"additionalProperties": false
},
"references": {
"type": "array",
"minItems": 1,
"description": "Facts outside this record that it points at. Spec section 3.1.2. An entry is a pointer, not evidence: the signature attests that this record points there, not the truth of what it points at. The block is assurance-neutral and does not affect runtime.platform. Two further rules in 3.1.2 bind verifiers rather than records, so this schema cannot express them: a verifier MUST NOT reject a record because an entry cannot be resolved, and MUST NOT treat a resolved entry as attested evidence.",
"items": {
"type": "object",
"required": [
"rel",
"id",
"resolver"
],
"properties": {
"rel": {
"type": "string",
"enum": [
"authorized-intent",
"approval-outcome",
"behavior-trace"
],
"description": "authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence."
},
"id": {
"type": "string",
"minLength": 1,
"description": "Identifier of the referenced fact within the resolver's system."
},
"resolver": {
"type": "string",
"minLength": 1,
"description": "Identifier of the party obliged to resolve id. A producer that cannot name one omits the entry. Which identifiers are self-asserted is not decidable from the record, so this constrains the field's presence and not its value."
},
"retention": {
"type": "string",
"pattern": "^P(\\d+W|(\\d+Y(\\d+M)?(\\d+D)?|\\d+M(\\d+D)?|\\d+D)(T(\\d+H(\\d+M)?(\\d+S)?|\\d+M(\\d+S)?|\\d+S))?|T(\\d+H(\\d+M)?(\\d+S)?|\\d+M(\\d+S)?|\\d+S))$",
"description": "Period for which resolver undertakes to keep id resolvable, as an ISO 8601 duration. An undertaking only: nothing in this specification enforces it."
},
"digest": {
"type": "string",
"pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$",
"description": "SHA-256 or SHA-384 digest of the referenced object, when the producer holds it at issue time."
}
},
"additionalProperties": false
}
},
"build_provenance": {
"type": "object",
"description": "SLSA provenance for the workload (agent code + container image).",
Expand Down
2 changes: 2 additions & 0 deletions src/agentrust_trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
JWK,
ModelInfo,
PolicyInfo,
Reference,
RuntimeInfo,
ToolTranscript,
TrustRecord,
Expand Down Expand Up @@ -66,6 +67,7 @@
"JWK",
"ModelInfo",
"PolicyInfo",
"Reference",
"RuntimeInfo",
"ToolTranscript",
"TrustRecord",
Expand Down
48 changes: 48 additions & 0 deletions src/agentrust_trace/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
from pydantic import BaseModel, ConfigDict, Field, model_validator

_DIGEST_RE = r"^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$"
# ISO 8601 duration, spelled out by alternation rather than with a negative
# lookahead so that the same pattern string can be used here and in the JSON
# Schema: pydantic's default regex engine (Rust) has no look-around, so a
# lookahead form builds in JSON Schema and raises here, and the two files would
# have to disagree. What the alternation buys: at least one component ("P" and
# "PT" alone are rejected), components in order, and the week form standalone.
_DURATION_TIME = r"(\d+H(\d+M)?(\d+S)?|\d+M(\d+S)?|\d+S)"
_DURATION_DATE = r"(\d+Y(\d+M)?(\d+D)?|\d+M(\d+D)?|\d+D)"
_DURATION_RE = rf"^P(\d+W|{_DURATION_DATE}(T{_DURATION_TIME})?|T{_DURATION_TIME})$"

DigestStr = Annotated[str, Field(pattern=_DIGEST_RE)]

Expand Down Expand Up @@ -140,6 +149,44 @@ class Origin(BaseModel):
ingested_at: Annotated[int, Field(ge=1700000000)] | None = None


class Reference(BaseModel):
"""A fact outside this record that the record points at. Spec section 3.1.2.

``origin`` records where evidence *came from* and can lower assurance.
``references`` records what a record *points at* and cannot. Before this
block existed, a record that needed to name something external had to use
``origin`` and take ``runtime.platform: "software-only"`` with it, which said
something untrue about how the evidence was obtained.

An entry is a pointer, not evidence. What the signature attests is that this
record points there, not the truth of what it points at. The pointer is
produced inside the boundary that produced the record; the target is not.
Two consequences the spec states as MUST NOT are verifier behaviour and so
are not expressible here: a verifier does not reject a record because an
entry cannot be resolved, and it does not treat a resolved entry as attested
evidence. Both live in the conformance suite. This model fixes the shape.

``rel`` is closed for the same reason ``Origin.kind`` is: the value of the
field is that a consumer can key on it.

``resolver`` names the party obliged to resolve ``id``. The specification
requires a producer that cannot name one to omit the entry rather than emit a
self-asserted resolver, and whether an identifier is self-asserted is not
decidable from the record, so the constraint here is presence and not value.

``retention`` states an undertaking that nothing in the specification
enforces. It is validated as an ISO 8601 duration and nothing more.
"""

model_config = ConfigDict(extra="forbid")

rel: Literal["authorized-intent", "approval-outcome", "behavior-trace"]
id: Annotated[str, Field(min_length=1)]
resolver: Annotated[str, Field(min_length=1)]
retention: Annotated[str, Field(pattern=_DURATION_RE)] | None = None
digest: DigestStr | None = None


class BuildProvenance(BaseModel):
model_config = ConfigDict(extra="forbid")

Expand Down Expand Up @@ -220,6 +267,7 @@ class TrustRecord(BaseModel):
tool_transcript: ToolTranscript | None = None
delegation: Delegation | None = None
origin: Origin | None = None
references: Annotated[list[Reference], Field(min_length=1)] | None = None
build_provenance: BuildProvenance
appraisal: Appraisal
transparency: Annotated[str, Field(min_length=1)] | None = None
Expand Down
45 changes: 45 additions & 0 deletions src/agentrust_trace/schema/trace-v0.2.json
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,51 @@
},
"additionalProperties": false
},
"references": {
"type": "array",
"minItems": 1,
"description": "Facts outside this record that it points at. Spec section 3.1.2. An entry is a pointer, not evidence: the signature attests that this record points there, not the truth of what it points at. The block is assurance-neutral and does not affect runtime.platform. Two further rules in 3.1.2 bind verifiers rather than records, so this schema cannot express them: a verifier MUST NOT reject a record because an entry cannot be resolved, and MUST NOT treat a resolved entry as attested evidence.",
"items": {
"type": "object",
"required": [
"rel",
"id",
"resolver"
],
"properties": {
"rel": {
"type": "string",
"enum": [
"authorized-intent",
"approval-outcome",
"behavior-trace"
],
"description": "authorized-intent: an authorization decided before execution, held in another system. approval-outcome: an attributable human approval attached to a step-up or defer decision. behavior-trace: a behavioural record of what the agent did, of which this record is the environment evidence."
},
"id": {
"type": "string",
"minLength": 1,
"description": "Identifier of the referenced fact within the resolver's system."
},
"resolver": {
"type": "string",
"minLength": 1,
"description": "Identifier of the party obliged to resolve id. A producer that cannot name one omits the entry. Which identifiers are self-asserted is not decidable from the record, so this constrains the field's presence and not its value."
},
"retention": {
"type": "string",
"pattern": "^P(\\d+W|(\\d+Y(\\d+M)?(\\d+D)?|\\d+M(\\d+D)?|\\d+D)(T(\\d+H(\\d+M)?(\\d+S)?|\\d+M(\\d+S)?|\\d+S))?|T(\\d+H(\\d+M)?(\\d+S)?|\\d+M(\\d+S)?|\\d+S))$",
"description": "Period for which resolver undertakes to keep id resolvable, as an ISO 8601 duration. An undertaking only: nothing in this specification enforces it."
},
"digest": {
"type": "string",
"pattern": "^sha(256:[0-9a-f]{64}|384:[0-9a-f]{96})$",
"description": "SHA-256 or SHA-384 digest of the referenced object, when the producer holds it at issue time."
}
},
"additionalProperties": false
}
},
"build_provenance": {
"type": "object",
"description": "SLSA provenance for the workload (agent code + container image).",
Expand Down
Loading
Loading