Docs Frameworks
LangGraph / LangChain
Status: verified. The adapter ships in
kiff-guard and passes the
conformance suite. The seam was checked against the current LangChain
middleware API, including the built-in ShellAllowListMiddleware that
uses the same block pattern.
Shape: middleware. The seam is wrap_tool_call — the guard runs the
tool via the handler continuation, so Guard.evaluate(run=...) fits
directly. (Not interrupt(); see the note below.)
Install
pip install "git+https://github.com/kiffhq/kiff-guard.git#subdirectory=packages/python/kiff-guard"
pip install langgraph langchain
Observe — audit with zero config
from langchain.agents import create_agent
from langchain.agents.middleware import wrap_tool_call
from kiff_guard import Guard
from kiff_guard.adapters.langgraph import kiff_wrap_tool_call
guard = Guard(mode="observe") # zero-config audit
kiff_mw = wrap_tool_call(kiff_wrap_tool_call(guard))
agent = create_agent(model="...", tools=[...], middleware=[kiff_mw])
Run the agent, then read the trail from guard.receipts.
Activate
from kiff_guard import export_yaml
print(export_yaml("my-domain", guard.catalog))
Review the draft and activate it in the dashboard.
Enforce — govern at runtime
from kiff_guard import Guard, HTTPClient, ToolMap
client = HTTPClient(api_key="kiff_live_...", tool_map=ToolMap().bind(...))
guard = Guard(client=client, tenant="<tenant>", agent="support", mode="enforce")
kiff_mw = wrap_tool_call(kiff_wrap_tool_call(guard))
agent = create_agent(model="...", tools=[...], middleware=[kiff_mw])
On a withheld decision the middleware returns a ToolMessage
(status="error") carrying the reason without calling the handler —
the tool never runs, and the model sees the message as the tool result.
This is exactly the short-circuit LangChain’s own
ShellAllowListMiddleware uses to reject disallowed shell commands.
The seam (verified)
wrap_tool_call(request, handler) -> ToolMessage | Command.
request.tool_call["name"] / ["args"] / ["id"] carry the call. Calling
handler(request) runs the tool; returning a ToolMessage without
calling it blocks. langchain is imported lazily, so import kiff_guard
never requires it.
Why not interrupt()
interrupt() pauses the graph and persists state for a human to
resume — the right tool for a human approval pause, but heavyweight and
asynchronous, not a synchronous “is this tool allowed right now” gate.
KIFF’s gate is a machine decision that returns in milliseconds, so it
belongs in wrap_tool_call. When a withheld decision is an
approval_required, the host app can bridge it into an interrupt() on
top of the gate.
Links
- Adapter code:
kiff_guard/adapters/langgraph.py - Seam research: langgraph.md
- LangChain middleware: docs.langchain.com