Studio: mirror confirm_tool_calls validation for confirm_code_execution

Review follow-up: validate confirm_code_execution at the same request-lifecycle
points as confirm_tool_calls so it can never be silently accepted where the
confirm gate cannot apply.

- External providers: reject confirm_code_execution (code_execution runs
  provider-side, so the local confirm gate cannot intercept it) instead of
  giving the caller a false approval guarantee.
- Pre-switch: reject a non-stream confirm_code_execution local tool request
  before automatic model loading, so an invalid shape does not evict the
  resident model only to 400 after the swap.
- Drop the code-execution tool scoping on the per-request stream requirement so
  it mirrors confirm_tool_calls exactly (removes _enables_code_execution_tool).

Tests: provider rejection for confirm_code_execution; existing streaming
requirement + gate tests still pass.
This commit is contained in:
danielhanchen 2026-07-07 09:01:45 +00:00
commit 5f5eaef386
3 changed files with 72 additions and 34 deletions

View file

@ -1763,16 +1763,6 @@ async def _select_request_tools(
return tools
def _enables_code_execution_tool(tools: list[dict]) -> bool:
"""True when the resolved tool list includes a local code-execution tool
(python/terminal). Used to scope the ``confirm_code_execution`` stream
requirement so a request that never exposes code execution isn't rejected."""
from core.inference.tools import CODE_EXECUTION_TOOL_NAMES
return any(
(t.get("function") or {}).get("name") in CODE_EXECUTION_TOOL_NAMES for t in (tools or [])
)
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
@ -5683,6 +5673,29 @@ 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.
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)
or bool(payload.openai_code_exec_container_id)
or bool(payload.anthropic_code_exec_container_id)
)
):
raise HTTPException(
status_code = 400,
detail = openai_error_body(
"confirm_code_execution is only supported for local streaming tools.",
status = 400,
code = "invalid_request_error",
param = "confirm_code_execution",
),
)
if _wants_multiple_choices(payload):
_raise_unsupported_n("external provider chat completions")
return await _proxy_to_external_provider(payload, request, current_subject)
@ -5759,6 +5772,31 @@ async def openai_chat_completions(
param = "confirm_tool_calls",
),
)
# Same pre-switch guard for confirm_code_execution: the code-execution
# confirm gate also needs streaming, so a non-stream request must not evict
# the resident model only to 400 after the swap.
if (
payload.confirm_code_execution
and not payload.bypass_permissions
and not payload.stream
and (
_effective_enable_tools(payload)
or (bool(payload.mcp_enabled) and _confirm_cli_policy is not False)
or bool(payload.enabled_tools)
or bool(payload.tools)
or bool(payload.openai_code_exec_container_id)
or bool(payload.anthropic_code_exec_container_id)
)
):
raise HTTPException(
status_code = 400,
detail = openai_error_body(
"confirm_code_execution requires stream=true for local tool execution.",
status = 400,
code = "invalid_request_error",
param = "confirm_code_execution",
),
)
# Reject a malformed tool_choice forcing object before the switch: a
# {"type": "function", "function": {}} with no name would otherwise be
# forwarded to llama-server and rejected only after the model swapped.
@ -6235,13 +6273,11 @@ async def openai_chat_completions(
payload.confirm_code_execution
and not payload.bypass_permissions
and not payload.stream
and _enables_code_execution_tool(tools_to_use)
):
raise _reject(
400,
openai_error_body(
"confirm_code_execution requires stream=true when a "
"code-execution tool (python/terminal) is enabled.",
"confirm_code_execution requires stream=true for local tool execution.",
status = 400,
code = "invalid_request_error",
param = "confirm_code_execution",
@ -6977,13 +7013,11 @@ async def openai_chat_completions(
payload.confirm_code_execution
and not payload.bypass_permissions
and not payload.stream
and _enables_code_execution_tool(_sf_tools_to_use)
):
raise _reject(
400,
openai_error_body(
"confirm_code_execution requires stream=true when a "
"code-execution tool (python/terminal) is enabled.",
"confirm_code_execution requires stream=true for local tool execution.",
status = 400,
code = "invalid_request_error",
param = "confirm_code_execution",

View file

@ -179,21 +179,3 @@ def test_confirm_tool_calls_still_gates_every_tool():
starts = _starts(events)
assert starts[0]["awaiting_confirmation"] is True
assert calls == [("web_search", {"query": "cats"})]
# ── scoping predicate used by the route stream requirement ───────────────────
def _spec(name):
return {"type": "function", "function": {"name": name}}
def test_enables_code_execution_tool_predicate():
from routes.inference import _enables_code_execution_tool
assert _enables_code_execution_tool([_spec("python")]) is True
assert _enables_code_execution_tool([_spec("terminal")]) is True
assert _enables_code_execution_tool([_spec("web_search"), _spec("python")]) is True
assert _enables_code_execution_tool([_spec("web_search"), _spec("render_html")]) is False
assert _enables_code_execution_tool([]) is False
assert _enables_code_execution_tool(None) is False

View file

@ -462,6 +462,28 @@ class TestChatCompletionRequestToolFields:
assert body["error"]["param"] == "confirm_tool_calls"
assert "only supported for local streaming tools" in body["error"]["message"]
def test_confirm_code_execution_rejected_for_provider_tools(self, monkeypatch):
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": ["code_execution"],
"confirm_code_execution": True,
},
)
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"]
def test_logprobs_rejected_until_supported(self, monkeypatch):
class _UnusedBackend:
is_loaded = False