agentleFS
arcjetarcjet/skills/integrate-arcjet-guard-openai-agents-py/SKILL.md

integrate-arcjet-guard-openai-agents-py

Skill4 starsChanged 6 months agoApache-2.0View on GitHub

Integrate Arcjet Guard into Python OpenAI Agents — wrap FunctionTool / function_tool with guard_tool and read a caller-owned session or conversation id via openai_agents_context. Use when asked to add Arcjet to openai-agents, Runner.run, rate limit those tools, screen inbound messages, or block prompt injection / PII. This is Python openai-agents, not the JS @openai/agents adapter.

  • Installs packages

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ask only what you cannot infer from the code.

    line 71
  2. Very specific:

    Default: none. Put the conversation / session id you already have on the app context you pass to Runner.run.

    line 78
  3. Very specific:

    This is not the JS adapter: Do not import @arcjet/guard/openai-agents/v0.

    line 88
  4. Very specific:

    Correlation is read, never minted: Never trace_id. Never construct OpenAIConversationsSession().

    line 95
  5. Distinctive:

    If the agent has no guard client yet, launch one once at module scope:

    line 112
  6. Very specific:

    Use action + rules on guard_tool.

    line 158

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Integrate 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 is FunctionTool.tool_input_guardrails + reject_content (JSON of ArcjetDenialResult). Do not raise.
  • Correlation → openai_agents_context reads a caller-owned session / conversation id. It never mints. It never reads trace_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.

  1. Which tools are risky (external side effects, irreversible, spends money, sends messages)? Those get guard_tool. Hosted / MCP / handoffs / as_tool are out of scope.
  2. What limits? (e.g. "10 lookups/min per user" → TokenBucket.)
  3. 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.
  4. Is an Arcjet outage unacceptable? Every helper defaults to on_guard_error="deny". Ask explicitly about inbound screening before Runner.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

  1. This is not the JS adapter. Do not import @arcjet/guard/openai-agents/v0.
  2. There is no inbound helper. SDK guardrails are not Arcjet. Screen with core guard() before Runner.run.
  3. needs_approval is HITL, not policy.
  4. Denial is reject_content only. A raise is a tripwire halt or is swallowed by default_tool_error_function.
  5. Correlation is read, never minted. Never trace_id. Never construct OpenAIConversationsSession().
  6. Key rate limits on the authenticated caller, not a model-supplied order id. Hand the agent the copy guard_tool returns — the original stays unguarded.
  7. Do not hand-wrap every tool with raw guard().
  8. 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

  1. python -m py_compile (or the project's type-check) passes.
  2. Exercise inbound PI (before Runner.run), a tool deny (reject_content, not a raise), a rate limit, and fail-closed (an unreachable guard). Confirm needs_approval is never treated as the gate.
  3. Confirm in the Arcjet Console / CLI that decisions share the caller-owned session / conversation id — not a trace_id.
  4. Manual E2E with a real ARCJET_KEY is 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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.