Skip to content
Tenbri pangolinTenbri

Quickstart

In five minutes you create a stream in a local directory, record an intent and its outcome, verify the stream against its pinned key, and query it. Pick the CLI, Python or TypeScript: the three write the same stream format, so tenbri verify checks a stream written from code and the other way round. Install what you pick first (Install).

The outputs below come from real runs on macOS. Your ids, keys, hashes, signatures and timestamps will differ. Long values are shortened with ….

Signing keys live outside the stream. For this walkthrough, use a keys directory next to it:

Terminal window
mkdir tenbri-demo && cd tenbri-demo
export TENBRI_KEYS=$PWD/keys

Without TENBRI_KEYS, every writer uses ~/.config/tenbri/keys.

  1. Create a stream.

    Terminal window
    tenbri init audit/support
    created stream tnb1:13410a435d5a70cc7f96ae05209410bb at audit/support
    stream key fingerprint (pin it): sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61
    keys: /…/tenbri-demo/keys/tnb1-13410a435d5a70cc7f96ae05209410bb
    verify with: tenbri verify audit/support --key sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61

    The directory must be new or empty. Copy the fingerprint somewhere the operator of the stream cannot change it: it is your trust root.

  2. Record an intent.

    tenbri append reads one entry as JSON on stdin. An intent is written before the action runs:

    Terminal window
    tenbri append audit/support <<'EOF'
    {"kind":"decision","phase":"intent","source":"sdk",
    "principal":{"kind":"agent","id":"support-assistant"},
    "action":{"type":"refund","target":"order:8812"},
    "inputs":[{"ref":"policy:refund","version":"14",
    "digest":"hmac-sha256:9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa9f2c41aa"}],
    "rule":{"ref":"refund.max_amount","version":"14","result":"allow"}}
    EOF

    It prints the entry as stored, in canonical form (one line, keys sorted):

    {"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE","inputs":[{"digest":"hmac-sha256:9f2c41aa…","ref":"policy:refund","version":"14"}],"kind":"decision","phase":"intent","prev":"sha256:b44fbc6c…","principal":{"id":"support-assistant","kind":"agent"},"rule":{"ref":"refund.max_amount","result":"allow","version":"14"},"seq":3,"sig":"ed25519:6258835f…","signer":"sess:2026-10-20:c4de0a227bf3","source":"sdk","stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","ts":"2026-10-20T14:45:26.108Z","v":1}

    The writer fills in v, id, stream, seq, ts, prev, signer and sig. You may give id and ts yourself; giving the others is an error:

    tenbri: stdin: invalid_field: `seq` is filled in by the writer

    Note the entry’s id. The outcome names it.

  3. Record its outcome.

    Terminal window
    tenbri append audit/support <<'EOF'
    {"kind":"decision","phase":"outcome","source":"sdk",
    "intent":"01M5CWCEYW1GZWQE795VT6BGKE","status":"ok",
    "principal":{"kind":"agent","id":"support-assistant"},
    "action":{"type":"refund","target":"order:8812"}}
    EOF
    {"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCF057ED20JDR0QJQ2ZR6","intent":"01M5CWCEYW1GZWQE795VT6BGKE","kind":"decision","phase":"outcome","prev":"sha256:465624fb…","principal":{"id":"support-assistant","kind":"agent"},"seq":4,"sig":"ed25519:d3240344…","signer":"sess:2026-10-20:c4de0a227bf3","source":"sdk","status":"ok","stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","ts":"2026-10-20T14:45:26.149Z","v":1}

    An intent takes exactly one outcome. A second one for the same intent fails, writes nothing and exits 2:

    tenbri: entry rejected: bad_intent: no open intent 01M5CWCEYW1GZWQE795VT6BGKE
  4. Verify against the pinned key.

    Terminal window
    tenbri verify audit/support --key sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61
    verified 4 entries, stream tnb1:13410a435d5a70cc7f96ae05209410bb, pinned to sha256:13410a435d5a70cc7f96ae05209410bb37cd301124e0719229b3184530789b61

    The exit code is 0. Without --key, verify can only say the stream agrees with its own genesis key, and exits 3:

    consistent: 4 entries, stream tnb1:13410a435d5a70cc7f96ae05209410bb
    NOT pinned: this only shows the stream agrees with its own genesis key; pass --key <fingerprint>
  5. Query by input.

    Find every entry that used version 14 of the refund policy:

    Terminal window
    tenbri query audit/support --input policy:refund@14 --key sha256:13410a43…
    {"verification":"verified","entry":{"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"intent",…,"seq":3,…},"seq":3,"hash":"sha256:465624fb…","as_of":{"stream":"tnb1:13410a435d5a70cc7f96ae05209410bb","seq":4,"hash":"sha256:467d3d96…"}}

    Each line is one match: the entry, its verification (verified with --key, unpinned without), its hash, and as_of, the head of the stream the answer was read from. --action refund selects by action type and matches both entries. Repeat --input to require several.

  6. Tail the stream.

    Terminal window
    tenbri tail audit/support -n 2 --no-follow
    {"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"intent",…,"seq":3,…}
    {"action":{"target":"order:8812","type":"refund"},"id":"01M5CWCF057ED20JDR0QJQ2ZR6","intent":"01M5CWCEYW1GZWQE795VT6BGKE",…,"phase":"outcome",…,"seq":4,…}

    Without --no-follow, tail keeps running and prints new entries as they are written.

Seq 1 and 2 are the key entries that creating the stream wrote: the genesis entry (the stream key certifying itself) and the certification of the session key that signs everything after. The stream id is tnb1: and the first 32 hex digits of the fingerprint.

Whichever way you wrote the stream, the CLI verifies it:

Terminal window
tenbri verify audit/support --key sha256:dc978283284e60b2751c968433a059d5f643ac06fdc7da25553a613fb313ae90
verified 4 entries, stream tnb1:dc978283284e60b2751c968433a059d5, pinned to sha256:dc978283284e60b2751c968433a059d5f643ac06fdc7da25553a613fb313ae90

Change one byte of entry 3 in a copy of the stream, then verify the copy:

Terminal window
cp -r audit/support tampered
sed -i.bak 's/order:8812/order:8813/' tampered/batches/000000000003.jsonl
rm tampered/batches/*.bak
tenbri verify tampered --key sha256:13410a43…
broken at seq 3: bad_signature: the signature does not verify
2 entries verified before it, stream tnb1:13410a435d5a70cc7f96ae05209410bb

The exit code is 1. query on a stream that does not verify also exits 1. From code, verify() returns a report with ok false and the break. In Python, print(r.ok, r.broken) after r = Tenbri.open("tampered", key=pin).verify() shows:

False Broken(seq=3, code='bad_signature', message='the signature does not verify')
tenbri-demo/
audit/support/
.lock held while HEAD is swapped; not part of the stream
HEAD
batches/000000000001.jsonl
batches/000000000002.jsonl
batches/000000000003.jsonl
batches/000000000004.jsonl
keys/tnb1-13410a435d5a70cc7f96ae05209410bb/
13410a435d5a.json the stream key's public description
13410a435d5a.secret its secret (0600)
c4de0a227bf3.json the session key's public description
c4de0a227bf3.secret

Each CLI call writes its own batch file. HEAD names the latest entry and is signed by the session key.