Studio: scope the external-provider and Anthropic confirm_code_execution rejections to code execution

confirm_code_execution guards only local python/terminal, so it should reject a
request on these paths only when code execution could actually run, leaving
non-code tool requests (web_search, ...) unaffected.

- External providers: reject only when the hosted code_execution tool or a
  code-exec container is enabled (not for any provider tool request).
- Anthropic /v1/messages: move the rejection before the model switch (so an
  invalid request no longer evicts the resident model) and scope it to requests
  whose selected server tools include python/terminal.

Tests: reject a code server tool / provider code_execution; allow web-search-only
on both paths.
This commit is contained in:
danielhanchen 2026-07-07 09:56:48 +00:00
commit 5237614e7a
3 changed files with 93 additions and 26 deletions

View file

@ -1788,6 +1788,21 @@ def _payload_may_enable_code_execution(payload) -> bool:
return True
def _anthropic_may_run_code_execution(payload, requested_studio_tools: set) -> bool:
"""True when the Anthropic ``/v1/messages`` server-tool path could run a
code-execution tool (python/terminal), computed pre-switch from the payload.
Mirrors ``_select_anthropic_server_tools``: an explicit selection
(requested server tools or ``enabled_tools``) must list python/terminal; with
no explicit selection the server path exposes every Studio tool when the tool
loop is enabled."""
from core.inference.tools import CODE_EXECUTION_TOOL_NAMES
selected = set(requested_studio_tools) | set(payload.enabled_tools or [])
if requested_studio_tools or payload.enabled_tools is not None:
return bool(CODE_EXECUTION_TOOL_NAMES & selected)
return bool(_effective_enable_tools(payload))
def _apply_rag_nudge(nudge: str, tools: list[dict], *, rag_scope) -> str:
"""Append the RAG grounding nudge to ``nudge`` when the knowledge-base tool
is active (search_knowledge_base present and a retrieval scope is set). The
@ -5698,16 +5713,16 @@ async def openai_chat_completions(
param = "confirm_tool_calls",
),
)
# Same for confirm_code_execution: an external provider runs code_execution
# server-side, so this local confirm gate cannot intercept it -- reject it
# rather than give the caller a false approval guarantee.
# confirm_code_execution guards only local python/terminal. An external
# provider that enables its hosted code_execution tool runs code
# server-side, which this local gate cannot intercept -- reject it rather
# than give a false approval guarantee. Scoped to actual provider code
# execution so a non-code provider request (e.g. web_search) is unaffected.
if (
payload.confirm_code_execution
and not payload.bypass_permissions
and (
payload.enable_tools is True
or bool(payload.enabled_tools)
or bool(payload.tools)
(payload.enabled_tools is not None and "code_execution" in payload.enabled_tools)
or bool(payload.openai_code_exec_container_id)
or bool(payload.anthropic_code_exec_container_id)
)
@ -5715,7 +5730,8 @@ async def openai_chat_completions(
raise HTTPException(
status_code = 400,
detail = openai_error_body(
"confirm_code_execution is only supported for local streaming tools.",
"confirm_code_execution cannot guard provider-hosted code execution; "
"it only applies to local python/terminal tools.",
status = 400,
code = "invalid_request_error",
param = "confirm_code_execution",
@ -10245,6 +10261,27 @@ async def anthropic_messages(
),
)
# confirm_code_execution guards only local python/terminal, which the server
# path runs server-side and cannot intercept. Reject it -- before the switch,
# payload-only, so an invalid request never evicts the loaded model -- but only
# when a code-execution tool is actually selected, so web-search-only requests
# are unaffected. bypass_permissions suppresses the confirm gate, so both flags
# together is fine.
if (
bool(getattr(payload, "confirm_code_execution", False))
and not bool(getattr(payload, "bypass_permissions", False))
and _anthropic_may_run_code_execution(payload, requested_studio_tools)
):
raise HTTPException(
status_code = 400,
detail = anthropic_error_body(
"confirm_code_execution cannot guard Anthropic server-side code "
"execution; it only applies to local python/terminal tools.",
status = 400,
err_type = "invalid_request_error",
),
)
# require_vision rejects a swap to a text-only target before it runs, so an
# image request can't evict the resident vision model only to hit the vision
# guard (_normalize_anthropic_openai_images) below after the load.
@ -10437,21 +10474,6 @@ async def anthropic_messages(
err_type = "invalid_request_error",
),
)
if bool(getattr(payload, "confirm_code_execution", False)) and not bool(
getattr(payload, "bypass_permissions", False)
):
api_monitor.fail(
monitor_id,
"confirm_code_execution is not supported for Anthropic Messages server tools.",
)
raise HTTPException(
status_code = 400,
detail = anthropic_error_body(
"confirm_code_execution is not supported for Anthropic Messages server tools.",
status = 400,
err_type = "invalid_request_error",
),
)
from core.inference.tools import ALL_TOOLS
openai_tools = _select_anthropic_server_tools(

View file

@ -1718,18 +1718,31 @@ class TestAnthropicMessagesToolRouting:
assert "confirm_tool_calls is not supported" in exc.value.detail["error"]["message"]
assert backend.calls == []
def test_confirm_code_execution_rejected_for_server_tools(self, monkeypatch):
def test_confirm_code_execution_rejected_for_code_server_tools(self, monkeypatch):
# A code-execution server tool (python) is what confirm_code_execution
# would guard; since the server path runs it server-side, reject it.
backend = _mock_backend(monkeypatch)
payload = _basic_payload(
confirm_code_execution = True,
tools = [{"type": "python", "name": "python"}],
)
with pytest.raises(HTTPException) as exc:
_drive(anthropic_messages(payload, request = None, current_subject = "t"))
assert exc.value.status_code == 400
assert "confirm_code_execution cannot guard" in exc.value.detail["error"]["message"]
assert backend.calls == []
def test_confirm_code_execution_allows_non_code_server_tools(self, monkeypatch):
# web_search is not code execution, so the flag must not reject it.
backend = _mock_backend(monkeypatch)
payload = _basic_payload(
confirm_code_execution = True,
tools = [{"type": "web_search_20250305", "name": "web_search"}],
)
with pytest.raises(HTTPException) as exc:
_drive(anthropic_messages(payload, request = None, current_subject = "t"))
assert exc.value.status_code == 400
assert "confirm_code_execution is not supported" in exc.value.detail["error"]["message"]
assert backend.calls == []
_drive(anthropic_messages(payload, request = None, current_subject = "t"))
assert backend.calls[0][0] == "tools"
def test_per_request_enable_tools_false_blocks_server_tool_alias(self, monkeypatch):
backend = _mock_backend(monkeypatch)

View file

@ -482,7 +482,39 @@ class TestChatCompletionRequestToolFields:
assert resp.status_code == 400
body = resp.json()
assert body["error"]["param"] == "confirm_code_execution"
assert "only supported for local streaming tools" in body["error"]["message"]
assert "cannot guard provider-hosted code execution" in body["error"]["message"]
def test_confirm_code_execution_allows_non_code_provider_tools(self, monkeypatch):
# A provider request that enables only non-code tools has no code execution
# for this flag to guard, so it must not be rejected on the flag alone.
import routes.inference as inference_route
called = {"proxied": False}
async def _fake_proxy(payload, request, current_subject):
called["proxied"] = True
return {"ok": True}
monkeypatch.setattr(inference_route, "_proxy_to_external_provider", _fake_proxy)
class _UnusedBackend:
is_loaded = False
client = self._v1_client(monkeypatch, _UnusedBackend())
resp = client.post(
"/v1/chat/completions",
json = {
"messages": [{"role": "user", "content": "hi"}],
"provider_type": "openai",
"external_model": "gpt-4.1",
"enable_tools": True,
"enabled_tools": ["web_search"],
"confirm_code_execution": True,
},
)
assert resp.status_code != 400
assert called["proxied"] is True
def test_logprobs_rejected_until_supported(self, monkeypatch):
class _UnusedBackend: