Metadata-Version: 2.4
Name: agentshield-langchain
Version: 0.1.0
Summary: LangChain callback handler that emits signed verdict envelopes for every tool call.
Project-URL: Homepage, https://github.com/yeick010/agentshield-langchain
Project-URL: Issues, https://github.com/yeick010/agentshield-langchain/issues
Author: yeick010
License: MIT License
        
        Copyright (c) 2026 yeick010
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agentshield,callback,eip-191,langchain,verdict
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: eth-account<0.14,>=0.10
Requires-Dist: langchain-core<2,>=0.3
Requires-Dist: pydantic<3,>=2.6
Requires-Dist: verdict-protocol==0.1.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: examples
Requires-Dist: langchain-openai<2,>=0.2; extra == 'examples'
Description-Content-Type: text/markdown

# agentshield-langchain

[![CI](https://github.com/yeick010/agentshield-langchain/actions/workflows/ci.yml/badge.svg)](https://github.com/yeick010/agentshield-langchain/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)

**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

```bash
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.

```python
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`](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_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:

```json
{
  "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:

```python
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

```bash
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`](SECURITY.md). **Do not** open a
public GitHub issue for security reports — email `security@agentshield.dev`.

## License

[MIT](LICENSE).

## Related

- [`verdict-protocol`](https://github.com/yeick010/verdict-protocol) — schema,
  JCS canonicalization, and EIP-191 signing primitives this package builds
  on.
- [`agentshield-mcp`](https://github.com/yeick010/agentshield-mcp) — MCP
  server with the deterministic outcome evaluator that v0.2 strategies will
  call into.
