Verifying the tool call transcript¶
A TRACE Trust Record commits the evidence of every tool call by hash. This tutorial explains what the tool_transcript field contains, how to verify that a received record's transcript hash is consistent with the actual tool calls, and how external execution receipts extend the chain.
What you need: A Trust Record with a tool_transcript field, the matching transcript file, and the issuer's public key.
What tool_transcript captures¶
The tool_transcript field in TrustRecord has three fields:
class ToolTranscript(BaseModel):
hash: DigestStr # sha256 or sha384 digest of all tool call content
call_count: int | None # number of calls in this session (optional)
transcript_uri: str | None # where the full transcript can be retrieved
hash is the binding between the Trust Record (which is signed) and the full transcript (which is stored externally). When the Trust Record signature verifies, the hash inside it is signed. When the hash matches the transcript you retrieve from transcript_uri, you know the transcript has not been altered since the record was signed.
The full transcript is NOT embedded in the Trust Record — it lives at transcript_uri. This keeps records small enough to sign and transmit while still committing all call-level evidence.
Step 1 — Retrieve and verify the record signature¶
Start by checking the Trust Record signature with the issuer's public key:
from agentrust_trace.sign import verify_record, load_key
with open("issuer_pub.pem", "rb") as f:
public_key = load_key(f.read())
with open("trust_record.json") as f:
import json
record = json.load(f)
result = verify_record(record, public_key_or_jwk=public_key)
# raises agentrust_trace.exceptions.VerificationError on failure
# returns True on success
verify_record confirms that the signed content of the record has not been altered. This includes the tool_transcript.hash field — if the signature is valid, you have a trusted copy of the hash.
Step 2 — Retrieve the transcript¶
The full transcript lives at tool_transcript.transcript_uri. Retrieve it and hold the raw bytes for hashing:
import requests
transcript_uri = record["tool_transcript"]["transcript_uri"]
response = requests.get(transcript_uri, timeout=30)
response.raise_for_status()
# Hold raw bytes — hash must be computed over the exact bytes served
transcript_bytes = response.content
Hash bytes, not parsed content
The tool_transcript.hash is computed over the raw bytes of the transcript as stored. Do not decode, re-encode, or reformat before hashing — JSON parsing and re-serialization changes whitespace and key order, which changes the hash.
Step 3 — Verify the transcript hash¶
Parse the hash field to determine the algorithm, then compute and compare:
import hashlib
expected = record["tool_transcript"]["hash"]
# expected is a DigestStr: "sha256:<hex>" or "sha384:<hex>"
algorithm, expected_hex = expected.split(":", 1)
if algorithm == "sha256":
computed = hashlib.sha256(transcript_bytes).hexdigest()
elif algorithm == "sha384":
computed = hashlib.sha384(transcript_bytes).hexdigest()
else:
raise ValueError(f"Unsupported digest algorithm: {algorithm}")
if computed != expected_hex:
raise RuntimeError(
f"Transcript hash mismatch.\n"
f" Expected: {expected}\n"
f" Computed: {algorithm}:{computed}"
)
print(f"Transcript verified: {len(transcript_bytes)} bytes, {algorithm}:{computed[:16]}...")
If this check passes, the transcript at transcript_uri is byte-for-byte what was hashed when the Trust Record was signed. Combined with the signature check from Step 1, this gives you end-to-end integrity: record → hash → transcript.
Step 4 — Inspect individual call records¶
The transcript is a JSON array of tool call records. Each entry captures one call:
[
{
"call_index": 0,
"tool_name": "read_file",
"input_hash": "sha256:...",
"output_hash": "sha256:...",
"started_at": "2026-06-23T09:14:58Z",
"duration_ms": 142
}
]
The inputs and outputs are themselves hashed — the raw argument and response values are not in the transcript by default. This protects sensitive tool arguments while still committing the content:
import json
calls = json.loads(transcript_bytes)
print(f"Total calls: {len(calls)}")
for call in calls:
print(f" [{call['call_index']}] {call['tool_name']}")
print(f" input: {call.get('input_hash', 'not committed')}")
print(f" output: {call.get('output_hash', 'not committed')}")
Cross-check against call_count if it was set in the Trust Record:
call_count = record["tool_transcript"].get("call_count")
if call_count is not None and len(calls) != call_count:
print(f"Warning: record says {call_count} calls but transcript has {len(calls)}")
External execution receipts¶
For high-assurance scenarios, individual calls may carry external execution receipts — signed by a third-party (the caller, an orchestrator, or a notary) rather than the agent that produced the Trust Record.
The spec (§3.3.1) defines the receipt structure:
| Field | Description |
|---|---|
issuer | URI identifying the signing party |
issuer_key_id | Key identifier within that party's key set |
signature | Signature over evidence_hash |
evidence_hash | Digest of the specific call being attested |
evidence_type | Content type of the evidence (e.g., application/json) |
linked_call_id | The call index this receipt binds to |
To verify a receipt against a specific call:
def verify_external_receipt(call, receipt, issuer_public_key):
expected_hash = call["input_hash"] # or output_hash depending on what was attested
algorithm, expected_hex = expected_hash.split(":", 1)
receipt_evidence_hash = receipt["evidence_hash"]
receipt_alg, receipt_hex = receipt_evidence_hash.split(":", 1)
# The receipt's evidence_hash must match the call's committed hash
if receipt_hex != expected_hex or receipt_alg != algorithm:
raise RuntimeError(
f"Receipt evidence_hash does not match call {call['call_index']}"
)
# The signature covers the evidence_hash bytes (algorithm-specific)
# Verify using the issuer's public key from their published key set
# (Key retrieval from issuer URI is application-specific)
# ...
return True
No SDK helper for receipt verification
The agentrust_trace SDK does not include an issuer key resolver or receipt chain verifier. Resolution of issuer URIs to public keys is application-specific — typically a DID document or a published JWK Set at a well-known endpoint.
Putting it together¶
A complete audit verification run:
from agentrust_trace.sign import verify_record, load_key
import hashlib
import json
import requests
def verify_audit_chain(record_path, public_key_path):
with open(public_key_path, "rb") as f:
public_key = load_key(f.read())
with open(record_path) as f:
record = json.load(f)
# Step 1: Verify record signature
verify_record(record, public_key_or_jwk=public_key)
print("Signature: OK")
tt = record.get("tool_transcript")
if not tt:
print("No tool_transcript — nothing further to verify")
return
# Step 2: Retrieve transcript
uri = tt.get("transcript_uri")
if not uri:
print("No transcript_uri — cannot retrieve transcript")
return
transcript_bytes = requests.get(uri, timeout=30).content
# Step 3: Hash check
algorithm, expected_hex = tt["hash"].split(":", 1)
hashfn = hashlib.sha256 if algorithm == "sha256" else hashlib.sha384
computed = hashfn(transcript_bytes).hexdigest()
if computed != expected_hex:
raise RuntimeError(f"Transcript hash mismatch: got {algorithm}:{computed}")
print(f"Transcript hash: OK ({algorithm}:{computed[:16]}...)")
# Step 4: Call count
calls = json.loads(transcript_bytes)
call_count = tt.get("call_count")
if call_count is not None:
match = "OK" if len(calls) == call_count else "MISMATCH"
print(f"Call count: {len(calls)}/{call_count} [{match}]")
else:
print(f"Calls in transcript: {len(calls)}")
Summary¶
| Step | What it proves |
|---|---|
verify_record() | Record was not altered after signing; tool_transcript.hash is trusted |
| Transcript hash check | Transcript bytes are exactly what was hashed at signing time |
| Call count check | Transcript was not truncated |
| External receipt check | Third-party confirms specific call inputs/outputs (optional) |
The chain of custody runs: hardware/software measurement → signed Trust Record → committed transcript hash → per-call hashes → optional external receipts. Each link is independently verifiable without contacting the operator who produced the record.