Developer documentation
Integrations
Last reviewed 31 August 2026
Framework integrations
Honest status per framework. "Supported" means the SDK primitives *are* the
integration and the shown code is what we test. "Generic" means the SDK works
fine but there is no framework-specific package -- you are using @trust /
trust() directly.
LangChain now has a real, tested adapter (cain.adapters.langchain), verified
against actual langchain-core objects rather than mocks — 13 tests covering the
framework's own invocation path, name/description/args-schema preservation, and
async. Everything else is still the SDK used directly, marked generic, with
dedicated adapters marked planned rather than implied to exist.
| Framework | Status | How |
| Custom Python | Supported | @trust on the function that acts |
| Custom TypeScript | Supported | trust({...}, fn) |
| MCP | Supported | cain protect mcp + MCPGate in the path |
| LangChain | Supported | protect_all(tools) — or @trust on your own functions |
| LangGraph | Generic | wrap the tool functions your nodes call |
| LlamaIndex | Generic | wrap the function behind your FunctionTool |
| CrewAI | Generic | wrap the function behind your @tool |
| OpenAI agents | Generic | wrap the function you dispatch to on a tool call |
| Google (Vertex / ADK) | Generic | wrap your FunctionDeclaration handler |
| Microsoft (AutoGen) | Generic | wrap the function you register as a tool |
| Dedicated adapters for the rest | Planned | not built |
The reason "generic" is usually enough: every one of these frameworks ultimately
calls a plain function to execute a tool. Guarding that function guards the
action, and it survives you changing frameworks.
LangChain
For tools you define, @trust is enough:
from langchain_core.tools import StructuredTool
from cain import trust
@trust(action="send_email", resource="customer_inbox")
def send_email(to: str, subject: str, body: str) -> str:
...
tool = StructuredTool.from_function(send_email)
For tools you did not define — third-party or built-in, already constructed —
and for guarding a whole list without having to remember each one:
from cain.adapters.langchain import protect, protect_all, unprotected
tools = protect_all(my_tools, skip={"web_search"}) # read-only tools cost nothing to skip
# Turn "did I remember to guard everything" into something that fails at boot:
assert not unprotected(tools)
protect() wraps in place and is idempotent. It preserves name, description
and args_schema, so the model sees exactly the same tool and your agent builds
the same prompt — the adapter guards execution and changes nothing else.
on_unauthorized="return_message" returns the refusal as the tool result instead
of raising, so the agent can see why and choose differently. The action still does
not happen; that part is not negotiable.
Forgetting one tool is the realistic failure mode, and it is silent — which is
why protect_all and unprotected exist rather than only a decorator.
LangGraph
Guard the tool functions rather than the nodes where you can -- a node may do
several things and you want the decision attached to the consequential one.
@trust(action="issue_refund", resource="billing")
def issue_refund(order_id: str, amount: int) -> str:
...
OpenAI agents
Guard the dispatch target, not the model call. The model producing a tool call is
not the action; executing it is.
@trust(action="delete_record", resource="crm")
def delete_record(record_id: str): ...
def handle_tool_call(call):
if call.function.name == "delete_record":
return delete_record(**json.loads(call.function.arguments))
CrewAI
from crewai.tools import tool
from cain import trust
@tool("Refund an order")
@trust(action="issue_refund", resource="billing")
def issue_refund(order_id: str) -> str: ...
Order matters: @trust should be closest to the function so the decision happens
around the real call.