integrate-arcjet-guard-openai-agents-py
About integrate-arcjet-guard-openai-agents-py
A Python integration skill for using Arcjet Guard with authored FunctionTool tools in the official OpenAI Agents SDK. It adds input guardrails to tool copies and reads caller-owned correlation IDs; hosted tools, MCP, and other surfaces are out of scope.
Key features
- 01Guards authored FunctionTool tools
- 02Rejects denied inputs through tool input guardrails
- 03Reads caller-owned correlation IDs
- 04Supports Guard rules and tool rate limits
Use cases
- 01Blocking a risky authored FunctionTool call
- 02Associating Guard decisions with a session ID
- 03Applying limits to tool calls
MCP support
MCP tools are explicitly out of scope for this integration. It does not describe an MCP server or MCP client capabilities.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ arcjet/ skills --skill integrate-arcjet-guard-openai-agents-pyAfter installing, type/integrate-arcjet-guard-openai-agents-pyin Claude Code to confirm it loaded.
Details
From the project READMEIntegrate Arcjet Guard into Python OpenAI Agents
arcjet.guard.openai_agents wraps the agent's existing Arcjet client. It
never talks to the Arcjet API itself. Shared Guard fundamentals (client,
rules, labels, decisions, capture, registration) live in
../arcjet/references/guards_python.md.
Load that reference for anything that is not OpenAI Agents-specific.
Official openai-agents>=0.19.0,<1 only — not the JS @openai/agents
adapter (@arcjet/guard/openai-agents/v0, docs
https://docs.arcjet.com/guards/openai-agents/), not community forks.
Importing arcjet.guard.openai_agents does not load LangChain.
Exports: guard_tool, openai_agents_context. Authored FunctionTool /
@function_tool only. Not hosted tools, MCP, Computer / Shell /
ApplyPatch, handoffs, or Agent.as_tool().
Two surfaces, one decision rule:
- An authored
FunctionTool→guard_tool. Gate isFunctionTool.tool_input_guardrails+reject_content(JSON ofArcjetDenialResult). Do not raise. - Correlation →
openai_agents_contextreads a caller-owned session / conversation id. It never mints. It never readstrace_id.
Docs: https://docs.arcjet.com/guards/openai-agents/.
The gate is tool_input_guardrails + reject_content
guard_tool returns a copy whose input guardrails start with Arcjet, so
on_invoke_tool never runs on DENY (or unevaluated Guard under the
default on_guard_error="deny"). Denial is
ToolGuardrailFunctionOutput.reject_content with JSON of
ArcjetDenialResult ({ arcjetDenied: true, … }). Do not raise —
raise_exception() is a tripwire halt, and a raise from
on_invoke_tool is swallowed by default_tool_error_function. Same
fail-closed default as #196:
only "allow" fails open; a DENY always blocks. Core guard() still
fails open (has_failed_open()).
needs_approval is not a policy gate
needs_approval is human-in-the-loop (state.approve / state.reject).
Same trap as JS OpenAI Agents needsApproval, LangGraph interrupt(),
and Genkit interrupt(). There is no inbound helper and no approval
helper. RunConfig.tool_execution.pre_approval_tool_input_guardrails=True
is an application opt-in only — this helper does not set it.
Screen inbound before Runner.run
There is no inbound helper. SDK input_guardrails / output_guardrails
/ tool output guardrails are the SDK's own tripwires, not Arcjet. Call
aj.guard(...) in the application and act on the decision. Core
guard() fails open: ALLOW is not proof the rules ran. Gate on
decision.has_failed_open() if this call site must fail closed;
guard_tool already defaults to that.
Questions to ask the human first
Ask only what you cannot infer from the code; suggest defaults.
- Which tools are risky (external side effects, irreversible, spends
money, sends messages)? Those get
guard_tool. Hosted / MCP / handoffs /as_toolare out of scope. - What limits? (e.g. "10 lookups/min per user" →
TokenBucket.) - Who is the user for metadata — an opaque user/tenant ID (never PII)?
Default: none. Put the conversation / session id you already have on
the app context you pass to
Runner.run. That id is the correlation id, not the user. - Is an Arcjet outage unacceptable? Every helper defaults to
on_guard_error="deny". Ask explicitly about inbound screening beforeRunner.run: failing closed there means the agent does not run, so"allow"is a routine and legitimate choice at that one call site.
The things readers get wrong
- This is not the JS adapter. Do not import
@arcjet/guard/openai-agents/v0. - There is no inbound helper. SDK guardrails are not Arcjet. Screen
with core
guard()beforeRunner.run. needs_approvalis HITL, not policy.- Denial is
reject_contentonly. A raise is a tripwire halt or is swallowed bydefault_tool_error_function. - Correlation is read, never minted. Never
trace_id. Never constructOpenAIConversationsSession(). - Key rate limits on the authenticated caller, not a model-supplied
order id. Hand the agent the copy
guard_toolreturns — the original stays unguarded. - Do not hand-wrap every tool with raw
guard(). inputs=is accepted. A missing decision is not a denial — verify in Console/CLI.
Step 1: Install and find the guard client
This module ships in PyPI arcjet 1.0.0:
pip install "arcjet[openai-agents]"
If the agent has no guard client yet, launch one once at module scope:
import os
from arcjet.guard import launch_arcjet
aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
Step 2: Gate authored tools — guard_tool
from agents import Agent, Runner, function_tool
from arcjet.guard import DetectPromptInjection, TokenBucket, launch_arcjet
from arcjet.guard.openai_agents import guard_tool, openai_agents_context
aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
lookup_limit = TokenBucket(
label="order.looked-up",
bucket="lookups",
refill_rate=10,
interval_seconds=60,
max_tokens=10,
)
user_id = authenticated_user_id
@function_tool # needs_approval=... is HITL — not this policy gate
def lookup_order(order_number: str) -> dict:
"""Look up an order by number."""
return {"order_number": order_number, "status": "shipped"}
lookup_order = guard_tool(
guard=aj,
tool=lookup_order,
action="order.looked-up",
rules=[lookup_limit(key=user_id, requested=1)],
on_guard_error="deny",
)
agent = Agent(
name="support-agent",
instructions="Help the user.",
tools=[lookup_order],
)
Use action + rules on guard_tool.
Step 3: Screen inbound before Runner.run
inbound = DetectPromptInjection()
app_context = {"session_id": conversation_id}
derived = openai_agents_context(app_context)
decision = await aj.guard(
label="message.received",
rules=[inbound(user_text)],
correlation_id=derived.correlation_id,
)
if decision.conclusion == "DENY":
raise Exception("message blocked")
if decision.has_failed_open():
raise Exception("inbound guard unavailable")
await Runner.run(agent, user_text, context=app_context)
There is no inbound helper.
Step 4: Correlation
openai_agents_context reads a caller-owned id. Preference: fields on
runContext.context / a bare app object (correlation_id, then
session_id, then conversation_id, then group_id, snake or
camelCase), then the same names on the envelope, then
correlation_id= / session_id= kwargs, then an enclosing
arcjet_sequence. It returns OpenAIAgentsContext — pass
.correlation_id to guard(). It never mints an id. It never reads
trace_id. It never constructs OpenAIConversationsSession(). Do not
invent a correlation id per turn.
Verify the integration
python -m py_compile(or the project's type-check) passes.- Exercise inbound PI (before
Runner.run), a tool deny (reject_content, not a raise), a rate limit, and fail-closed (an unreachable guard). Confirmneeds_approvalis never treated as the gate. - Confirm in the Arcjet Console / CLI that decisions share the
caller-owned session / conversation id — not a
trace_id. - Manual E2E with a real
ARCJET_KEYis still-to-verify until you run it.
The example fastapi-openai-agents-guard stays with Runtime — do not
invent a new example name. Do not add an example in this skills repo.