Repository navigation
Integrations LangChain
Stable: tested with
tests/adapter_contract.pyagainst the pinned library on every push to main and every pull request (what's stable).
tracekit.integrations.langchain.TracekitMiddleware is a LangChain v1 AgentMiddleware. Before each tool call it
asks the Tracekit signer for a policy decision; after the tool runs it records the outcome. It works with any object
that implements the signer RPC (tracekit.signer.rpc_schema.SignerAPI), including tracekit.testing.FakeSigner.
The older callback adapter in tracekit.adapters.langchain (adapters.md) is unchanged.
pip install tracekit-ai "langchain>=1.4,<1.5" "langgraph>=1.2,<1.3"
pip install langgraph-checkpoint-sqlite # optional: a durable checkpointer for approvalsPython 3.10 or newer (LangChain v1's own requirement).
from tracekit.integrations.langchain import TracekitCheckpointer, TracekitMiddleware
run = signer.register_run({"request_id": "run-start-1", "agent": {"name": "billing-agent"}})
agent = create_agent(model, tools, checkpointer=TracekitCheckpointer(InMemorySaver(), signer, run),
middleware=[TracekitMiddleware(signer, run["run_id"], run["run_token"])])A resumed run (another process, after a restart) builds the middleware with the same run_id and run_token.
TracekitCheckpointer is optional; see Checkpoint commitments.
A graph that runs its tools with ToolNode directly, without create_agent, uses tracekit_tool_node. It applies
the same gate as the middleware (decisions, approvals, results), so everything below holds for it too:
from tracekit.integrations.langchain import TracekitCheckpointer, tracekit_tool_node
graph = StateGraph(MessagesState)
graph.add_node("model", call_model)
graph.add_node("tools", tracekit_tool_node(tools, signer, run)) # other keyword arguments go to ToolNode
graph.add_edge(START, "model")
graph.add_conditional_edges("model", tools_condition)
graph.add_edge("tools", "model")
app = graph.compile(checkpointer=TracekitCheckpointer(InMemorySaver(), signer, run))| When | RPC | Sent |
|---|---|---|
| before the tool runs | decide |
tool_call_id, attempt (0), tool name, the args as LangChain decoded them (args_source: parsed) |
| after the tool runs | complete |
status ok or error, a sha256: commitment to the ToolMessage content (not the content), or the exception type and message |
The signer classifies the tool and computes the args digest itself; nothing the middleware sends is trusted for that.
Under create_agent the middleware records each model response (wrap_model_call): its reasoning blocks (a
summary, with the provider's signature or encrypted content), an open model's reasoning_content or inline <think>
(its full reasoning), its text, and the calls of the agent's tools under the ids they are decided by (not a
structured-output tool's), each added to its response only once it reaches the middleware: a call another middleware
stops first (a person's reject in HumanInTheLoopMiddleware, ToolCallLimitMiddleware, the recursion limit) is not
recorded. A call a person edited there is recorded as the model asked for it and decided as edited, so it reads as
reconcile.args_mismatch. A tool executor's tool gets the model's id for its call from the middleware, and the
executor decides it under that id. A tracekit_tool_node graph has no model hook: record its model
calls with tracekit.instrument() or the LLM gateway. The message itself is kept only when the signer's content vault is on; without it the record holds a commitment. A message that can't be recorded only warns. Which frameworks and providers return readable reasoning, and how to turn it on: reasoning.
-
allow: the tool runs.
-
deny: the tool does not run. The model gets an error ToolMessage,
Tool call blocked by policy: <reason or rule ids>, and the agent run goes on. -
ask: the middleware opens an approval and pauses the run with LangGraph
interrupt(). The interrupt payload is{"tracekit": {"approval_id", "tool_call_id", "tool", "rule_ids"}}. A person approves or rejects in the signer; then the app resumes the thread:agent.invoke(Command(resume={"approval_id": approval_id}), config)
On resume LangGraph re-runs the tool node, so the middleware asks the signer again, this time naming the approval. The signer, not the saved state, decides: the call runs only if that approval was granted for this exact call and these exact args, and was not used before. Otherwise the call is denied as above. The
approval_idin the resume value is only a hint; leaving it out does not skip the check.
Exceptions raised by the tool are recorded and re-raised unchanged.
interrupt() can only hold a run that LangGraph can save. Without a checkpointer, an ask is answered with a deny
(approval required, and the agent has no checkpointer to wait for it) and no approval is opened. Use InMemorySaver
for a single process, or a durable saver such as SqliteSaver / PostgresSaver when the resume may happen in
another process.
TracekitCheckpointer(saver, signer, run) wraps any LangGraph BaseCheckpointSaver, sync or async. Each time the
saver writes a checkpoint or its pending writes, the wrapper reads back what the saver now holds and sends the signer a
state_write with its digest, under the key langgraph:<thread_id> (langgraph:<thread_id>/<checkpoint_ns> in a
subgraph). Each write names the digest the signer last recorded for that key (prev_digest).
When a run loads a checkpoint to resume and it differs from the one last committed, the next write starts from the
changed digest, and the signer records a signed capture.gap of kind state_tamper. This catches a stored checkpoint
edited between a pause and its resume, even in another process. A tool call edited that way is also refused when it
runs: its args no longer match the approval (TK-APPROVAL-MISMATCH).
The digest (checkpoint_digest) is a sha256: over canonical JSON of the hashes of the checkpoint's serialised parts
(LangGraph's msgpack serialiser):
- the checkpoint's own fields (
id,ts,channel_versions,versions_seen, ...); - every channel value,
messagesand__pregel_tasksamong them.__pregel_tasksholds the pending tool calls a resumedcreate_agentrun actually executes, so covering onlymessageswould miss an edit to them; - every pending write: results of tasks that finished, interrupts, resume values.
Not covered: the checkpoint's metadata and parent_config. Each part is hashed on its own, so a saver that returns
channels or writes in another order gives the same digest.
Resuming from an older checkpoint in a new process (time travel, replaying a snapshot) also differs from the last one
committed and is recorded as state_tamper.
A commitment that fails (signer unreachable) is a warning, never an exception: the saver has already written, and
raising would fail the run after it. The next commitment starts from the last digest the signer recorded, so a lost one
is not read as tampering; the signer records it as a client_counter_gap.
Generated from docs/ at v1.4.1. Edit the docs in the repository, not here.
Get started
- Quickstart: the v2 signer
- Quickstarts
- Quickstart: Claude Agent SDK
- Quickstart: a custom agent
- Quickstart: LangChain and LangGraph
- Quickstart: MCP client
- Quickstart: OpenAI Agents SDK
- Coding agents other than Claude Code
- tracekit doctor
- Walkthrough: Tracekit end to end
How it works
- How Tracekit works
- Overview
- The life of a tool call
- The evidence format
- Verification and assurance
- Witnesses and monitoring
- Policy
- Approvals
- Detection and containment
- Capture and completeness
- Custody
- Provenance and tracekit why
- Deployment and isolation
- Storage, durability and performance
- Integrations
- Security model
- Architecture
- tracekit why: architecture
Integrations
- Integrations
- Adapters
- OpenTelemetry
- Remote clients
- Kubernetes agent-sandbox
- agentgateway
- agentsh and Tracekit
- Browser Use
- Claude Agent SDK
- Claude Code's sandbox
- Codex's sandbox
- CrewAI
- Docker MCP Gateway
- E2B
- Falco
- Gemini CLI's sandbox
- Google ADK
- Haystack
- LangChain v1
- LiteLLM
- LlamaIndex
- MCP client
- microsandbox
- Microsoft Agent Framework
- OpenAI Agents SDK
- NVIDIA OpenShell
- Pydantic AI
- sandbox-runtime's proxy on its own
- Strands Agents
- Tetragon
- Vercel AI SDK
- Wassette
Policy and approvals
- Policy reference
- Approvals
- Findings
- Reading the model's reasoning
- Incidents and containment
- Webhooks
Deploy and operate
- Deploying the signer
- The signer as a container
- The signer on Kubernetes
- The compose stack
- Identity
- Tool executor
- MCP gateway
- LLM gateway
- Egress observation
- Witnesses
- Log monitor
- Record-key issuer
- Signer observability
- Viewer
- tracekit why: causal investigation and replay
- Migrating
- Platforms
Verify the evidence
- Reading a v2 verify report
- Evidence format v2
- Auditor guide
- Sample evidence bundle
- Signing
- What Tracekit guarantees
Security and privacy
- Threat model: the v2 signer
- Threat model: the v1 laptop signer
- HTTP security checklist
- Threat-model review packet
- Privacy
- Known limits and trade-offs
- What's stable
Compliance
- Compliance mapping
- EU AI Act
- ISO/IEC 42001
- NIST AI RMF and the Generative AI Profile
- OWASP Top 10 lists for LLM and agentic applications
Evaluation
Demos
- Demos
- Agent approvers within bounds
- Completeness and tamper evidence
- Detection and containment
- Network coverage: an egress observer
- Guardrails: allow, deny, or ask a person
- MCP tools, gated and recorded
- Prompt injection held by provenance
- Why: which input caused the action
Help