LangChain callback handler that emits signed verdict envelopes for every tool call.
Project description
agentshield-langchain
Signed audit receipts for every LangChain tool call. Drop-in callback. SOC 2 / GDPR / EU AI Act friendly.
This package is not a runtime guardrail, not a prompt-injection defense, and not a DLP layer. It is an append-only, cryptographically signed post-flight audit trail for the tool calls your agent already made.
Why
- SOC 2 vendor questionnaire. "Show me proof of every action your agent took on customer data, signed under a key you control." This produces that evidence as one JSON line per tool call.
- Audit trail for AI agents. Tamper-evident record bound to the exact inputs and outputs of each tool invocation, verifiable offline by anyone holding the public key — no server round-trip.
- Dispute resolution. When a customer asks "did your agent really call
book_flight(...)on my account?", you produce the signed envelope and they verify it themselves. No "trust our logs."
Install
pip install 'agentshield-langchain[examples]'
The [examples] extra pulls langchain-openai so you can run
examples/quickstart.py end-to-end. The core package itself only depends on
langchain-core, eth-account, and pydantic.
Quickstart
LangChain 1.x removed create_tool_calling_agent; the supported pattern is
bind_tools + a manual tool loop. The callback fires on_tool_start /
on_tool_end for every tool.invoke(tool_call, config=config) call.
import io
import json
import os
from langchain_core.messages import HumanMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from verdict_protocol import Envelope, verify
from agentshield_langchain import AgentShieldCallback, StdoutSink
# DEV-ONLY KEY — never commit, never use in production.
# See "Key management" below for production setups.
PRIVATE_KEY = "0x" + os.urandom(32).hex()
@tool
def get_weather(city: str) -> str:
"""Return the current weather for a given city."""
return f"sunny in {city}, 22°C"
@tool
def book_flight(origin: str, destination: str, date: str) -> dict:
"""Book a flight from origin to destination on date."""
return {"booking_id": "ABC123", "from": origin, "to": destination, "date": date}
buf = io.StringIO()
callback = AgentShieldCallback(
agent_id="quickstart-agent",
evaluator_id="local-quickstart",
private_key=PRIVATE_KEY,
sink=StdoutSink(stream=buf),
)
tools = [get_weather, book_flight]
tools_by_name = {t.name: t for t in tools}
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0).bind_tools(tools)
config = {"callbacks": [callback]}
messages = [HumanMessage("What's the weather in Rome? Then book a flight Milan to Rome on 2026-06-01.")]
ai_msg = llm.invoke(messages, config=config)
messages.append(ai_msg)
# Pass the full ToolCall (not just args) so the callback fires properly.
for tc in ai_msg.tool_calls:
selected = tools_by_name[tc["name"]]
observation = selected.invoke(tc, config=config)
messages.append(observation)
final = llm.invoke(messages, config=config)
for line in buf.getvalue().strip().splitlines():
rec = json.loads(line)
env = Envelope.model_validate(rec["envelope"])
ok = verify(env, rec["signature"], expected_signer=env.signer)
print(f"intent={env.claim.intent} outcome={env.verdict.outcome} verify={ok}")
A copy of this is shipped as examples/quickstart.py.
What this is NOT
- ❌ Runtime guardrails / policy enforcement. Use OPA, Cedar, or a dedicated policy engine. AgentShield only signs what already happened; it does not block.
- ❌ Prompt-injection defense. Use Llama Guard, Lakera, or a similar classifier upstream of the LLM call.
- ❌ DLP / PII redaction. Use Microsoft Purview, Nightfall, or a
dedicated DLP product. The envelope carries digests, but the
paramsandoutputyou feed in still flow through your sink in the clear unless you redact them. - ❌ IAM / authentication. Use Auth0, Okta, or your existing identity
provider. The
agent_idin the envelope is a label, not an authenticated principal. - ✅ What it IS: an append-only, EIP-191-signed audit log of every tool call your LangChain agent makes, verifiable offline against a public key you control.
Threat model
- Trusted. The Python process running the agent, the private key material in memory, the LangChain runtime, and the verifier holding the public key.
- Adversary can. Read your sink output (logs, queue, file), modify bytes in transit between sink and storage, replay old envelopes, run their own LangChain agent under a different key, attempt to forge an envelope without your private key, and tamper with stored envelopes after the fact.
- This package defeats. Forgery (any byte change in
claim,verdict, orsignerinvalidates the EIP-191 signature). Silent tampering of stored envelopes (verification will fail). Substitution of inputs or outputs (digests bind them intocontent_hash). Repudiation by the agent operator who controls the signing key (the signature is a public, wallet-verifiable commitment). - This package does NOT defeat. Theft of the private key (any holder
can sign arbitrary envelopes — rotate). Pre-signature manipulation (if
you sign garbage, you sign garbage). Replay (the verifier must dedupe
by
content_hashand checkcreated_at/evaluated_atranges). Sink-level DoS (a failing sink drops envelopes and logs — by design, the agent must keep running).
Why EIP-191 not JWS
- Wallet-verifiable. Any Ethereum wallet, hardware key, or block-explorer tooling can verify the signature. JWS requires a JOSE library and key-format negotiation.
- Fixed 65-byte signature. Predictable wire size; no algorithm
field, no
alg: nonefoot-gun, no JWK juggling. eth-accountis battle-tested for byte-exact determinism. The same canonical bytes produce the same signature across machines and language bindings — exactly what an audit primitive needs.- Future-proofs an on-chain ledger. The same envelope you verify offline today can be anchored to an EVM contract tomorrow without a signature-format migration. See ADR-0002 §3.
Key management (recommended setups)
| Setup | Where the key lives | When to use |
|---|---|---|
| Dev | os.urandom(32).hex() per-process, never persisted |
Local dev, examples, throwaway demos. |
| Single-tenant prod | Environment variable injected by your secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager) at process start | One agent, one signing identity, one customer or team. |
| Multi-tenant prod | One key per tenant, fetched from your KMS at agent boot, scoped to that tenant's process / pod | SaaS where each customer wants their own verifiable signing identity. |
| HSM / hardware-backed | Key never leaves the HSM; sign over a remote interface (e.g. AWS KMS Sign with ECDSA_SHA_256, then EIP-191-wrap) |
Regulated workloads, compliance-driven deployments. Requires a custom signer wrapper around eth_account — not shipped in v0.1. |
| Hosted (coming) | AgentShield-hosted signing service with attestation | When you want signed receipts without owning a key. Not in v0.1. Track agentshield-mcp. |
In every case: never commit a key, never log a key, and rotate keys on the same cadence as your other production secrets.
Wire format
The default sink writes one compact JSON line per tool call to
sys.stdout. Each line has this shape:
{
"envelope": {
"schema_version": "0.1.0",
"claim": {
"agent_id": "my-agent",
"intent": "<tool name>",
"params_digest": "0x…",
"created_at": "2026-05-01T12:00:00Z"
},
"verdict": {
"evaluator_id": "local",
"claim_digest": "0x…",
"output_digest": "0x…",
"outcome": "match",
"evaluated_at": "2026-05-01T12:00:00Z"
},
"content_hash": "0x…",
"signer": "0x…"
},
"signature": "0x…<130 hex chars>"
}
output_digest wrapping convention
output_digest is computed over the JSON object {"output": <value>},
not over <value> directly. This is load-bearing: the hosted evaluator
in agentshield-mcp and any downstream SDK that wants to produce a matching
digest must use the same {"output": …} wrapping or digests will
diverge byte-for-byte. The wrapper exists so that scalar tool outputs
("sunny in Rome", 42, true, null) round-trip through JCS
canonicalization unambiguously.
Verification is one line, anywhere you store the envelope:
from verdict_protocol import Envelope, verify
verify(Envelope.model_validate(record["envelope"]), record["signature"], record["envelope"]["signer"])
Out of scope (v0.1)
The following are not in this package and never will be without a new ADR — they live in other repos / future versions:
- Networking / HTTP transport. No
httpx, norequests, nourllib. Wire your sink to push toagentshield-mcp /v1/ledger(or anywhere) when you're ready. - Key rotation / KMS. The caller passes a private key; rotation is the caller's problem.
- Async callbacks.
BaseCallbackHandleronly — sync hooks. Async (AsyncCallbackHandler) lands in v0.2. - LLM call tracking. Only tool calls in v0.1 — that is the audit primitive that matters for agent post-flight review.
- Outcome strategies beyond
AlwaysMatch. Deterministic evaluators ship in v0.2 alongsideagentshield-evaluator. - Streaming / partial outputs. The handler waits for
on_tool_end. - Tool-error envelopes. A failing tool fires
on_tool_error(noton_tool_end); v0.1 does not emit an envelope for it. The cache holds at most one entry per failed tool call and is freed when the callback is garbage-collected. Error envelopes ship in v0.2.
If you need any of the above, don't patch this package — open an ADR in
agentshield-mcp or wait for v0.2.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
ruff check . && ruff format --check .
mypy
pytest
CI runs the same gate on Python 3.10, 3.11, and 3.12 with 100 % line and branch coverage required.
Security
Found a vulnerability? See SECURITY.md. Do not open a
public GitHub issue for security reports — email security@agentshield.dev.
License
MIT.
Related
verdict-protocol— schema, JCS canonicalization, and EIP-191 signing primitives this package builds on.agentshield-mcp— MCP server with the deterministic outcome evaluator that v0.2 strategies will call into.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agentshield_langchain-0.1.0.tar.gz.
File metadata
- Download URL: agentshield_langchain-0.1.0.tar.gz
- Upload date:
- Size: 10.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dbc245a886c466174ae74330f558381d2d94e9933fcd723e386232444c584c29
|
|
| MD5 |
932591b321c774da58cba0f8dec4b275
|
|
| BLAKE2b-256 |
82e14d54f9dde12fefa2027040c20c16e201439bf11832f0ce3c806aeb98408a
|
File details
Details for the file agentshield_langchain-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agentshield_langchain-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67a940055c908a1e57f939d83082f57d830f439d4e86484a468bd47ac41dd51a
|
|
| MD5 |
86f6d3f8aaa8fd4a11a2f9eccc8afaa6
|
|
| BLAKE2b-256 |
b2377c4d66990ffa87b51d2f9c509e06fdf5335f21e5a1b523d9095c94047fda
|