Agent Action Capsule · build your own evidence layer
b3 / 11

The local evidence log

Seam ① Record. Build: an append-only local log with signed checkpoints and inclusion proofs. Conform to: draft-mih-scitt-checkpointed-local-log-02 (reference implementation: checkpointed-local-log; any conforming log).

Once you can seal a record, keep every one you seal in an append-only log on your own disk, sign a small checkpoint over it every N records or T minutes, and hand out a record with its inclusion proof and checkpoint so anyone can check it offline.

Six records become MMR leaves, a signed checkpoint commits to the two peaks, an inclusion proof for one record reaches the root, and the record, proof and checkpoint are exported and checked Left: a Merkle Mountain Range with MMR positions written in each node. Step 1: records 0 and 1 become leaves 0 and 1 and merge into node 2; the leaf value is SHA-256 of 0x00 followed by the capsule_id. Step 2: records 2 and 3 become leaves 3 and 4, merge into node 5, and node 5 merges with node 2 into node 6, a peak of height 2. Step 3: records 4 and 5 become leaves 7 and 8 and merge into node 9; the log has six records, size 10, and peaks 6 and 9; leaf 7 is the served half d804c0bf. Step 4: dashed lines take the peaks to a checkpoint card on the right. It shows the v0.1.4 form: kind mmr_checkpoint, log_id sender-log, mmr_size 10, prev_size 0, root 6473683c, key_id 1d2ffc84, an Ed25519 signature and a timestamp. The cut rule is shown: something new, and either 100 records or 300 seconds. A note says the -02 draft carries the peaks concatenated instead of a bagged root. Step 5: the proof path is highlighted. Leaf 7 is hashed, combined with sibling node 8 (c93642d9) into node 9, and node 9 is combined with left peak node 6 (d1b17708) into the root 6473683c, which matches the checkpoint. Step 6: a bundle box holds the capsule, the inclusion proof and the checkpoint, with an arrow to the peer. Step 7: a receiver box lists three checks (the half's signature, the checkpoint's signature by the same key, the proof reaching the root), then held, or else a signed refusal, for example inclusion_unverified. The node's own log (capsules.jsonl, MMR) leaf = SHA-256(0x00 ‖ capsule_id) h2 h1 h0 0 1 2 r0r1 mmr_size 3 · 2 records · peaks [2] 3 4 5 6 r2r3 mmr_size 7 · 4 records · peaks [6] 7 8 9 r4r5 r4 = d804c0bf… (served half) mmr_size 10 · 6 records · peaks [6, 9] Checkpoint · v0.1.4 form, Ed25519 kind mmr_checkpoint log_id sender-log mmr_size 10 prev_size 0 root 6473683c… (peaks bagged) key_id 1d2ffc84… signature e5783347… timestamp 2026-09-27T00:00:10Z cut when: new records AND (100 records OR 300 s) CLL -02: peaks 6 ‖ 9, no bagged root node 9 = H(be64(10) ‖ leaf 7 ‖ node 8 c93642d9…) root = H(node 9 ‖ node 6 d1b17708…) = 6473683c… ✓ witness peaks_left Exported bundle (record_push_bundle 1) capsule the half, signed inclusion leaf_index 4, size 10, path checkpoint the card above The other side, offline ✓ half's signature ✓ checkpoint by the same key ✓ proof reaches root → held, status received any check fails → signed refusal, e.g. inclusion_unverified

1What it is

A local evidence log is a producer's own append-only log of the records it seals: each record's id goes in as a leaf of a Merkle Mountain Range (MMR) on the producer's disk, and nothing leaves unless the producer sends it.

Every N records or T minutes the producer signs a small checkpoint over the whole log. Any record can then be shown to sit at a position under that checkpoint (an inclusion proof), and any later checkpoint can be shown to extend it (a consistency proof).

What you export is one record plus its inclusion proof plus the checkpoint: a file or a push that another party checks offline against the producer's key. The producer can also register its checkpoints with a transparency service it does not run.

The reference implementation is checkpointed-local-log (CLL), in Python, Go, TypeScript and Rust. Any log that meets the CLL draft's checks is acceptable: a verifier checks the proofs and the signed checkpoint, not which library produced them.

Where the library sits today

The Mesh-LLM plugin is a worked example with four layers, each in its own repository. They are listed bottom-up, with the versions pinned in the plugin's v0.1.4 Cargo.lock and the current release of each.

