Skip to content

Agent guard framework integrations

What is Arcjet? Arcjet is the runtime security platform that ships with your code. Enforce budgets, stop prompt injection, detect bots, and protect personal information with Arcjet's AI security building blocks.

AI frameworks normally pass a model’s generated arguments directly to a tool’s execute or invoke function. Arcjet’s framework integrations add a security checkpoint between those two steps, so a remote policy or SDK rule evaluates every tool attempt before the tool can create a side effect.

Use an integration when you want to protect tools without writing decision handling around every function. The wrapper preserves the framework’s normal tool definition and result flow, selects policy with a stable action, and maps the actor and relevant tool arguments into policy inputs.

Use protect() on HTTP routes. Use the helpers on each framework page for agent tools and other non-HTTP actions.

You choose the context that each policy receives. Map only the tool arguments that the policy needs, and take the actor from your authenticated application context. This keeps the policy contract explicit and avoids sending unrelated data.

Pick the page that matches the object you hold when the effect runs:

FrameworkImport pathStart here
Vercel AI SDK v7@arcjet/guard/vercel-ai/v7Vercel AI SDK agent guard
LangChain (Python)arcjet.guard.langchainLangChain agent guard
LangGraph JS@arcjet/guard/langgraph/v1LangGraph agent guard
Genkit JS@arcjet/guard/genkit/v1Genkit agent guard
OpenAI Agents@arcjet/guard/openai-agents/v0OpenAI Agents agent guard
Vercel Eve@arcjet/guard/vercel-eve/v0Vercel Eve agent guard
Mastra@arcjet/guard/mastra/v1Mastra agent guard
Claude Agent SDK@arcjet/guard/claude-agent-sdk/v0Claude Agent SDK agent guard

Each JavaScript path is versioned. Unversioned aliases such as @arcjet/guard/vercel-ai do not resolve.

Don’t mix adapters. A Mastra createTool, Eve defineTool, LangGraph tool(), OpenAI Agents tool(), Claude Agent SDK tool(), or Genkit ai.defineTool is not an AI SDK tool(). Wrapping one of those with @arcjet/guard/vercel-ai/v7 throws or misses the real execute path.

Wrap authored tool({ execute }) handlers with guardTool. Thread createAgentContext() through aiToolsContext on generateText / streamText. There is no guardInbound; screen user text with a direct guard() call before the model runs.

import { launchArcjet, policyInput } from "@arcjet/guard";
import {
aiToolsContext,
createAgentContext,
guardTool,
} from "@arcjet/guard/vercel-ai/v7";
import { generateText, tool } from "ai";
import { z } from "zod";
const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });
export async function runAgent(user: { id: string }, prompt: string) {
const sendEmail = guardTool(
arcjet,
tool({
description: "Send an email",
inputSchema: z.object({ to: z.string(), body: z.string() }),
execute: ({ to, body }) => emailProvider.send({ to, body }),
}),
{
action: "email.sent",
actor: user.id,
inputs: ({ to, body }) => ({
recipient: policyInput.server.string(to),
body: policyInput.local.string(body),
}),
},
);
const tools = { sendEmail };
const context = createAgentContext({ correlationId: user.id });
return generateText({
model: "openai/gpt-4o-mini",
prompt,
tools,
toolsContext: aiToolsContext(context, tools),
});
}

On a policy denial the tool does not execute and the model receives an ArcjetDenialResult. Don’t throw from guardTool. guardAction throws so application code can catch. For more information about guardAction, capture, and inbound screening, see the Vercel AI SDK agent guard.

LangChain tools and create_agent agents call authored tools. The Python SDK sits at those boundaries. This is not the JavaScript LangGraph Graph API adapter.

