bellbook 0.2.0

Tamper-evident, replay-verifiable records of captured agent activity: content-addressed typed records, deterministic verdicts, offline receipt validation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
# Bellbook specification

**Spec version: 0.2.** This document is versioned independently of
the `bellbook` crate; the crate's CHANGELOG states which spec version each
release implements (see §14).

This document is **normative**: conformance is defined by this
specification, not by any implementation. Where the reference
implementation or its tests disagree with this document, that is an
implementation bug (or, if the document is wrong, a spec bug to be fixed
by a documented spec change) - implementations never redefine meaning.
Please file an issue for any divergence found.

## 1. Model

Bellbook has one durable primitive: a typed **Record** in an append-only
**Log**. A single-writer **commit protocol** appends each record together with
a deterministically derived **Verdict**. A **Verifier** can replay any log
from genesis (or from a **Checkpoint**) and confirm every rule below. Derived
**State** is a pure fold over accepted records and is reproducible by anyone
holding the log.

Trust boundary: everything *proposing* records (LLMs, tools, humans, the host
runtime) is untrusted. Truth is only what committed to the log and what the
verifier derives from it.

## 2. Records

```
Record {
  id:       Hash256   // SHA-256 of the canonical form (see §3)
  space:    Hash256   // trust domain; refs never cross spaces
  thread:   Hash256   // conversation/work grouping
  time:     u64       // logical counter, strictly time == prev + 1
  author:   { id, type, signature? }
  kind:     Kind
  schema:   Hash256   // SHA-256(utf8(schema name)), e.g. "bellbook.request.v1"
  data:     bytes     // JSON payload for the schema
  refs:     [ { type, target: RecordId } ]  // sorted, deduped
  evidence: Deterministic | Verified | Reported | Inferred | Assumed
}
```

**Kinds:** `Request`, `Action`, `Response`, `Result`, `Summary`, `Approval`,
`Capability`, `Usage`, `Refusal`, `Verdict`, `Plan`, `Retraction`.

**Author types:** `User`, `Provider` (LLM), `System`, `Executor` (tool),
`Verifier` (verdicts only).

**Normative kind-to-author-type table.** The verifier enforces which
author types may produce each kind; a violation rejects with
`AuthorRoleInvalid`. Signatures cannot substitute for this check: a
validly signed actor could still declare a forbidden author type or emit
a forbidden kind.

| Kind | Allowed author types |
|------|----------------------|
| `Request` | `User` |
| `Action`, `Response`, `Summary`, `Plan` | `Provider` |
| `Result` | `Executor` |
| `Approval` | `User` |
| `Capability` | `User`, `System` |
| `Usage` | `Provider`, `System` |
| `Refusal` | `User`, `System` |
| `Retraction` | `User`, `Provider`, `System` |
| `Verdict` | `Verifier` |

Rationale: authority-granting and authority-exercising roles must never
coincide. Capabilities and approvals come from the human principal (or,
for capabilities, deployment configuration), never from the governed
agent or its executor; results come only from the executor that ran the
tool; retraction is open to every accountable party (an agent retracting
its own wrong claim is behavior the model rewards) but never to the
`Executor` or `Verifier`, whose records are attestations that others
retract.

**Identity-to-role binding.** The declared `author.type` is
adversary-controlled - a governed agent could simply *claim* `User` on an
Approval - so the table alone prevents accidents, not impersonation.
`VerifierRules::author_roles` binds actor ids to roles: a registered
actor's records must declare exactly the registered role, and for
**every kind except `Verdict`** the author MUST be registered
(`AuthorRoleInvalid` otherwise) - an unregistered actor claiming `User`
could otherwise close requests with Refusals, inject Summaries into
context, skew Usage feedback, or retract records. `Verdict` stays on
its deterministic special path.
Combined with `author_keys` (§3.2), the identity itself becomes
cryptographic: a pinned actor's records must be validly signed under a
pinned key (unsigned records claiming that identity reject with
`SignatureMissing`), the key proves who signed, and `author_roles`
proves what that identity is allowed to be. A pinned Provider key
signing an Approval that declares `User` rejects. Without pinned keys,
role registration binds the id to a role but does not authenticate who
supplied the id - pin keys for every authority-bearing actor in
production.

**Retraction ownership.** A `Retraction` is valid only when its author is
the target's author, or when the retractor is listed in
`VerifierRules::admin_retraction_actors` (an explicit administrative
override, e.g. the human principal). One actor declaring another's
record wrong is contrary evidence or a `Refusal` - never a retraction,
which has operational teeth (§7.1).

**Ref types:**
- `Cause` - this record exists because of the target (Result → Action,
  Verdict → subject, delegated Request → parent).
- `Use` - the target's content was used as input. A `Use` ref may name a
  rejected, retracted, or tainted record (an actor may genuinely have
  consumed bad input), but such a target contributes floor evidence
  (`Assumed`) to derivation (§7).
- `Require` - the target must be accepted state for this record to be valid
  (Action → Capability/Approval). Enforced generically for every kind: a
  `Require` ref whose target is rejected, retracted, or tainted rejects
  the record with `RefUnresolved`.
- `Replace` - this record supersedes the target. The target is never deleted;
  it enters `state.replaced_records`. Only `Summary`, `Capability`,
  `Approval`, and `Plan` records may be replaced, and only by a record of the
  same kind with a compatible payload.

