Skip to content
Back to skills

Integrate Arcjet Guard Openai Agents Py

ASecurity

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.

  • 4 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 23, 2026
ai-agentspythonshellbashfastapirailsgitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 23, 2026

npx -y skills add arcjet/skills --skill integrate-arcjet-guard-openai-agents-py --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Integrate Arcjet Guard Openai Agents Py?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Integrate Arcjet Guard Openai Agents Py
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arcjet-integrate-arcjet-guard-openai-agents-py/badge)](https://www.skillsdirectory.com/skills/arcjet-integrate-arcjet-guard-openai-agents-py)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: integrate-arcjet-guard-openai-agents-py
description: 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.
license: Apache-2.0
compatibility: Requires Python >= 3.10 and official openai-agents>=0.19.0,<1 via arcjet[openai-agents] on PyPI arcjet 1.0.0+. This is text Agent + Runner.run + authored FunctionTool — not Realtime, Sandbox, hosted, MCP, Computer / Shell / ApplyPatch, handoffs, or Agent.as_tool().
metadata:
  author: arcjet
  type: core
  library: arcjet
---

# 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](../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](https://github.com/arcjet/arcjet-py/pull/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**:

```bash
pip install "arcjet[openai-agents]"
```

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

```python
import os
from arcjet.guard import launch_arcjet

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
```

## Step 2: Gate authored tools — `guard_tool`

```python
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`

```python
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.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in ai-agents →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.