Skip to main content

OpenAI Agents quickstart — governed Runner tools

The adapter converts MCP definitions into strict OpenAI Agents SDK FunctionTool instances. This example uses the public scripted model helper to make one deterministic call to a tool that the running bridge actually advertises.

Before you start

  • Python 3.10+; Python 3.11 and 3.12 are CI-covered.
  • Cordum bootstrapped with ./tools/scripts/quickstart.sh.
  • cordum-mcp-bridge on PATH.
  • The secure environment from Framework integrations.

:::note Strict JSON Schema The adapter normalizes nested object schemas for the SDK's strict mode. Use strict=False only when an upstream tool intentionally advertises a loose schema. :::

1. Install

python -m venv .venv
source .venv/bin/activate
pip install "cordum-adapters[openai-agents]"

2. Build a governed Runner

Save as openai_agents_quickstart.py:

import asyncio
import os

from agents import Agent
from cordum_agent_adapters.audit import CordumConversationLogger
from cordum_agent_adapters.mcp_client import McpStdioClient
from cordum_agent_adapters.openai_agents import (
build_openai_agent_tools,
run_governed,
tee_events,
)
from cordum_agent_adapters.testing import FakeModel, ScriptedTurn

TOOL_NAME = "cordum.workflow.run"


def require_tool(tool_defs: list[dict], name: str) -> list[dict]:
selected = [tool for tool in tool_defs if tool.get("name") == name]
if not selected:
advertised = sorted(str(tool.get("name")) for tool in tool_defs)
raise RuntimeError(f"{name} is not advertised; got {advertised}")
return selected


async def main() -> None:
client = McpStdioClient(
command=["cordum-mcp-bridge"],
env=os.environ.copy(),
)
try:
selected = require_tool(client.list_tools(), TOOL_NAME)
logger = CordumConversationLogger(client)
tools = build_openai_agent_tools(client, tools=selected, logger=logger)
model = FakeModel(
turns=[
ScriptedTurn(
tool_calls=[
{
"name": TOOL_NAME,
"arguments": {
"workflow_id": "demo-workflow",
"input": {},
"dry_run": True,
},
}
]
),
ScriptedTurn(content="Review the actual tool output above."),
]
)
agent = Agent(
name="governed_runner",
instructions="Use only the Cordum tool assigned to you.",
tools=tools,
model=model,
)

result = await run_governed(
agent,
"Start demo-workflow as a dry run.",
client=client,
logger=logger,
)
async for event in tee_events(result, logger):
item = getattr(event, "item", None)
if getattr(item, "type", "") == "tool_call_output_item":
print("tool output:", item.output)
finally:
client.close()


if __name__ == "__main__":
asyncio.run(main())

Run it after loading the common TLS environment:

python openai_agents_quickstart.py

Replace demo-workflow with a workflow in your tenant. The output comes from the bridge; this page does not prescribe a successful response or dashboard row. run_governed adds a correlation identifier to OpenAI trace metadata, and conversation logging remains optional and best-effort.

3. Exercise a hard deny

Upload a v1 bundle through the HTTPS policy API:

version: v1
rules:
- id: block-tutorial-workflow-run
match:
capabilities:
- cordum.workflow.run
decision: deny
reason: "Tutorial hard-deny check"

A hard deny returns isError: true; McpStdioClient raises McpToolError. The OpenAI Agents adapter converts that error into a tool-result message for the Runner loop.

4. Recognize approval holds

The pack bridge keeps an approval-gated job nonterminal and blocks the original Runner tool call. Resolve the job in the dashboard before the bounded CORDUM_MCP_CALL_TIMEOUT; approval lets the same call continue. A timeout is returned through the tool-error path, and this bridge does not use an approval-reference retry.

What's next