For implementers, 5 September 2026
If you are already on LangGraph, CrewAI, AutoGen, OpenAI Agents or Pydantic AI, this is two commands and you do not have to read the rest of this page. Each adapter ships an example that runs against the real library, needs no API key and no network, and produces a record that reaches TR-4.
python3 adapters/pydantic-ai/example.py > record.jsonl python3 spec/testimony_validate.py record.jsonl
Forty lines of ordinary framework code, plus a recorder and a function where your approval queue goes. Read that file and you have seen the whole shape. The five, and the seam each one uses, are at the foot of this page.
Until today this site explained the format and left you to write it. That is a project: work out the required members from prose, get the digest rule right unaided, and find out whether you have finished by asking somebody. Two files make it an afternoon, and neither of them needs anything else of ours at runtime.
testimony_emit.py is one standard-library file with no dependencies, MIT licensed, meant to be copied into your repository rather than depended on.
from testimony_emit import Record
r = Record()
r.scope(acts=True, description="Support agent. Refunds to 500 EUR.")
e = r.evidence(kind="api", source="crm://customers/8842")
r.belief(subject="customer:8842", proposition="requested_refund",
asserted_by={"id": "agent", "kind": "agent"}, evidence=[e])
d = r.decision(action_type="issue_refund", risk_class="high",
risk_source="registry",
proposed_by={"id": "agent", "kind": "agent"},
verdict="permitted", executed=True)
r.approval(decision=d, approver={"id": "sam@example.com", "kind": "human"},
identity_source="auth-session")
r.seal()
print(r.jsonl())
That reaches TR-4. It refuses at the point of the mistake rather than leaving you to find it from a validator after the run is over:
a refused action cannot also have executed risk_source: 'model' is the proposing side of the same system the approver is the proposer, 'agent'. An agent's own credential signing off its own action meets every other requirement and is worth nothing 'corp-sso' is not a known source. Use one of auth-session, api-key, jwt, ... or 'x-corp-sso' to declare your own
What it cannot check is what no emitter can. Whether the evidence you cite is what you say it is, whether the risk class really came from a registry, whether the name in an approval came from the session it claims. Those are attestations, the specification says so, and the validator reports them as attestations rather than counting them as proof.
If you want to see what this is for rather than read about it, there is a script that writes the log almost everyone writes, asks it the four questions somebody asks after something goes wrong, and answers none of them. Then it writes the same run as a record and answers all four.
git clone https://github.com/troybrandonc-bit/machine-testimony python3 demo/what_your_log_cannot_answer.py
No install, no dependencies, and it names no product. Swap in your own log format and if it answers all four questions, this project has nothing to offer you, which is a good outcome and worth two minutes to find out.
The conformance corpus is 57 records and the verdict each one should get. The runner takes your command and compares.
python3 conformance/run.py --command "python3 my_validator.py --json {file}"
53 of 53 agree with the reference
run.py imports nothing of ours. Copy it into your
repository and run it in your own CI, which is the only kind of
conformance claim this project is willing to make about anybody.
Your implementation reaches the same verdict as the reference on all 57 cases. It does not mean the same check names, the same wording, or the same number of checks. Those are this project's prose, and a corpus that compared them would be testing whether you had transliterated somebody else's file rather than implemented a specification. An independent implementation is the entire point, so the comparison is deliberately blind to everything except the answer.
It is also not a certificate. It is a statement about 57 cases, and the specification is larger than any 57 cases.
digest-of-nothing is sixty-four zeros where a digest
should be. anchor-over-another-record carries a genuine,
correctly signed RFC 3161 token issued over a different record.
Both reached TR-4 in this project's own reference validator until 5 September 2026, because it checked that an integrity entry carried a digest and never that the digest was the digest of the record. An implementation that accepts either has the same hole, which is why they are in the corpus rather than in a changelog.
You almost certainly have the timestamps and the actions. The four things a log usually lacks, in the order they cost you:
That list is a claim about your records, and you should not have to take it on trust any more than anyone should take a conformance level on trust. Point the converter at a file of them and it will tell you which of the four you are missing, without you reading any of this page first.
python3 spec/testimony_convert.py your_records.jsonl approval
approvals = Mapping('approval', {
"decision": "approval.receipt_id", # the name matches
"approver.id": "approval.approver", # the name matches
"approver.kind": const("human"), # from the member, not your data
# "identity_source": ??? # nothing in your records looks like this
})
It read your field names and proposed the mapping. What it could not find is the interesting half: nothing in that log says how the approver was identified, so the record it would produce cannot say either. That is the first item on the list above, arriving in one command rather than after a reading.
The proposal is a guess from a name and says so on every line. Correct it before you trust anything it produces, because a wrong mapping makes a clean record that answers a question your system never answered. It converts nothing on its own. testimony_convert.py measures it instead. Declare which of your fields mean what, and it builds what it can and names every required field it had no value for.
from testimony_convert import Mapping, convert, const
approvals = Mapping("approval", {
"decision": "approval.receipt_id",
"approver.id": "approval.approver",
"approver.kind": const("human"),
"identity_source": "approval.identity_source",
}, when=lambda row: "approval" in row)
print(convert(my_receipts, approvals).report())
1 source row, 0 entries built
0 converted with every required field, 1 short
required fields your records had no value for:
identity_source 1 of 1 row
The report is the useful half rather than the record. It is a measurement of your format, and you can run it without showing anybody your records or your code.
It never supplies a value it was not given. A required field with no source is reported missing and the row is not converted, so a gap arrives as a gap. A converter that filled in an approver would produce a clean conversion and a false one, which is the exact failure this format exists to make visible.
Expect it to stop short somewhere, and expect that to be the finding. Converting only the decisions out of a gate's own log leaves a high-risk action that ran with no approval behind it, so the record reaches TR-2 and names that as the reason. Nothing went wrong. Their log could not answer the question, and now the level says so.
There are two implementations and both are this author's, one in Python and one in TypeScript, written separately and compared against each other on every case. That is not an independent implementation in the sense that matters, and the Internet-Draft says so in its implementation status section rather than leaving you to notice.
If you build one, it is the most useful thing that could happen to this format, and you do not need to tell anybody or ask for anything. The licence is MIT for the tooling and CC BY 4.0 for the specification.
A case you fail is not necessarily your bug. If you have read the specification and reached a different answer, that disagreement is worth more than the corpus is: open an issue naming the case. The reference has been wrong before, the specification has been vaguer than it knew, and two of the checks exist because somebody said so.