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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ dependencies = [
"click>=8.1.0,<9.0",
"python-dateutil>=2.8.0,<3.0",
"jsonschema>=4.0.0,<5.0",
"agentrust-trace>=0.2.0,<0.3.0",
"agentrust-trace>=0.5.1,<0.6.0",
]
Comment thread
imran-siddique marked this conversation as resolved.

[project.optional-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion agent-governance-python/agent-mesh/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ dev = [
# `pip install agent-mesh[dev]` brings everything needed for tests.
"agent_hypervisor>=3.7.0,<6.0",
# TRACE Trust Record emission (ADR-0032)
"agentrust-trace>=0.2.0,<0.3.0",
"agentrust-trace>=0.5.1,<0.6.0",
]

[project.urls]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@

from .audit import AuditEntry

#: TRACE EAT profile URI emitted by AGT. Single source of truth: trace_sink.py
#: imports this rather than repeating the literal, because a wire-format constant
#: duplicated across modules drifts one call site at a time. See ADR-0032 and its
#: 2026-07-28 amendment for why the v0.1 URI was replaced.
TRACE_EAT_PROFILE = "tag:agentrust-io.com,2026:trace-v0.2"


_DENY_OUTCOMES = frozenset({"denied"})
_DENY_EVENT_TYPES = frozenset({"policy_violation", "tool_blocked"})
Expand All @@ -25,7 +31,7 @@ class TraceModelConfig:
enforcement_mode: str
build_provenance: dict
verifier: str
eat_profile: str = "tag:agentrust.io,2026:trace-v0.1"
eat_profile: str = TRACE_EAT_PROFILE


@dataclass
Expand All @@ -51,7 +57,7 @@ class TrustRecord:
data_class: str
build_provenance: dict
appraisal: dict
transparency: str
transparency: Optional[str]
tool_transcript: dict


Expand Down Expand Up @@ -95,7 +101,11 @@ def session_to_trust_record(session: TraceSession, config: TraceModelConfig) ->
data_class=session.data_class,
build_provenance=config.build_provenance,
appraisal={"status": appraisal_status, "verifier": config.verifier},
transparency="",
# None, not "": a Level 0/1 record is not anchored, so there is no receipt
# URI to name. An empty string looks populated and resolves to nothing, which
# is worse than an absent field. Conformance requires this at Level 2 only,
# where TR-ANC runs.
transparency=None,
tool_transcript={"hash": _jcs_hash(entries), "call_count": call_count},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from typing import Any, Optional

from .audit import AuditLog
from .trace_model import TRACE_EAT_PROFILE

_SUBJECT_RE = re.compile(r"^(spiffe://|did:)")

Expand Down Expand Up @@ -89,7 +90,7 @@ def session_to_trust_record(
bp_digest = config.build_provenance_digest or measurement

record: dict[str, Any] = {
"eat_profile": "tag:agentrust.io,2026:trace-v0.1",
"eat_profile": TRACE_EAT_PROFILE,
"iat": iat,
Comment thread
imran-siddique marked this conversation as resolved.
"subject": agent_did,
"model": {
Expand Down Expand Up @@ -117,7 +118,10 @@ def session_to_trust_record(
"status": "affirming",
"verifier": config.appraisal_verifier,
},
"transparency": "",
# None, not "": this sink does not anchor to a transparency log, so there
# is no receipt URI. An empty string looks populated and resolves to
# nothing. Conformance requires this at Level 2 only, where TR-ANC runs.
"transparency": None,
}

if config.model_version:
Expand Down Expand Up @@ -182,7 +186,7 @@ def emit(self, audit_log: Optional[AuditLog]) -> Optional[str]:
except ImportError as exc:
raise RuntimeError(
"agentrust-trace is required for TRACE emission. "
"Install it with: pip install agentrust-trace>=0.2.0"
"Install it with: pip install 'agentrust-trace>=0.5.1,<0.6.0'"
) from exc

record = session_to_trust_record(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"builder": "github-actions",
"digest": f"sha256:{_ZEROS}",
},
verifier="https://verifier.agentrust.io",
verifier="https://verifier.agentrust-io.com",
)


Expand All @@ -50,13 +50,13 @@ def test_golden_path_fields_present(self):
)
record = session_to_trust_record(session, _CONFIG)

assert record["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1"
assert record["eat_profile"] == "tag:agentrust-io.com,2026:trace-v0.2"
assert record["subject"] == "did:mesh:spiffe://cluster/ns/default/sa/agent-1"
assert record["data_class"] == "public"
assert isinstance(record["iat"], int) and record["iat"] > 0
assert record["transparency"] == ""
assert record["transparency"] is None # unanchored at Level 0/1
assert record["appraisal"]["status"] == "affirming"
assert record["appraisal"]["verifier"] == "https://verifier.agentrust.io"
assert record["appraisal"]["verifier"] == "https://verifier.agentrust-io.com"
assert record["tool_transcript"]["call_count"] == 1
assert record["tool_transcript"]["hash"].startswith("sha256:")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def test_eat_profile_sentinel(self):
record = session_to_trust_record(
_AGENT_DID, log, _policy_hash(_POLICY_YAML), TraceConfig("./out/")
)
assert record["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1"
assert record["eat_profile"] == "tag:agentrust-io.com,2026:trace-v0.2"

def test_subject_is_agent_did(self):
log = _make_audit_log()
Expand Down Expand Up @@ -171,7 +171,7 @@ def test_emit_file_is_valid_trust_record(self, tmp_path):
sink = TRACEAuditSink(cfg, _AGENT_DID, _policy_hash(_POLICY_YAML))
path = sink.emit(log)
data = json.loads(Path(path).read_text())
assert data["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1"
assert data["eat_profile"] == "tag:agentrust-io.com,2026:trace-v0.2"
assert data["subject"] == _AGENT_DID

def test_emit_returns_none_for_empty_log(self, tmp_path):
Expand Down Expand Up @@ -229,7 +229,7 @@ def tool(action, resource=None):
data = json.loads(Path(path).read_text())
assert data["subject"] == _AGENT_DID
assert data["policy"]["enforcement_mode"] == "enforce"
assert data["eat_profile"] == "tag:agentrust.io,2026:trace-v0.1"
assert data["eat_profile"] == "tag:agentrust-io.com,2026:trace-v0.2"

def test_close_session_returns_none_for_empty_audit(self, tmp_path):
# Audit is empty because the tool is never called
Expand Down
71 changes: 71 additions & 0 deletions docs/adr/0032-agt-emits-trace-v01-trust-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,76 @@ sink is affected. The HMAC-chained audit log continues to be written in parallel
TRACE extends.
- agentrust-io/trace-spec v0.2.0 -- the claim schema and conformance tests.
- agentrust-trace v0.2.0 (PyPI) -- `TrustRecord`, `sign_record`, `load_signing_key`.
That is the version this ADR was accepted against, kept as the historical record.
Do not install it: the current pin is `>=0.5.1,<0.6.0`, and the 2026-07-28
amendment below explains what changed in between, including a breaking
canonicalization change.
- agentrust-io/cmcp#124 -- Phase 2 TEE enforcement; the runtime that will
supersede this record for TEE deployments.

## Amendment 2026-07-28: the profile URI moves to TRACE v0.2

**Resolved by:** agentrust-io/trace-spec#107.

The Decision section above specifies `eat_profile` as the constant
`"tag:agentrust.io,2026:trace-v0.1"`. That URI is no longer correct, and it was
never valid.

RFC 4151 permits a tag URI only where the minting authority controlled the named
domain on the date in the URI. `agentrust.io` was never controlled by the TRACE
project; it resolves to third-party parked addresses. The identifier therefore
asserted authority over a name belonging to someone else, who could at any point
publish a conflicting definition at it.

TRACE v0.2 corrects it to `tag:agentrust-io.com,2026:trace-v0.2`. The *specification*
change is that one identifier: no field was added, removed, or re-typed.

The *library* jump is a larger cutover, and describing it as one constant undersells
it. AGT moves from `agentrust-trace` 0.2.0 to 0.5.x, which crosses two releases with
consequences of their own:

- **0.3.0 changed canonicalization to RFC 8785 (JCS), and it is breaking.** The prior
`json.dumps` pre-image was non-conformant. Records AGT signs after this bump are
**not cross-verifiable with 0.2.0-era verifiers**, and vice versa, because the
signature covers a different byte sequence. Any consumer pinned to 0.2.0 must move
with us.
- **0.3.0 also stopped self-verifying from the embedded `cnf.jwk` by default.**
`verify_record` now requires an explicit trusted key unless `allow_embedded_key=True`
is passed, and enforces `iat` freshness. Anything that verified AGT records by
trusting the key inside them needs updating.
- 0.4.0 is additive only (the `azure-cvm-sev-snp` platform value and the optional
`delegation` block), so it imposes nothing.

None of that is a reason not to move; the 0.2.0 canonicalization was simply wrong.
It is a reason to say so here rather than let a downstream verifier discover it.

The upstream cutover is deliberate rather than a transition window: a v0.2 verifier
requires the new URI and rejects the old one, because a verifier accepting both
would keep the invalid identifier live indefinitely. Records AGT has already
emitted remain verifiable as v0.1 records, and do not become invalid retroactively.
Two different packages carry the pre-cutover behaviour, and they are on different
version lines, which is worth stating precisely: the **library** `agentrust-trace`
0.4.x still accepts the v0.1 profile, and the **conformance suite**
`agentrust-trace-tests` 0.3.x still requires it. Both stay published. Note the
canonicalization caveat above, though: a record signed by AGT under 0.2.0 needs a
0.2.0-era verifier, not merely a pre-cutover one.

AGT's `agentrust-trace` dependency moves from `>=0.2.0,<0.3.0` to
`>=0.5.1,<0.6.0`. The floor is 0.5.1 rather than 0.5.0 deliberately: 0.5.0 still
required a non-empty `transparency`, so a resolve that landed on it would reject
the `None` this amendment describes and reproduce the exact ValidationError the
change exists to fix. A floor that admits a broken version is not a floor.

That bump also changes one field. The Decision section says `transparency` is an
empty string for Phase 1, because SCITT anchoring is out of scope there. It is now
`None` instead. An empty string looks populated and resolves to nothing, which is a
worse thing to put in a trust record than an absent field; `None` says plainly that
this record is not anchored. Conformance requires `transparency` only at Level 2,
where `TR-ANC` runs, so an unanchored Phase 1 record remains conformant at the level
it claims. This needs `agentrust-trace` 0.5.1 or later, which stopped requiring a
non-empty value at every level (agentrust-io/trace-spec#109). The References section above still cites v0.2.0 because that is
what this ADR was accepted against; it is a historical record, not a statement of
the current pin.

The original Decision text is retained above per the ADR immutability convention.
The file name still says `v01` for the same reason.
Loading