Payload structs for every kind live in `src/record/payloads.rs`; frozen
schema-name constants in `src/base/schema.rs`.

## 3. Identity and hashing

- Canonical id form = the record with only `id` omitted, serialized as
  **RFC 8785 (JCS)** canonical JSON: object members sorted by the UTF-16 code
  units of their names, no whitespace, minimal JCS string escaping, and
  ECMAScript number formatting. A completed `author.signature` is included;
  for unsigned records the absent signature field is omitted, preserving one
  canonical unsigned form.
  `id = SHA-256(canonical id form)`. An independent JCS implementation in any
  language computes byte-identical canonical forms and therefore identical
  ids.
- Integers must stay within the I-JSON safe range (|n| ≤ 2^53 − 1); the
  serializer errors rather than lose precision. Hash-valued fields
  serialize as JSON arrays of 32 byte values; payload `data` serializes as
  a JSON array of byte values.
- **Canonical payloads:** a record's `data` MUST be exactly the JCS
  canonical serialization of its schema's payload type. The verifier
  decodes the payload and re-encodes it canonically; any byte difference
  rejects with `InvalidPayload`. This uniformly rejects duplicate keys,
  unknown fields, non-canonical member order, whitespace, and
  non-canonical number spellings - so two conforming verifiers can never
  reach different verdicts through permissive-parsing differences.
- Refs are sorted by (ref-type ordinal, target bytes) and deduplicated
  **before** hashing.
- Schema ids are `SHA-256(utf8(name))` of frozen names, e.g.
  `bellbook.request.v1`.

Because ids are content addresses and refs point at ids, the log is a DAG in
which any mutation of history invalidates every dependent record.

### 3.1 Test vectors

[`spec/test-vectors-v0.2.json`](spec/test-vectors-v0.2.json) contains one
record of every kind from a fixed unsigned scripted log: the record's exact
canonical id form (the JCS bytes fed to SHA-256, with `id` omitted and the
absent signature field omitted) and the resulting id, plus the UTF-8 names the
space/thread/scope hashes derive from. It also contains a deterministic signed
Request with its secret-key seed, public key, signing form, signature, completed
canonical id form, and final id, plus a valid alternate-key signature and id.
The substitution case demonstrates that changing signers changes identity;
substituting only the key on the original envelope fails verification. A
third-party implementation conformance-tests canonicalization, signing, strict
verification, and hashing without running Rust. The vectors are regenerated
only on an intentional format change (a new spec version); the crate's test
suite fails if the implementation drifts from the committed vectors.

### 3.2 Signatures

Records may carry a detached **Ed25519** (RFC 8032) signature in
`author.signature`:

- The signature covers the canonical, domain-separated **signing form**:
  `{"domain":"bellbook.record-signature.v0.2","record":<record form>}`,
  where `<record form>` is the record with `id` and `author.signature`
  omitted and the whole envelope is serialized as JCS. The explicit protocol
  and spec-epoch domain prevents a signature made for another protocol or
  Bellbook epoch from being replayed as a v0.2 record. After signing, the
  record id is computed from the canonical id form (§3), which includes the completed
  signature. This avoids a circular dependency while ensuring signature
  removal or substitution changes the record id and every dependent ref and
  head attestation.
- `signature.key_id` is the signer's public key as exactly 64 lowercase
  hex characters (self-describing - a verifier needs no side channel to
  check integrity). `signature.sig` is the 64 signature bytes. Lowercase
  is the only accepted spelling: uppercase or non-hex `key_id` content is
  `SignatureInvalid`, so conforming verifiers agree byte-for-byte on what
  verifies.
- Verification is **strict** (`verify_strict` semantics: non-canonical
  point encodings and weak keys are rejected), so conforming
  implementations agree on the same accept/reject boundary.
- Which kinds *require* a signature is rules-configurable
  (`signature_required_kinds`; empty by default; must not include
  `Verdict` - verdicts are materialized unsigned by the commit protocol).
  **In addition, a key-pinned actor's records always require a
  signature**, regardless of kind: an actor id is just a string anyone
  can write, so an unsigned record claiming a pinned identity would
  bypass exactly the authentication pinning exists to provide. A
  required-but-absent signature rejects with `SignatureMissing`. Any
  *present* signature, required or not, must verify - else
  `SignatureInvalid`.
- Key→identity binding: `author_keys` maps an actor id to the set of
  public keys it may sign with. For listed actors, a signature under any
  other key is `SignatureInvalid`. Unlisted actors may sign with any key
  (the signature then proves integrity and key possession, not identity).
  Key generation, storage, and rotation are host concerns. Pair with
  `author_roles` (§2) to bind the identity to a role: the key proves who
  signed, the role registration proves what that identity may author.

Why Ed25519: deterministic signatures (no nonce-reuse catastrophes),
small keys and signatures (32/64 bytes), fast verification, ubiquitous
library support in every ecosystem a third-party verifier would be
written in, and standard in the transparency-log ecosystems external
anchoring targets (§11.1).

## 4. Verification

### 4.1 verify_record

`verify_record(record, prior, rules, state) -> VerdictData` deterministically
judges one record against the committed prefix. Checks include: id
recomputation, space match, schema→kind consistency against the frozen map,
author role against the normative table (§2), ref ordering (refs must be
strictly sorted by (ref-type ordinal, target) and
deduplicated, so equal content always yields equal ids),
ref resolution (targets must exist, in the same space), replacement validity,
evidence must equal the derived evidence (§7), canonical payload
round-trip (§3), and per-kind rules:

