Skip to main content

Sanning Proof

Offline verification for the Sanning evidence plane. If someone handed you an evidence pack and asked you to check it, this package is the whole answer: no account, no API key, no network call, and no relationship with Sanning. Sanning is never in the trust path.

Two kernels, one contract. This page covers the Python one.

  • Python: sanning-proof on PyPI. A library, with no command of its own.
  • TypeScript: @sanning/proof on npm, browser and Node ≥ 20. Ships the proof command.

If instead you are producing evidence, the write SDK is sanning-anchor and the hosted plane is console.sanning.io. For what the evidence plane is and how anchoring, reading and verifying fit together, see the Sanning documentation.

Install

pip install sanning-proof

Check a pack you were handed

A pack is one zip holding bundle.json beside a logs/ directory. Unzip it and check it from your terminal:

sanning-proof verify pack/bundle.json --logs pack/logs

Exit codes are the contract, so a script can read them: 0 verified, 1 failed, 2 malformed or usage, 3 undetermined. 3 is deliberately not 1: undetermined means a check could not be run, and reporting that as failed would say the evidence is bad when it is not.

Two things leave a run undetermined. An unreachable gateway leaves the on-chain half unanswered, which is the network being down rather than the evidence being bad. And a record nobody hashed leaves the PACK incomplete: the headline is the verdict on the pack, the signed bundle plus whatever disclosed bytes travelled beside it, which is the same verdict the verify page prints. A pack can be incomplete while the bundle it carries is impeccable, so both verdicts are printed whenever they differ, along with every reason the pack is not verified.

Add gateway URLs as a second argument to check the on-chain half as well; omit them for a fully offline verification, which needs no account, no API key and no network. Only arweave witnesses are re-fetched. A witness of any other kind is not checked, and with gateways passed the run answers exit 3.

The same command exists in the TypeScript kernel as npx @sanning/proof verify. A cross-kernel leg runs both over the same bundles and compares their exit codes, including the --logs lanes, so a verdict cannot drift between them unnoticed. Their printed output is not identical: the TypeScript command reports more per-event detail.

Or from Python, if you would rather have the result as an object than as an exit code:

import json
import pathlib

from sanning_proof import verify_evidence_bundle

pack = pathlib.Path("pack")
bundle = json.loads((pack / "bundle.json").read_text())
content = {p.stem: p.read_bytes() for p in (pack / "logs").glob("*.json")}

result = verify_evidence_bundle(bundle, content=content)
print(result.status)                                    # "verified"
print(sum(1 for e in result.events if e.ok), "verified")

content= binds each disclosed raw log to the commitment inside its own signed record. Omit it and every event's content check is undetermined rather than failed: absence of a disclosure is not evidence of tampering.

Nothing leaves your machine. The kernel does no I/O of its own.

Check a case: several packs, one verdict

When a case spans more than one pack (two agents, two organizations, one hand-off between them), check the packs together:

sanning-proof case assessor-pack/ challenger-pack/
unzip decision-file.zip -d case/ && sanning-proof case case/   # a decision file
sanning-proof case case/ --json               # the full reading, for a script

Each argument is a folder: one pack (bundle.json plus logs/), or a folder that holds several packs, such as an unpacked decision file. case does not open archives. Given a .zip, it exits 2 and says to unzip it first and pass the folder.

Packs are found by the verify page's splitting rule, so the page and this command apply the same rule to the same unpacked tree: every bundle.json is a pack, a file belongs to the deepest pack whose bundle.json or logs/ it sits in, and every other file is listed as outside every pack and never opened. A .zip inside the folder is a file like any other. Several folders are read as one folder holding them, each under its own name. Each bundle.json is read with verify's own reader, so the two commands classify one bundle the same way.

The walk is bounded and follows no link:

  • A symbolic link is never followed. It is listed as outside every pack and not read, even under a pack's logs/, and a link named on the command line exits 2.
  • A folder nested more than 32 levels deep, more than 50,000 files and folders in all, or a name that is not valid UTF-8 exits 2. A bad name is refused rather than escaped, because two different names would then read as one.
  • A bundle.json nested more than 512 levels deep exits 2, from case and from verify alike.

