Your LangChain agent calls tools. It searches the web, reads files, queries databases, calls APIs. But can you prove what it did?
Logs capture what happened. Cryptographic receipts prove it. The difference matters when an auditor, a customer, or a regulator asks "show me exactly what the agent did and prove it wasn't altered after the fact."
This tutorial adds Ed25519-signed, hash-chained audit trails to a LangChain agent in under 5 minutes. No external service, no API keys, no infrastructure. Everything verifies offline with a public key.
What you'll build
A LangChain agent where every tool call produces a signed receipt containing:
- What: which tool was called, with what parameters
- Who: the agent's Ed25519 public key
- When: timestamp
- Proof: Ed25519 signature over JCS-canonicalized (RFC 8785) payload
- Chain: SHA-256 hash linking to the previous receipt (tamper-evident ordering)
If anyone modifies a receipt after the fact, the signature breaks. If anyone deletes or reorders receipts, the hash chain breaks.
Install
pip install signet-auth[langchain] langchain-openai langchain-community
Step 1: Create a signing identity
from signet_auth import SigningAgent
# Creates an Ed25519 keypair, stored locally in ~/.signet/
# If the key already exists, just load it:
try:
agent = SigningAgent("my-langchain-agent")
except Exception:
agent = SigningAgent.create("my-langchain-agent", owner="acme-corp")
print(f"Public key: {agent.public_key}")
That's it. No key server, no certificate authority. The private key stays on disk, the public key is what verifiers use.
Step 2: Add the signing callback
Signet ships a LangChain callback handler that signs every tool call automatically. Two lines:
from signet_auth.langchain import SignetCallbackHandler
signer = SignetCallbackHandler(agent)
This handler signs the full tool lifecycle: on_tool_start (what was called), on_tool_end (what it returned, hashed), and on_tool_error (what went wrong). If signing fails, the handler logs a warning and lets the agent continue. It never crashes your chain.
Step 3: Wire it into your agent
from langchain import hub
from langchain_community.tools import DuckDuckGoSearchRun
from langchain.agents import AgentExecutor, create_react_agent
from langchain_openai import ChatOpenAI
# Standard LangChain setup
llm = ChatOpenAI(model="gpt-4o-mini")
tools = [DuckDuckGoSearchRun()]
prompt = hub.pull("hwchase17/react")
# Create and run agent with signing callback
agent_executor = AgentExecutor(
agent=create_react_agent(llm, tools, prompt),
tools=tools,
callbacks=[signer],
)
result = agent_executor.invoke({"input": "What is the weather in Tokyo?"})
Every tool call now produces a signed receipt. No code changes to the tools themselves.
Step 4: Inspect the receipts
import json
for i, receipt in enumerate(signer.receipts):
data = json.loads(receipt.to_json())
print(f"\nReceipt #{i+1}")
print(f" Tool: {data['action']['tool']}")
print(f" Params hash: {data['action']['params_hash']}")
print(f" Signature: {data['sig'][:40]}...")
print(f" Timestamp: {data['ts']}")
Output:
Receipt #1
Tool: duckduckgo_search
Params hash: sha256:a1b2c3...
Signature: ed25519:Mz4xNTk2NjQ0NDgw...
Timestamp: 2026-04-19T10:30:00Z
Receipt #2
Tool: _tool_end
Params hash: sha256:d4e5f6...
Signature: ed25519:Nk5yODk3MjE1Njg4...
Timestamp: 2026-04-19T10:30:01Z
Notice the start/end pair: the first receipt captures the tool call, the second captures a hash of the output. Together they prove what was called and what it returned.
Step 5: Verify a receipt
Anyone with the public key can verify, offline:
from signet_auth import verify
receipt = signer.receipts[0]
is_valid = verify(receipt, agent.public_key)
print(f"Valid: {is_valid}") # True
Tamper with any field and verification fails:
data = json.loads(receipt.to_json())
data["action"]["tool"] = "evil_tool" # tamper
from signet_auth import Receipt
tampered = Receipt.from_json(json.dumps(data))
print(verify(tampered, agent.public_key)) # False
Step 6: Verify the audit chain
The audit log is a hash-chained JSONL file. Each entry's hash covers the previous entry, so deleting or reordering receipts breaks the chain:
from signet_auth import audit_verify_chain, default_signet_dir
signet_dir = default_signet_dir()
chain_status = audit_verify_chain(signet_dir)
print(f"Chain intact: {chain_status.valid}")
print(f"Entries: {chain_status.length}")
What this gives you
| Without Signet | With Signet |
|---|---|
| "The agent called web_search" (log entry) | Ed25519 signature proving it, verifiable by anyone with the public key |
| Logs can be edited after the fact | Signature breaks if any field is modified |
| No ordering proof | Hash chain breaks if receipts are deleted or reordered |
| Trust the operator's logs | Verify independently, offline |
When you need this
- Regulated industries: EU AI Act Article 12 requires "automatic recording" of AI system activities. Signed receipts satisfy this with cryptographic proof, not just logs.
- Enterprise deployments: When the question is "can you prove what the agent did?", signed receipts are the answer.
- Agent-to-agent: When Agent B needs to verify what Agent A actually did before acting on its output.
- Incident response: After something goes wrong, tamper-evident receipts let you reconstruct exactly what happened without trusting anyone's claim.
Next steps
-
Bilateral co-signing: Have both the agent and the tool server sign each interaction independently. Neither party can fabricate receipts. See
signet proxyfor MCP integration. - Policy attestation: Evaluate YAML policy rules and include the decision (allow/deny/require_approval) inside the signed receipt.
- Delegation chains: Prove that Agent A was authorized by Human B to perform a specific action with scoped constraints.
All of these are in signet-auth today.
pip install signet-auth
GitHub: Prismer-AI/signet
Signet is open source (Apache 2.0). Rust core with Python and TypeScript bindings. No external service, no API keys.
Top comments (0)