Skip to content

fix(openai-agents): populate tool_call field to enforce safety checks#354

Open
DebasmitaBose0 wants to merge 2 commits into
sreerevanth:mainfrom
DebasmitaBose0:security/346-openai-agents-safety-bypass
Open

fix(openai-agents): populate tool_call field to enforce safety checks#354
DebasmitaBose0 wants to merge 2 commits into
sreerevanth:mainfrom
DebasmitaBose0:security/346-openai-agents-safety-bypass

Conversation

@DebasmitaBose0

@DebasmitaBose0 DebasmitaBose0 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Closes #346

Summary

This PR fixes a security vulnerability where tool calls made via the OpenAI Agents framework silently bypassed pre-execution safety validations, resolving #346.

Problem

The AgentWatchOpenAIAgentsAdapter class emitted TOOL_CALL events but failed to populate the top-level tool_call (ToolCallData) field on the AgentEvent.
Because SafetyEngine.check_event early-returns without performing policy evaluation if event.tool_call is None:

if event.event_type != EventType.TOOL_CALL or event.tool_call is None:
    return event
  
### ll safety validations (such as blocking recursive deletion commands, RCE pipes, or credential manipulation) were bypassed when using this adapter.

### Proposed Changes
Populated tool_call in Adapter (agentwatch/adapters/openai_agents.py): Updated on_tool_call to construct and populate event.tool_call with ToolCallData.
Command Promotion Logic: Handled both dictionary and string inputs. If input is a dictionary, command-like keys (e.g. command, cmd, etc.) are auto-promoted to raw_command (scannable by the safety engine). If input is a string and the tool name is command-like (e.g., bash, shell), it is promoted to raw_command.
Populated tool_result in Adapter: Updated on_tool_result to populate event.tool_result with ToolResultData.
Added Reproducible Test: Added test_tool_call_populates_tool_call_data verifying that emitted events now have the tool_call field correctly initialized and mapped.
Added Regression Test: Added test_openai_agents_safety_bypass_regression validating that a dangerous command (e.g., rm -rf /) sent from the adapter goes through the safety pipeline and is successfully blocked (event.status == ExecutionStatus.BLOCKED).


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

* **New Features**
  * Tool-use events now include richer details such as captured input and command-like content, improving visibility into agent actions.
  * Tool-result events now carry the tool name and output for easier inspection.

* **Bug Fixes**
  * Improved handling of different tool input formats so event data is recorded more consistently.
  * Strengthened safety handling for risky command execution, helping block dangerous actions more reliably.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The OpenAI Agents adapter's on_tool_call handler now parses the input parameter into arguments and an optional raw_command, assigns a ToolCallData object to event.tool_call, and on_tool_result assigns a ToolResultData object to event.tool_result. Two new tests validate field population and safety engine blocking.

Changes

Fix Safety Bypass: Populate ToolCallData/ToolResultData in OpenAI Agents Adapter

Layer / File(s) Summary
Adapter: parse input and set ToolCallData/ToolResultData
agentwatch/adapters/openai_agents.py
Imports ToolCallData and ToolResultData; on_tool_call now derives arguments and raw_command from dict or string input (promoting command-like keys or command-like tool_name), then sets event.tool_call; on_tool_result now sets event.tool_result with tool_name and output.
Tests: field correctness and safety blocking regression
tests/test_openai_agents_adapter.py
Adds pytest import; sync test asserts tool_call.tool_name, arguments, and raw_command for string and dict input shapes; async test routes a bash rm -rf / call through SafetyEngine.check_event and asserts the event is blocked with "FS_DELETE_CRITICAL" in matched policies.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • sreerevanth/AgentWatch#69: Safety/risk matching relies on raw_command, which this PR now correctly populates in the OpenAI adapter.
  • sreerevanth/AgentWatch#106: Implements sync/async fail-closed safety enforcement that this PR's regression test directly exercises.
  • sreerevanth/AgentWatch#220: Prior implementation of the OpenAI Agents adapter's tool-call/tool-result emission that this PR extends with structured data objects.

Suggested labels

bug, security, backend, high-priority, level: advanced, level3

