Metadata-Version: 2.4
Name: wai-agent-audit
Version: 0.1.0
Summary: Signed, tamper-evident receipts for A2A and MCP agent interactions (triple-entry accounting)
Author: wAI Engineering
License-Expression: MIT
Project-URL: Homepage, https://www.waiindustries.com/
Keywords: a2a,mcp,agents,audit,receipts,triple-entry,merkle,opentimestamps
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: python-dotenv>=1.0
Requires-Dist: httpx>=0.27
Requires-Dist: fastapi>=0.115
Requires-Dist: uvicorn>=0.34
Requires-Dist: mcp<2,>=1.9
Requires-Dist: pydantic>=2
Requires-Dist: pydantic-settings>=2
Requires-Dist: pynacl>=1.6.2
Requires-Dist: sqlmodel>=0.0.39
Requires-Dist: opentimestamps>=0.4.5
Provides-Extra: demo
Requires-Dist: crewai[litellm]>=1.0; extra == "demo"
Requires-Dist: claude-agent-sdk>=0.2.110; extra == "demo"
Requires-Dist: a2a-sdk[http-server]>=1.1; extra == "demo"
Provides-Extra: dev
Requires-Dist: pytest>=9.0; extra == "dev"
Requires-Dist: anyio>=4; extra == "dev"

# Agent Receipts

Two AI agents negotiate a real foreign-exchange trade. Every message they
exchange — and every tool call they make — passes through a **receipt
middleware** that they don't know exists. It hashes the payload, signs a
small receipt with each party's own Ed25519 key, and writes it to **three
copies of a ledger**. Edit any past row in any copy and a standalone
verifier catches it. That's the whole project: tamper-evident receipts
for agent interactions (triple-entry accounting, backing an academic
paper).

## Use it on your own agents

```bash
pip install agent-audit-sdk
```

Declare your agents, wrap a boundary, and every interaction across it is
receipted. Two ways to run the same front:

```python
from agent_audit import Agent, Receipts

receipts = Receipts(
    agents=[Agent("planner", planner_seed), Agent("researcher", researcher_seed)],
    ledger_path="receipts.db",
)
front = receipts.a2a(
    upstream="http://researcher:9000", caller="planner", callee="researcher"
)

# (a) as an endpoint — point the planner's researcher URL at :9102 and
#     change nothing inside either agent
await receipts.serve((front, 9102))

# (b) in your own app — mount it, and drive the ledger worker from your
#     lifespan with `async with receipts.running():`
app.mount("/researcher", front.build_app())
```

`receipts.mcp(...)` does the same for an agent-to-tool boundary. Seeds are
32-byte hex — `python -m scripts.gen_signing_seeds` prints a set.

Two agents, no LLM, no framework, end to end in one command:

```bash
python -m examples.two_agents
```

```
planner  -> researcher : research_request(topic='container throughput')
researcher -> planner  : container throughput looks stable this quarter

3 receipts recorded - neither agent did anything to make this happen:

  seq 1  a2a_message      planner -> researcher  research_request  proof OK
  seq 2  a2a_message   researcher -> planner     findings          proof OK
  seq 3  task_summary  researcher -> planner     completed         proof OK

  the question itself is not in the ledger: True
  every receipt signed by both parties: True
  three copies, all agreeing: [3, 3, 3] True

  VERDICT: MATCH
```

**Recording is never something an agent opts into.** It happens at the
transport boundary, which is why there is no `record_receipt` tool: an
agent that could choose to call it could choose not to, and self-reported
bookkeeping is the thing triple-entry exists to replace.

Reading back is a separate, read-only surface — in process
(`TransparencyService`), over HTTP (`transparency_service.main`, plus a
console at `/ui`), as a CLI (`python -m verifier`), or as MCP tools an
agent can audit with:

```bash
python -m agent_audit.mcp_server   # get_receipts · get_tasks · verify_ledger · get_anchors
```

## The scenario

- **Buyer** (Claude Agent SDK) wants to buy €10,000 with USD, and will
  tolerate at most 80 bps of spread.