- an `Action` must name an active `Request` **in whose scope it
  operates** (`ActionData.scope == RequestData.scope` - a capability
  held for another scope must not let a provider serve a request with
  out-of-scope actions) and resolve a non-retracted,
  non-tainted `Capability` (`Auto`, or `Ask` plus an unexpired,
  non-retracted `Approval` - exact, class, or wildcard, in that
  priority), **and must carry `Require` refs naming the exact capability
  and (for Ask) the exact approval that authorize it** - so the audit
  graph shows which authority allowed each action, and retracting that
  authority taints the action (`AuthorityRefMissing` otherwise). The
  exact-approval target is `SHA-256(canonical((action_author_id,
  ActionData)))` - it binds the acting author together with the action
  content, so one actor's approval never authorizes another actor's
  byte-identical action - the matched approval's `actor_id` must equal
  the acting author and its declared `scope` must equal the action's
  scope (an approval must not visibly claim one scope while authorizing
  another through its hash), and **exact approvals are single-use**: an accepted
  action consumes the approval (it leaves `valid_approvals`), so
  repeating the identical action needs a fresh approval. Class approvals
  stay reusable - that is their explicit purpose;
- a `Result` must close an open `Action` with a matching exec-mode
  schema; an external result must additionally be signed by an author
  with pinned keys (§7);
- a `Response`'s `turn_index` must equal the count of previously accepted
  responses for its request (gap-free, in order), and
  `closes_request = true` is only valid when the request has no open
  actions **and no running plan** (a plan must reach Completed or
  Abandoned first - a final response must not silently strand in-flight
  work);
- an `Approval` must set exactly one of `target_action`/`action_class`,
  and the exact form must declare its subject `actor_id`;
- a `Summary` must carry at least one `Use` ref to its sources (a claim
  with no sources is unfounded, and sources are epistemic dependence);
- a `Usage` record's payload `actor` must equal its envelope author, and
  its consuming record must be an accepted Result/Refusal in the same
  thread;
- `Plan` task graphs must be acyclic, `inputs_from`/`depends_on` must
  name real tasks, and a task's `result_record_id` - allowed only on
  `Done`/`Failed` tasks - must resolve to an accepted `Result` whose
  action belongs to the plan's request. **Plans are advisory
  orchestration metadata, not compliance proof**: `result_record_id` is
  a *related result* (supporting evidence), not task-to-proof binding -
  there is no task id in `ActionData`, so the verifier cannot bind a
  specific task to a specific action, and it does not check task/result
  status agreement or citation uniqueness. The checks above keep plans
  internally consistent and their citations real; they do not make a
  Completed plan a proof object;
- a `Request`'s parentage is unambiguous: `parent_request_id: None`
  means zero Request `Cause` refs, and a declared parent means exactly
  one `Cause` ref naming exactly that parent - contradictory delegation
  graphs (undeclared, surplus, or mismatched parents) reject;
- a `Retraction` must Cause-ref an accepted record that is neither a
  `Verdict` nor another `Retraction`, and its author must own the target
  or be a configured administrator (§2, §7.1).

`VerdictData { result: Accept | Reject, reason: Option<ReasonCode> }` with
reason codes:

`UnknownSchema`, `KindSchemaMismatch`, `SignatureMissing`, `SignatureInvalid`,
`RefUnresolved`, `RefCrossSpace`, `RequestMissing`, `CapabilityMissing`,
`CapabilityDenied`, `ApprovalMissing`, `ApprovalExpired`, `ActionClosed`,
`ReplacementInvalid`, `ExternalReceiptRequired`, `EvidenceBelowThreshold`,
`Refused`, `InvalidPayload`, `InvalidCheckpoint`, `AuthorRoleInvalid`,
`AuthorityRefMissing`.

Signature checks follow §3.2: `SignatureMissing` for a required-but-absent
signature, `SignatureInvalid` for any present signature that fails strict
Ed25519 verification or was made with a key not pinned for its actor.

### 4.2 verify_log

`verify_log(records, rules, checkpoint?) -> LogVerdict` replays a whole log:

1. If a checkpoint is given, every field is validated (`InvalidCheckpoint`
   on any mismatch) **before anything else, including the empty-log
   case**: `log_length` must fit the actual log, so a records slice
   shorter than the checkpoint's coverage (including an empty slice,
   i.e. attested history was deleted) rejects; `log_hash` must
   equal the recomputed `SHA-256(concat(ids))` over the prefix; the boundary
   must not split a subject/verdict pair; and `last_time`,
   `last_record_id`, and `state_hash` must agree with the verified prefix
   and the state rebuilt from it.
2. Enforce strictly gap-free logical time: `records[i].time ==
   records[i-1].time + 1`, and the first record of a non-empty log at
   `time == 1` unconditionally. A checkpoint covering an empty prefix
   grants nothing: replay starts at genesis and the genesis-time rule
   applies.
3. Recompute every record's `id` from the replay start point onward.
4. From the replay start point onward, require every non-verdict record
   to be **immediately followed** by its verdict, and verify the verdict
   record's own envelope in full - a
   forged log can put anything in a verdict record: id recomputes;
   verdict schema and `Verifier` author type; **no signature** (verdicts
   are deterministic verifier output with no external signer, so every
   conforming implementation must materialize the same unsigned envelope;
   a present signature rejects, and would be included in the id);
   space equals the verifier's space; evidence is `Deterministic`;
   payload decodes as `VerdictData`;
   exactly one ref, a `Cause` edge to a resolving prior non-verdict
   subject in the same space and thread (an unresolved subject rejects
   with `RefUnresolved`, never passes); at most one verdict per subject.
