agent-custody
Proof of what your AI agents did. Every tool call an agent makes through the gateway becomes a signed record of who authorized it, what the agent saw, what it did, and what depended on it, checkable by anyone with the public keys. What each check proves, and against whom, is a table, not a slogan.
It is a chain of custody for both what an agent does and what it believes: authorize, execute, record, verify, trace, remediate. The agent is not trusted. The layer around it is, and every record says exactly how far that trust extends.
Packages
| package | what it is | status |
|---|---|---|
@agent-custody/receipts | Signed receipts for tool calls: an MCP gateway with Cedar policy and a Merkle transparency log, an in-process SDK with framework adapters, and an offline verifier | working, twenty-five runnable tutorials |
agent-custody on PyPI | The Python client of the sidecar: decide, record, wrap, and adapters for LangChain, the OpenAI Agents SDK, the Claude Agent SDK, CrewAI, Google ADK, and a Hermes Agent plugin, tested against the real packages | working; Go, Java, and Rust clients live in examples/languages |
@agent-custody/state | Governed memory: a fact ledger where every write cites the receipt that caused it, carries valid time and transaction time, and can be superseded or rolled back | ledger on JSONL or SQLite, memory server behind the gateway or shared over HTTP, quarantine and verified provenance, blast radius, certified forget, retention and legal holds, write-through to Mem0 and Zep, eval CLI with signed reports |
Receipts are the unit. State is the ledger of what the agent came to believe from them. Both append to the same kind of signed log and are checked by the same kind of verifier.
What each piece is for, and when you need it
Each part exists because a specific thing goes wrong without it. Use the table to decide what to turn on.
| piece | the failure it prevents | you need it when |
|---|---|---|
SDK interceptor (@agent-custody/receipts, in the agent's process) | The only record of what an agent did is its own log: mutable, unsigned, written by the thing you are trying to check. | Any agent that calls tools. Turn it on first; it is a hook or a wrapped function. It is honest about being self-reported: every field is labelled claimed. |
Gateway (@agent-custody/receipts, a separate process between agent and tools: MCP servers, REST APIs described as tools, or other agents reached over A2A) | An in-process hook can be skipped, and a policy that reads the agent's own arguments can be fed lies. | The call moves money, touches production, or handles personal data, or a security owner has to sign off on what agents may do. Denied calls never reach the tool and still produce a receipt. For tools named in precommit, the gateway logs a signed authorization before forwarding and withholds the call if the log will not take it, so the evidence exists before the side effect does. Only calls through the gateway are covered; a credential in the agent's own environment bypasses it. |
| Delegation grant and Cedar policy (used by the gateway) | "The agent was allowed to do that" is a comment in a config file, not something a human signed. | Always with the gateway. The grant is signed by the principal and names agent, tools, and validity window; the policy decides on facts the gateway fetched itself, never on the agent's claims. |
| Transparency log (local file, automatic) | A signed receipt can be deleted or replaced after the fact and nobody would know. | Always; every receipt is a leaf with an inclusion proof. Costs nothing to use. |
Remote log (log in the config, plus the log command or a hosted log) | The operator holds the local log file and can rewrite history in it. | A receipt will be shown to someone who does not trust the operator: an auditor, a counterparty, a customer, a regulator. The log's key, not yours, signs the tree heads. |
Verifier (verify and audit commands, or verifyBundle) | Trust that depends on access to the system that produced the record is not trust. | Whenever a receipt leaves the team that made it: an audit, a CI gate, acceptance by the other side of an agent-to-agent call. It needs public keys and nothing else. |
The rule of thumb: record everything with the SDK, enforce the consequential calls with the gateway, log remotely once a receipt has an audience outside the team, put beliefs in the ledger the moment memory outlives a session, and turn on quarantine, forget, and holds the moment that memory holds anything about a person.
Getting started
Node 22 or later. Both packages ship compiled JavaScript with type declarations.
npm install @agent-custody/receipts @agent-custody/state
1. Give the agent's process a signing key and a config. The key signs every receipt. The config says where receipts and the log go.
npx agent-custody keygen --dir keys --name app
{ "agentId": "support-bot", "identity": { "keyFile": "keys/app.key" }, "receiptsDir": "receipts", "logFile": "log.jsonl" }
2. Record a tool call as a receipt. Wrap the functions the agent calls, or record a call explicitly. Either way a signed receipt lands in receipts/ and a leaf in the Merkle log.
import { createSdkIssuer, loadSdkConfig } from "@agent-custody/receipts";
const issuer = createSdkIssuer(loadSdkConfig("./sdk.json"));
const lookup = issuer.wrap("crm.lookup", async (args: { id: string }) => crm.lookup(args)); // records every call
const customer = await lookup({ id: "acct:42" });
const bundle = issuer.record({ tool: "crm.lookup", args: { id: "acct:42" } }, { status: "executed", result: customer }); // or record one by hand and keep the bundle
3. Verify it, offline. Anyone with the public key and a copy of the log can check the signature, the statement, and the inclusion proof. No access to the agent or the issuer needed.
import { loadPublicKey, verifyBundle } from "@agent-custody/receipts";
verifyBundle(bundle, { issuerKeys: [loadPublicKey("keys/app.pub")], principalKeys: [], logFile: "log.jsonl" }).ok; // true
npx agent-custody verify receipts/<id>.json --issuer-key keys/app.pub --log log.jsonl # the same check from the shell
4. Record what the agent now believes, citing the receipt. The ledger is bitemporal: it knows when a fact was true and when the agent learned it. A belief points back at the receipt that produced it.
import { receiptIdOf } from "@agent-custody/receipts";
import { Ledger } from "@agent-custody/state";
const ledger = new Ledger("./ledger.jsonl"); // or "./ledger.sqlite", or "postgres://…"
const belief = await ledger.assert({ subject: "acct:42", predicate: "plan", value: customer.plan, space: "org", actor: "support-bot", source: { receiptId: receiptIdOf(bundle) } });
5. When a belief is wrong, undo it without losing the record. Retract removes it from the present, keeps it visible to questions about the past, and restores whatever it had superseded. The receipt id says exactly which call produced the bad belief.
await ledger.retract({ factId: belief.fact.factId, actor: "user:admin", reason: "CRM lookup returned a stale plan" });
await ledger.asOf({ subject: "acct:42" }); // [] now
await ledger.asOf({ subject: "acct:42", validAt: earlier, txAt: earlier }); // still shows what was believed then
await ledger.history(belief.fact.factId); // assert, retract, with actors and reasons
This whole loop is one runnable file, packages/state/examples/02-receipt-to-belief.ts, executed by the test suite.
Where to go next
- Enforce instead of record: put the gateway between the agent and its MCP tools, its REST APIs, or any agent it delegates to over A2A, with a signed delegation grant and a Cedar policy. Denied calls never reach the tool and still get a receipt. packages/receipts/docs/usage.md
- Hook an existing framework: Claude Code and the Claude Agent SDK, the OpenAI Agents SDK, the Vercel AI SDK, LangChain, OpenClaw, DeepSeek Harness; in Python also CrewAI, Google ADK, and Hermes Agent. packages/receipts/docs/sdk.md
- Write policies, for tools and for memory, with tested examples, and read what a verified receipt does and does not prove. policies.md, verification.md
- Answer a security questionnaire with a file: which artefact satisfies which SOC 2, ISO 27001, EU AI Act, and UK GDPR requirement. compliance.md
- Give a security reviewer the attacker's view: who could make a receipt false, what stops them, and what is not defended. threat-model.md
- Log to a server the operator does not control, so tree heads are signed by a key that is not yours. verification.md
- Verify a receipt in the browser, nothing uploaded, and check a second implementation against the published conformance vectors. agent-custody.dev/verify,
This repository is built under custody
Every tool call an agent makes in a Claude Code session in this repository becomes a signed receipt, its hash committed to our tenant on the hosted log within seconds; a force push is refused by policy and the refusal is a receipt too. The hook is committed in .claude/, the receipts and keys stay outside the repository, and agent-custody.dev/custody shows the live tenant and sample receipts anyone can verify.
Security
Report a vulnerability privately to partha@charioteerconsulting.com; SECURITY.md has the scope, the response times, and how keys and tokens are protected.
Working in the repository
bun install # one install for the whole workspace
bun run test # every package
bun run build # dist/ for every package, what consumers install
bun run test:python # the Python package, with uv; needs node for the sidecar
bun run ci # everything the GitHub workflow runs; the pre-push hook runs it before any push to main
bun run demo # the receipts demo: keys, grant, policy, tool calls, verification, a tampering attempt
Everything runs on plain Node 22 or later. No build step. Package-level commands, such as the CLI and the tutorials, run from inside that package's directory; each package README says how. CONTRIBUTING.md has the rules for a change, a release, and a publish.
Layout
package.json workspace root: typecheck, test, and demo across packages
tsconfig.base.json compiler options shared by every package
packages/receipts/ the receipts package: src, test, examples, scripts, docs
packages/python/ the Python client package: agent_custody, tests run with uv against a real sidecar
packages/state/ the state package: src, test, examples
site/ the website, generated from this repository's markdown by VitePress
Plan
The receipts package's roadmap is in its README. The state package has the ledger on JSONL, SQLite, or Postgres with every query answered from an index, the memory server behind the gateway and shared over HTTP, quarantine with three provenance levels and value-level evidence, policy over the fact being changed, consumed facts and blast radius, certified forget with keyed digests and verified removal from stores, retention sweeps and legal holds, the custody pack for counsel, a ledger that lives in a JSONL file, SQLite, or your Postgres, write-through to Mem0 and Zep, and the eval CLI with signed reports; its remaining items are indexed queries at scale, more store adapters, and the hosted plane, each tracked as an issue in its README.