Suggested reviewers

  • sreerevanth

Poem

🐇 A bunny once watched as commands slipped right through,
No tool_call set — safety hadn't a clue!
Now arguments bloom and raw_command is clear,
FS_DELETE_CRITICAL blocked, nothing to fear.
The rabbit hops safely, no rm -rf here! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the adapter fix and its safety-check purpose.
Linked Issues check ✅ Passed The adapter now populates AgentEvent.tool_call and tests confirm dangerous tool calls are blocked, matching the issue.
Out of Scope Changes check ✅ Passed The changes stay focused on the OpenAI Agents adapter and its tests, with no unrelated code paths introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🧪 PR Test Results

Check Result
Tests (pytest tests/) ✅ success
Lint (ruff check .) ❌ failure
Coverage (agentwatch) 74.69%

Python 3.12 · commit ff5060a

@sreerevanth

sreerevanth commented Jun 20, 2026

Copy link
Copy Markdown
Owner

@DebasmitaBose0
Thanks for the contribution.

This addresses a real safety gap and the direction of the fix looks correct.

Before merge, please:

  • Resolve the current merge conflicts in tests/test_openai_agents_adapter.py
  • Rebase against the latest main branch
  • Confirm that tool_call population results in safety enforcement before tool execution rather than only improving observability of emitted events

Once conflicts are resolved and the enforcement path is verified, I'll take another look.

@sreerevanth

Copy link
Copy Markdown
Owner

@DebasmitaBose0

This PR has been waiting for several days and is now holding up work in the same area of the codebase.

Please either:

  1. Rebase and resolve the conflicts,
  2. Push the remaining fixes,
  3. Or let me know if you're no longer working on it.

At the moment, this security-related change is sitting in a stale state while other contributors continue modifying the same files, which is creating unnecessary conflict resolution work for everyone else.

If I don't hear back or see progress soon, I'll have to assume the PR is inactive and move forward accordingly.

Please update the branch and ping me once it's ready for review.

@DebasmitaBose0

Copy link
Copy Markdown
Contributor Author

@sreerevanth Sorry for the inconvenience caused. I am currently fixing this conflict, plz give me some time....

@DebasmitaBose0

Copy link
Copy Markdown
Contributor Author

