- by
- alice · person
- action
- issue_refund
- target
- stripe:charge/ch_81f2
TenbriProof of what your agents decided.
A tamper-evident audit trail for AI agents. Each decision is signed and chained to the one before, so anyone can verify offline who acted, under which policy version, from which inputs, and who approved.
cargo install tenbri-cli --version 0.1.0-alpha.2Try it
Change one number. Watch the chain break.
A refund an agent issued after Alice approved it. Edit the amount and see what verification says.
- agent
- support-assistant
- action
- issue_refund
- inputs
- policy:refund @14
- cents
- agent
- support-assistant
- intent
- seq 2
- status
- ok
$ tenbri verify ./refunds --key sha256:9c1e… verified 3 entries, stream tnb1:1702…b7d8, pinned to sha256:9c1e…
Hashes are computed in your browser with SHA-256, as the spec does. Signatures are simulated.
What you get
Everything an audit needs, in one small tool.
Intent before the action, outcome after.
An agent writes what it is about to do and why, then what happened. Approvals are their own signed entries, and an intent names the approval it relies on by hash. An intent with no outcome after an hour gets a reconcile entry, so silence is visible too.
- Kinds: decision, approval, key, reconcile, note
- Inputs pinned by ref, version and content digest
- Links to entries in other streams: cause, handoff, approval
- OpenTelemetry trace and span ids on every entry
$ echo '{"kind":"decision","phase":"intent","source":"sdk",
"principal":{"kind":"agent","id":"support-assistant"},
"action":{"type":"reply_ticket","target":"zendesk:ticket/4821"},
"inputs":[{"ref":"policy:refund","version":"14", …}]}' \
| tenbri append ./support
$ echo '{"kind":"decision","phase":"outcome","source":"sdk",
"intent":"01M4XBH0Y60000000000000003","status":"ok", …}' \
| tenbri append ./supportAnyone can check it, offline.
Verification needs the stream and one fingerprint, nothing from your systems. It checks canonical bytes, the hash chain, every signature and every key. A checkpoint you saw before also catches truncation and forks.
- Ed25519 signatures over canonical JSON (RFC 8032, strict)
- RFC 6962 leaf hashes; Merkle checkpoints as C2SP signed notes
- Exit codes: 0 ok · 1 broken · 3 not pinned
$ tenbri verify ./support --key sha256:9c1e… verified 1204 entries, stream tnb1:1702…b7d8, pinned to sha256:9c1e… # someone edits one byte of seq 3 $ tenbri verify ./support --key sha256:9c1e… broken at seq 3: bad_signature 2 entries verified before it, stream tnb1:1702…b7d8 # someone deletes the tail $ tenbri verify ./support --key sha256:9c1e… --checkpoint cp.txt broken at seq 9: truncated
Find every decision made under the old rule.
When a policy changes, list what was decided under the previous version and review it. Each result comes with its verification status, one JSON per line, ready for jq.
- Filter by input ref@version, repeatable
- Filter by action type
- Follow live with tail, like tail -f
# everything decided under refund policy v14
$ tenbri query ./support --input policy:refund@14
# every ticket reply, with the contract it relied on
$ tenbri query ./support --action reply_ticket \
--input contract:acme-sante@3
# watch decisions as agents write them
$ tenbri tail ./support -n 20Say what each action must record.
Declare actions in tenbri.toml: which inputs they need, whether a rule and an approval are required, and typed fields. Check whole streams against it in CI.
- Unknown actions: reject or warn
- Required inputs, rule, approval and durability
- Typed extension fields
# tenbri.toml unknown_actions = "reject" [actions.issue_refund] inputs = ["policy:refund"] rule = true approval = true durability = "replicated" target = "stripe:charge/*" [actions.issue_refund.fields."ext.refund.amount"] required = true type = "integer" $ tenbri schema check --schema tenbri.toml ./refunds ./refunds seq 3: rule: required ./refunds: 3 entries, 1 break their schema
A key per stream, a session key per run.
Init makes a stream key and prints its fingerprint: pin it wherever you verify. Writers sign with short-lived session keys certified by the stream key. Rotation and revocation are entries in the chain, and revocation can reach back in time.
- Session keys last 24 hours by default
- Rotate and revoke from Rust (Writer::rotate, revoke)
- Secret keys in files or the macOS and Windows keychain
$ tenbri init ./support --keychain created stream tnb1:1702…b7d8 at ./support stream key fingerprint (pin it): sha256:9c1e… keys: ~/.config/tenbri/keys verify with: tenbri verify ./support --key sha256:9c1e…
Plain files, in a bucket you own.
A stream is a directory or an object-store prefix. Nothing is hosted, nothing phones home. Keep the keys apart from the stream, and give auditors read access to the bucket, not to your systems.
- Local disk, Cloudflare R2 and MinIO
- AWS S3: tested on MinIO, real S3 before beta.1
- Google Cloud Storage and Azure Blob next
$ tenbri init ./support # local disk $ tenbri init s3://audit/agents/support # AWS S3, MinIO $ tenbri init r2://audit/agents/support # Cloudflare R2 # MinIO export AWS_ENDPOINT_URL=http://localhost:9000 AWS_ALLOW_HTTP=true # R2 export CLOUDFLARE_ACCOUNT_ID=… AWS_ACCESS_KEY_ID=… AWS_SECRET_ACCESS_KEY=…
A daemon for every language: tenbrid.
Run tenbrid next to your agents and write over REST or gRPC. Writes are idempotent, reads stream, and verification is one call. An OpenAPI file and the proto contract are in the repo.
- Bearer-token auth
- Idempotency-Key on every write
- Tail resumes from Last-Event-ID
POST /v1/streams/{stream}/intents
POST /v1/streams/{stream}/outcomes
POST /v1/streams/{stream}/approvals
POST /v1/streams/{stream}/entries
GET /v1/streams/{stream}/entries
GET /v1/streams/{stream}/entries/{id}
GET /v1/streams/{stream}/tail
GET /v1/streams/{stream}/verify
service tenbri.v1.TenbriService {
AppendIntent · AppendOutcome · AppendApproval · AppendBatch
GetEntry · QueryEntries · TailEntries · VerifyStream
}Embed the writer.
Use tenbri-writer, tenbri-keys and tenbri-store from Rust today. The tenbri facade crate and the Python, Node and WASM bindings are reserved names; their code lands before v0.1.0.
- Async writer over any store
- verify() returns a report you can act on
- Python, Node and WASM verifier next
let sk = Key::new_stream()?;
let session = Key::new_session(now_ms(), SESSION_LIFETIME_MS)?;
let mut w = Writer::create(store.clone(), &sk, session).await?;
let i = w.intent(decision("reply_ticket")).await?;
// … the agent acts …
w.outcome(&i, Status::Ok).await?;
let report = verify(&store, Some(&sk.fingerprint())).await?;Next on the way to v0.1.0
- Receipts an auditor verifies alone
- Blob encryption and per-subject erasure
- RFC 3161 timestamps
- SQLite and Parquet index
- Python, Node and WASM verifier
- Claude Agent SDK, OpenAI Agents SDK, LangGraph adapters
Records these frameworks ask for
- EU AI Act Art. 12, 14, 19, 26
- GDPR Art. 22
- DORA
- NIS2
- SOC 2
- ISO/IEC 27001
- ISO/IEC 42001
- NIST AI RMF
Tenbri gives you the records. It does not make a system compliant on its own.
Early, open, and built in public.
0.1.0-alpha.2 is out. The format is provisional until its external review ends. Apache-2.0, written in Rust, no telemetry.