Python
pip install agent-custody # the sidecar client and the memory client
pip install "agent-custody[langchain]" # plus one adapter: langchain, openai-agents, crewai, claude-agent-sdkPython 3.10 or later. The package talks to the sidecar for receipts and to the memory server over MCP for beliefs; it holds no key and signs nothing itself.
Client
from agent_custody import Client, PolicyDeniedError, SidecarError, receipt_id_of
client = Client("http://127.0.0.1:8788/", timeout=10.0)| method | request | response |
|---|---|---|
client.health() | GET /health | {"agentId": "…", "keyid": "…", "log": {"kind": "http", "where": "…"}} |
client.decide(tool, args=None, *, model=None, session=None) | POST /decide with {"tool", "args", "model"?, "session"?} | the policy decision {"decision": "allow"|"deny", "reasons": [...], "errors": [...], "policyDigest": "…"}, or None when the sidecar has no policy |
client.record(tool, args, outcome, policy=None, *, model=None, session=None) | POST /record with {"event", "outcome", "policy"} | the receipt bundle {"envelope", "treeHead", "inclusion"} |
client.wrap(tool, fn, *, model=None) | decide, run fn(args), record | a callable that returns fn's result; raises PolicyDeniedError(tool, reason, receipt_id) after recording the denial; records {"status": "error"} and re-raises when fn raises |
outcome is {"status": "executed"|"failed", "result": …}, {"status": "denied", "reason": "…"}, or {"status": "error", "error": "…"}. session is {"id": …, "toolUseId": …}. receipt_id_of(bundle) decodes the statement and returns the receipt id. SidecarError is raised for any HTTP failure, with the sidecar's error text.
refund = client.wrap("stripe.refund", lambda args: stripe.Refund.create(**args))
try:
refund({"customer_id": "cust_123", "amount": 2500})
except PolicyDeniedError as e:
print(e.reason, e.receipt_id)Adapters
| import | entry point | behaviour |
|---|---|---|
agent_custody.langchain | ReceiptCallbackHandler(client) | record-only: on_tool_start / on_tool_end issue an executed receipt per tool run with the LangChain tool_call_id as the session's tool-use id; pass it in callbacks=[...] |
agent_custody.openai_agents | wrap_tools(client, tools) | enforce: each FunctionTool's on_invoke_tool decides, runs, records; a denial is returned as the tool's result text |
agent_custody.crewai | wrap_tools(client, tools) | enforce: one CustodyTool per tool, same name, description, and schema; _run decides, runs, records; a denial is returned as Denied by policy: … (receipt …) |
agent_custody.claude_agent_sdk | claude_hook(client) | the async hook callable (input_data, tool_use_id, context) -> dict for the Claude Agent SDK; handle_hook_event(client, input_data) is the same for any host that passes the hook JSON. Same semantics as Claude Code |
MemoryClient
The memory tools over MCP, against a memory server started with agent-custody-memory serve --http. Writes made this way are claimed and quarantined until a gateway confirms them; that is the honest position of an SDK-only agent.
from agent_custody.memory import MemoryClient, MemoryError
async with MemoryClient("http://127.0.0.1:8790/mcp", token="…") as memory:
fact = await memory.write("acct:42", "plan", "pro", space="team:support", actor="py-agent")
facts = await memory.read(subject="acct:42", include_claimed=True)| method | tool called | response |
|---|---|---|
write(subject, predicate, value, *, space, actor=None, supersedes=None, valid_from=None) | memory.write | {"fact": {...}, "eventId", "txTime", "supersedes"}; the fact's provenance is claimed |
read(*, subject=None, predicate=None, space=None, valid_at=None, tx_at=None, include_claimed=False, require_verified=False) | memory.read | the list of facts believed at that moment |
retract(fact_id, reason, *, actor=None) | memory.retract | {"eventId", "factId", "txTime", "actor", "reason", "source", "removedFrom", "verification"} |
history(fact_id) | memory.history | the list of events, oldest first |
A refused connection (wrong token) raises MemoryError("memory server at … refused the connection: HTTP 401"); a tool error raises MemoryError with the server's text. The tool contracts are on the memory page.