capsule_emit.checkpoint
September 11, 2026 · View on GitHub
Default wiring (since 0.5.0)
capsule_emit.core._emit_capsule() — the internal primitive seal() /
received() (and the who()/can()/did()/audit() slot verbs nested in
seal()) all wrap — wires this in by default
— no opt-in code required. Every ledger you seal into participates in a
checkpoint/witness stream automatically:
- Cadence: 100 entries or 15 minutes, whichever comes first, both
configurable. Once a ledger accumulates
capsule_emit.witness.DEFAULT_CADENCE_ENTRIES(100) entries since its last checkpoint — orcapsule_emit.witness.DEFAULT_CADENCE_SECONDS(900) seconds have elapsed since the first unwitnessed entry after the last checkpoint — a signed peaks checkpoint over that ledger's MMR is built and registered with the witness Transparency Service at its/checkpointsroute (single-host witness ruling, 2026-08-27). The witness is a separate, live service —witness.agentactioncapsule.org(capsule_emit.checkpoint.emit.DEFAULT_TS_URL), checkpoint-primary and semantically a witness, not the anchor. This is the only default egress channel as of 0.5.0 — the older per-capsule anchor channel is now an explicit, non-default opt-in (seedocs/why-anchoring.md), not something every default sealing call also dispatches. A bundle (capsule + inclusion proof + stamped checkpoint) is already per-record proof — the witness host's separate/registerroute (opt-in, SCITT-interop) exists for verifiers that need a per-record SCITT Receipt specifically, not as an upgrade path from a checkpoint stamp; the default sealing path never calls it (see "Checkpoint-only" below andtests/test_witness_no_egress_to_register.py). - An idle log is silent, never a heartbeat. The age leg is checked
lazily, only inside
witness.maybe_checkpoint— which itself only ever runs right after a realseal()/received()call appends a new entry. There is no background timer or polling thread, so a ledger with no new activity is never checkpointed on age alone: it is structurally impossible, not a runtime guard. Checkpoint-stamp entries (see below) reinforce this — they're written directly throughledger.append_to_ledger, never throughcore._emit_capsule(), so persisting a stamp never advances the entry counter or resets the age clock. - Multiple witnesses.
witness_url=(andCAPSULE_WITNESS_URL) accept a single endpoint or several — a list, or a comma-separated string — and the same checkpoint is registered with every endpoint named, independently; one endpoint failing never blocks the others. A single default endpoint is used unless you opt into more. - Checkpoint-only — what crosses the wire is the checkpoint itself (its
size, a root hash, a timestamp, and the signing key's id —
CheckpointRecord.to_dict(), POSTed to/checkpoints) so the witness can verify the checkpoint's own signature before ever counter-signing it. No capsule content, no per-record digest, and no filesystem path, ever leave the process this way —log_idis a stable hash of the ledger's resolved path, never the path itself (seewitness._public_log_id). - Async, fire-and-forget — the checkpoint build (local, no network) and its TS registration (the only network call) run on a daemon thread; a cadence-crossing sealing call never blocks on it.
- Lazy — nothing above is imported or computed until a checkpoint is
actually due. A caller who seals once and exits, or whose ledger
never crosses the cadence threshold, pays zero cost: no MMR built, no
capsule_emit.checkpointimport, no network dependency touched. This is what keepsimport capsule_emit(and a single below-cadence sealing call) exactly as cheap as before this default flipped on.
First-use notice. capsule-emit prints one line to stderr, once per
process, at the first seal() / received() call where witnessing is enabled —
before the first byte ever leaves the process, not gated on a checkpoint
actually being due (the default cadence is 100 entries, so a short-lived
process might otherwise never trigger one and never see the notice). It
states what will be sent (a signed checkpoint of the log — never capsule
content), where (the resolved endpoint(s), at their /checkpoints route),
and how to turn it off. It never prints a second time in the same process.
Turning it off:
seal(..., witness=False) # this call's ledger opts out
export CAPSULE_WITNESS=off # opt out everywhere, no code change
An explicit witness= kwarg always overrides the env var. Repoint the
endpoint (or add more) with seal(..., witness_url=...) or
CAPSULE_WITNESS_URL=…; override the entry-count cadence with
CAPSULE_WITNESS_CADENCE_ENTRIES=… and the age-based cadence with
CAPSULE_WITNESS_CADENCE_SECONDS=….
Kill switch scope (O16-03)
witness=False / CAPSULE_WITNESS=off is ONE switch that zeroes ALL
egress, not just the checkpoint stream. It also gates:
status's stamp-fetch — the read-only GET that independently re-confirms a witness receipt (see Checking status below). It skips this network call whenever the kill switch is set, even if you didn't also pass--offline—status ./ledger.jsonlon a process withCAPSULE_WITNESS=offnever touches the network, full stop.- The legacy anchor channel — even if it was explicitly re-enabled via
anchor=True/CAPSULE_ANCHOR=legacy-on(seedocs/why-anchoring.md), the witness kill switch overrides it. Ananchor=True, witness=Falsecall never dispatches the legacy channel.
This is what makes the local-only posture (turning witnessing off) an
honest, absolute "nothing leaves this process" guarantee rather than one
that a separately-configured legacy channel or a status call could quietly
poke a hole in. A no-network test in CI asserts zero egress across all three
paths simultaneously.
What trust tier this reaches — be precise. A single-TS default checkpoint
is witnessed (single witness): it upgrades the stream from
self-attested to third-party-checkable — the witness vouches that the
records under this checkpoint existed, in that order, and haven't been
rewritten since (existence + order + non-deletion). It does not vouch
that the records' content is true, and it is not the multi-witness,
equivocation-resistant tier described in
why anchoring makes it trustworthy
— that tier specifically requires witnesses a verifier can cross-check: the
same checkpoint independently co-signed by, or registered to, more than one
independently-operated log. Register the default checkpoint with more than
one Transparency Service (seal(..., witness_url=[url1, url2]) or a
comma-separated CAPSULE_WITNESS_URL) to climb from single-witness to
multi-witness; the zero-config default does not do this for you.
Signing. The default path signs checkpoints with the SAME persisted
Ed25519 identity (capsule_emit.signing.LocalKeypairSigner) that signs
capsule content — resolved with the identical precedence seal() uses
(signer=/signing_key_path=/CAPSULE_SIGNING_KEY_PATH/a key file next to
the ledger). A checkpoint signed in one process therefore verifies in a
later one, including by a stranger who holds only the checkpoint bytes (see
"Bundle" below, and checkpoint.verify_checkpoint_signature_offline) — this
replaced an earlier ephemeral, in-process-only HMAC key
(witness._AutoSigner) specifically because that could never be verified
again once the signing process exited. Use the manual API below with your
own Signer if you want a different identity.
Test & dev — the stub witness
CAPSULE_WITNESS=stub runs the identical checkpoint mechanics — MMR sync,
checkpoint build, signature, stamp persistence — against a local, in-process
stub instead of a real Transparency Service: zero network, and the grade
never leaves self-attested, no matter how many stub stamps accumulate
(CheckpointRecord.grade() excludes stub-sourced WitnessRecords from the
witnessed any-of). Use it for unit tests, CI, and eval-before-procurement —
it exercises the real code path, not a mock, with no endpoint to stand up and
no network flake to work around.
export CAPSULE_WITNESS=stub # this process's checkpoints run through the stub
CAPSULE_ENV × CAPSULE_WITNESS matrix:
CAPSULE_ENV | CAPSULE_WITNESS=stub | anything else |
|---|---|---|
production | refuses to run — StubWitnessInProductionError, raised synchronously at the first seal()/received(), before anything is written | normal (real witness, or off) |
| unset / anything else | stub mode runs; a scream prints once to stderr at the first stub-armed seal(), and status/--json mark the checkpoint "stub_witness": true | normal |
Three hard rules, enforced in code, not just documented:
- Explicit opt-in only, never a fallback. An unreachable real witness
endpoint is a warning and a retry concern (see the idle-silence /
statuslag numbers above) — it never silently downgrades to stub. CAPSULE_ENV=production+ stub set is a startup error, not a warning — see the matrix above.- Stub stamps never reach rung 2.
statusreports a stub-only latest checkpoint asself-attestedwith an explicit⚠ STUB WITNESSline, and each stubWitnessRecordinwitnessesis labeledis_stub: true.
The normative stub marker. The CLL I-D's "Stub Countersignatures" section
(draft-mih-scitt-checkpointed-local-log-00) defines it: a stub
countersignature's COSE protected header MUST carry the parameter
cll-stub (label TBD1, pending IANA assignment) with value true, and
MUST list that label in crit ({{Section 3.1 of RFC9052}}) — a verifier
that recognizes cll-stub treats the countersignature as conferring no
witnessing; one that doesn't rejects it under crit processing. Either way
the result is unwitnessed, never witnessed — exactly what grade()'s stub
exclusion enforces here. capsule_emit.checkpoint.STUB_MARKER is
"cll-stub", matching the spec's name and value. What's still pending is
the wire encoding, not the marker itself: capsule-emit's stub receipts are
still a JSON placeholder (register_checkpoint_stub's receipt_b64), not a
real COSE_Sign1 countersignature — that lands with separate COSE-wire work.
The placeholder already uses cll-stub: true (plus a crit-shaped list
naming it), so a real Transparency Service's verify_receipt_offline never
mistakes it for a real receipt today, and only the encoding — not the
marker's name or value — changes once COSE-wire work ships.
Direct / manual use
The primitives below remain independently usable, and stay opt-in in the
original sense — nothing in capsule_emit's top-level import path touches
them; import capsule_emit alone never loads an MMR module, never pulls in
a checkpoint dependency. Reach for this directly when you want your own
cadence, your own persisted signing key, or a Transparency Service other than
the default:
from capsule_emit.checkpoint import MmrLedger, CheckpointConfig, emit_checkpoint
What it is
A Merkle Mountain Range (MMR) index over your own capsule ledger, plus a signed, tamper-evident peaks checkpoint you can optionally register with a Transparency Service (TS) for independent, third-party freshness evidence.
core— the pure MMR algorithm: position math, domain-separated hashing, inclusion/consistency proofs. No I/O, MMRIVER-draft-compatible.store— an in-memory node backing (MemoryNodeStore).index—MmrLedger, a decorator over any object shaped likeLogSource(append/scan/fetch/find_gaps/verify, matched structurally — never by importing a concrete log implementation). Wraps your own append-only capsule log with inclusion and range proofs.emit— builds, signs, and (optionally) registers a checkpoint:{log_id, mmr_size, root, prev_size, prev_root, key_id, timestamp, signature}, pluswitnessesonce one or more Transparency Services have stamped it.key_iddoubles as a peer identifier when a deployment checkpoints several independent logs (e.g. one per mesh node).
Checkpoint/stamp persistence — the stamp is a log entry, not just an in-memory field
Since 0.5.0, capsule_emit.core._emit_capsule()'s default wiring (capsule_emit.witness)
writes every checkpoint it builds — signature, mmr_size, and whatever
witnesses it collected — back into the same ledger it covers, as its own
JSONL line:
{"kind": "checkpoint_stamp", "v": 1, "capsule_id": "<checkpoint.entry_digest()>",
"checkpoint": {"v": 1, "kind": "mmr_checkpoint", "log_id": "...",
"mmr_size": 100, "root": "...", "prev_size": 0,
"prev_root": "", "key_id": "...", "timestamp": "...",
"signature": "...", "witnesses": [...]}}
This is not a re-issue of any capsule and does not change any capsule's
capsule_id — it is a distinct entry that becomes an MMR leaf the next
checkpoint's mmr.sync() folds in. The leaf is addressed by
CheckpointRecord.entry_digest() — a hash over the entire persisted entry
(to_dict(): signing body, signature, and witnesses alike) — not
CheckpointRecord.digest() (the signing-body-only value registered with the
TS). That is what "checkpoint N's stamp is covered by checkpoint N+1" means
in practice: the history carries the evidence of its own witnessing, so
flipping or deleting a byte of a persisted stamp's witnesses changes its
leaf and breaks the covering checkpoint's root, rather than that evidence
living only in the CheckpointRecord.witnesses list of a process-local
object a restart discards. Stamp entries never wake the cadence/idle
timer — they aren't written through core._emit_capsule(), so they never touch
witness.maybe_checkpoint's per-sealing-call counter or its age clock;
persisting a stamp neither advances the entry count nor resets (nor starts)
the 15-minute window (tests/test_witness_stamp_persistence.py,
tests/test_witness_idle_silence_and_age_cadence.py).
kind/v are the entry's format-version marker: capsule_emit.ledger.read_ledger
filters checkpoint_stamp entries out by default, so every capsule-only
consumer (the CLI, ledger.view/view_chains/show, server, permalink,
approval, holds) keeps seeing exactly the capsule stream it always has,
unaffected by this change. Use capsule_emit.ledger.read_ledger_entries to
read the raw file, stamps included — that's what the checkpoint layer's own
MMR indexing (capsule_emit.witness._JsonlLogSource.scan) does, so stamp
entries are indexed as leaves too.
Checking status — capsule-emit status
capsule-emit status ./ledger.jsonl # renders text, re-checks the latest witness receipt
capsule-emit status ./ledger.jsonl --offline # local-only, no network call
capsule-emit status ./ledger.jsonl --json # machine-readable
status answers, from the ledger alone: how many capsules are sealed, how
many checkpoints exist and each one's ladder rung (self-attested /
witnessed — the checkpoint's EFFECTIVE grade, original registration plus
any later backfill; see "Witness outage" below), and two honest lag
numbers — records awaiting the next checkpoint (sealed capsules, and a
checkpoint's own not-yet-covered stamp entry, past the latest checkpoint's
covered leaf count) and checkpoints awaiting a witness stamp (checkpoints
no configured witness has confirmed yet, originally or via backfill). A
third field, witness_backlog, breaks the second number down per
currently-configured witness (--witness-url, repeatable; defaults to
CAPSULE_WITNESS_URL) — how many checkpoints that specific witness has
not yet confirmed, so a multi-witness deployment can see that one witness
being down never hid the others having already advanced.
This is the CLI's read-verb family (log, status, show, bundle,
disclose, verify): reads never write. Unless --offline is given,
status makes exactly one kind of network call — a read-only GET of a
Transparency Service's public key (capsule_emit.checkpoint .verify_receipt_offline) to independently re-confirm a witness receipt the
ledger already holds for its latest checkpoint. It never re-registers a
self-attested checkpoint to try to obtain a new stamp — that would create a
new Transparency Service log entry, i.e. a write, and push (which forces
a new checkpoint), not status, is where writes belong. --offline
skips even the read-only re-check and reports only what the ledger already
records. So does CAPSULE_WITNESS=off (the kill switch, see
Kill switch scope above) — a witness-disabled
process reports each witness as unconfirmed (witness disabled) and never
attempts the GET, whether or not --offline was also given.
Witness outage: durable retry, not a drop (O5)
Witnessing is default-on, so outage handling is launch behavior, not an
edge case. When a configured witness is unreachable, the checkpoint it
would have stamped is not lost, not silently dropped, and not stuck: it is
already persisted as a self-attested log entry (see "Checkpoint/stamp
persistence" above), and capsule_emit.witness retries it — per witness,
independently — the moment that witness comes back.
There is no separate queue file. The durable "queue" is the ledger
itself: every checkpoint stamp on disk that a given witness has not yet
confirmed IS that witness's pending backlog, computed fresh on demand
(witness.checkpoint_witness_backlog(ledger_path, urls)). That means:
- Nothing is lost on restart. A pending checkpoint's evidence lives in the ledger, not in a process's memory — killing the process mid-outage and starting a fresh one changes nothing about what still needs a stamp.
- No unbounded in-memory growth. Nothing accumulates in a Python
list across calls; each retry pass is a read over the ledger that's
already there for other reasons (the same full-rescan precedent
MmrLedger.sync()already sets). - A late stamp can't rewrite history, so it is recorded as its own
small
checkpoint_witness_backfillledger entry citing the original checkpoint'sentry_digest— never a mutation of the original stamp.
Per-witness cursors. With more than one witness configured,
witness.retry_pending_witness_stamps(ledger_path, ts_url=...) drains each
one's backlog independently: one witness still down stops (at its own
first failure this call) without touching another witness's already-clear
backlog or blocking its drain. The next call — whether the next real
seal() / received() crossing cadence, or another explicit call — re-derives
the same backlog from the ledger and resumes at the same point; there is no
cursor to lose or desync.
When it runs. _build_and_register (the same fire-and-forget worker
that builds and registers each newly-due checkpoint) calls
retry_pending_witness_stamps first, before handling the checkpoint due
this cycle — so the backlog drains automatically on the next real write
after a witness returns, matching this module's existing "no background
timer, only real writes drive network activity" design. An operator who
wants to force a drain without waiting on the next sealing call can call
retry_pending_witness_stamps directly.
from capsule_emit import witness
backlog = witness.checkpoint_witness_backlog(ledger_path, ["https://witness.example"])
witness.retry_pending_witness_stamps(ledger_path, ts_url="https://witness.example")
Fail-closed: require_witness=True ([capsule-emit-witness-required-profile])
Everything above — the default cadence, the outage backlog, the retry drain —
is deliberately best-effort: a seal()/received() call never blocks on
witnessing, and a witness that is down simply means the checkpoint stays
self-attested until it comes back. That is the right default for most
callers, but a profile that specifically requires a witness (per
JamesCarnley's projnanda/nandatown#217 review of the acceptance shape for a
witness-required anchoring profile) needs the opposite posture: the absence
of a witness must be explicit, never silent.
require_witness=True (passed to seal() / received() / _emit_capsule())
gives you that:
from capsule_emit import seal
from capsule_emit.witness import WitnessRequiredError
try:
capsule = seal(payload, ledger=ledger_path, require_witness=True)
except WitnessRequiredError:
... # no witness confirmed this capsule -- handle explicitly, never ignore
Instead of the async, cadence-batched maybe_checkpoint, this forces an
immediate, synchronous checkpoint (like witness.push()) covering the
just-sealed capsule, and raises capsule_emit.witness.WitnessRequiredError
unless a configured witness actually confirms it — whether because
witnessing itself is disabled (witness=False / CAPSULE_WITNESS=off) or
every configured Transparency Service was unreachable or returned no
receipt. The capsule is still sealed, signed, and appended to the ledger
before this check runs (there is no rollback of an already-signed record),
but the caller never gets back a normal, ok-looking EmitResult for a
capsule that quietly stayed local-only.
EmitResult.witness_outcome names which of four states applied — distinct
from the legacy, non-default anchored/anchor_status fields (unchanged,
kept as a compat alias — see docs/why-anchoring.md),
which report only the old per-record anchor channel:
witness_outcome | When |
|---|---|
local_sealed | Witnessing is disabled for this ledger — no witness channel at all. |
checkpoint_queued | Default (require_witness=False) path: counted toward cadence, or a due checkpoint just dispatched async — outcome not yet known. |
witness_receipt_obtained | require_witness=True and a witness confirmed synchronously. |
witness_unavailable | require_witness=True and no witness could be confirmed — paired with WitnessRequiredError being raised, so a caller sees the exception, not this value, in practice. |
Default behavior (require_witness=False, i.e. every existing caller) is
unchanged: it is exactly the best-effort path described earlier in this
document.
Bundle — the hand-to-anyone artifact (O16 audit item 14)
The verification chain above (checkpoint/emit.py's module docstring) is four
separate, caller-composed primitives — inclusion, checkpoint signature, TS
receipt, rollback/consistency. capsule_emit.bundle.bundle() assembles all
of them, plus the record's own receipt and the prior checkpoint's
consistency proof, into one standalone object for a single record — the
frozen surface's §2.5 shape:
from capsule_emit.bundle import bundle, verify_bundle
b = bundle("ledger.jsonl", capsule_id) # or an unambiguous >=8-char prefix
ok, errors = verify_bundle(b) # pure, offline, never raises
Anti-equivocation: verify with bundle(), not verify_input_digest()
Named explicitly here because it was the gap in the acceptance shape
JamesCarnley's projnanda/nandatown#217 review called out: verify_input_digest()
(capsule_emit.verify) is a plain JCS-SHA256 digest check of one payload
against the value a capsule already claims to have sealed — it says nothing
about the log, the checkpoint, or any witness. It cannot tell you whether
the capsule was ever entered into an append-only history, whether that
history was later rewritten, or whether a different history was shown to a
different party (equivocation). A verifier that only calls
verify_input_digest() and treats a match as "this capsule is authentic and
un-equivocated" is trusting exactly the weaker of the two primitives this
library ships.
For anti-equivocation, verify with capsule_emit.bundle (bundle() +
verify_bundle(), above): it is the one primitive that actually chains a
record to a signed checkpoint, an inclusion proof, a witness stamp, and the
prior checkpoint's consistency proof — the full two-sided append bracket.
verify_input_digest() still has a legitimate, narrower job (confirming a
plaintext candidate matches a specific committed field, e.g. when handling a
disclose()d payload) — it is simply the wrong tool for "was this capsule
honestly logged," and a profile or verifier claiming anti-equivocation MUST
reach for bundle/verify_bundle instead.
The discipline that makes any of this matter: verify against the
ORIGINALLY pinned commitment, never whichever capsule the presenter or a
local cache happens to supply. A verifier that re-fetches "the current
capsule for this id" from a mutable cache — rather than checking the exact
capsule bytes it first pinned (by capsule_id, or better, by an already-held
Bundle/checkpoint) — has no anti-equivocation property left, no matter how
strong verify_bundle()'s own checks are: a cache-overwrite between the
first pin and the later re-check silently substitutes a different capsule
underneath the verifier, and it never notices. This was the concrete bug in
the reviewed PR's patch (a cache-overwrite that let a later, different
capsule quietly stand in for the one originally pinned). Pin once, at first
sight, and verify against that pin every time after — never against
"whatever is there now."
Bundle carries:
receipt— the record itself, as persisted.inclusion_proof— proves the receipt is a leaf undercheckpoint's root.checkpoint— the covering checkpoint (the first one whosemmr_sizereaches this record), carrying whatever witness stamp(s) it collected incheckpoint.witnesses.prior_checkpoint/consistency_proof— the checkpoint immediately beforecheckpointfor this log, and the proof thatcheckpoint's root genuinely extends it. This is the bracket's lower bound: the record wasn't yet inprior_checkpoint. Both areNonetogether, and only whencheckpoint.prev_size == 0— the covering checkpoint is the log's first, so there is no earlier checkpoint to bound against; that is the honest edge case, not a gap.
A record only becomes bundle-able once some checkpoint's mmr_size reaches
it — bundle() raises BundleError for a record still awaiting its first
checkpoint (see capsule_emit.status for a read-only way to check that lag
first). bundle() never caches anything: every call re-reads the ledger and
re-derives the MMR fresh, so a bundle can be built by a completely different
process than the one that sealed the record, at any later time, as long as
the log still retains it.
verify_bundle() checks every link the two-sided append bracket depends on
— inclusion, both checkpoints' signatures (via
checkpoint.verify_checkpoint_signature_offline, which needs only the
checkpoint's own key_id, never a private key or a live Signer — this is
what makes bundle verification possible for a stranger at all), the
prev_size/prev_root linkage, and the consistency proof — all entirely
offline. A passing consistency check is labelled for exactly what it proves:
"history intact between checkpoints N and M" (anti-rewrite, i.e. the
history within this bundle wasn't reordered/truncated) — never "no fork" /
"not equivocated", since one offline bundle can never rule out a divergent
history it doesn't see; that guarantee is the witness's and multi-witness
config's job. It also checks witness-stamp authenticity
(checkpoint.verify_witness_stamp_offline per WitnessRecord, per
[stamp-authenticity-on-read-not-presence]): a stamp from the pinned default
witness (DEFAULT_TS_PUBLIC_KEY_PEM) is signature-verified with no network
call and no caller setup; a stamp from any other Transparency Service, with
no caller-supplied ts_pubkey_pem, verifies as a genuine receipt shape
only and does not confer full trust — a checkpoint that claims witness
stamps but has none that verify at all is fatal (ok=False).
Bundle.to_dict() / Bundle.from_dict() round-trip through plain JSON —
the point of "standalone": a bundle survives being written to a file and
handed to someone else's process.
checkpoint_cose — the COSE_Sign1 wire form ([cll-checkpoint-cose-wire])
Bundle.checkpoint_cose carries the covering checkpoint as a COSE_Sign1
statement over a CBOR claims map, built once at production time (in
witness._build_and_register, the only place the signing key is actually
available) and carried through unchanged from there on — bundle() never
re-signs anything. None for a bundle built from a ledger predating this
field, or if the COSE serialization failed at production time (best-effort;
never blocks the JSON checkpoint path).
Where a JSON CheckpointRecord uses dev-ergonomic field names,
checkpoint_cose's CBOR claims use the CLL I-D's own §3 spec names —
log_size/commitment/prev_commitment/issued_at instead of
mmr_size/root/prev_root/timestamp, log_id moved onto the signed
CWT iss header, key_id onto the COSE kid header. The full dev↔I-D
field-mapping table — plus a known iss/sub semantics deviation from
the I-D's producer/log identity split, worth reading before relying on
those two claims for cross-implementation identity matching — lives in
cll.checkpoint.cose_wire's module docstring (this is the
[cll-id-field-mapping-doc] resolution: ship the mapping table, don't rename
CheckpointRecord's own fields). capsule_emit.checkpoint.cose_wire is a
deprecated alias for the same module — import cll.checkpoint.cose_wire
directly.
If the checkpoint has a prior, the claims ALSO carry a real MMR consistency
(extension) proof — not just the prev_size/prev_commitment fields —
because those fields alone are exactly as trustable as any other
self-reported string: verify_checkpoint_cose_offline independently
recomputes whether the claimed prev_commitment peaks actually bag up to
the claimed commitment, and rejects a checkpoint whose continuity is
merely asserted, not proven (same anti-REWRITE-not-anti-FORK honesty as
verify_bundle's own consistency check above).
from capsule_emit.checkpoint.cose_wire import verify_checkpoint_cose_offline
b = bundle("ledger.jsonl", capsule_id)
if b.checkpoint_cose is not None:
result = verify_checkpoint_cose_offline(b.checkpoint_cose) # no capsule-emit trust needed
assert result.ok, result.errors
verify_bundle() performs this same check automatically when
checkpoint_cose is present, cross-checking the decoded fields against
Bundle.checkpoint and failing the bundle if they disagree; absence is
never fatal.
Disclose — bundle's conscious sibling (O16 audit item 10)
bundle above is always safe — digests only, no producer decision needed.
capsule-emit disclose is the deliberate, recorded act of handing
bundle plus content to a named audience:
capsule-emit disclose ledger.jsonl <id|range> --audience auditor \
--reveal <id>:agent_input=input.json --reveal <id>:agent_output=output.json
from capsule_emit.disclose import disclose, verify_disclosure
d = disclose(
"ledger.jsonl", capsule_id, audience="auditor",
reveal={capsule_id: {"agent_input": payload_in, "agent_output": payload_out}},
)
ok, errors = verify_disclosure(d) # pure, offline, never raises
<id|range> selects records three ways: a single capsule_id (or an
unambiguous >=8-char prefix); id1..id2, a contiguous range inclusive in
ledger order; or id1,id2,..., an explicit list. The first two always
produce the honest "contiguous" completeness mode; the explicit-list form
always produces "producer-selected", regardless of whether the ids happen
to be contiguous, because the caller chose to enumerate rather than bound a
range — a completeness statement can never quietly overstate what was
disclosed.
--payloads all (the default) requires a supplied payload
(--reveal id:field=payload.json) for every disclosure-eligible field
(agent_input/agent_output) that has a committed digest on every
selected record, unless the field is named in --suppress — this is the
equivocation-honesty rule: a partial disclosure can never masquerade as a
complete one. --payloads selected discloses exactly the payloads
supplied, nothing implied either way. --suppress FIELD withholds a field
for this audience even when a payload is available, and is recorded on the
disclosure record rather than silently dropped — suppression is a per-call
choice, never a default.
The act seals its own receipt. disclose() builds one
capsule_emit.disclosure.build_disclosure_envelope per selected record
(the existing single-capsule payload primitive — see disclosure.py), then
appends a disclosure record — who disclosed what range to which
audience, when — to the SAME ledger, signed with the same producer
Signer seal() uses (kind: "disclosure_record",
capsule_emit.ledger.DISCLOSURE_RECORD_KIND). It becomes an MMR leaf like
any other entry the next time a checkpoint covers it: disclosures are
receipts too, and the audit trail of showing evidence is part of the
history. It is excluded from read_ledger() (and therefore ledger view/show/verify) and from bundle()'s own record resolution — it is
the log's bookkeeping, never a bundle target itself.
Viewing is not disclosing. Reading your own ledger — ledger show,
status — mints no receipt; only a disclose() call, when content is
about to cross a boundary to another party, does.
Disclosure.to_dict() / Disclosure.from_dict() round-trip through plain
JSON, same as Bundle. verify_disclosure() checks the disclosure
record's own signature, that it agrees with the Disclosure object, every
bundle it carries, and — for every disclosed payload field — that it
recomputes to the digest committed on that record's receipt: a tampered
payload names itself the same way a tampered bundle does.
Registration is opt-in, always — for direct/manual use of this API
CheckpointConfig.ts_urls defaults to an empty list — nothing is
registered anywhere until you set one. (This is the manual API described in
this section; capsule_emit.core._emit_capsule()'s own default path above does not
use CheckpointConfig — it resolves its endpoint the same way the anchor
does, via witness_url= / CAPSULE_WITNESS_URL.) The free public-good
witness tier at witness.agentactioncapsule.org (DEFAULT_TS_URL — a
separate, live witness service serving POST /checkpoints) is documented and
available, but a generated config shows it commented out
(emit.EXAMPLE_CONFIG_TOML), so opting in is an explicit uncomment. Any
conforming SCITT Transparency Service can be substituted — nothing here is
tied to one operator.
from capsule_emit.checkpoint import CheckpointConfig, due_for_checkpoint, lag_exceeded
cfg = CheckpointConfig(cadence_entries=100, cadence_seconds=900, max_lag_entries=200)
# cfg.ts_urls == [] until you set it — e.g. cfg.ts_urls = [DEFAULT_TS_URL]
due_for_checkpoint(cfg, entries_since_last=3, seconds_since_last=920) # True: age leg
due_for_checkpoint(cfg, entries_since_last=0, seconds_since_last=920) # False: no unwitnessed work
Cadence and scheduling are yours: due_for_checkpoint/lag_exceeded are
pure helpers over your own counter — this package never runs a timer or a
cron of its own (no timing-jitter, no scheduling as a service).
due_for_checkpoint takes an optional seconds_since_last for the age leg
("100 entries or 15 minutes, whichever first") — pass the time since the
first unwitnessed entry, not since your last poll. Omitting it (or passing
entries_since_last=0) falls back to the entry-count leg alone: the age leg
never fires when there's no unwitnessed work, matching the default sealing
wiring's idle-silence guarantee above.
Minimal example
from capsule_emit.checkpoint import MmrLedger, emit_checkpoint
class MySigner:
def __init__(self, key_id, secret):
self.key_id = key_id
self._secret = secret
def sign(self, digest_hex: str) -> str:
import hashlib, hmac
return hmac.new(self._secret, digest_hex.encode(), hashlib.sha256).hexdigest()
mmr = MmrLedger(my_log) # my_log: your own append-only capsule log
mmr.sync() # or append() through mmr directly
cp = emit_checkpoint(mmr, MySigner("node-a", b"..."), log_id="my-log")
# cp.root, cp.mmr_size, cp.signature -- ready to store or register.
Provenance
Ported from capsule-ledger's capsule_ledger/mmr/{core,index,store}.py
per Amendment E (2026-08-21): the CLL core is substrate a counterparty needs
in order to verify a log, so it lives in the neutral producer library rather
than forked per consumer. capsule-ledger consumes this package through its
public interface — see its own docs for the ledger-specific wiring.