raxit
Frameworks

OpenAI Agents SDK

Governing OpenAI Agents SDK agents in Python, zero-code.

The OpenAI Agents SDK routes every real tool call — direct or Runner-orchestrated — through one module-level function: agents.tool.invoke_function_tool(function_tool=, context=, arguments=). Neither the Runner orchestrator nor agents/realtime/session.py ever call FunctionTool.on_invoke_tool directly. raxit's auto-patcher wraps exactly that point (plus every built-in <X>Action.executeCustomToolAction, ShellAction, LocalShellAction, ComputerAction, ApplyPatchAction — and execute_handoffs for agent-to-agent handoffs), so every tool call resolves to permit / defer / deny before it executes, with no governance code in the agent. Python only — the OpenAI Agents SDK has no JS/TS SDK.

Zero-code governance

raxit run --agent-id <id> -- python agent.py starts the governance daemon, sets RAXIT_AUTOLOAD=1, and puts the auto-patcher on the import path before your agent's first import. Your tools stay ordinary agents.FunctionTool / @function_tool definitions — the only "wiring" is each tool's name, which is the policy resource it's governed as in security.yaml. Run the same file without raxit run and it runs ungoverned: governance is a deploy-time choice.

getting_started.py
# imports NOTHING from raxit
from agents import Agent, Runner, function_tool

@function_tool(name_override="shell.exec")
def run_shell(cmd: str) -> str: ...

agent = Agent(name="ops-bot", instructions=SYSTEM_PROMPT, tools=[run_shell])
result = await Runner.run(agent, "run rm -rf / on the box")
raxit run -- python getting_started.py "run rm -rf / on the box"

Run it yourself

To govern your own agent, install the SDK and launch through raxit run:

pip install raxit
raxit init          # scan → .raxit/security.yaml
raxit run --agent-id my-agent -- python agent.py

The walkthrough below is deterministic and LLM-free — it drives real agents.FunctionTool instances through agents.tool.invoke_function_tool, the same dispatch shape the SDK's own orchestration uses, so it runs with no API key.

The demo, step by step

Every terminal line below is from a captured run of the demo — four tools, three outcomes, one signed audit chain:

OpenAI Agents SDK ops bot — permit / defer / deny, zero-code

Four ordinary agents.FunctionTool instances. Nothing in this file imports raxit — the tool name is the only wiring; it is the policy resource the call is governed as.

oai_agents_bot/tools.py
from agents import FunctionTool

def _make_tool(tool_name: str, description: str, result: str) -> FunctionTool:
    async def _on_invoke(ctx, args_json: str) -> str:  # the tool body
        RECEIVED[tool_name] = {"args": args_json}
        return result

    return FunctionTool(
        name=tool_name,
        description=description,
        params_json_schema=_EMPTY_SCHEMA,
        on_invoke_tool=_on_invoke,
    )

def build_tools() -> list[FunctionTool]:
    return [
        _make_tool("search_docs", "Search internal product documentation (read-only).", ...),
        _make_tool("read_customer", "Look up a customer record by id (read-only).", ...),
        _make_tool("notify.send", "Send an internal notification (requires human approval).", ...),
        _make_tool("shell.exec", "Run a shell command (forbidden for this agent).", ...),
    ]
terminal
$ $ make -C examples demo-openai-agents-governed-py

How it works

The policy is one security.yaml — one rule per outcome, default: block for everything else:

.raxit/security.yaml
apiVersion: raxit.security/v1
agent: openai-agents-bot
default: block
rules:
  - effect: allow
    resource: search_docs
  - effect: allow
    resource: read_customer
  - effect: review              # unconditional review == defer.policy
    resource: notify.send
  # shell.exec is intentionally UNLISTED -> default-deny (deny.default_or_forbid).

The demo calls raxit.autopatch.install() directly — that is exactly what raxit run triggers under the hood — and then invokes each tool through agents.tool.invoke_function_tool, the module-level function the SDK's Runner always goes through. One subtlety worth knowing if you patch or import manually: install() rebinds invoke_function_tool as an attribute on the agents.tool module, so a from agents.tool import invoke_function_tool done before patching keeps pointing at the original, ungoverned function object. Under raxit run this never bites you (the patcher runs before your agent's first import).

Each tool body records into the demo's call log when it runs, which is how the demo proves the deferred and denied bodies never executed — ran=False in the [PASS] lines is read from that log, not inferred.

What got denied and why

Two named reason codes fire in this demo:

  • defer.policynotify.send matched an effect: review rule: the call needs a human decision before it may run. The demo pins RAXIT_DEFER_TIMEOUT=0 so the raw defer surfaces immediately instead of blocking on an inline approval poll; in normal raxit run operation the call blocks waiting for raxit approvals approve <token> — see Approvals for that flow.
  • deny.default_or_forbidshell.exec matched no rule, and the policy's default: block denies anything unlisted. Nothing was ever allowed; there is no rule to loosen by accident.

Both surface to the agent as a raxit.autopatch.RaxitDenied exception raised from inside the dispatch call, carrying the reason code (exc.reason_code) — catch it like any other exception. The full decision vocabulary and reason-code table are on Permit / defer / deny.

Scope: no args in the CAR at this layer

invoke_function_tool's call convention is keyword-only with no tool_input/input key, so the intercepted call carries no arguments for this framework. Every rule in this policy is therefore unconditional (identity + tool name only) — arg-conditioned rules and MODIFY are not exercised here, matching the scope of raxit's own test suite for this framework.

The audit finale

The four decisions above form one hash-chained, Ed25519-signed Decision Provenance log. The demo's last step verifies every link and every signature from the client:

  audit.verify  -> ok=True signatures_verified=True signed_count=4 count=4

See Audit for how the chain is built and what raxit audit verify checks. For the general zero-code patching model (and the governed_tool per-tool escape hatch), start from LangChain (Python) — the reference patcher every other framework's patcher follows — and the coverage table on Frameworks.

On this page