Review a policy change
Your refund policy moved from version 14 to version 15. Legal asks which refunds were decided under
version 14, and how they ended. Each intent records the policy it read as an input (policy:refund
at a version), so a query on that input lists them. You will record five decisions across both
versions, list the ones under version 14, and join them to their outcomes.
Prerequisites
Section titled “Prerequisites”The tenbri CLI, jq and openssl:
cargo install tenbri-cli --version 0.1.0-beta.1 --lockedPython 3.10 or later.
pip install --pre tenbriuv add --prerelease allow tenbripoetry add --allow-prereleases tenbriNode 22.18 or later, which runs .ts files directly, in a project with "type": "module"
(npm pkg set type=module).
npm install tenbri@nextpnpm add tenbri@nextyarn add tenbri@nextbun add tenbri@nextcurl, jq, and a tenbrid serving the stream with a token for you
(tenbrid over REST sets one up):
cargo install tenbri-server --version 0.1.0-beta.1 --lockedIn every terminal:
export TENBRI_KEYS=./keysexport TENBRI_DIGEST_KEY='a 32-byte secret, kept apart....'-
Record decisions under both versions.
Each decision is an intent that read
policy:refundat a version, then its outcome:okorrefused.The
decidehelper writes an intent with the keyed digest of the policy text it read, then its outcome.decide.sh tenbri init audit/support > init.outPIN=$(sed -n 's/^stream key fingerprint (pin it): //p' init.out)decide() { # charge, policy version, statusdigest="hmac-sha256:$(printf 'refund policy, version %s' "$2" \| openssl dgst -sha256 -hmac "$TENBRI_DIGEST_KEY" -r | cut -d' ' -f1)"id=$(tenbri append audit/support <<EOF | jq -r .id{"kind":"decision","phase":"intent","source":"sdk","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"issue_refund","target":"stripe:charge/$1"},"inputs":[{"ref":"policy:refund","version":"$2","digest":"$digest"}]}EOF)tenbri append audit/support <<EOF > /dev/null{"kind":"decision","phase":"outcome","source":"sdk","intent":"$id","status":"$3","principal":{"kind":"agent","id":"support-assistant"},"action":{"type":"issue_refund","target":"stripe:charge/$1"}}EOF}decide ch_1001 14 okdecide ch_1002 14 refuseddecide ch_1003 15 okdecide ch_1004 14 okdecide ch_1005 15 refusedTerminal window source decide.shrun.readpins the policy text the agent read to its next decision. RaisingRefusedinside the decision records the outcome asrefused.decide.py import osfrom tenbri import Refused, TenbriDECISIONS = [ # charge, policy version, status("ch_1001", "14", "ok"),("ch_1002", "14", "refused"),("ch_1003", "15", "ok"),("ch_1004", "14", "ok"),("ch_1005", "15", "refused"),]log = Tenbri.open("./audit/support",create=True,digest_key=os.environ["TENBRI_DIGEST_KEY"].encode(),)print("pin this:", log.fingerprint)with log.run("agent:support-assistant") as run:for charge, version, status in DECISIONS:run.read("policy:refund", version, f"refund policy, version {version}")try:with run.decision("issue_refund", f"stripe:charge/{charge}"):if status == "refused":raise Refused(f"policy {version} says no")except Refused:passlog.close()Terminal window python decide.pypin this: sha256:b548a90ee31e0ba79ee424cc485eb5b0385e2b8713b053c4c1ac284c67a29b5frun.readpins the policy text the agent read to its next decision. ThrowingRefusedinside the decision records the outcome asrefused.decide.ts import { Refused, Tenbri } from 'tenbri'const decisions = [ // charge, policy version, status['ch_1001', '14', 'ok'],['ch_1002', '14', 'refused'],['ch_1003', '15', 'ok'],['ch_1004', '14', 'ok'],['ch_1005', '15', 'refused'],]const log = await Tenbri.open('./audit/support', {create: true,digestKey: process.env.TENBRI_DIGEST_KEY!,})console.log('pin this:', log.fingerprint)await log.run({ principal: 'agent:support-assistant' }, async (run) => {for (const [charge, version, status] of decisions) {run.read('policy:refund', { version, content: `refund policy, version ${version}` })try {await run.decision({ action: 'issue_refund', target: `stripe:charge/${charge}` }, async () => {if (status === 'refused') throw new Refused(`policy ${version} says no`)})} catch (e) {if (!(e instanceof Refused)) throw e}}})await log.close()Terminal window node decide.tspin this: sha256:0823b1cf12c6a78c060c8a942a97aef22acdd91530f85def7461d9beba9d5879Write the decisions with any of the other tabs, or with
POST /v1/streams/support/intentsand/outcomesas in tenbrid over REST. Then serve the stream:Terminal window tenbrid --config tenbrid.toml -
List the decisions under version 14, and join them to their outcomes.
The input filter is
ref@version. The inputs are on the intents; each outcome names its intent byid, so read everyissue_refundentry once and join on it.queryprints one JSON object per line: the entry, itsseq, its entryhash, itsverification, andas_of(what the answer is current to). With--keyand--verify, every row is checked against the pinned fingerprint.Terminal window tenbri query audit/support --input policy:refund@14 --key $PIN --verify \| jq -c '{seq, id: .entry.id, target: .entry.action.target, verification}'{"seq":3,"id":"01M5CWMYAHYECM0BSJK1NVGYEF","target":"stripe:charge/ch_1001","verification":"verified"}{"seq":5,"id":"01M5CWMYCCWGQ2HAG6Y39JHKAF","target":"stripe:charge/ch_1002","verification":"verified"}{"seq":9,"id":"01M5CWMYG4S718BMP25J3ZCPAV","target":"stripe:charge/ch_1004","verification":"verified"}Count per version. A bare ref,
--input policy:refund, matches any version:Terminal window tenbri query audit/support --input policy:refund@14 --key $PIN --verify | jq -s lengthtenbri query audit/support --input policy:refund@15 --key $PIN --verify | jq -s lengthtenbri query audit/support --input policy:refund --key $PIN --verify | jq -s length325Join to the outcomes:
Terminal window tenbri query audit/support --action issue_refund --key $PIN --verify | jq -s -c 'map(.entry) as $all| $all[] | select(.phase == "intent" and any(.inputs[]?; .ref == "policy:refund" and .version == "14")) as $i| $all[] | select(.intent == $i.id)| {target: .action.target, status}'{"target":"stripe:charge/ch_1001","status":"ok"}{"target":"stripe:charge/ch_1002","status":"refused"}{"target":"stripe:charge/ch_1004","status":"ok"}--status refusedlists only the refused outcomes, across both versions.inputstakes"ref@version"strings or(ref, version)pairs; several must all match.review.py import osfrom tenbri import Tenbriwith Tenbri.open("./audit/support", key=os.environ["PIN"]) as log:intents = log.query(inputs=["policy:refund@14"])print(len(intents), "decisions read policy:refund at version 14")outcomes = {m.entry["intent"]: m.entryfor m in log.query(action="issue_refund")if m.entry.get("phase") == "outcome"}for m in intents:intent = m.entryprint(m.verification, intent["seq"], intent["action"]["target"], outcomes[intent["id"]]["status"])Terminal window PIN=sha256:b548a90ee31e0ba79ee424cc485eb5b0385e2b8713b053c4c1ac284c67a29b5f python review.py3 decisions read policy:refund at version 14verified 3 stripe:charge/ch_1001 okverified 5 stripe:charge/ch_1002 refusedverified 9 stripe:charge/ch_1004 okinputstakes'ref@version'strings or[ref, version]pairs; several must all match.review.ts import { Tenbri } from 'tenbri'const log = await Tenbri.open('./audit/support', { key: process.env.PIN! })const intents = await log.query({ inputs: ['policy:refund@14'] })console.log(intents.length, 'decisions read policy:refund at version 14')const outcomes = new Map((await log.query({ action: 'issue_refund' })).filter((m) => m.entry.phase === 'outcome').map((m) => [m.entry.intent, m.entry]),)for (const { verification, entry: intent } of intents) {console.log(verification, intent.seq, intent.action?.target, outcomes.get(intent.id)?.status)}await log.close()Terminal window PIN=sha256:0823b1cf12c6a78c060c8a942a97aef22acdd91530f85def7461d9beba9d5879 node review.ts3 decisions read policy:refund at version 14verified 3 stripe:charge/ch_1001 okverified 5 stripe:charge/ch_1002 refusedverified 9 stripe:charge/ch_1004 okGET /entriestakes the same filters as query parameters (input,action,status,after_seq,limitand more). A bareinput=policy:refundmatches any version.Terminal window export TENBRI_URL=http://127.0.0.1:7070export TOKEN=tnbt_… # your tokencurl -s "$TENBRI_URL/v1/streams/support/entries?input=policy:refund@14" \-H "Authorization: Bearer $TOKEN" \| jq -c '.entries[] | {seq, id: .entry.id, target: .entry.action.target, verification}'{"seq":3,"id":"01M5CWMYAHYECM0BSJK1NVGYEF","target":"stripe:charge/ch_1001","verification":"verified"}{"seq":5,"id":"01M5CWMYCCWGQ2HAG6Y39JHKAF","target":"stripe:charge/ch_1002","verification":"verified"}{"seq":9,"id":"01M5CWMYG4S718BMP25J3ZCPAV","target":"stripe:charge/ch_1004","verification":"verified"}Join to the outcomes:
Terminal window curl -s "$TENBRI_URL/v1/streams/support/entries?action=issue_refund" \-H "Authorization: Bearer $TOKEN" | jq -c '[.entries[].entry] as $all| $all[] | select(.phase == "intent" and any(.inputs[]?; .ref == "policy:refund" and .version == "14")) as $i| $all[] | select(.intent == $i.id)| {target: .action.target, status}'{"target":"stripe:charge/ch_1001","status":"ok"}{"target":"stripe:charge/ch_1002","status":"refused"}{"target":"stripe:charge/ch_1004","status":"ok"}A response holds up to 100 entries by default (
limit, at most 1000); when more match, pass itsnext_after_seqasafter_seqfor the next page.
What happened
Section titled “What happened”Each intent recorded the exact policy version it read, with a keyed digest of its text, inside a
signed entry, so the list cannot be edited after the fact without failing verification. The query
matched ref and version exactly, and every row was checked against the pinned fingerprint. The
digest also lets you prove later which policy text the agent saw, if you keep the text and the
digest key.
Related
Section titled “Related”- Streams and entries
- Entry format
- CLI reference
- API reference
- Adapters: the Claude Agent SDK, OpenAI Agents SDK and LangGraph adapters record each tool call as a decision for you.