Known differences from the verify page: the page opens a dropped .zip; the page drops a leading byte-order mark from bundle.json, which verify's reader refuses; the page reads whatever the browser hands it for a link, a bad name or a very deep tree; and the page loses a record named __proto__, which this command keeps.

The first line is the case verdict. Then come the kernel's one-line summary and every exception the reading names, in the kernel's own words, followed by the packs, the files outside them, and the on-chain half.

Verdict Exit
verified 0
failed 1
malformed 2 (also usage, an input that cannot be read as a case, or an unexpected error in the command)
incomplete 3
partial 3
mixed 4

mixed means the case has more than one outcome, so read the exceptions. It has its own code because each of the others would claim something a mixed case has not established: 0 and 3 would soften a case that holds a failure, and 1 would accuse a case whose outcomes are all undetermined.

What a case reading establishes. Every pack is verified offline. Every record every pack supplied is read in one pile under the hand-off rules of envelope-spec §4.1, never partitioned by pack, issuer or organization. A hand-off record with no counterpart among the case's records is always named, and when a pack signed by the key it names is in the case, the reading says that pack holds no counterpart, unless every candidate it had was accounted for by another record (A20), which reads as no counterpart and names no pack.

What it does not establish. That the case is complete, since a party can decline to hand over a pack. That a party which presented no pack under the named key recorded nothing. That two organizations are independent of each other. That any witness was re-fetched on chain: the reading makes no network call, and the output says on-chain NOT CHECKED unless you pass --gateways <url,url>. With gateways, each pack's witnesses are re-fetched pack by pack and reported beside the reading, which they never change. A witness that does not match exits 1, and an on-chain half that could not be settled turns 0 into 3.

An unexpected error inside the command exits 2, never 1, so a defect in the command is never reported as failed evidence.

The same command exists as npx @sanning/proof case, and a cross-kernel leg runs both over every case/ corpus vector and every tree in ts/vectors/case-split.json, and requires identical output, identical --json and identical exit codes.

What a verdict claims

A verdict is recomputed from the artifact, never read off it. A bundle whose own asserted verdict disagrees with the recomputed one is reported as the recomputed one.

A verified verdict is offline, and it does not mean the chain was consulted. A bundle names the witness that holds its bytes; asking that witness whether it does is a separate, online act.

What a verifier can prove, and what it must not claim, is specified in specs/evidence-bundle.md §5.3.4, which ships inside this package, under "The standard". It draws four boundaries, covering completeness, the chain, identity and per-agent slices. Read it before you write down what a pack proved. A verifier that overclaims is worth less than no verifier. The plain reading is at what a verdict means.

The rest of the family

To check one signed envelope on its own, rather than a whole bundle:

from sanning_proof import verify_envelope

result = verify_envelope(envelope)
result.ok   # spec_version accepted + payload binding + Ed25519 signature

Three arguments cover the rest:

  • verify_envelope(envelope, expected_content_hash=...) binds an artifact you already hold to the provenance an envelope commits to, and reports which role it matched.
  • verify_envelope(envelope, payload_bytes=...) checks an external-commitment envelope against the committed bytes.
  • verify_proof_bundle(bundle) proves a leaf event was in a signed checkpoint.

The TypeScript kernel has parity, including the RFC 9162 Merkle primitives.

Sign an envelope

Most producers never call this kernel to sign: the write SDK, the agent daemon and the MLflow plugin all sign through it, and reaching for sanning-anchor is the shorter path. For a custom producer:

from sanning_proof import canonical_json, sha256_hex, sign_envelope, signing_key_from_seed_hex

key = signing_key_from_seed_hex(SEED_HEX)
envelope = sign_envelope({
    "spec_version": "sanning.agent/v1",
    "event_id": EVENT_ID,
    "signed_at": "2026-01-01T00:00:00Z",
    "payload": payload,
    "payload_hash": sha256_hex(canonical_json(payload)),
}, key)

Replace SEED_HEX with a 32-byte hex Ed25519 seed and EVENT_ID with the event's UUID. sign_envelope takes the envelope minus signature and returns it with public_key and signature filled in, per the spec.

