Developer documentation

Integrations

Last reviewed 31 August 2026

QuickstartCLI referencePython SDKTypeScript SDKMCPIntegrationsPoliciesActionProofEvidenceCAIN TraceConformanceTroubleshootingDeveloper portalMarketplaceFree tierBenchmarksArchitectureCAIN IdentityCAIN ControlCAIN BudgetCAIN GovernanceCAIN MemorySelf-Hosted MCPGateCAIN PrivateCAIN TrajectoryCAIN Agent SecurityCAIN Drift7-Moat ArchitectureChangelog

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.

FrameworkStatusHow
Custom PythonSupported@trust on the function that acts
Custom TypeScriptSupportedtrust({...}, fn)
MCPSupportedcain protect mcp + MCPGate in the path
LangChainSupportedprotect_all(tools) — or @trust on your own functions
LangGraphGenericwrap the tool functions your nodes call
LlamaIndexGenericwrap the function behind your FunctionTool
CrewAIGenericwrap the function behind your @tool
OpenAI agentsGenericwrap the function you dispatch to on a tool call
Google (Vertex / ADK)Genericwrap your FunctionDeclaration handler
Microsoft (AutoGen)Genericwrap the function you register as a tool
Dedicated adapters for the restPlannednot 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.