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.