Metadata-Version: 2.4
Name: vouchwell
Version: 0.6.0
Summary: Tamper-evident receipts for AI agent actions. Python SDK, wire-compatible with the vouchwell CLI.
Author: IdeaboticsAI
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/vouchwell/vouchwell
Project-URL: Source, https://github.com/vouchwell/vouchwell/tree/main/sdk/python
Project-URL: Issues, https://github.com/vouchwell/vouchwell/issues
Keywords: ai-agents,audit,transparency-log,merkle,ed25519,eu-ai-act
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: System :: Logging
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: cryptography>=41
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.2; extra == "openai-agents"
Provides-Extra: test
Requires-Dist: pytest>=7; extra == "test"
Requires-Dist: langchain-core>=0.3; python_version >= "3.10" and extra == "test"
Requires-Dist: openai-agents>=0.2; python_version >= "3.10" and extra == "test"

# vouchwell (Python)

Tamper-evident receipts for what your AI agents do, from Python. This is the
Python SDK for [Vouchwell](https://github.com/vouchwell/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.

```bash
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

```python
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:

1. **An intent receipt, before the call runs.** A crash mid-call still leaves
   evidence it was attempted.
2. **An outcome receipt, when it returns or raises**, linked to the intent.

Async functions work the same. So does a context manager:

```python
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

```python
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.

```python
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:

```python
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

```python
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:

```python
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

```python
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.
