sanning-anchor
Produce verifiable evidence from your Python agents. Hash locally, anchor a signed commitment, verify offline — with no Sanning account needed to check it.
pip install sanning-anchor
Anchor an event
from nacl.signing import SigningKey
from sanning_anchor import Anchorer
anchorer = Anchorer(
api_key="sanning_...",
subject={"type": "producer", "producer_id": "my-agent"},
signing_key=SigningKey.generate(),
environment="production",
)
result = anchorer.anchor(
content=b'{"step": "tool_call", "tool": "search"}',
event_type="myapp.tool_call",
)
result.record_bytes # ← RETAIN THESE
What leaves your process, and what does not
Your content never does. It is hashed locally; only the hash goes into the signed envelope. The control plane is content-blind by construction, not by policy — it never receives the bytes.
record_bytes is your retention obligation. It is what payload_hash
commits to. Sanning never holds it, so if you lose it you hold a commitment to
something you can no longer produce.
What you do not need
No Arweave wallet. No chain identity. No data item. Placement is Sanning's act, which is why this package has no blockchain code in it at all — and why it is a few hundred lines rather than a chain client.
LangChain
pip install "sanning-anchor[langchain]"
from sanning_anchor import AnchorCallbackHandler
with AnchorCallbackHandler(anchorer) as handler:
agent.invoke(inputs, config={"callbacks": [handler]})
for outcome in handler.results:
my_store.put(outcome.event_id, outcome.record_bytes) # ← RETAIN THESE
Every chain, model, tool and retriever step is anchored, with LangChain's
run_id/parent_run_id tree committed alongside a per-run seq and
prev_event_id. That makes the trail deletion-evident and reorder-evident:
a missing event leaves a gap in seq, a moved one breaks the chain.
Prompts, outputs and tool I/O go into the committed record, which stays with
you. Pass map_payload to redact before hashing; returning None skips an
event without leaving a gap. What you receive there is a copy, so redacting
cannot touch your agent's live state.
When a step cannot be anchored
The trail never claims completeness it does not have. If an event cannot be anchored — a network blip, a bad key, a value that has no faithful JSON form — three things happen, and none of them is a log line you have to be watching for:
- the event still burns its
seqslot, so the survivors cannot close ranks over it: an auditor sees a hole inseqand a danglingprev_event_id, offline, with no cooperation from us; - its
record_bytesare still retained where the record got as far as being signed — after a failure they are the only copy, and a timeout tells you nothing about whether the plane accepted it; - leaving the
withblock raisesIncompleteTrailError, naming every gap.
A gap is reported as undelivered, never as a bad record. An anchoring failure
is ours, not yours — but it is never reported as fine either.
handler.is_complete # False if anything was dropped
handler.gaps # the outcomes that are not in the anchored trail
handler.close() # raises IncompleteTrailError; returns the outcomes
AnchorCallbackHandler(anchorer, raise_on_gap=True) # stop the agent instead
AnchorCallbackHandler(anchorer, on_event=alert) # stream every outcome
Transient failures (a 5xx, a 429, a transport error) are retried before any of that. A 4xx is not: the plane has told us the envelope is wrong, and repeating it is a slower failure, not a recovery.
Verifying
Use the open kernel, which needs no account and no network:
pip install sanning-proof
from sanning_proof import verify_envelope
verify_envelope(result.envelope, payload_bytes=result.record_bytes)
Byte-identical to the TypeScript SDK
sanning-anchor and @sanning/anchor produce the same signed bytes for the
same event. Both are gated against the same pinned conformance corpus, so a
pack produced by one verifies identically under the other. This matters more
than it sounds: two SDKs disagreeing by one byte would produce evidence that
cross-verifies as tampered.
The LangChain adapters commit JSON.stringify(payload), which json.dumps is
not (1.0 → 1.0 where JavaScript gives 1; NaN and Infinity, which are
not JSON at all; integer-like keys unsorted). That serializer has its own
cross-language corpus, generated from the real JSON.stringify and asserted
from both sides.
Where it deliberately refuses rather than diverges: an integer outside
±(253−1), a set, a reference cycle. JavaScript would round the first
one — and two different 64-bit trace ids can round to the same double, which is
a false integrity verdict rather than a formatting difference. So it fails at
anchor time, when you can still fix it, rather than at verification, when
the record is already sealed. Convert the value in map_payload.
MIT licensed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sanning_anchor-0.1.1.tar.gz.
File metadata
- Download URL: sanning_anchor-0.1.1.tar.gz
- Upload date:
- Size: 79.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6916f1be93ddace81c432330311cd946e8e4a6b2aa6ac568f6cfee5c1b75482d
|
|
| MD5 |
f076acf69f84ab1a5b20d2911746f2c2
|
|
| BLAKE2b-256 |
f8caaf39b883ff44c352b0dd3174d0853eb1ecc71c8c0300183b72e5a1b8c174
|
Provenance
The following attestation bundles were made for sanning_anchor-0.1.1.tar.gz:
Publisher:
release-python.yml on sanning-io/anchor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanning_anchor-0.1.1.tar.gz -
Subject digest:
6916f1be93ddace81c432330311cd946e8e4a6b2aa6ac568f6cfee5c1b75482d - Sigstore transparency entry: 2711453361
- Sigstore integration time:
-
Permalink:
sanning-io/anchor@74ea139ffe802e7fb4834b5081fe192d102b3512 -
Branch / Tag:
refs/tags/py-v0.1.1 - Owner: https://github.com/sanning-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@74ea139ffe802e7fb4834b5081fe192d102b3512 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sanning_anchor-0.1.1-py3-none-any.whl.
File metadata
- Download URL: sanning_anchor-0.1.1-py3-none-any.whl
- Upload date:
- Size: 49.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
15a3ad80bf3d0d79e68b74e8002d145ffbd6a1c4ba6c684da86a586d509df40c
|
|
| MD5 |
d9931780f5c357a441d2ea2d813c5599
|
|
| BLAKE2b-256 |
26d4e17920fab7c9639d7ddbbc6edc93f6dc5fac2d47ea6e7643f2820fd0c9d9
|
Provenance
The following attestation bundles were made for sanning_anchor-0.1.1-py3-none-any.whl:
Publisher:
release-python.yml on sanning-io/anchor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sanning_anchor-0.1.1-py3-none-any.whl -
Subject digest:
15a3ad80bf3d0d79e68b74e8002d145ffbd6a1c4ba6c684da86a586d509df40c - Sigstore transparency entry: 2711454215
- Sigstore integration time:
-
Permalink:
sanning-io/anchor@74ea139ffe802e7fb4834b5081fe192d102b3512 -
Branch / Tag:
refs/tags/py-v0.1.1 - Owner: https://github.com/sanning-io
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-python.yml@74ea139ffe802e7fb4834b5081fe192d102b3512 -
Trigger Event:
push
-
Statement type: