Bellbook
A tamper-evident, replay-verifiable record of captured agent activity.
Bellbook is a small embeddable Rust library with one durable primitive: a typed
Record in an append-only log. Activity the host captures is written down as
a typed entry - what was requested, what was done, what came back, what was
approved, what was refused, and the verdict on whether it followed the rules.
Modifying hash-covered record content or the committed sequence becomes
detectable. Detecting complete replacement from genesis requires an external
anchor. A verifier can replay the whole record to confirm its internal
consistency: no action without a verdict, no gaps, and no forged record ids.
In one line: it turns "the agent says it did X" into "here is tamper-evident, replay-verifiable evidence of the agent activity that was recorded."
As of spec v0.3, Bellbook also records how software evolves: Candidate
source states bound to a Git tree, Evaluations of them, and set-valued
Selections between them - so a chosen line of work carries verifiable
candidate selection and lineage. When an evaluation is later retracted
(say, a benchmark turns out to be broken), replay marks every candidate that
rested on it compromised, transitively at any depth, and one reaffirming
selection on surviving evidence restores the line - with the whole episode
permanently on the record. See Recording evolution
and cargo run --example broken_benchmark.
One honest boundary up front: Bellbook proves consistency, not completeness. It verifies that captured history is intact, rule-conforming, and honestly graded; whether everything the agent did was captured depends on how the host instruments its runtime (SPEC §13). And it provides integrity, not confidentiality: records and receipts carry full payloads in the clear, so a receipt inherits the sensitivity of everything committed - never put credentials in records, and treat sharing a receipt as disclosure (see SECURITY.md).
Where Bellbook fits
Bellbook is an evidence layer, not an identity provider or runtime
policy engine. Identity systems establish who an agent is and what
access it holds. Policy engines decide whether an action may execute.
Bellbook preserves captured requests, authority, actions, and results as a
portable receipt that another party can verify independently. The spec does
not include a record for an external policy engine's decision. Bellbook's own
Verdict records are its deterministic
judgment that the ledger followed its rules; they are never a
substitute for an external policy engine's permit/deny decision, and
the separately scoped PolicyDecision work is not implemented.
See docs/ECOSYSTEM.md for how surrounding systems
relate to Bellbook and
docs/INTEROPERABILITY.md for the boundary
definitions.
Not a logger, not a database, not a runtime - an evidence kernel your process embeds.
How it works
- Content-addressed records. A record's
idis the SHA-256 of its RFC 8785 (JCS) canonical id form (only id excluded; a completed signature is included), so an independent implementation in any language computes identical ids (test vectors). Records reference earlier records by that hash through typed refs (Cause,Use,Require,Replace), forming a DAG. Edit any byte of history and every id and ref that depends on it breaks. - Every record is judged. Committing a record runs a deterministic
verifier and appends a
Verdict(Accept/Reject+ reason) immediately after it. Rejected records stay in the log - the log records what was attempted, not just what was allowed. - The whole log replays.
verify_logwalks the log from genesis (or a checkpoint): recomputes every id, enforces gap-free logical time (time == prev + 1), requires every non-verdict record to be immediately followed by its verdict, and re-derives every verdict from the replay start onward to compare with what's stored - a forged verdict is caught, not trusted. (Records inside a checkpoint prefix are attested by the prefix hash instead of re-derivation.) - Governance is in-band. Capabilities, approvals, refusals, and expiries
are records too, so "was this action allowed at the time?" is answered by
the log itself, deterministically. Author roles are enforced and actor
identities are bound to roles in the rules; pin an actor's signing keys
in
author_keysand its records must be validly signed, so the agent cannot author its own approvals, even by claiming to be the user - that guarantee is cryptographic exactly when the claimed identity has pinned keys, and configuration-level otherwise. Exact approvals are bound to one actor's one action and are single-use, every action must name the exact authority that allowed it, retracted authority stops authorizing, and retraction itself is ownership-bound. - Evidence classes. Every record carries how its content is known - an
ordered five-class lattice, strongest to weakest:
Deterministic(verifier-derived),Verified(a signed attestation from a key-pinned external party - origin verified, never the real-world effect),Reported(asserted by an external party),Inferred(derived by reasoning),Assumed(unverified assumption) - and derived records inherit the weakest evidence among the sources they declare (Use/Requirerefs), so evidence can never be inflated. Rules can set per-kind minimum-evidence thresholds. - Ed25519 signatures. Records can carry a detached signature over a Bellbook-epoch-domain-separated, id-free signing form; the completed signature is then included in the final record id, so signatures and head attestations cannot be substituted without detection. Rules configure which kinds require one and which keys each actor may sign with; verification is strict and real, not a stub.
- Retraction with taint. A
Retractionrecord asserts an earlier record's content was wrong - append-only, nothing erased. Records that epistemically depended on it (viaUse/Requirerefs) are marked tainted; a tainted chain still replays and verifies, and the report surfaces exactly which claims no longer rest on anything. - Evolution semantics (spec v0.3). Three more record kinds capture how
work evolves: a
Candidatebinds a source state (a Git tree,reportedor committed to a canonicalmanifest), anEvaluationrecords one criterion's judgment of one candidate, and a set-valuedSelectionchooses among considered candidates under an objective, grounded on evaluations. AReplaceon a Selection reaffirms an earlier decision. On top of these, the replay report gains a standing section: a purely replay-derived lineage dimension that marks which candidates rest on decisions and states that no longer stand. When a benchmark's evaluations are retracted, taint reaches the selections that used them and standing marks the descendant line compromised at any depth; one reaffirming selection on surviving evidence restores it. Standing is re-derived by every validator like the taint set, never embedded in a receipt, and never merged into kernel taint or evidence (SPEC §7.2; runcargo run --example broken_benchmark). - Honest threat model. Tamper-evident, not tamper-proof: replay detects any interior edit to committed history, but the ledger's owner can rewrite the whole log from genesis. SPEC §11 states this plainly and defines the mitigations - key-pinned signatures and a canonical head attestation to anchor externally.
- Crash-safe, verified single writer.
LogWriterholds an exclusive file lock, refuses existing history that does not replay under its opening rules, rejects stale or fabricated derived state, keeps raw append and its time source private, and uses an intent-file protocol to restore an interrupted record/verdict pair exactly once. Opening and appending are bounded to 64 MiB by default; trusted larger logs can opt into an explicit higher limit.
batch_commit preserves the atomicity of each subject/verdict pair but is not
a transaction across the entire batch: an error on a later proposal leaves
earlier pairs durable. Integrations that retry batches should use
checked_batch_commit with the expected head.
Quickstart
use *;
let space = default_space;
// Bind actor identities to roles: every non-Verdict record needs a
// registered author (pin keys in `author_keys` to authenticate them).
let rules = new
.with_author_role
.with_author_role;
let mut writer = open?;
let mut state = default;
let = writer.commit?;
assert_eq!;
// Replay-verify the entire log - recomputes ids, times, and every verdict.
let report = verify_log;
assert_eq!;
Run the full working demo (commit → verify → tamper → detect):
cargo run --example quickstart
See SPEC.md for the record model, verification rules, commit protocol, and storage format.
Feature flags
| Flag | Default | Effect |
|---|---|---|
persist |
on | File-backed log through the verified LogWriter API, with locking through fs4 and a configurable 64 MiB default file-size bound. Raw storage mutation is internal. Disable for the pure in-memory model, verifier, and receipt validation (no file-I/O dependency). |
Validating a receipt
A log exports as a portable, self-contained Receipt; anyone can verify
it offline - ids, chain, every verdict re-derived, signatures, evidence,
taint - with no Rust knowledge required. The CLI ships with the crate
(cargo install bellbook, or cargo run --bin bellbook -- … from a
checkout):
bellbook validate receipt.json # human-readable report
bellbook validate receipt.json --json # same report as JSON
bellbook validate receipt.json --require-profile bellbook-core-v1
# plus baseline-profile conformance
bellbook export --log ./log --rules rules.json --profile bellbook-core-v1
# a receipt that declares the claim;
# every validator re-checks it, unasked
Exit codes: 0 clean, 1 invalid, 2 valid-but-tainted, 3 validates
but a declared or required profile is not met. See SPEC §12 for the
receipt format and the normative truth rules. Two honesty notes.
Clean is relative to the rules embedded in the receipt (compare the
reported rules_hash against a rule set you trust) - under default rules
it means "internally consistent", not "meets a shared security
baseline". The bellbook-core-v1
profile is the shared baseline for that comparison: a content-addressed
clause table over the rule shape, evaluated when the receipt declares it
or a caller requires it, and reported alongside the verdict (never
changing it, and never trusting the declaration). And a receipt proves the recorded process, not source
contents: a Candidate's Git OIDs are pointers the repository resolves
(under manifest binding a party holding the tree can recompute the hash
and bind the receipt to actual contents; under reported binding it is a
verifiable record of an unverified claim, and the receipt says which), and
the lineage, standing, and taint guarantees are conditional on the
producer's recording discipline - basis, parent, and refs are
producer claims a verifier cannot check against intent
(SPEC §13).
Recording evolution (CLI)
New here? Quickstart: a verifiable receipt from a best-of-N harness takes you from a best-of-N loop to a portable receipt in a few minutes, with both the CLI and the Python package side by side. Delivering work? Quickstart: a delivery receipt a skeptic can check records what was required, what was produced, and who judged it, and shows the claim rejected on replay when the checks did not pass.
The same binary records the v0.3 evolution kinds against a persistent log,
then bundles the log into a receipt - the whole record -> receipt -> validate
loop is CLI-only, no language binding required. rules init writes a starter
rule set so you never hand-author one. Each recording command commits one
record and prints its id; --json prints { id, result, reason? } that
round-trips, so pipelines can chain ids without scraping text.
bellbook rules init --author <id>:<role>... [--admin <id>]... [--reaffirmer <id>]...
[--max-context <n>] [--out <file>]
bellbook request add --log <dir> --rules <file> --author <id> --objective <s>
bellbook requirement add --log <dir> --rules <file> --author <id> --request <id> \
--key <s> --description <s> [--optional] [--expected-evidence <s>]
bellbook candidate add --log <dir> --rules <file> --author <id> \
--git-tree <oid> [--artifact <scheme>:<digest>[:<name>]...]
[--continues <sel> --parent <cand>
| --derives-from <id>... | --upgrades <cand>]
bellbook eval add --log <dir> --rules <file> --author <id> \
--candidate <id> --criterion <s> (--passed | --failed | --score <v> --scale <n>
| --blocked | --insufficient | --stale | --not-run)
[--evaluator <id> --basis recomputed|declared
[--requirement <id>...] [--artifact <scheme>:<digest>...]]
bellbook select --log <dir> --rules <file> --author <id> --objective <s> \
--consider <id>... (--choose <id>... --uses-eval <id>... | --none) [--replaces <sel>]
bellbook retract --log <dir> --rules <file> --author <id> \
--target <record-id> --reason <text> # receipt reports Tainted from then on
bellbook lineage --log <dir> --rules <file> <id> [--json]
bellbook query <name> [<id>|<objective>] # descent|descendants|siblings|frontier
(--log <dir> --rules <file> | --receipt <file>) # |standing|evidence|selected
bellbook export --log <dir> --rules <file> [--out <file>] # log -> receipt
The grammar above shows the load-bearing flags; optional ones
(--git-commit, --algo, --manifest, --note, --procedure,
--uses, --rationale, --provenance, --evaluator-version,
--procedure-hash, --input-hash, and --json on every command) are
omitted for brevity. Run bellbook with no arguments for the full usage.
eval add writes the extended evaluation (bellbook.evaluation.v2: who
decided, with what procedure, over what input, judging which artifacts
against which requirements, with the fail-closed outcomes) when
--evaluator and --basis are given, and the v1 shape otherwise; query
reports a node's artifact identities and requirement bindings where
present.
The log is single-writer by design. LogWriter takes an exclusive
lock on the directory for the life of the process, so exactly one
recording process may hold a log at a time; a second concurrent writer
fails to open rather than corrupting the log. This is deliberate: parallel
candidate generation is the intended workload, parallel recording is
not. Generate candidates concurrently, then record them serially from one
process (a loop over the commands above, or checked_batch_commit for
retry-safe batches). The CLI is not a coordination layer, and --upgrades
refuses to record a binding upgrade whose --git-tree differs from its
target's, so a rebinding never silently changes the source identity.
For the evolution semantics end to end - a benchmark found broken, the compromise it casts over a line of work, and the one-record recovery - run the flagship worked example:
cargo run --example broken_benchmark
It records candidates, evaluations, and selections across several
generations, retracts the broken benchmark, and prints the replay report's
standing section changing from sound, to a compromised descendant line, to
restored (with the retraction and taint permanently on the record).
Python
The same core is available from Python. Prebuilt wheels (Linux, macOS, Windows) are on PyPI, so no Rust toolchain is required:
pip install bellbook
bellbook.validate(bytes) and bellbook.read(bytes) reach the exact
Clean / Tainted / Invalid decision the Rust verifier does - over the same
core, never a reimplementation - and bellbook.Writer records the v0.3
evolution kinds and the v0.4 requirement binding (requests, requirements,
artifact-bound candidates, extended evaluations) to a log and exports a
receipt that can declare the profiles it claims. See
bindings/python/ for the full API.
Status
The published release is 0.11.0, implementing spec v0.4 (RFC-0003:
requirement binding - the Requirement record, first-class artifact
identity, the extended Evaluation, and receipt profile declarations -
on top of the v0.3 evolution semantics: Candidate, Evaluation, and
Selection records with replay-derived lineage standing) and publishing
three profiles, bellbook-core-v1, delivery-receipt-v1, and
bellbook-core-signed-v1. SPEC.md is the
authority for what each version means (v0.3 design notes in
spec/v0.3-delta.md; v0.4 design in
RFC-0003). Earlier epochs stay
valid: a 0.4 validator replays a v0.3 receipt under the v0.3 schema set
and reaches the identical decision (the v0.3 vectors and corpus are
byte-frozen and re-checked in CI, including under the published 0.7.0
binary), and 0.2.0 implementing spec v0.2 stays published and frozen as
the validator for v0.2 artifacts. What 1.0 promises about the wire
format, the crate, the CLI, and the Python package, and how anything
promised can change, is docs/STABILITY.md.
It ships exactly what is implemented and tested today: the
content-addressed (JCS-canonical) record model, the deterministic verifier
with replayable verify_log (identity-to-role binding, authority binding
and revocation, single-use exact approvals, explicit request lifecycle,
advisory plan consistency checks), Ed25519 signatures, retraction with
taint, the v0.3 evolution kinds (Candidate / Evaluation / Selection) with
source binding, the v0.4 requirement binding (Requirement,
ArtifactRef, the extended Evaluation with fail-closed outcomes,
receipt profile declarations), the selection and reaffirmation rule
battery, and the
replay-derived standing section, derived state with incremental/full-build
equivalence, checkpoints, the crash-safe writer with idempotent
compare-and-append, and portable receipts with the offline bellbook validate CLI (including the bellbook-core-v1 baseline and
delivery-receipt-v1 profile checks, with declared profiles evaluated
unasked), the
request/requirement/candidate/eval/select/retract/lineage
recording commands, the read-side query command (the RFC-0002 named set: descent,
descendants, siblings, frontier, standing, evidence, selected - over a log
or a receipt), and rules init / export to generate a starter rule set and
bundle a log into a receipt - fully tested (every rejection reason code has a
triggering test), clippy-clean, no unsafe, no panics in library code.
The repository also carries a language-neutral conformance corpus
(spec/conformance/v0.4/: record, malformed, receipt-replay, standing,
and named-query cases, run by tests/conformance.rs; the frozen
spec/conformance/v0.3/ and spec/conformance/v0.2/ corpora stay valid
under their own epochs' rules and are re-checked in CI) and an
independent Python implementation of the verifier and the named query
set (conformance/python/) that shares no
code with this crate, recomputes every record id, and re-derives every
verdict, standing section, and query answer across the corpus, agreeing
with this reference on every case - including the deliberately malformed and forged
inputs it must reject.
The repository also publishes three profiles, each a content-addressed
clause table with its own vectors under spec/profiles/, re-derived by
the Python validator and reported alongside the verdict, never changing
it: bellbook-core-v1, the shared minimum rule shape for comparing
receipts across organizations
(docs/profiles/bellbook-core-v1.md);
delivery-receipt-v1, the grammar of a delivery claim - every
required requirement judged passed by a distinct, fully bound evaluator
over evidence the record carries for the claimed candidate, under the
baseline, standing at the head - with a fraud battery of one rejecting
vector per clause
(docs/profiles/delivery-receipt-v1.md);
and bellbook-core-signed-v1, the signed tier above the baseline -
signatures required on every evolution kind, every such author key-pinned,
every evaluation a selection rests on attested - reached from the baseline
by adding signatures and switching evaluation schema ids
(docs/profiles/bellbook-core-signed-v1.md).
Open work, not implemented (sequenced in RFC-0003 and SPEC §12.2):
PolicyDecisionrecord +bellbook-policy-enforced-v1profile - first-class capture of external policy-engine permit/deny decisions, kept strictly separate from Bellbook's own Verdicts, followed by a reference adapter for an open-source authorization engine (see docs/ECOSYSTEM.md).
The Python bindings (pip install bellbook) and the outward standards
mapping (docs/STANDARDS.md: OpenTelemetry, W3C PROV,
in-toto, SCITT) have since shipped.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.