Skip to main content

LangChain callback handler that emits signed verdict envelopes for every tool call.

Project description

agentshield-langchain

CI License: MIT Python 3.10+

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 params and output you 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_id in 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, or signer invalidates the EIP-191 signature). Silent tampering of stored envelopes (verification will fail). Substitution of inputs or outputs (digests bind them into content_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_hash and check created_at / evaluated_at ranges). 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: none foot-gun, no JWK juggling.
  • eth-account is 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_accountnot 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, no requests, no urllib. Wire your sink to push to agentshield-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. BaseCallbackHandler only — 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 alongside agentshield-evaluator.
  • Streaming / partial outputs. The handler waits for on_tool_end.
  • Tool-error envelopes. A failing tool fires on_tool_error (not on_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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

agentshield_langchain-0.1.0.tar.gz (10.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agentshield_langchain-0.1.0-py3-none-any.whl (12.2 kB view details)

Uploaded Python 3

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

Hashes for agentshield_langchain-0.1.0.tar.gz
Algorithm Hash digest
SHA256 dbc245a886c466174ae74330f558381d2d94e9933fcd723e386232444c584c29
MD5 932591b321c774da58cba0f8dec4b275
BLAKE2b-256 82e14d54f9dde12fefa2027040c20c16e201439bf11832f0ce3c806aeb98408a

See more details on using hashes here.

File details

Details for the file agentshield_langchain-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for agentshield_langchain-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 67a940055c908a1e57f939d83082f57d830f439d4e86484a468bd47ac41dd51a
MD5 86f6d3f8aaa8fd4a11a2f9eccc8afaa6
BLAKE2b-256 b2377c4d66990ffa87b51d2f9c509e06fdf5335f21e5a1b523d9095c94047fda

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page