Ledger and evidence packets
The ledger is the product. A hash chain proves order and integrity; a signature proves who wrote it; a packet lets a third party verify both with nothing but the file.
Signed ledgers#
A hash chain proves order and integrity; a signature proves who wrote it. Entries can carry a
signature over their hash, kept outside the hashed payload so signing never changes a hash.
byspec ledger keygen # writes .byspec/ledger-key.pem (0600) and .byspec/ledger-key.pubThen set ledger.signer: ed25519-file in byspec.config.yaml. Every verify run signs its
entries with the private key; a missing key fails the run with exit 3 rather than writing unsigned
entries. Commit the .pub file next to the ledger and keep the .pem out of git.
byspec ledger verify # hashes, links, and every signature that is present
byspec ledger verify --require-signatures # also fails on unsigned entries (exit 1)Verification looks up public keys by the entry's key_id among .byspec/*.pub, so a chain signed
by several keys over time still verifies. ledger.signer: kms (asymmetric ECDSA P-256 in AWS KMS,
kms_key_id) is what the cloud control plane uses for tenant ledgers; the CLI does not sign with
KMS locally. ledger.store: none, or --no-ledger, runs verification without touching a ledger at
all, which is how the cloud sandbox runs the CLI while the control plane owns the chain.
Source: README.md § Signed ledgers
Evidence packets#
byspec export # byspec-<run_id>.tar.gz for the last run
byspec export <run_id> --out audit.tar.gz
byspec export --verify audit.tar.gz # "packet ok" and exit 0, or the problems and exit 1A packet is a tar.gz containing run.json (the Run exactly as saved), ledger.jsonl (this run's
entries, in order, with signatures), evidence/<criterion>/<n>-<kind>.txt, spec/<file> for every
spec file the run parsed, and manifest.json written last with the sha256 and size of every other
file, the run's ledger sub-chain summary, and the public keys needed to verify signatures. A packet
is therefore self-verifying: --verify recomputes every hash, re-links the sub-chain, and checks
each signature offline, with no access to the repository or to AWS. Packets produced by the cloud
control plane (Section "Cloud control plane") have the same layout and verify with the same command.
Source: README.md § Evidence packets
Ledger entries and the chain#
Layout#
.byspec/
├── ledger.jsonl # append-only
├── last-run.json
├── runs/<run_id>.json
├── evidence/<run_id>/<criterion_id>/<n>-<kind>.txt
└── cache/.byspec/cache/ SHOULD be gitignored. .byspec/ledger.jsonl, runs/, and evidence/ MAY be committed; the reference byspec init gitignore commits the ledger and ignores evidence and cache.
LedgerEntry#
{
"seq": 42,
"ts": "2026-09-11T14:03:22.118Z",
"run_id": "0192...",
"spec_path": "docs/byspec-implementation-spec.md",
"spec_sha256": "…",
"commit": "a1b2c3…",
"criterion_id": "RUN-004",
"actor": { "kind": "ci", "tool": "github-actions" },
"verdict": "PASS",
"checks": [{ "index": 0, "type": "test", "status": "pass", "evidence_sha256": ["…"] }],
"judge": { "verdict": "PASS", "confidence": 0.91, "model": "…", "cache_hit": false },
"prev_hash": "…",
"hash": "…"
}hash = sha256(canonical_json(entry without "hash")). prev_hash is the previous entry's hash, or 64 zeros for seq: 1. ts and run_id are inside the hashed payload (the ledger records when, not just what). Canonical JSON is key-sorted with no whitespace via fast-json-stable-stringify.
Operations#
appendEntries({ root, entries }): opens the file in append mode, reads the last line to getprev_hashandseq, writes each entry as one line, fsyncs. Never truncates. Concurrent appends are prevented with a lock file (.byspec/ledger.lock,proper-lockfile); a held lock beyond 30 seconds is aLEDGER_CORRUPT-class error, not a silent skip.verifyChain({ root }): streams the file, recomputes every hash andprev_hashlink, returns{ ok, entries, first_bad_seq? }.showLedger({ root, criterionId?, last? }): read-only queries.
Source: docs/byspec-implementation-spec.md § 11. Ledger and evidence store
Packet layout#
byspec export [run_id] --out <file.tar.gz> (default byspec-<run_id>.tar.gz) writes a packet containing:
manifest.json # { format: "byspec-packet/1", run_id, created_at, files: [{ path, sha256, bytes }], ledger: { entries, first_seq, last_seq, chain_ok }, public_keys: [{ key_id, alg, pem }] }
run.json # the Run, exactly as .byspec/runs/<run_id>.json
ledger.jsonl # only the entries with this run_id, in seq order (with signatures when present)
evidence/<criterion_id>/<n>-<kind>.txt
spec/<source.file> # every spec file the run parsed, verbatimmanifest.json is written last and lists every other file's sha256 so a packet is self-verifying. byspec export --verify <file.tar.gz> recomputes the hashes, re-verifies the run's ledger sub-chain (prev_hash of the first packet entry is accepted as given; every link inside the packet is checked) and signatures, and exits 0 only when all hold. The control plane produces the same packet from S3 and DynamoDB (Section 9.3), so a packet from either origin verifies with the same command.
Source: docs/byspec-v2-spec.md § 6. Evidence export packets