- **Seller** (CrewAI) is an FX broker with a 20 bps floor. It prices
  quotes off the **real ECB reference rate**, fetched live from the
  Frankfurter API through a custom MCP server — no mocked data.
- They negotiate over the A2A protocol, up to 3 rounds. Deal or no-deal,
  every turn is receipted.

## Architecture

```
buyer ──A2A──▶ ┌────────────────────┐ ──A2A──▶ seller
               │ receipt middleware │
   seller ──MCP──▶ │  :9102 A2A front   │ ──MCP──▶ frankfurter (ECB rates)
               │  :9201 MCP front   │
               └─────────┬──────────┘
                         │ per interaction: hash payload → sign twice → append
                         ▼
               ledger.db (SQLite): sender_copy | receiver_copy | neutral_copy
                         ▲                 each row: prev_hash + entry_hash chain
                         │
                  verifier (standalone, read-only)
```

The middleware is a transparent proxy. Agents are pointed at it purely by
env vars (`SELLER_URL`, `FX_MCP_URL`) — zero agent code changes, which is
the point: it must work on agents whose internals you don't control. A
receipt stores only the payload's SHA-256, never the payload, so the
schema works for any future interaction type.

## Run it

```bash
docker compose up -d --build                                  # 5 services
docker compose exec buyer python -m scripts.run_negotiation   # one negotiation
docker compose exec middleware python -m scripts.export_public_keys  # once
docker compose exec middleware python -m verifier             # check the ledger
```

(Bare-metal, one terminal per service: see `docs/running.md`.)

## What you should see

The negotiation prints a `deal` (with the agreed quote) or `no_deal`.
The ledger (`ledger-data/ledger.db`, browsable with any SQLite tool)
then holds one receipt per interaction, all tied to one task id:

```
seq 1: a2a_message    request_quote  buyer->seller
seq 2: mcp_tool_call  get_ecb_rate   seller->frankfurter-mcp
seq 3: a2a_message    quote          seller->buyer            task=35cc9c59
seq 4: a2a_message    counter        buyer->seller            task=35cc9c59
seq 5: a2a_message    accept         seller->buyer            task=35cc9c59
seq 6: task_summary   deal           seller->buyer            task=35cc9c59
```

And the verifier reports:

```
  sender_copy      6 rows   chain OK   signatures OK
  receiver_copy    6 rows   chain OK   signatures OK
  neutral_copy     6 rows   chain OK   signatures OK
  cross-copy     counts OK, contents identical, stored hashes identical

VERDICT: MATCH
```

## Prove the tamper-evidence

Flip one field in one copy with any SQLite tool, rerun the verifier:

```
  receiver_copy    6 rows   chain BROKEN at seq 3   2 signature failure(s)
VERDICT: MISMATCH
```

SQLite being editable is fine — the guarantee is that tampering is
*detectable*, not impossible. Detection is the product.

## Where things live

| Path | What |
|---|---|
| `agent_audit/` | **The published SDK** — `Agent`, `Receipts`, and the read-only MCP server |
| `receipt_middleware/` | Proxy fronts, receipt schema, signing, spool, ledger, anchoring |
| `transparency_service/` | Query + verify over HTTP |
| `frontend/` | The console served at `/ui` |
| `verifier/` | Standalone verifier (imports only the receipt schema) |
| `agents/buyer`, `agents/seller` | The two negotiating agents (the demo) |
| `custom_mcps/frankfurter/` | MCP server wrapping the ECB rates API |
| `config/` | All env vars and fixed demo constants |
| `docs/technical-spec.md` | Every design decision, with the why |

The demo runs *on* the SDK rather than beside it — `receipt_middleware/main.py`
builds the same `Receipts` object the snippet above does, so the published
surface is the one exercised every day.

Honest caveat (stated in the spec too): this POC is single-operator —
one middleware holds all keys and writes all three copies. Real
multi-party non-repudiation needs independently operated middlewares;
that and public-chain anchoring of the neutral copy's root hash are the
next steps.