5. **Re-derive each verdict** with `verify_record` against the replayed state
   and compare with the stored verdict - stored verdicts after the replay
   start are checked, never trusted. Records inside a checkpoint prefix are
   attested by the prefix hash rather than re-derived; because checkpoints
   must align to pair boundaries, every verdict after the replay start is
   always re-derived.
6. Fold accepted records into `State` as it goes.

## 5. Commit protocol and crash recovery

`LogWriter::open` takes an exclusive file lock (`.lock`), replay-verifies
the complete prefix under the supplied rules, recovers an interrupted tail,
re-verifies the result, and restores a private time counter. Raw storage and
intent machinery are not public APIs: callers receive read-only record access
through the locked writer, while durable writes go through `LogWriter`.
`LogWriter::open` rejects a file larger than 64 MiB before reading it and
enforces the same bound before append. Each commit reserves capacity for its
complete subject/verdict pair before either frame is written;
`open_with_max_bytes` lets a host opt a trusted larger log into an explicit
limit.

`commit(proposal, rules, state)` first requires the rules to match those used
at open and the supplied derived state to equal the state rebuilt from the
current log; `RulesMismatch` or `StateMismatch` is returned before any
write. It then:

1. Derive evidence from the proposal's schema and its refs' evidence (§7).
2. Without advancing logical time, materialize the complete pair: assign the
   next two times, sort/dedup refs, attach the optional writer-produced
   signature over the signing form, compute the final subject `id`, run
   `verify_record` against the prior prefix, and materialize its deterministic
   verdict (`Cause` → subject).
3. Preflight serialization and reserve file capacity for **both** frames.
4. Write intent file (`.intent`, `written: false`), fsync.
5. Append the subject record, fsync; update intent to `written: true`.
6. Append the already-derived verdict record, fsync.
7. Clear the intent file and apply subject + verdict to in-memory `State`.
8. Publish the two consumed logical times only after the durable pair and
   state fold succeed.

Any error before the durable phase leaves the handle reusable and consumes no
logical time. Once the intent is durable, an error makes that handle return
`RecoveryRequired` on all later write attempts; the caller must drop and reopen
it so open-time recovery can inspect and repair the tail.

Recovery on open occurs only after the complete prefix has replayed
successfully: **the log tail is the final recovery authority, never the
intent file.** A commit appends the fsynced subject first and its
verdict second, so the only interrupted-commit signature is a trailing
non-verdict record; whenever the final complete record is not a Verdict,
its verdict is recomputed and appended - regardless of whether `.intent`
is present, absent, empty, or torn (a crash can leave any of those, and
none may change the outcome). The intent file is a crash-marker only and
is cleared after recovery. Intent updates are atomic and durable
(temp-file write, fsync, rename, directory fsync where the platform
supports it), so a crash mid-update leaves the old intent or the new
one - never a truncated file. A torn trailing frame in the log file
(from a crash mid-append) is truncated away on open, so subsequent
appends continue from the last complete record. Frames are refused
before they can overflow the u32 length prefix (`RecordTooLarge`), and
frame-boundary arithmetic is checked so hostile lengths cannot overflow a
platform `usize`.

`batch_commit` orders proposals by the SHA-256 of their canonical form before
committing, so batch commit order is deterministic and independent of caller
order. It commits those pairs sequentially: every subject/verdict pair has the
failure-atomic guarantee above, but the batch as a whole is not transactional.
If a later pair errors, earlier pairs remain durable. Hosts that may retry use
the compare-and-append contract below rather than blindly retrying
`batch_commit`.

### 5.1 Appender contract: idempotent compare-and-append

Serious hosts crash-retry, and duplicates in an append-only ledger are
permanent pollution - so appends MUST be idempotent. The contract:

- The appender supplies the **expected parent head** - the id of the last
  record the batch was built against (all zeros for an empty log).
- If the log is at that head, the batch commits normally.
- If the identical batch - recognized by content, in deterministic batch
  order - already landed immediately after the expected head, the call is
  a **success no-op returning the same resulting head** (and the same
  per-record verdicts, read back from the log) as the original append,
  even if unrelated records were appended afterwards. A crash mid-batch
  leaves a batch prefix landed; a retry recognizes the prefix and commits
  only the remainder, converging on the same head.
- Anything else - the log moved to a different head with records that are
  not this batch, or an unknown expected head - is a **conflict**
  (`HeadConflict`), never a duplicate append. The appender then rebuilds
  its batch against the current head.

The library affordance is `LogWriter::checked_batch_commit(expected_head,
proposals, rules, state)` with `LogWriter::head()` as the token source.
A retry must resend the identical batch. The caller's `state` must
reflect the current log (rebuild via `build_state_unchecked` after
reopening); the writer checks exact equality before both normal and no-op
appends, and remains bound to the rules used at open.

## 6. Storage format (`persist` feature)

`records.log` is a flat file of length-prefixed frames: `u32` big-endian
length followed by the record's canonical JSON. Sidecar files: `.lock`
(exclusive writer lock), `.intent` (commit intent). The full log is held in
memory with an id → position index; `scan(from, to)` returns records by
logical-time range.

