Your agent ran twice. Which step caused it?
Two 47-step runs, side by side, is a spot-the-difference puzzle. behalf records every action an agent takes and shows you the first divergence — not the noise.
$ npx onbehalf diff run_9f2a run_c71e ── first divergence ────────────────── hop 3, t+60s step 12 billing-agent → orders.search 9f2a [0] ord_5512 $12.00 → ok c71e [0] ord_5518 $1200.00 → ok ← different first result ── consequence ─────────────────────────────────── step 31 billing-agent → refund.issue(...) 9f2a amount=12.00 target=ord_5512 → ok c71e amount=1200.00 target=ord_5518 → ok ── 20 downstream differences suppressed (--all to show)
Forty-seven steps each, twenty-two of them different, both runs ok from end to end. An error tracker has nothing to show you. diff names the one step that caused the other twenty-one.
Try it
Two recorded runs of one agent, unpacked onto your machine. No network, no key, no account; nothing is written until you type demo.
$ npx onbehalf demo $ npx onbehalf runs two runs of one agent. both ok. $ npx onbehalf diff run_9f2a run_c71e which step made them differ $ npx onbehalf why run_c71e:31 and on whose behalf it was done
What those commands render, if you would rather look first. The verifier on its own: cargo install behalf-verify. The Go module:go install github.com/behalf-sh/behalf/cmd/behalf@latest. macOS and Linux, x64 and arm64; not Windows — the log's storage driver is POSIX-only, WSL works. Apache-2.0, except the log service and witness (FSL-1.1-ALv2). Get started has the rest.
Every action is a receipt. Try covering one up.
Each action an agent takes across a trust boundary — a tool call, a delegation, an approval — becomes a signed, hash-chained action receipt in an append-only log. Edit one and everything after it stops verifying:
$ sed 's/1200.00/12.00/' run_c71e.jsonl > covered.jsonl $ behalf-verify covered.jsonl ✖ TAMPERED receipt 31: content hash mismatch (expected 4f9d… computed 9690…) chain breaks at 31; receipts 32-46 unverifiable. $ echo $? 1 stderr: class=content index=31
Exit codes are stable — 0 verified, 1 tampering detected, 2 unverifiable — so CI can gate on them. Verification is offline: no network, no call to us. Hand the export file and the verifier to anyone and they can check it themselves, or drop it on the browser verifier— the file never leaves the tab.
And on whose behalf?
Every receipt carries the delegation chain that authorised it — human → agent → sub-agent → tool. why walks it, and says which hops are signed and which are only claimed:
$ npx onbehalf why run_c71e:31 refund.issue(amount=1200.00) run_c71e step 31 ✔ alice@acme.com verified OIDC/google 02:16:58Z │ delegated: "resolve ticket 4417" │ scope: tickets.*, orders.read, refund.issue<=100.00 ▼ ✔ support-orchestrator @1.4.2 verified ed25519 ..whCQN8 ▼ ✖ billing-agent UNVERIFIED │ actor "alice@acme.com" is caller-asserted. no signature. ▼ refund.issue amount=1200.00 ⚠ scope: refund.issue<=100.00 delegated; 1200.00 issued. (recorded, not enforced) chain intact for 2 of 3 hops.
The refund was issued under a name the caller typed in. Nothing signed it. The human at the root delegated a hundred dollars, and the receipt says so — recorded, not enforced.
How it works
Capture
A proxy sits on the trust boundary and records the request that was actually forwarded — not what the agent says it did.
Receipt
Each action is signed at the edge (DSSE, Ed25519) and hash-chained. The stored bytes are the signed bytes are the hashed bytes — there is no canonicalization step to argue about.
Verify
A separate offline verifier, in Rust, checks the whole export and exits 0, 1, or 2. Reconstruction of what happened, checkable by a third party who has never spoken to us.
Verified, asserted, broken
A receipt is verified when its signature and chain position check out against the keys in the export. History imported from other tools, and any hop we could not verify at write time, is marked asserted — recorded honestly as a claim, not evidence. A chain that fails is broken, and everything after the break is reported unverifiable.
The offline verifier re-checks four of the six delegation-chain invariants an export carries — authority, depth, expiry, linkage — and prints on every run, including clean ones, the two it does not (capability monotonicity, proof of possession) and that the identity root is not checked offline. A green tick means exactly the checks it names.
The output never lets a green tick imply more than the cryptography delivers. What we explicitly do not claim — and the adversaries we do and do not defend against — is written down in the threat model.
Where it stands
- done Receipt schema v1 — frozen, published
- done Export format v1, writer, and an offline verifier
- done Tamper suite in CI on every commit — 30 cases across exports, log storage, payloads and the witness
- done
npx onbehalf demo, the CLI and the local log service —onbehalfon npm - done
behalf-verifyon crates.io; source onGitHub - done Apache-2.0 — verifier, CLI, capture surfaces, schemas. FSL-1.1-ALv2 for the log service and witness
- not built Published key log — an export's keys cannot yet be attributed to a real emitter (/keys/)
- not built Importers for existing history; a public witness network
Want the release notes?