vouchwell (Python)
Tamper-evident receipts for what your AI agents do, from Python. This is the
Python SDK for Vouchwell, and it
writes exactly the format the vouchwell CLI reads:
- a log written here passes
vw verify; - a bundle exported here passes
vw check; - logs and bundles written by the JavaScript side open and verify here.
CI runs both directions against the real CLI on every commit.
pip install "vouchwell @ git+https://github.com/vouchwell/vouchwell#subdirectory=sdk/python"
That installs straight from this repository. The package is not on PyPI yet;
once it is, pip install vouchwell will do the same.
Python 3.9+. The one dependency is cryptography, for Ed25519.
Record an agent's tool calls
from vouchwell import ProofLog, Recorder
log = ProofLog.open_or_create(".vouchwell")
rec = Recorder(log, agent="support-bot", principal="ops@acme.com", namespace="stripe")
@rec.tool("refund", metrics=lambda order_id, amount_cents: {"amount_usd": amount_cents / 100})
def refund(order_id: str, amount_cents: int):
return stripe.Refund.create(payment_intent=order_id, amount=amount_cents)
refund("pi_123", 4200)
Each call leaves two signed receipts:
- An intent receipt, before the call runs. A crash mid-call still leaves evidence it was attempted.
- An outcome receipt, when it returns or raises, linked to the intent.
Async functions work the same. So does a context manager:
with rec.call("crm.update", params={"id": 42}) as call:
call.result = crm.update(42)
Arguments and results are committed to, not stored. A receipt holds a
salted hash plus a preview with secrets and personal data masked (API keys,
card numbers, emails and so on). The salts stay in the log's private
salts.jsonl, so you can later prove what a value was:
log.reveal(seq, "params", value). Or you can destroy the salts, and the value
becomes unprovable forever while the receipt still verifies:
log.shred(predicate).
Check calls before they run
def decide(target, params):
if target.endswith(".delete"):
return {"outcome": "deny", "rules": ["no-deletes"], "reason": "agents may not delete"}
return {"outcome": "allow", "policy": "v3"}
rec = Recorder(log, agent="support-bot", principal="ops@acme.com", decide=decide)
A refused call never runs. It gets one receipt, and PolicyDenied is raised.
An escalate outcome is a denial here, recorded as policy:no-approver,
because nobody in the process can approve it.
The full policy language (budgets, rate limits, escalation to a person, Slack
approvals, monitor mode) lives in the vw proxy MCP proxy and in the
JavaScript Recorder in @vouchwell/core. For an agent that uses MCP, wrap
its servers with the proxy instead. A Python port of the policy engine is
planned.
Use it with LangChain or the OpenAI Agents SDK
Wrap the agent's tools once; use the wrapped ones in their place. Each call is
checked with decide, recorded as intent and outcome, and a refused call
never runs: the model is told why in the tool's reply, as the MCP proxy does,
instead of the agent crashing.
from vouchwell.integrations.langchain import record_tools # pip install "vouchwell[langchain]"
agent = create_react_agent(model, record_tools(rec, [search, refund]))
from vouchwell.integrations.openai_agents import record_tools # pip install "vouchwell[openai-agents]"
agent = Agent(name="Support", tools=record_tools(rec, [lookup_order, refund]))
Hosted OpenAI tools (web search and the like) run on OpenAI's side, so they are passed through unrecorded. Both adapters need Python 3.10+, and are tested in CI against the real packages.
Find calls that never finished
Each call's intent is on disk before the tool runs. If the process is killed while a call is out, the intent has no outcome, and that is worth knowing:
from vouchwell import find_unfinished
found = find_unfinished(log.entries)
for u in found["unfinished"]:
print(u["seq"], u["ts"], u["target"], u["principal"])
inFlight holds calls from the last five minutes that may still be running,
and abandoned the ones a recorder gave up on while shutting down. vw verify
reports the same list, and vw verify --fail-on-unfinished exits 3 when there
is one.
Hand over evidence
import json
from vouchwell import verify_bundle
log.checkpoint() # sign the current state
open("evidence.json", "w").write(json.dumps(log.bundle()))
# Anyone can verify it, here or with `vw check evidence.json`:
verify_bundle(json.load(open("evidence.json")))
# -> {"ok": True, "issues": [], "checked": 12}
To require an independent witness, pin its key. Get the key from somewhere the bundle can't influence:
verify_bundle(bundle, min_witnesses=1, trusted_witnesses={"pw1…": "<public key>"})
vw report (from the npm CLI) turns a log written here into an evidence pack
for auditors.
Ship to a hub
from vouchwell import push
push(log, "https://hub.acme.com", token=os.environ["VOUCHWELL_TOKEN"], name="support-bot")
It sends whatever the hub doesn't have yet, and is safe to call again. Plain
http:// is refused unless the hub is on this machine, so the API key never
crosses a network unencrypted.
What's here
| Module | |
|---|---|
canonical |
RFC 8785 canonical JSON: UTF-16 key order, ECMAScript number formatting |
merkle |
RFC 6962 trees, inclusion and consistency proofs |
unfinished |
find_unfinished: calls sent and never answered |
integrations |
langchain.record_tools, openai_agents.record_tools |
keys |
Ed25519 identities, canonical base64url |
receipt |
Build, seal, sign and verify receipts |
checkpoint |
Signed tree heads, witness countersignatures |
log |
ProofLog, the on-disk log the CLI shares, and verify_bundle |
record |
Recorder, for Python agents |
remote |
push to a hub |
Tested against the RFC 6962 Certificate Transparency reference tree and the RFC 8032 Ed25519 vectors, plus byte-level canonicalization parity with the JavaScript implementation over hundreds of awkward values.
License: Apache-2.0.
Metadata
Release files for vouchwell 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vouchwell-0.6.0.tar.gz | 43.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vouchwell-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 78.9 kB
Release files / vouchwell-0.6.0.tar.gz
| Download URL | vouchwell-0.6.0.tar.gz |
|---|---|
| Size | 43.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd7cfde00e16fee58aba5ac5a0dda09294959979f73b481ad084a60bfb959aec
|
|
BLAKE2b-256 checksum How to use checksums |
eda5d277863134145c6695156d81d3057c2a8a71c8214a9f63c1d8abdcbfa1fb
|
| 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 Oct 12, 2026.
Transparency logRelease files / vouchwell-0.6.0-py3-none-any.whl
| Download URL | vouchwell-0.6.0-py3-none-any.whl |
|---|---|
| Size | 35.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7b001a3ac0cb9fce01095ef693e6a65e5615a543136ff4a8087aa2fd64043fc0
|
|
BLAKE2b-256 checksum How to use checksums |
ac20a5f9b975a4641ac40f36d4a26e5f08e1e16e163e12aada2cb513949b2e59
|
| 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 Oct 12, 2026.
Transparency log