fix(guardrails): extract tool names from additional_tools input items on Responses API

This commit is contained in:
mateo-berri 2026-07-20 20:33:25 -07:00
parent 9ad8698aab
commit ab912c963d
2 changed files with 77 additions and 10 deletions

View file

@ -198,16 +198,27 @@ class OpenAIResponsesHandler(BaseTranslation):
def extract_request_tool_names(self, data: dict) -> List[str]:
"""Extract tool names from Responses API request (tools[].name for function
and custom, tools[].server_label for mcp)."""
names: List[str] = []
for tool in data.get("tools") or []:
if not isinstance(tool, dict):
continue
if tool.get("type") in ("function", "custom") and tool.get("name"):
names.append(str(tool["name"]))
elif tool.get("type") == "mcp" and tool.get("server_label"):
names.append(str(tool["server_label"]))
return names
and custom, tools[].server_label for mcp), including tools nested in
additional_tools input items (Codex CLI ships its tool definitions there)."""
input_items = data.get("input")
additional_tools = (
tool
for item in (input_items if isinstance(input_items, list) else ())
if isinstance(item, dict) and item.get("type") == "additional_tools"
for tool in (item.get("tools") or ())
)
names = (self._responses_tool_name(tool) for tool in (*(data.get("tools") or ()), *additional_tools))
return [name for name in names if name]
@staticmethod
def _responses_tool_name(tool: object) -> str | None:
if not isinstance(tool, dict):
return None
if tool.get("type") in ("function", "custom") and tool.get("name"):
return str(tool["name"])
if tool.get("type") == "mcp" and tool.get("server_label"):
return str(tool["server_label"])
return None
def _extract_and_transform_tools(
self,

View file

@ -86,6 +86,40 @@ class TestExtractRequestToolNames:
"get_current_weather",
]
def test_openai_responses_additional_tools_input_items(self):
"""Codex CLI 0.143+ ships tool definitions inside an additional_tools
input item instead of (or alongside) top-level tools; those nested tools
must be extracted too or a restricted key could smuggle a disallowed
tool past allowlist enforcement (VERIA finding on PR #33228)."""
data = {
"tools": [{"type": "function", "name": "get_current_weather"}],
"input": [
{"role": "user", "content": "hi"},
{
"type": "additional_tools",
"role": "developer",
"tools": [
{"type": "custom", "name": "exec", "description": "x"},
{"type": "mcp", "server_label": "dmcp", "server_url": "http://x"},
],
},
],
}
assert extract_request_tool_names("/v1/responses", data) == [
"get_current_weather",
"exec",
"dmcp",
]
def test_openai_responses_string_input_ignored(self):
data = {
"tools": [{"type": "function", "name": "get_current_weather"}],
"input": "hi",
}
assert extract_request_tool_names("/v1/responses", data) == [
"get_current_weather"
]
def test_anthropic_tools(self):
data = {"tools": [{"name": "get_weather"}, {"name": "run_sql"}]}
assert extract_request_tool_names("/v1/messages", data) == [
@ -173,6 +207,28 @@ class TestCheckToolsAllowlist:
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
assert "restricted_tool" in str(exc_info.value.message)
@pytest.mark.asyncio
async def test_disallowed_additional_tools_input_item_raises_on_responses_route(self):
token = _token(metadata={"allowed_tools": ["other_tool"]})
body = {
"input": [
{
"type": "additional_tools",
"role": "developer",
"tools": [{"type": "custom", "name": "exec"}],
},
],
}
with pytest.raises(ProxyException) as exc_info:
await check_tools_allowlist(
request_body=body,
valid_token=token,
team_object=None,
route="/v1/responses",
)
assert exc_info.value.type == ProxyErrorTypes.tool_access_denied
assert "exec" in str(exc_info.value.message)
@pytest.mark.asyncio
async def test_team_allowlist_used_when_key_empty(self):
token = _token(