Byspec
Documentation

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.

sh
byspec ledger keygen                       # writes .byspec/ledger-key.pem (0600) and .byspec/ledger-key.pub

Then 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.

sh
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#

sh
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 1

A 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#

json
{
  "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 get prev_hash and seq, 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 a LEDGER_CORRUPT-class error, not a silent skip.
  • verifyChain({ root }): streams the file, recomputes every hash and prev_hash link, 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, verbatim

manifest.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