The standard

The specifications ship inside this package, beside the kernel that implements them, so the contract and the implementation claiming to satisfy it arrive together and can be read against each other:

pip download --no-deps --no-binary :all: sanning-proof
tar xzf sanning_proof-*.tar.gz && ls sanning_proof-*/specs/
Specification Covers
envelope-spec.md The producer-neutral Verifiable Event Envelope family contract
evidence-bundle.md The sanning.evidence/v1 report wrapper, its body types, and the verdict boundaries in §5.3.4
evidence-export.md The sanning.evidence.export/v2 wire format
log-store.md The sanning.logstore/v1 store a pack's logs/ is materialized from
architecture.md The kernel, producer, connector and transport factoring standard
governance.md Who decides, and how

Three envelope profiles are registered against the family contract: sanning.agent/v1, the agent daemon's inline-payload profile; sanning.mlflow/v1, the MLflow plugin's external-commitment profile; and sanning.events/v2, the anchor SDK's minimal-disclosure profile (sanning.events/v1 until the producer-to-agent cut, and no longer accepted). This Python kernel accepts all three. Additive minors are accepted within a major, while unknown majors and malformed versions fail closed.

Conformance discipline: both kernels reproduce one pinned corpus byte for byte, across JCS-canonical bytes, payload hashes, envelope-for-signature bytes, deterministic signatures, Merkle roots and audit paths. Neither passes by agreeing with the other. If this package disagrees with a vector, the package is wrong and the vector is not.

Kernel scope

Deliberately small: canonicalization (RFC 8785), SHA-256 hashing, Ed25519 sign and verify, RFC 9162 binary Merkle inclusion proofs, and the profile registry. No I/O, no networking, no key lifecycle. Gateway fetching, attestation polling and key storage belong to the products that import it.

Three dependencies, and adding to them is a design decision rather than a convenience: PyNaCl and jcs for the kernel primitives, and cryptography for RSA-PSS attestation verification.

A malformed envelope returns a failed result rather than raising. A verifier that crashes on a hostile artifact has handed that artifact a way to avoid being checked, so both kernels treat adversarial input as something to report on, not something to fall over on.

Security

Report vulnerabilities to security@sanning.io. We support responsible disclosure and ask for a reasonable opportunity to investigate before public disclosure.

License

MIT. The verifier is deliberately open-licensed so anyone can audit it and verify evidence independently of Sanning.

Release files for sanning-proof 0.12.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sanning-proof 0.12.0
File Size Uploaded
sanning_proof-0.12.0.tar.gz 1.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for sanning-proof 0.12.0
File Interpreter ABI Platform
sanning_proof-0.12.0-py3-none-any.whl Python 3 none any Details

Total release size: 1.9 MB

Release files / sanning_proof-0.12.0.tar.gz

Download URL sanning_proof-0.12.0.tar.gz
Size 1.7 MB
Tags Source
SHA-256 checksum
How to use checksums
05e77840ab8390247938c261436a2f0dca1a4c24df38cdfd6e11cdb6f8a5c896
BLAKE2b-256 checksum
How to use checksums
0cb001c9be6a5beb40b68b6786310cc3f1290abd993f8539e3417cb7b7da3a8d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / sanning_proof-0.12.0-py3-none-any.whl

Download URL sanning_proof-0.12.0-py3-none-any.whl
Size 190.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
85ec3fefaf0fa1cf0fad57c4a9197c2fdf4e4b07a4d0724135c9c2866baeadc8
BLAKE2b-256 checksum
How to use checksums
88b1f0de5f957cc9eb377b4e41db47d8048c40393c33bce735f29de1e8554db9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

0.15.0

2 release files

0.14.0

2 release files

0.13.0

2 release files

This release

0.12.0 This release

2 release files

0.11.1

2 release files

0.11.0

2 release files

0.10.0

2 release files

0.9.2

2 release files

0.9.1

2 release files

0.9.0

2 release files

0.8.3

2 release files

0.8.2

2 release files

0.8.1

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.4.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page