attenu

Blog · · by , Attenu

Two agents in the Agents SDK, and the relation between them

What they built well. The OpenAI Agents SDK has, quietly, the most complete set of capability gates of any framework we integrate. A local tool can be hidden per turn with FunctionTool.is_enabled. An MCP server's tools can be hidden with tool_filter, which receives a ToolFilterContext carrying the run context, the agent and the server name. A handoff can be hidden with Handoff.is_enabled. And a call that is visible can still be stopped before its body runs by FunctionTool.tool_input_guardrails, which the runner executes ahead of RunHooks.on_tool_start — the only pre-tool hook that can actually refuse. Handoffs carry input_filter for the conversation the receiver inherits. The effective tool and handoff list for each model call is already on the agent span; the maintainers said so on 2026-08-25 when they closed #4626, and they were right to.

The one thing left open, in their words. Issue #4618, opened 2026-08-24 and closed the next day as a docs request:

The Agents SDK now has several useful mechanisms for controlling what an agent can see at runtime… I think there is an opportunity to document a single production pattern for using them together when capability availability comes from the same runtime policy.

The same issue draws the line that matters: "visibility filtering is not itself a security boundary… argument/resource-level authorization belongs at invocation time." Every gate is present. What no gate expresses is a relation between two agents — that what the receiver of a handoff may do should be provably no more than what the sender may do. In agents/run_internal/turn_preparation.py, check_handoff_enabled calls is_enabled(context_wrapper, agent) with the sending agent, and the receiver is named by Handoff.agent_name. Both sides are in reach; the comparison is the application's to write.

The code. One Authority per agent — a scope set and ceilings — and four callbacks that read it. triage holds {billing.*, kb.*} with a USD 500 cap. billing requests {billing.read, billing.refund} with a USD 50 cap, and is granted meet(triage, request), so the request can narrow but never widen. sre requests infra.deploy, which triage does not hold. The whole policy is under 40 lines, and each gate is one function: is_enabled asks whether the running agent holds the tool's scope, tool_filter asks the same of an MCP tool, Handoff.is_enabled asks whether the receiver's request is narrower than the sender's authority, and the invocation check asks again with the call's arguments in hand.

The denial. Run the tree on the plain SDK with a scripted model and the sre handoff is offered to the model like any other, and a USD 250 credit executes. Run it under the policy and three things change. transfer_to_sre never reaches the model, and the refusal is on the ledger — escalation by routing, closed structurally rather than by prompt. The USD 250 credit is denied on the ceiling before the tool body runs; the sink the tool writes into stays empty, and in the unguarded run it does not. And kb_export — which is visible to triage, because triage holds kb.* — is denied anyway, on an argument no visibility gate can see. Switch every visibility gate off and exactly the same calls are denied; that test is in the recipe, because #4618's point about visibility deserves to be demonstrated rather than repeated.

The verify. AuditLog.verify on the hash-chained ledger, then attenu-guard verify bundle.json on the signed bundle: integrity, child ⊆ parent, containment — checked from the bundle alone, with no service of ours running and no call to the model vendor. That is a different job from tracing, which the SDK already does well; it is the artifact you hand to someone who was not there.

What remains theirs. Routing, all four gates, the MCP and handoff machinery, approvals, tracing, and the judgement about which capability belongs to which agent. And what this recipe cannot do: a direct Python call around the SDK runs — a test proves it, so nobody mistakes mediation for a sandbox. Read the trust boundary before you rely on it.

Verified against openai-agents 0.22.0 on 2026-08-25; the same tests pass on 0.21.1. Recipe: https://github.com/attenu-io/attenu-guard/tree/main/examples/integrations/openai_agents/one_policy · https://attenu.io

Rafael Asor is the founder of Attenu and the maintainer of attenu-guard and attenu-derive, open-source Python libraries for AI agent permissions across sub-agent handoffs. He is the author of the IETF Internet-Draft draft-asor-wimse-agent-delegation-chain and is based in Tel Aviv.