Pick the surface that matches what you hold when the effect runs:

  • Any Python callable: guard_action / guard_action_sync in core arcjet.guard. No extra.
  • A LangChain BaseTool you call yourself: guard_tool (arcjet[langchain], langchain-core>=1.2.5,<2).
  • An agent from create_agent: ArcjetMiddleware + ToolPolicy (arcjet[langchain-agents], langchain>=1.3,<2, langgraph>=1.2,<2).
  • Observe only: ArcjetCaptureHandler / ArcjetAsyncCaptureHandler. These cannot deny a call. LangChain ignores callback return values.

For more information about install, helpers, fail-closed behavior, and correlation, see the LangChain agent guard.

LangGraph Graph API agents (StateGraph + ToolNode) call authored tools and ToolNode / MCP tools. There is no guardInbound. Screen inbound text with a direct guard() call before graph.invoke, or in the first graph node. interrupt() is human-in-the-loop (HITL) confirmation, not policy. Use guardTool() for authored tool() / StructuredTool handlers and guardToolNode() for unwrapped and MCP tools that execute inside ToolNode.

This is not the Python LangChain agent guard. It is not LangChain createAgent / wrapToolCall.

guardTool returns the plain payload. Don’t fabricate a ToolMessage to force status: "error". For more information about inbound screening, authored tools, and ToolNode denials, see the LangGraph agent guard.

Genkit JS flows (ai.generate() / chat.send() plus authored ai.defineTool) have no guardInbound. Screen user text with a direct guard() call before generate() or send(). Middleware model is not Guard. interrupt() / defineInterrupt / @genkit-ai/middleware toolApproval / restartTool are human HITL confirmation, not policy. There is no guardApproval. Use guardTool() on the returned ToolAction and guardMiddleware() as a { name, instantiate } generateMiddleware tool hook. Don’t also wrap these tools with @arcjet/guard/vercel-ai/v7. This is not Go or Python Genkit.

guardTool returns { arcjetDenied: true, … } as completed toolResponse.output. Don’t throw. Don’t call interrupt(). For more information about inbound screening, authored tools, and generate()-wide denials, see the Genkit agent guard.

OpenAI Agents text Agent workflows (run() + authored tool()) have no guardInbound. Screen user text with a direct guard() call before run(). needsApproval and hosted requireApproval are human HITL confirmation, not policy. Use guardTool() to wrap FunctionTool.invoke after tool({ execute }). Hosted tools, handoffs, and Runner agent_tool_start are not deny points.

This is not Realtime, Sandbox, hosted tools, MCP, agent.asTool(), or computer/shell.

guardTool returns { arcjetDenied: true, … } from invoke. Don’t throw. For more information about inbound screening, authored tools, and execute denials, see the OpenAI Agents agent guard.

Eve agents can have zero authored tools. OpenAPI and MCP connections have no local execute, so the enforcement point is guardApproval() on the connection. Screen inbound channel text with guardInbound() before the agent starts. Hooks are observe-only (handlers return void).

guardTool throws ArcjetDeniedError. Pass onDeny: "result" to return the payload instead. For more information about inbound screening, authored tools, connection approvals, and lifecycle hooks, see the Vercel Eve agent guard.

Mastra has no guardInbound (channels already hit processInput) and no guardApproval (requireApproval is a human-in-the-loop control). Screen inbound text with guardProcessor. Gate authored tools with guardTool. Gate MCP or workspace tools you did not wrap with guardHooks (beforeToolCall can deny).

guardTool returns { arcjetDenied: true, … } as the tool result. Don’t throw. For more information about processors, tools, and hooks, see the Mastra agent guard.

Claude Agent SDK agents call authored tool() handlers, built-in tools, and MCP tools. There is no guardInbound. Screen inbound prompts with guardHooks({ inbound }) on UserPromptSubmit. canUseTool is not a policy gate. Use guardTool() for authored tools and PreToolUse for unwrapped built-ins and MCP tools.

guardTool returns an MCP CallToolResult with isError: true and the payload on structuredContent. Don’t throw. For more information about inbound screening, authored tools, and PreToolUse denials, see the Claude Agent SDK agent guard.