## 7. Evidence

Evidence is a five-class ordered lattice describing how a record's content
is known, strongest → weakest:

| Class || Meaning |
|-------|---|---------|
| `Deterministic` | proven | Derived by the verifier itself (Verdict records). |
| `Verified` | attested | A signed attestation from a key-bound external party (see below - what is verified is the attestation's origin, never the real-world effect). |
| `Reported` | - | An external party (user, provider, executor, host) asserted it. |
| `Inferred` | - | Derived by reasoning from other records. |
| `Assumed` | - | Proceeded on an unverified assumption. |

Base evidence by schema - every frozen schema is classified explicitly (an
exhaustive mapping; adding a schema requires classifying it):

- `bellbook.verdict.v1``Deterministic`
- `bellbook.result.external_receipt.v1``Verified`
- `bellbook.request.v1`, `bellbook.action.v1`, `bellbook.response.v1`,
  `bellbook.result.v1`, `bellbook.result.effect_confirmation.v1`,
  `bellbook.capability.v1`, `bellbook.approval.v1`, `bellbook.refusal.v1`,
  `bellbook.usage.v1``Reported`
- `bellbook.summary.v1`, `bellbook.plan.v1``Inferred`

No core schema has base `Assumed`; it is the floor, reserved for
host-declared assumptions and for evidence degradation. Unknown schemas
(rejected with `UnknownSchema` regardless) map to `Assumed`, never to a
stronger class. `bellbook.result.effect_confirmation.v1` is deliberately
`Reported`, not `Verified`: it asserts an observation the verifier cannot
check, which is exactly the Reported class.

**What `Verified` means for external results.** The
`bellbook.result.external_receipt.v1` schema earns its `Verified` base
only because the verifier enforces that such a record is a **signed
attestation from a key-bound executor**: it must carry an Ed25519
signature (verified strictly, §3.2) and its author must have pinned keys
in `author_keys` - otherwise it rejects (`SignatureMissing` /
`SignatureInvalid`). What is verified is that the named executor really
produced this attestation about this action - never that the claimed
real-world effect held, which no log-level verifier can check.
Verification of receipt *content* against external systems (issuers,
transparency logs, in-toto/SCITT-style statements) is a host or profile
concern layered on the `output` payload.

Effective (derived) evidence = weakest of (base, evidence of every record
referenced by a **`Use` or `Require`** ref), where a rejected, retracted,
or tainted target contributes the floor (`Assumed`) - depending on
invalid or withdrawn content is an unverified assumption. `Cause` and
`Replace` refs
are provenance, not epistemic dependence: exactly as they do not
propagate taint (§7.1), they do not affect derivation - a Result exists
*because of* its Action and truthfully reports what the tool returned
without resting on the action's claim. The verifier rejects records whose
stored evidence differs from the derived value, so evidence can never be
inflated: a summary over reported inputs is at best `Inferred`; anything
resting on an `Assumed` input is `Assumed`.

**Evidence thresholds.** `VerifierRules::evidence_thresholds` maps a Kind
to a minimum evidence strength. A record whose derived evidence is weaker
than the threshold configured for its kind is rejected with
`EvidenceBelowThreshold`. Policies like "the highest-confidence claims may
only rest on proven/observed inputs" are expressed as a threshold (e.g.
`Summary → Verified`), not as special-case rules. No thresholds are
configured by default.

### 7.1 Retraction and taint

A `Retraction` (`bellbook.retraction.v1`) asserts that an accepted
record's content was **wrong, and nothing replaces it**. This is a
distinct kind rather than a payload on the `Replace` machinery - a
deliberate design decision: `Replace` expresses *supersession* (a
same-kind record with a compatible payload identity takes over the slot,
and only `Summary`/`Capability`/`Approval`/`Plan` are replaceable), so it
cannot express negation-without-successor, and it cannot target a
`Result` at all - the paradigm retraction case.

Rules:

- A Retraction carries `RetractionData { target_id, reason }` and exactly
  one `Cause` ref to `target_id`. The target must be an accepted record in
  the same space (any thread); it may be of any kind except `Verdict`
  (the verifier's own deterministic output is not retractable - dispute
  the *subject*, not the judgment) and `Retraction` (retraction is not
  un-assertable; contrary evidence is a new record, not an undo).
- Retraction is **append-only**: nothing is edited or deleted. The target
  stays in the log; its id enters `state.retracted_records`.
- **Taint** propagates forward through the DAG to dependents via `Use`
  and `Require` refs, transitively. `Cause` refs do **not** propagate
  taint: causation is provenance, not epistemic dependence - a `Result`
  exists because of its `Action` and truthfully reports what the tool
  returned even if the action's stated intent proves wrong; a record that
  *rests on* another's content must say so with a `Use` (or `Require`)
  ref. `Replace` refs likewise do not propagate (the replacement stands
  on its own content).
- Taint is implemented **on the evidence-derivation engine**: for records
  committed after the retraction, a `Use`/`Require` ref to a retracted or
  tainted record contributes the floor class (`Assumed`) to weakest-link
  derivation - taint *is* evidence degradation. Records committed before
  the retraction have immutable stored evidence, so they are surfaced via
  `state.tainted_records` instead (maintained through the reverse
  epistemic-dependence index `state.epistemic_dependents`).