@sreerevanth kindly check now

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agentwatch/adapters/openai_agents.py`:
- Around line 85-90: The tool-call path in OpenAI agent handling is still
emitting events via `_emit()` before safety is guaranteed, so the caller may
continue to execution before `SafetyEngine.check_event()` finishes. Update the
tool-call flow around `ToolCallData` creation and `_emit()` so safety is checked
on an awaited preflight path, or return/raise a block decision before any tool
execution can proceed. Ensure the fix is applied in the event emission path used
by the tool-call handler, not just inside the subscriber.
- Around line 77-81: The string-input handling in openai_agents.py only promotes
command-like tools to raw_command for a narrow set of names, so tools like cmd
or execute_command can leave executable text in arguments["input"] and bypass
the safety scan. Update the tool-name check in the string branch of the input
handling logic to recognize additional command-oriented names in the same place
where raw_command is assigned, so all command-style string inputs are routed
through raw_command instead of input.

In `@tests/test_openai_agents_adapter.py`:
- Around line 114-128: The test in safety_handler currently waits on a fixed
asyncio.sleep, which makes the regression timing-dependent. Update the test
around AgentWatchOpenAIAgentsAdapter.on_tool_call and bus.subscribe_fn to use a
deterministic signal from the async handler instead of sleeping, then await that
signal with a timeout so the assertion only proceeds once the event has been
processed.
- Line 4: The async test setup is redundant because asyncio_mode = "auto"
already handles it, so remove the unnecessary pytest dependency from the test
module. Update the async test in test_openai_agents_adapter by dropping both the
`@pytest.mark.asyncio` decorator and the import pytest statement, leaving the test
to run under the existing auto asyncio configuration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ebee1c7c-524d-4528-9204-855ab16779a7

📥 Commits

Reviewing files that changed from the base of the PR and between 8e29ef6 and ff5060a.

📒 Files selected for processing (2)
  • agentwatch/adapters/openai_agents.py
  • tests/test_openai_agents_adapter.py

Comment on lines +77 to +81
elif isinstance(input, str):
arguments = {"input": input}
# If it's a command-like tool name, promote the string input to raw_command
if any(kw in tool_name.lower() for kw in ("bash", "shell", "terminal", "run")):
raw_command = input

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Broaden string command promotion to avoid residual safety bypasses.

For string inputs, only tool names containing bash, shell, terminal, or run populate raw_command. Common command tools like cmd or execute_command would leave dangerous string content in arguments["input"], which the safety path does not scan as command text.

🛡️ Suggested fix
         elif isinstance(input, str):
             arguments = {"input": input}
             # If it's a command-like tool name, promote the string input to raw_command
-            if any(kw in tool_name.lower() for kw in ("bash", "shell", "terminal", "run")):
-                raw_command = input
+            tool_name_lower = tool_name.lower()
+            command_like_names = (
+                "bash",
+                "shell",
+                "terminal",
+                "run",
+                "cmd",
+                "command",
+                "exec",
+                "powershell",
+                "pwsh",
+            )
+            if input.strip() and any(kw in tool_name_lower for kw in command_like_names):
+                raw_command = input.strip()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
elif isinstance(input, str):
arguments = {"input": input}
# If it's a command-like tool name, promote the string input to raw_command
if any(kw in tool_name.lower() for kw in ("bash", "shell", "terminal", "run")):
raw_command = input
elif isinstance(input, str):
arguments = {"input": input}
# If it's a command-like tool name, promote the string input to raw_command
tool_name_lower = tool_name.lower()
command_like_names = (
"bash",
"shell",
"terminal",
"run",
"cmd",
"command",
"exec",
"powershell",
"pwsh",
)
if input.strip() and any(kw in tool_name_lower for kw in command_like_names):
raw_command = input.strip()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/adapters/openai_agents.py` around lines 77 - 81, The string-input
handling in openai_agents.py only promotes command-like tools to raw_command for
a narrow set of names, so tools like cmd or execute_command can leave executable
text in arguments["input"] and bypass the safety scan. Update the tool-name
check in the string branch of the input handling logic to recognize additional
command-oriented names in the same place where raw_command is assigned, so all
command-style string inputs are routed through raw_command instead of input.

Comment on lines +85 to 90
event.tool_call = ToolCallData(
tool_name=tool_name,
arguments=arguments,
raw_command=raw_command,
)
self._emit(event)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Ensure safety is enforced before the tool can execute.

_emit() uses publish_sync; inside an already-running event loop this schedules safety subscribers asynchronously and returns immediately. That means SafetyEngine.check_event() can block the event after the caller has already continued to tool execution. Please route tool calls through an awaited preflight path or return/raise a block decision before execution proceeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agentwatch/adapters/openai_agents.py` around lines 85 - 90, The tool-call
path in OpenAI agent handling is still emitting events via `_emit()` before
safety is guaranteed, so the caller may continue to execution before
`SafetyEngine.check_event()` finishes. Update the tool-call flow around
`ToolCallData` creation and `_emit()` so safety is checked on an awaited
preflight path, or return/raise a block decision before any tool execution can
proceed. Ensure the fix is applied in the event emission path used by the
tool-call handler, not just inside the subscriber.

from agentwatch.adapters.openai_agents import AgentWatchOpenAIAgentsAdapter
from agentwatch.core.event_bus import EventBus
from agentwatch.core.schema import EventType, ExecutionStatus
import pytest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify pytest async mode and remaining redundant asyncio markers.
rg -n "asyncio_mode\s*=" -g 'pyproject.toml' -g 'pytest.ini' -g 'setup.cfg' -g 'tox.ini' .
rg -n "`@pytest`\.mark\.asyncio|import pytest" tests/test_openai_agents_adapter.py

Repository: sreerevanth/AgentWatch

Length of output: 243


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the test file around the pytest import and async test.
sed -n '1,180p' tests/test_openai_agents_adapter.py

Repository: sreerevanth/AgentWatch

Length of output: 4323


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether pytest is used anywhere else in this file.
rg -n "\bpytest\b" tests/test_openai_agents_adapter.py

Repository: sreerevanth/AgentWatch

Length of output: 201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure to locate the async test and any fixtures.
ast-grep outline tests/test_openai_agents_adapter.py --view expanded

Repository: sreerevanth/AgentWatch

Length of output: 601


Remove the redundant pytest async marker/import. asyncio_mode = "auto" already covers this async test, so both @pytest.mark.asyncio and import pytest can be dropped.

🧰 Tools
🪛 GitHub Actions: PR Tests / 0_Test & lint.txt

[error] 1-4: ruff check: I001 Import block is un-sorted or un-formatted. Help: Organize imports.

🪛 GitHub Actions: PR Tests / Test & lint

[error] 1-4: ruff check (I001): Import block is un-sorted or un-formatted. Help: Organize imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_openai_agents_adapter.py` at line 4, The async test setup is
redundant because asyncio_mode = "auto" already handles it, so remove the
unnecessary pytest dependency from the test module. Update the async test in
test_openai_agents_adapter by dropping both the `@pytest.mark.asyncio` decorator
and the import pytest statement, leaving the test to run under the existing auto
asyncio configuration.