LayerPackageWhat it does
logcheckpointed-local-logThe MMR, leaf and node hashing, signed checkpoints, and inclusion and consistency proofs, in Python, Go, TypeScript and Rust. v0.1.4 pins crate 0.2.1; the latest crate tag is 0.3.1. (action-state-group/checkpointed-local-log)
bookany conforming bookRecord semantics over a log it embeds and never exposes: append, query, bundle, request and respond. The Evidence Layer draft (§8) says what a book must supply and names no implementation. v0.1.4 has no separate book layer of its own; capsule-emit 0.0.6 supplies the log wrapper, canonicalization and padding records.
producercapsule-emitSeals and signs each capsule, writes capsules.jsonl, and decides when to cut a checkpoint and where to send it. v0.1.4 pins crate 0.0.6. (action-state-group/capsule-emit)
host plugincapsulesThe timer that drives the checkpoints, push to the other side, receipt of pushed records, and the Evidence page. (Mesh-LLM/capsules v0.1.4)

That layering is moving. capsule-emit's main already builds its checkpoints on checkpointed-local-log directly (capsule-emit#326, merged); the plugin still pins capsule-emit 0.0.6. The checkpoint timer stays in the plugin. Section 5 gives the status of each piece.

2Design choices

Chosen: the log holds a 32-byte digest per record (the plugin uses capsule_id), and its leaf value is SHA-256(0x00 ‖ entry).
Rejected: putting record content in the log. A digest lets the content be erased from separate storage later while its entry stays in the log, and the 0x00 prefix keeps a leaf from ever matching an interior node. (CLL §1.1 "Entry", §2 item 2, §3)
Chosen: append each record when it is sealed or received, and never modify or remove an entry.
Rejected: assembling the log at the end of a period, which "permits curation that no later verification can detect". (CLL §2 items 1 and 2)
Chosen: checkpoint on two triggers, a count and a clock. capsules v0.1.4 uses 100 records or 300 s, the clock running from when a tick first sees records no checkpoint covers. It also cuts a checkpoint at start-up, at shutdown, and when a record is about to be pushed (at most one per 100 ms).
Rejected: checkpointing only when someone asks. Once a producer publishes checkpoints, the draft requires it to declare a cadence and meet it, because "a checkpoint regime with discretionary gaps supports none of the claims". Registering every record as it is made is the central-log cost the draft is written to avoid. (CLL §4.4 item 1, §1; capsules checkpoint_cadence.rs)
Chosen: before a checkpoint leaves the node, pad the log with padding records until its record count is a multiple of a bucket size. capsules v0.1.4 defaults to 32; a setting of 0 turns padding off.
Rejected: exact sizes. Anyone holding two checkpoints would learn how many records the producer committed between them, across every counterparty, not only their own. (Evidence Layer -00 §12.1; capsules CAPSULES_CHECKPOINT_PAD_BUCKET)
Chosen: the book embeds its commitment log and exposes no type from it. Callers ask for records, bundles and answers, and the book builds every proof.
Rejected: a book tied to one mechanism. The Evidence Layer draft requires four properties (append order, inclusion, consistency, checkpoint identity) and no particular mechanism. A checkpointed local log is one way to meet them; registration with a SCITT service is another. (Evidence Layer -00 §8)
Chosen: checkpoints stay local by default. They go to a transparency service only if the operator names one (the plugin's setting calls it a witness: CAPSULES_CHECKPOINT_WITNESS_URLS), and an unreachable service never blocks local checkpointing.
Rejected: sending checkpoints out by default. Also rejected: counting a service the producer runs itself as independent, since the draft calls that "a replica". (capsules checkpoint_cadence.rs, CAPSULES_CHECKPOINT_WITNESS_URLS; CLL §4.4 item 3)

3What it shows / what it does not

Shows

  • The producer signed every checkpoint and chose what to present, so the next three points hold only as far as the verifier trusts the producer (CLL §5.3):
  • A record with a given capsule_id is in the log at a given position under a checkpoint the producer's key signed (an inclusion proof, as in the example below).
  • A later checkpoint extends an earlier one without rewriting it (a consistency proof, CLL §4.3 step 8).
  • A presented range of records is complete under one checkpoint (CLL §6).
  • Once checkpoints are registered under a Consistency Registration Policy at a service independent of the producer, each record existed no later than the first registered checkpoint covering it, and the registered history was not rewritten (CLL §7).

Does not show

  • That any record's content is true (CLL §7).
  • That the producer appended everything it did, or that this is its only log (CLL §7).
  • When a record was made, if no checkpoint was registered. The checkpoint's time is the producer's own claim.
  • Anything beyond the producer's own word for records appended after the last registered checkpoint (CLL §4.4 item 4).
  • The exact number of records: padding hides it only to within the bucket size, and checkpoint times still show when the producer is active (Evidence Layer -00 §12.1).
  • In v0.1.4, a gap in checkpoints as something a verifier can detect. The plugin cuts no checkpoint while nothing new is appended. The -02 draft asks a producer that publishes checkpoints to declare a cadence and keep to it even when idle (CLL §4.4 item 1).

4Example

This is a bundle as the plugin pushes it to the other side of an exchange: a served half, its inclusion proof, and the checkpoint that proof resolves against. It comes from the plugin's record-push test corpus. The corpus was built from public material, its keys were discarded, and the request and response digests are placeholders. The test's expected answer is that the receiver keeps the half with status received. A copy whose root was changed after signing gets a signed refusal, inclusion_unverified.

{
  "capsule": {
    "action_id": "serve_exchange/c79b9e3c-75b1-4ddf-b917-d38da457a94b",
    …
    "capsule_id": "d804c0bfc4df59def6edce207cefea1d6e4656792da7a1aeb3586017f2c977d5",
    "effect": { … "type": "inference_completion" },
    "key_id": "1d2ffc84c4a0bbeee8bae343e01feff0419e9f9f989cc5dc6ed3bdac3e40cbb9",
    "signature": "d284584ca3…",
    "spec_version": "draft-mih-scitt-agent-action-capsule-05",
    …
  },
  "checkpoint": {
    "key_id": "1d2ffc84c4a0bbeee8bae343e01feff0419e9f9f989cc5dc6ed3bdac3e40cbb9",
    "kind": "mmr_checkpoint",
    "log_id": "sender-log",
    "mmr_size": 10,
    "prev_root": "",
    "prev_size": 0,
    "root": "6473683c244f0220be10b3ee131a978bafbc29097a5563645a3258352aea0047",
    "signature": "e5783347bc7400e44902dd43d3b98c809d42226d1a5d3b0ae67996f726e424e449f957753953f00c3619425a505562310700e9fd7d80da1bea4768fb174af806",
    "timestamp": "2026-09-27T00:00:10Z",
    "v": 1
  },
  "inclusion": {
    "leaf_index": 4,
    "proof": {
      "kind": "inclusion",
      "leaf_index": 4,
      "peaks_left": ["d1b17708d8ce4bab928477715734afe2e841f174f51bbab7ce097287bc3480b1"],
      "peaks_right": [],
      "size": 10,
      "v": 1,
      "witness": ["c93642d9c7be9e0de1344d8c4002b1233519e95033ff67d7d3e38d7444d39254"]
    }
  },
  "record_push_bundle": 1
}
FieldTypeMeaning
record_push_bundleuintBundle format version: 1. A receiver refuses any other value.
capsule.capsule_idhexSHA-256 of the JCS bytes of the record without capsule_id, signature and key_id (draft-mih-scitt-agent-action-capsule-05); it is also the log entry for this record.
capsule.key_idhexThe sender's raw Ed25519 public key, which signed the record.
inclusion.leaf_indexuintThe record's index among leaves: 4, the fifth record. That is MMR position 7.
proof.sizeuintThe MMR size the proof is for. It must equal the checkpoint's mmr_size.
proof.witnesshex[]Sibling node values from the leaf up to its peak: node 8.
proof.peaks_left / peaks_righthex[]The other peaks, left and right of the one the path reaches: node 6 on the left, none on the right.
checkpoint.kindstringmmr_checkpoint: the log implementation's internal JSON checkpoint record (a single bagged root), signed separately from the COSE wire form.
checkpoint.log_idstringNames the log. A real node uses capsules/<first 16 hex of SHA-256(public key)> by default (CAPSULES_LOG_ID overrides it), unique per node, because a witness keeps one history per log id.
checkpoint.mmr_sizeuintNode count of the MMR committed to: 10, which is six records.
checkpoint.prev_size / prev_rootuint / hexThe previous checkpoint's size and root. Here 0 and empty: this is the log's earliest checkpoint.
checkpoint.roothexThe peaks folded right to left into one value: SHA-256(node 9 ‖ node 6).
checkpoint.signaturehexEd25519 by key_id over the hex SHA-256 of the sorted, compact JSON of the other fields (2f422cdd…).
checkpoint.timestampstringWhen the checkpoint was cut, as the producer states it.

Recompute it

  1. leaf 7 = SHA-256(0x00 ‖ d804c0bf…), over the 32 bytes of capsule_id.
  2. node 9 = SHA-256(be64(10) ‖ leaf 7 ‖ c93642d9…): the parent's position plus one, then left child, then right child.
  3. root = SHA-256(node 9 ‖ d1b17708…) = 6473683c…, the checkpoint's root. Then check the checkpoint's signature under key_id.

Under CLL -02 the same log at size 10 would carry commitment = node 6 ‖ node 9 (64 bytes, tallest peak first), and a bagged root "MUST NOT be carried" (CLL §4.1). The explain series' "Checkpointed local log and MMR" page walks through a -02 checkpoint.

Source: Mesh-LLM/capsules:vectors/parity/corpus/record_push.json (case bundle_valid) and vectors/parity/golden/record_push.json @ b4e24453f572b2d9fe8f2cefbd0f35a5eb54b0ad (tag v0.1.4)

5Status

Tags: live in a tagged release; built-not-live merged but not released; designed written in a draft, not yet produced; planned an open issue or PR.

6Specs

7What you need before the next step