- **Replay of a tainted chain still passes.** Taint marks claims
  unreliable; it never makes honest history unverifiable. `verify_log`'s
  report carries `retracted_records` and `tainted_records` (populated on
  Accept), so a consumer distinguishes three outcomes: *clean* (Accept,
  empty sets), *tainted* (Accept, non-empty sets), *invalid* (Reject).
- **Retracted authority is deactivated operationally, not just marked.**
  Retracting a `Capability` or `Approval` removes it from the active
  authority maps, and the verifier additionally refuses to let a
  retracted or tainted authority record authorize a new `Action` - a
  grant whose content was asserted wrong must stop granting, so
  governance state and epistemic state always agree. (Planned
  supersession is still `Replace`; expiry is still expiry - retraction is
  the "this was wrong" path.) For non-authority kinds, retraction leaves
  operational slots (open actions etc.) untouched: it is an epistemic
  marker, and closing an open action is what `Result`/`Refusal` are for.
  Retracted records are excluded from context selection (§9); tainted
  records are not.

Worked example: an `Action` runs, its `Result` reports success, and a
`Summary` is committed with a `Use` ref to that result ("the change is
deployed and working"). Later the real-world outcome contradicts the
result - the change was reverted. The host appends a `Retraction`
targeting the result. The result's id enters `retracted_records`; the
summary, reached through the `Use` edge, enters `tainted_records`. The
log still replays Accept - the history of what happened is intact - but
any consumer of the report knows the summary's claim no longer rests on
anything. A later summary that tried to `Use` the retracted result would
derive `Assumed` evidence and could be rejected outright by an evidence
threshold.

## 8. State

`State` is a pure fold over (record, verdict) pairs - rejected records change
nothing. It tracks accepted ids, active requests, open actions and their
per-request counts, active capabilities/approvals/summaries/plans, replaced
records, usage counts, and the epistemic sets from §7.1: retracted record
ids, tainted record ids, and the reverse `Use`/`Require` dependence index
that lets a late retraction taint dependents committed before it. Invariant: incrementally applying each pair
(`apply_record`) yields a `State` identical to rebuilding from scratch
(`build_state_unchecked`) for any log.

`State` serializes to valid JSON (and canonicalizes under JCS): maps whose
keys are not strings - tuple and hash keys - serialize as sorted sequences
of `[key, value]` pairs.

Request lifecycle: a request leaves `active_requests` only on an
**explicit terminal event** - a `Response` with `closes_request = true`
(valid only when the request has no open actions) or a `Refusal`
targeting the request itself. Completion is never inferred from a
transient zero count of open actions: sequential workflows (action →
result → next action) and plan updates on the same request remain valid
until the request is explicitly closed.

State also tracks the per-request accepted-response count
(`response_turns`, backing the turn-ordering rule) and reverse indexes
from capability/approval record ids to their lookup keys
(`capability_index`, `exact_approval_index`, `class_approval_index`), so
a retraction deactivates authority in O(log n) without scanning.

## 9. Context

`build_context(records, state, rules, thread)` deterministically selects the
working set shown to an untrusted proposer: accepted, non-replaced,
non-retracted, **non-tainted**, non-verdict records of one thread, newest
first (ties broken by id), capped at `rules.max_context_records`, plus
usage-feedback counts for the selected records. Retracted records are
excluded because their content was asserted wrong; tainted records
(unreliable, resting on retracted content) are excluded **by default** -
the safe behavior is not opt-in. A host that wants them anyway uses
`build_context_with(..., ContextPolicy::IncludeTainted)`, and the
returned `Context::tainted_records` identifies which selected records
are tainted so they can be labeled.

## 10. Checkpoints

`Checkpoint { log_length, last_time, last_record_id, state_hash, log_hash }`
where `log_hash = SHA-256(concat(record ids))` and `state_hash =
SHA-256(canonical(State))`. `verify_log` can start from a checkpoint after
validating all five fields against the actual prefix (§4.2 step 1);
checkpoint boundaries must align to subject/verdict pairs. Checkpoint
validation precedes every other check: presenting fewer records than the
checkpoint covers - including none at all - rejects with
`InvalidCheckpoint`, so a retained checkpoint detects deletion or
truncation of the history it attests. A checkpoint over an empty prefix
accelerates nothing and exempts nothing (§4.2 step 2).

**Checkpoint trust.** A checkpoint skips verdict re-derivation and
per-kind rule checks for its prefix - that is the acceleration. Prefix
record ids are still recomputed (content-binding), but forged verdicts
inside the prefix are *attested by the checkpoint, not detected*. The
**only** source of checkpoint trust is a prior successful replay
verification of the prefix under the exact same rules. The API enforces
this with an opaque `TrustedCheckpoint` type that `verify_log` requires:
it cannot be deserialized from the wire and is produced either by
`TrustedCheckpoint::from_verified_log` (runs full verification, succeeds
only on Accept) or by the explicit, greppable
`TrustedCheckpoint::assume_verified` assertion (for re-hydrating a
checkpoint that an earlier verification produced and that was stored
where the ledger's writer cannot rewrite). The checkpoint is bound to the
rules it was verified under; a rules mismatch rejects with
`InvalidCheckpoint`.

External anchoring (§11.1) is **not** a trust path. An anchored
attestation proves particular bytes existed at a point in time and were
not subsequently changed - it says nothing about whether those bytes
ever passed verification, and an attacker can anchor a forged history.
Anchoring protects an *already-verified* checkpoint against later
rewriting; it never substitutes for verification. A checkpoint supplied
by the same untrusted party as the records proves nothing, which is why
receipts carry none (§12).

## 11. Threat model

Bellbook is **tamper-evident, not tamper-proof**, and this section states
plainly where the line sits. Overselling tamper-evidence would get the
format copied badly; an integrator has to know exactly what a green
replay does and does not prove.

**What replay verification detects.** Any *interior* edit to committed
history - modifying a record's bytes, deleting or inserting a record,
reordering, forging or altering a verdict - breaks id recomputation,
gap-free logical time, subject/verdict pairing, or verdict re-derivation,
and the log rejects. Content addressing makes every dependent ref break
too.

**What it does not stop.** The ledger's *owner* - anyone with write access
to the storage and no external constraints - can discard the entire log
and rewrite history from genesis: re-propose whatever records they like,
re-run the commit protocol, and produce a fully self-consistent forgery
that replays green. Replay proves internal consistency, not provenance.
Two further honest limits:

- A verifier and producer must agree on `VerifierRules` out of band; a
  verdict re-derivation is only meaningful under the rules the producer
  committed under.
- Acceptance is not truth. An accepted record proves the claim was made,
  in order, under the governance rules - the evidence lattice (§7) is what
  grades how much the *content* can be trusted.

**Mitigations** (integration guidance, deliberately not library code):

1. **Signatures (§3.2).** Pin actors' keys in `author_keys` and require
   signatures for the kinds that matter. A from-genesis rewrite then
   cannot re-forge those actors' records without their private keys.
   Signatures bind authorship, not sequence - combine with anchoring.
2. **External anchoring.** Periodically store the head attestation
   (§11.1) somewhere the ledger's writer cannot rewrite: the host's own
   database, a transparency log, a timestamping service, or simply a
   counterparty's records. A rewrite-from-genesis (or truncation to an
   old prefix) then diverges from the anchored head. Anchoring cadence
   bounds the exposure window: history older than the last anchor is
   bound; unanchored recent history is not.

### 11.1 Head attestation format

The thing an integrator anchors is a fixed, minimal, JCS-canonical
structure, so independent anchoring implementations stay interoperable -
any witness receives exactly these bytes:

```
HeadAttestation {
  head_hash:    Hash256  // SHA-256(concat(record ids)), whole log -
                         // the same computation as Checkpoint.log_hash
  record_count: u64      // records covered (subjects and verdicts)
  spec_version: string   // e.g. "0.2"
  timestamp:    string   // canonical RFC 3339 UTC (`YYYY-MM-DDTHH:MM:SSZ`), host-supplied wall-clock time
}
```

The Rust API represents the wire string as `CanonicalUtcTimestamp`; both
construction and deserialization reject every other spelling or an invalid
calendar value. Canonical bytes = RFC 8785 serialization of the structure
(fields in JCS key order: `head_hash`, `record_count`, `spec_version`,
`timestamp`).
To audit against an anchor, recompute `head_hash` over the log prefix of
`record_count` records and byte-compare. An anchor attests *existence
and immutability* of those bytes - it never attests that they were
verified, and it is never a substitute for replay verification or a
source of checkpoint trust (§10). Witness services, transports,
and anchor storage are host concerns; this spec defines only the format.

## 12. Receipts

A **Receipt** is the portable form of a ledger: a self-contained bundle a
third party validates offline, without trusting the producer.

```
Receipt {
  spec_version: string          // e.g. "0.2"
  rules:        VerifierRules   // what the log was committed under
  records:      [Record]        // full sequence from genesis
}
```

A receipt deliberately carries **no checkpoint**: checkpoint trust must
come from outside the artifact being validated (§10), and a checkpoint
inside an untrusted receipt would let the producer attest their own
forged prefix. `validate(bytes) -> Report` therefore always replays from
genesis, re-deriving everything from the receipt's own bytes: record ids
(JCS canonicalization + SHA-256), gap-free logical time, subject/verdict
pairing with full verdict re-derivation, signature verification, evidence
derivation, and taint status. The `Report` carries
a three-way status - **Clean** (verified, no retracted/tainted claims),
**Tainted** (verified history containing retracted or tainted claims,
each listed by id), **Invalid** (unparseable, unsupported spec version,
or failed replay) - plus the recomputed `head_hash` (compare against an
externally anchored head attestation, §11.1) and `rules_hash` (compare
against rules agreed out of band: acceptance is always relative to the
embedded rules, and a validator cannot know whether those rules are the
ones the parties intended).

Receipt decoding is strict for this version. Unknown fields in the
receipt, rule document, record envelope, author, signature, ref, or typed
payload are structural failures, as are duplicate logical keys in rule
maps and pair-encoded maps. This prevents extension-looking data that the
validator did not actually enforce and gives every conforming validator
one interpretation of the same wire document. Future extensions require
a new spec or schema version rather than silently ignored fields.

**"Clean" is relative to the embedded rules.** Under default rules no
signatures are required and no evidence thresholds are set, so Clean
means "this history is internally consistent under the rules it names" -
not "this meets a shared security baseline". Receipts are comparable
across organizations only when the parties compare `rules_hash` against
an agreed rule set; a fixed baseline profile is planned (§12.2) but not
part of this spec version.

Validation is resource-bounded: `validate` applies default
`ValidationLimits` (64 MiB receipt bytes - the same default as the CLI,
so parsing an adversarial receipt cannot demand gigabytes of memory
before per-record limits are reached - plus record count, per-record
payload bytes, and refs per record), and
`validate_with_limits` lets callers tighten, raise, or lift them. Limit
violations are structural failures (Invalid), reported before any
verification work. Deriving state from an untrusted log goes through
`verify_and_build_state` (verification first, state only on Accept);
`build_state_unchecked` trusts stored verdicts and is only for logs that
already passed replay.

The reference CLI wraps this for auditors with no Rust knowledge:
`bellbook validate <file>` prints the human-readable report;
`--json` prints the same report as JSON; `--max-size <bytes>` bounds the
file size before it is read (default 64 MiB, `0` = unlimited) - the CLI
is the trust boundary for untrusted receipts, so the bound lives there.
Exit codes: 0 clean, 1 invalid, 2 valid-but-tainted.

### 12.1 Future profile design principles (non-normative)

Future receipt profiles should prevent silence from being mistaken for
evidence. In particular, a profile that introduces required claims or
verification attempts should define explicit representations for
conflicting evidence, checks that did not run or failed open, and values
that were not measured. The v0.2 core has no `Inconclusive` result,
Requirement record, or verification-attempt record, so these principles
are intentionally not part of v0.2 conformance.

Existing core mechanisms still preserve useful facts without erasure:
refusals record work that was not performed, rejected records remain in
the log, evidence classes distinguish asserted or inferred content from
verified content, and retractions mark claims that later proved wrong.
They do not, by themselves, implement a complete truth-reporting profile.

### 12.2 Profiles (reserved)

Earlier drafts of this section sketched a "task receipt profile"
promising requirement-to-proof binding, confirmed-vs-derived
requirements, reviewer verdicts, and artifact identity. The core schema
cannot yet express those concepts (there is no Requirement record, no
structured reviewer verdict, and artifact identity would be parsed out of
an opaque `output` string), so the sketch has been **removed from the
normative spec** rather than promise what cannot be checked. Profiles -
including a minimal `bellbook-core-v1` baseline profile fixing author
roles, required signature kinds, key pinning, and evidence thresholds so
that "Clean" becomes comparable across organizations - are future,
separately versioned documents with their own test vectors.

### 12.3 Conformance

An implementation may claim **Bellbook conformance for a given spec
version** iff it (a) passes that version's published test vectors (§3.1)
and (b) implements the normative schemas and verification rules in that
version, including the strict receipt decoding requirements above.
There is no badge program and no registry - the claim is defined so that
it can be checked, disputed, and falsified by anyone holding the vectors.

## 13. Known limitations

- **Integrity, not confidentiality.** Records and receipts carry full
  payloads in the clear (prompts, responses, action parameters, tool
  output); content addressing and signatures prove integrity and origin,
  never secrecy. A receipt inherits the sensitivity of everything
  committed to the log, and sharing one is disclosure. Hosts MUST NOT
  place credentials or secrets in record payloads, SHOULD redact or
  minimize sensitive content before commit, and own encryption, access
  control, and retention. Selective disclosure (e.g. commitment-based
  attachments revealing hashes instead of content) is future work.
- **Bellbook proves consistency, not completeness.** The untrusted host
  controls what gets captured: an action can be omitted before anything
  reaches the log, and a perfectly Clean receipt can therefore be an
  internally consistent *subset* of what actually happened. What replay
  proves is that the captured history is tamper-evident, rule-conforming,
  and honestly graded - capture completeness depends on how the
  integration instruments its runtime, which is outside this spec.
  (External anchoring, §11.1, bounds *when* history could have been
  edited; it cannot conjure records that were never written.)
- Author identity is cryptographically bound only for actors pinned in
  `author_keys` on records that carry signatures; unsigned records (and
  unpinned actors) remain claims. Key management and rotation are host
  concerns.
- Verdicts inside a checkpoint prefix are attested by the checkpoint
  rather than re-derived (ids are recomputed; all checkpoint fields are
  validated). Checkpoint trust is the caller's responsibility (§10);
  receipts never carry one.
- The in-memory log index assumes logs fit comfortably in RAM.
  `LogWriter::open` and appends use a 64 MiB default file bound, configurable
  through `open_with_max_bytes`; passing `u64::MAX` is only for trusted
  storage. Receipts carry the full record sequence, so the same general bound
  applies to validation. Validation cost is linear in the number of records plus
  hashing proportional to total content size: ref resolution, subject
  lookup, and duplicate-verdict detection go through an id index built
  during replay, never per-lookup scans of the prefix. `ValidationLimits`
  (finite by default; `unlimited()` opts out) bounds the input a
  validator will accept at all.

## 14. Spec versioning and backward validity

This specification carries its own version, independent of the crate
version; the crate's CHANGELOG states which spec version each release
implements.

- **Backward validity guarantee:** a ledger or receipt that is valid under
  spec vN remains verifiable under vN's rules forever. Verifiers keep vN
  rule-sets, keyed by the schema version they find in the records or the
  receipt; a newer spec version never invalidates an existing artifact.
- Payload schema names (`bellbook.<kind>.v1`) version payload *shapes*;
  the record envelope, evidence classes, and canonicalization are governed
  by the spec version, which portable artifacts (receipts, head
  attestations) carry explicitly. Hosts embedding raw logs pin the crate
  version, whose CHANGELOG names the spec version it implements.
- Spec 0.2 is the first published compatibility epoch. The backward-validity
  guarantee starts with artifacts produced under this version.