Source: Coding guidelines

Comment on lines +114 to +128
captured = []
async def safety_handler(event):
checked = await safety.check_event(event)
captured.append(checked)

bus.subscribe_fn(safety_handler, handler_id="test.safety")

adapter = AgentWatchOpenAIAgentsAdapter(event_bus=bus)

# Trigger a highly dangerous command
adapter.on_tool_call("bash", input={"command": "rm -rf /"})

# Wait a brief moment for async subscription
import asyncio
await asyncio.sleep(0.05)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Replace the fixed sleep with a deterministic signal.

await asyncio.sleep(0.05) makes this regression timing-dependent; a slow audit or busy CI loop can fail even when the handler eventually blocks. Signal from the handler and wait with a timeout instead.

✅ Suggested fix
+    import asyncio
+
     # Intercept tool calls through safety engine
     captured = []
+    done = asyncio.Event()
+
     async def safety_handler(event):
         checked = await safety.check_event(event)
         captured.append(checked)
+        done.set()
         
     bus.subscribe_fn(safety_handler, handler_id="test.safety")
@@
-    # Wait a brief moment for async subscription
-    import asyncio
-    await asyncio.sleep(0.05)
+    await asyncio.wait_for(done.wait(), timeout=1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
captured = []
async def safety_handler(event):
checked = await safety.check_event(event)
captured.append(checked)
bus.subscribe_fn(safety_handler, handler_id="test.safety")
adapter = AgentWatchOpenAIAgentsAdapter(event_bus=bus)
# Trigger a highly dangerous command
adapter.on_tool_call("bash", input={"command": "rm -rf /"})
# Wait a brief moment for async subscription
import asyncio
await asyncio.sleep(0.05)
import asyncio
# Intercept tool calls through safety engine
captured = []
done = asyncio.Event()
async def safety_handler(event):
checked = await safety.check_event(event)
captured.append(checked)
done.set()
bus.subscribe_fn(safety_handler, handler_id="test.safety")
adapter = AgentWatchOpenAIAgentsAdapter(event_bus=bus)
# Trigger a highly dangerous command
adapter.on_tool_call("bash", input={"command": "rm -rf /"})
await asyncio.wait_for(done.wait(), timeout=1)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_openai_agents_adapter.py` around lines 114 - 128, The test in
safety_handler currently waits on a fixed asyncio.sleep, which makes the
regression timing-dependent. Update the test around
AgentWatchOpenAIAgentsAdapter.on_tool_call and bus.subscribe_fn to use a
deterministic signal from the async handler instead of sleeping, then await that
signal with a timeout so the assertion only proceeds once the event has been
processed.

@sreerevanth

Copy link
Copy Markdown
Owner

@sreerevanth kindly check now

ci is red still and adress coderabbit errors too

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat] [ELUSOC] Security Safety Bypass in OpenAI Agents Adapter due to Unpopulated tool_call Field

2 participants