Every JavaScript adapter uses one payload, ArcjetDenialResult. The fields, wording, and retry-after rules are the same, so a model sees one shape. The envelope is per-framework, because each SDK reports a tool that did not run in a different way:

AdapterIdiomatic envelopeWhy not the others
AI SDK / MastraReturn { arcjetDenied: true, … } as the tool resultA throw becomes a generic tool error and drops the fields
OpenAI AgentsReturn { arcjetDenied: true, … } from invokeA throw hits errorFunction or ToolCallError and can kill the run
LangGraphReturn { arcjetDenied: true, … }. ToolNode wraps a ToolMessage with status: "success"Fabricating a ToolMessage to force status: "error" crashes the graph reducer
GenkitReturn { arcjetDenied: true, … } as completed toolResponse.outputA throw / interrupt() is HITL and the wrong envelope
Claude Agent SDKMCP CallToolResult with isError: true and the payload on structuredContentA throw is a raw exception. Omitting isError looks like success
Vercel EveThrow ArcjetDeniedError. Opt in to a returned payload with onDeny: "result"Eve projects a throw as a failed action.result. A silent return can violate outputSchema
const result: ArcjetDenialResult = {
arcjetDenied: true,
reason: "RATE_LIMIT",
message:
"Arcjet denied this call (RATE_LIMIT). It may be retried after 30 seconds.",
retryable: true,
retryAfterSeconds: 30,
};

You can import ArcjetDenialResult from @arcjet/guard/vercel-ai/v7 and the other JavaScript adapter namespaces.

guardTool must produce an envelope the model can inspect. guardAction throws so application code can catch. These cannot share one handler.

Only rate-limit denials set retryable: true and include retryAfterSeconds. When Guard is unavailable and onGuardError is "deny", the payload uses reason: "ERROR", retryable: true, and retryAfterSeconds: 5.

The wrappers default to fail closed when Guard is unavailable:

  • Vercel AI does not execute the tool and returns a retryable ArcjetDenialResult to the model with reason ERROR. A throw from guardTool becomes a generic tool error and drops the fields.
  • LangChain guard_tool raises ArcjetToolUnavailableError. A policy denial raises ArcjetToolDeniedError, or follows the wrapped tool’s handle_tool_error behavior when configured. guard_action and ArcjetMiddleware raise ArcjetUnavailableError / ArcjetDeniedError.
  • Vercel Eve helpers default to onGuardError: "deny". guardTool throws ArcjetDeniedError. Pass onDeny: "result" to return the payload instead. On a channel, "allow" is a legitimate choice because failing closed stops the agent answering.
  • Mastra helpers default to onGuardError: "deny". guardTool returns { arcjetDenied: true, … } as the tool result. A throw becomes a generic tool error and drops the fields. "allow" is a legitimate choice on the inbound processor because failing closed stops the agent answering.
  • Claude Agent SDK helpers default to onGuardError: "deny". guardTool returns an MCP CallToolResult with isError: true and the payload on structuredContent. On inbound UserPromptSubmit, "allow" is a legitimate choice because failing closed stops the agent answering.
  • LangGraph helpers default to onGuardError: "deny". guardTool returns the plain payload. ToolNode wraps a ToolMessage whose status is success. Direct guard() before invoke fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • Genkit helpers default to onGuardError: "deny". guardTool returns { arcjetDenied: true, … } as completed toolResponse.output. Don’t throw. Don’t call interrupt(). Direct guard() before generate() / send() fails open. Check decision.hasFailedOpen() when that call site must fail closed.
  • OpenAI Agents helpers default to onGuardError: "deny". guardTool returns a plain ArcjetDenialResult and does not throw. A throw hits ToolCallError and drops the fields. The runner stringifies the payload onto a function_call_result with status: "completed". Direct guard() before run() fails open. Check decision.hasFailedOpen() when that call site must fail closed.

Set onGuardError: "allow" in JavaScript or on_guard_error="allow" in Python only when executing without a complete security decision is acceptable.

For the direct client and wrapper differences, see Availability and fail behavior.