From 3f883a3e5f5a290844305d7280b55abd28912820 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 08:19:14 +0000 Subject: [PATCH 01/11] Studio: add confirm_code_execution to gate only python/terminal tool calls Adds an opt-in request field, confirm_code_execution, that routes local code-execution tool calls (python, terminal) through the existing confirmation gate while other tools (web_search, render_html, MCP, ...) continue to run without a prompt. This lets a caller require approval for code execution specifically, without the friction of confirm_tool_calls prompting on every tool. It is independent of confirm_tool_calls, defaults off (no change to existing behavior), requires stream=true when a code-execution tool is enabled (same as confirm_tool_calls), and bypass_permissions still takes precedence. The tool-call parser and tool detection are unchanged, so no tool-calling behavior is affected when the flag is off. - models: new confirm_code_execution field on ChatCompletionRequest - tools: CODE_EXECUTION_TOOL_NAMES = {python, terminal} - safetensors and gguf loops: needs_confirm also fires for code-execution tools when confirm_code_execution is set (bypass still wins) - routes: thread the flag to both local loops; require streaming when a code-execution tool is enabled - tests: loop-level gate behavior, the scoping predicate, and the route streaming requirement for both backends --- studio/backend/core/inference/llama_cpp.py | 18 +- studio/backend/core/inference/orchestrator.py | 2 + .../core/inference/safetensors_agentic.py | 14 +- studio/backend/core/inference/tools.py | 6 + studio/backend/models/inference.py | 4 + studio/backend/routes/inference.py | 48 +++++ .../tests/test_confirm_code_execution.py | 199 ++++++++++++++++++ .../tests/test_openai_tool_passthrough.py | 92 ++++++++ 8 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 studio/backend/tests/test_confirm_code_execution.py diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 5e6287f528..5af194cc9e 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -8446,6 +8446,7 @@ class LlamaCppBackend: seed: Optional[int] = None, disable_parallel_tool_use: bool = False, confirm_tool_calls: bool = False, + confirm_code_execution: bool = False, bypass_permissions: bool = False, ) -> Generator[dict, None, None]: """ @@ -8456,7 +8457,11 @@ class LlamaCppBackend: {"type": "content", "text": "token"} -- streamed content tokens (cumulative) {"type": "reasoning", "text": "token"} -- streamed reasoning tokens (cumulative) """ - from core.inference.tools import build_rag_autoinject, execute_tool + from core.inference.tools import ( + CODE_EXECUTION_TOOL_NAMES, + build_rag_autoinject, + execute_tool, + ) if not self.is_loaded: raise RuntimeError("llama-server is not loaded") @@ -9373,7 +9378,16 @@ class LlamaCppBackend: # Bypass wins over the confirm gate at the loop level too, # so a direct internal caller with both flags never prompts. - needs_confirm = bool(confirm_tool_calls) and not bypass_permissions + # confirm_code_execution narrows the gate to python/terminal so + # a code-execution call pauses for approval even when + # confirm_tool_calls is off (search/render tools stay instant). + needs_confirm = ( + bool(confirm_tool_calls) + or ( + bool(confirm_code_execution) + and decision.tool_name in CODE_EXECUTION_TOOL_NAMES + ) + ) and not bypass_permissions approval_id = new_approval_id() if needs_confirm else "" decision_slot = ( begin_tool_decision(session_id, approval_id) if needs_confirm else None diff --git a/studio/backend/core/inference/orchestrator.py b/studio/backend/core/inference/orchestrator.py index 19d2230278..018d245186 100644 --- a/studio/backend/core/inference/orchestrator.py +++ b/studio/backend/core/inference/orchestrator.py @@ -1225,6 +1225,7 @@ class InferenceOrchestrator: session_id: Optional[str] = None, rag_scope: Optional[dict] = None, confirm_tool_calls: bool = False, + confirm_code_execution: bool = False, bypass_permissions: bool = False, use_adapter: Optional[Union[bool, str]] = None, stats_holder: Optional[dict] = None, @@ -1291,6 +1292,7 @@ class InferenceOrchestrator: session_id = session_id, rag_scope = rag_scope, confirm_tool_calls = confirm_tool_calls, + confirm_code_execution = confirm_code_execution, bypass_permissions = bypass_permissions, ) diff --git a/studio/backend/core/inference/safetensors_agentic.py b/studio/backend/core/inference/safetensors_agentic.py index 81c25b777e..84d4775beb 100644 --- a/studio/backend/core/inference/safetensors_agentic.py +++ b/studio/backend/core/inference/safetensors_agentic.py @@ -426,6 +426,7 @@ def run_safetensors_tool_loop( session_id: Optional[str] = None, rag_scope: Optional[dict] = None, confirm_tool_calls: bool = False, + confirm_code_execution: bool = False, bypass_permissions: bool = False, ) -> Generator[dict, None, None]: """Drive an agentic tool loop on top of a cumulative-text generator. @@ -453,7 +454,7 @@ def run_safetensors_tool_loop( conversation = list(messages) # Forced first-pass RAG (mirrors the GGUF loop) so doc Qs don't lose to web_search. - from core.inference.tools import build_rag_autoinject + from core.inference.tools import CODE_EXECUTION_TOOL_NAMES, build_rag_autoinject _auto = None if confirm_tool_calls else build_rag_autoinject(conversation, rag_scope) if _auto: @@ -1056,7 +1057,16 @@ def run_safetensors_tool_loop( # Bypass wins over the confirm gate at the loop level too, so a # direct internal caller passing both flags never prompts. - needs_confirm = bool(confirm_tool_calls) and not bypass_permissions + # confirm_code_execution narrows the gate to python/terminal so a + # code-execution call pauses for approval even when confirm_tool_calls + # is off (search/render tools still run without a prompt). + needs_confirm = ( + bool(confirm_tool_calls) + or ( + bool(confirm_code_execution) + and decision.tool_name in CODE_EXECUTION_TOOL_NAMES + ) + ) and not bypass_permissions approval_id = new_approval_id() if needs_confirm else "" decision_slot = begin_tool_decision(session_id, approval_id) if needs_confirm else None start_event = decision.tool_start_event() diff --git a/studio/backend/core/inference/tools.py b/studio/backend/core/inference/tools.py index 82c50933fc..9414d54ed5 100644 --- a/studio/backend/core/inference/tools.py +++ b/studio/backend/core/inference/tools.py @@ -832,6 +832,12 @@ ALL_TOOLS = [ SEARCH_KNOWLEDGE_BASE_TOOL, ] +# Built-in tools that run arbitrary model-authored code/commands (through the +# sandbox unless bypassed). The confirmation gate can target just these +# (confirm_code_execution) so a code-execution call pauses for approval while +# search/render tools stay instant. +CODE_EXECUTION_TOOL_NAMES = frozenset({"python", "terminal"}) + # OpenAI's function.name regex ^[a-zA-Z0-9_-]{1,64}$, enforced before streaming. # MCP tool names with '.', '/', spaces, etc. would 400 the whole request, so we diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index 0f27b695fe..d610a3e98a 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -773,6 +773,10 @@ class ChatCompletionRequest(BaseModel): None, description = "[x-unsloth] When true, pause before each tool call and wait for the user to allow/deny it via POST /api/inference/tool-confirm.", ) + confirm_code_execution: Optional[bool] = Field( + None, + description = "[x-unsloth] When true, pause only before local code-execution tool calls (python/terminal) and wait for the user to allow/deny each via POST /api/inference/tool-confirm; other tools (web_search, render_html, ...) still run without a prompt. Independent of confirm_tool_calls; requires stream=true when a code-execution tool is enabled; bypass_permissions still takes precedence.", + ) bypass_permissions: Optional[bool] = Field( False, description = "[x-unsloth] Bypass Permissions: when true, skip the tool-call confirmation gate AND disable the python/terminal execution sandbox (safety checks, command blocklist, resource limits). Secret env vars are still stripped. Takes precedence over confirm_tool_calls.", diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 5332037e0d..9ea197921f 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1763,6 +1763,18 @@ 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 @@ -6221,6 +6233,22 @@ async def openai_chat_completions( param = "confirm_tool_calls", ), ) + if ( + 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.", + status = 400, + code = "invalid_request_error", + param = "confirm_code_execution", + ), + ) if _wants_multiple_choices(payload): raise _reject_unsupported_n("GGUF tool chat completions") # ── Tool-use system prompt nudge ────────────────────── @@ -6289,6 +6317,8 @@ async def openai_chat_completions( # never prompt while bypassing. confirm_tool_calls = bool(payload.confirm_tool_calls) and not bool(payload.bypass_permissions), + confirm_code_execution = bool(payload.confirm_code_execution) + and not bool(payload.bypass_permissions), bypass_permissions = bool(payload.bypass_permissions), ) @@ -6945,6 +6975,22 @@ async def openai_chat_completions( param = "confirm_tool_calls", ), ) + if ( + 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.", + status = 400, + code = "invalid_request_error", + param = "confirm_code_execution", + ), + ) _sf_nudge = _build_tool_action_nudge( tools = _sf_tools_to_use, model_name = model_name, @@ -7014,6 +7060,8 @@ async def openai_chat_completions( # never prompt while bypassing. confirm_tool_calls = bool(payload.confirm_tool_calls) and not bool(payload.bypass_permissions), + confirm_code_execution = bool(payload.confirm_code_execution) + and not bool(payload.bypass_permissions), bypass_permissions = bool(payload.bypass_permissions), use_adapter = payload.use_adapter, stats_holder = _sf_stats_holder, diff --git a/studio/backend/tests/test_confirm_code_execution.py b/studio/backend/tests/test_confirm_code_execution.py new file mode 100644 index 0000000000..ac8cfc1012 --- /dev/null +++ b/studio/backend/tests/test_confirm_code_execution.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Tests for ``confirm_code_execution``: a narrower confirmation gate that +pauses only before local code-execution tools (python/terminal) while other +tools (web_search, render_html, ...) still run without a prompt. + +These drive the real ``run_safetensors_tool_loop`` with hand-crafted fake +generators (no model), mirroring ``test_tool_confirm_loop.py``, and cover the +scoping predicate the route layer uses to require streaming. +""" + +import pytest + +from core.inference.safetensors_agentic import run_safetensors_tool_loop +from state import tool_approvals +from state.tool_approvals import resolve_tool_decision + +_SESSION = "code-exec-session" + +_TOOLS = [ + {"type": "function", "function": {"name": "python"}}, + {"type": "function", "function": {"name": "terminal"}}, + {"type": "function", "function": {"name": "web_search"}}, +] + + +@pytest.fixture(autouse = True) +def _clear_pending(): + with tool_approvals._lock: + tool_approvals._pending.clear() + yield + with tool_approvals._lock: + tool_approvals._pending.clear() + + +class _FakeExecuteTool: + def __init__(self): + self.calls = [] + + def __call__( + self, + name, + arguments, + *, + cancel_event = None, + timeout = None, + session_id = None, + rag_scope = None, + disable_sandbox = False, + ): + self.calls.append((name, arguments)) + return f"RESULT[{name}]" + + +def _tool_call(name, args_json): + return f'{{"name": "{name}", "arguments": {args_json}}}' + + +def _multi_turn(turns): + turn_iter = iter(turns) + + def _gen(_messages): + try: + yield next(turn_iter) + except StopIteration: + return + + return _gen + + +def _drive(turns, decisions, **loop_kwargs): + """Run the loop, resolving each gated tool_start with the next decision. + + Non-gated calls (awaiting_confirmation False) execute without consuming a + decision. Returns (events, execute_calls). + """ + decision_iter = iter(decisions) + exec_fn = _FakeExecuteTool() + gen = run_safetensors_tool_loop( + single_turn = _multi_turn(turns), + messages = [{"role": "user", "content": "hi"}], + tools = _TOOLS, + execute_tool = exec_fn, + session_id = _SESSION, + **loop_kwargs, + ) + events = [] + for ev in gen: + events.append(ev) + if ev["type"] == "tool_start" and ev.get("awaiting_confirmation"): + resolve_tool_decision(ev["approval_id"], next(decision_iter), session_id = _SESSION) + return events, exec_fn.calls + + +def _starts(events): + return [e for e in events if e["type"] == "tool_start"] + + +def _ends(events): + return [e for e in events if e["type"] == "tool_end"] + + +# ── confirm_code_execution gates only python/terminal ──────────────────────── + + +def test_python_call_is_gated_and_executes_on_allow(): + events, calls = _drive( + [_tool_call("python", '{"code": "print(1)"}'), "done"], + ["allow"], + confirm_code_execution = True, + ) + starts = _starts(events) + assert len(starts) == 1 + assert starts[0]["awaiting_confirmation"] is True + assert starts[0]["approval_id"] + assert calls == [("python", {"code": "print(1)"})] + assert _ends(events)[0]["result"] == "RESULT[python]" + + +def test_terminal_call_is_gated_and_skipped_on_deny(): + events, calls = _drive( + [_tool_call("terminal", '{"command": "ls"}'), "done"], + ["deny"], + confirm_code_execution = True, + ) + starts = _starts(events) + assert len(starts) == 1 + assert starts[0]["awaiting_confirmation"] is True + # Denied: the tool never runs. + assert calls == [] + + +def test_web_search_is_not_gated_by_confirm_code_execution(): + # No decision is supplied: a gated call would block waiting for one. + events, calls = _drive( + [_tool_call("web_search", '{"query": "cats"}'), "done"], + [], + confirm_code_execution = True, + ) + starts = _starts(events) + assert len(starts) == 1 + assert starts[0]["awaiting_confirmation"] is False + assert not starts[0]["approval_id"] + assert calls == [("web_search", {"query": "cats"})] + + +def test_bypass_permissions_overrides_confirm_code_execution(): + events, calls = _drive( + [_tool_call("python", '{"code": "print(1)"}'), "done"], + [], + confirm_code_execution = True, + bypass_permissions = True, + ) + starts = _starts(events) + assert starts[0]["awaiting_confirmation"] is False + assert calls == [("python", {"code": "print(1)"})] + + +def test_default_off_does_not_gate_code_execution(): + events, calls = _drive( + [_tool_call("python", '{"code": "print(1)"}'), "done"], + [], + # Neither flag set: unchanged legacy behavior, python runs immediately. + ) + starts = _starts(events) + assert starts[0]["awaiting_confirmation"] is False + assert calls == [("python", {"code": "print(1)"})] + + +def test_confirm_tool_calls_still_gates_every_tool(): + # confirm_tool_calls is the broad gate; web_search is prompted under it even + # though confirm_code_execution would not touch it. + events, calls = _drive( + [_tool_call("web_search", '{"query": "cats"}'), "done"], + ["allow"], + confirm_tool_calls = True, + ) + 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 diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index ccbd78e2b1..07368976a0 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -612,6 +612,51 @@ class TestChatCompletionRequestToolFields: assert "confirm_tool_calls requires stream=true" in entry["error"] assert monitor.active_count() == 0 + def test_confirm_code_execution_requires_streaming_for_safetensors_tools(self, monkeypatch): + import routes.inference as inference_route + + class _NoGGUFBackend: + is_loaded = False + supports_tools = False + + class _InferenceBackend: + active_model_name = "test-model" + models = {"test-model": {"chat_template_info": {"template": "chatml"}}} + + def generate_chat_completion_with_tools(self, **kwargs): + raise AssertionError("tool loop should be rejected before starting") + + def generate_chat_completion(self, **kwargs): + raise AssertionError("plain path should not be used") + + monkeypatch.setattr( + inference_route, + "_detect_safetensors_features", + lambda backend, chat_template: {"supports_tools": True}, + ) + monitor = ApiMonitor(max_entries = 3) + monkeypatch.setattr(inference_route, "api_monitor", monitor) + client = self._v1_client(monkeypatch, _NoGGUFBackend(), _InferenceBackend()) + resp = client.post( + "/v1/chat/completions", + json = { + "messages": [{"role": "user", "content": "run code"}], + "enable_tools": True, + "enabled_tools": ["python"], + "confirm_code_execution": True, + "stream": False, + }, + ) + + assert resp.status_code == 400 + body = resp.json() + assert body["error"]["param"] == "confirm_code_execution" + assert "requires stream=true" in body["error"]["message"] + [entry] = monitor.snapshot() + assert entry["status"] == "error" + assert "confirm_code_execution requires stream=true" in entry["error"] + assert monitor.active_count() == 0 + def test_multiturn_tool_loop_messages(self): req = ChatCompletionRequest( messages = [ @@ -1493,6 +1538,53 @@ class TestGgufVisionToolRouting: assert "confirm_tool_calls requires stream=true" in entry["error"] assert monitor.active_count() == 0 + def test_confirm_code_execution_requires_streaming_for_gguf_tools(self, monkeypatch): + import routes.inference as inf_mod + + def _plain(**kwargs): + raise AssertionError("plain GGUF path should not be used") + + def _tools(**kwargs): + raise AssertionError("tool loop should be rejected before starting") + + backend = SimpleNamespace( + is_loaded = True, + is_vision = False, + supports_tools = True, + model_identifier = "test-gguf", + context_length = 4096, + generate_chat_completion = _plain, + generate_chat_completion_with_tools = _tools, + ) + monkeypatch.setattr(inf_mod, "get_llama_cpp_backend", lambda: backend) + monitor = ApiMonitor(max_entries = 3) + monkeypatch.setattr(inf_mod, "api_monitor", monitor) + + payload = ChatCompletionRequest( + model = "default", + enable_tools = True, + enabled_tools = ["python"], + confirm_code_execution = True, + stream = False, + messages = [{"role": "user", "content": "run code"}], + ) + + with pytest.raises(HTTPException) as exc: + self._drive( + openai_chat_completions( + payload, + request = self._Request(), + current_subject = "test", + ) + ) + assert exc.value.status_code == 400 + assert exc.value.detail["error"]["param"] == "confirm_code_execution" + assert "requires stream=true" in exc.value.detail["error"]["message"] + [entry] = monitor.snapshot() + assert entry["status"] == "error" + assert "confirm_code_execution requires stream=true" in entry["error"] + assert monitor.active_count() == 0 + def test_standard_gguf_stream_splits_reasoning_content(self, monkeypatch): def _generate(**_kwargs): yield " Date: Tue, 7 Jul 2026 08:20:38 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/core/inference/safetensors_agentic.py | 3 +-- studio/backend/routes/inference.py | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/studio/backend/core/inference/safetensors_agentic.py b/studio/backend/core/inference/safetensors_agentic.py index 84d4775beb..003355fe4f 100644 --- a/studio/backend/core/inference/safetensors_agentic.py +++ b/studio/backend/core/inference/safetensors_agentic.py @@ -1063,8 +1063,7 @@ def run_safetensors_tool_loop( needs_confirm = ( bool(confirm_tool_calls) or ( - bool(confirm_code_execution) - and decision.tool_name in CODE_EXECUTION_TOOL_NAMES + bool(confirm_code_execution) and decision.tool_name in CODE_EXECUTION_TOOL_NAMES ) ) and not bypass_permissions approval_id = new_approval_id() if needs_confirm else "" diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 9ea197921f..52f52e33af 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1768,10 +1768,8 @@ def _enables_code_execution_tool(tools: list[dict]) -> bool: (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 []) + (t.get("function") or {}).get("name") in CODE_EXECUTION_TOOL_NAMES for t in (tools or []) ) From 772ae0146db393fcb2f8a362d5af8839951cf491 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 08:31:41 +0000 Subject: [PATCH 03/11] Studio: extend confirm_code_execution to the GGUF provisional card and Anthropic server tools Follow-up polish from review, both only reachable when the new flag is set: - gguf loop: suppress the streamed provisional "running" card for a python/terminal call when confirm_code_execution gates it, so a large code-execution call no longer flashes a card before the approve/deny prompt (mirrors the confirm_tool_calls suppression). - routes: reject confirm_code_execution for Anthropic Messages server tools with a 400, matching the existing confirm_tool_calls rejection, instead of silently ignoring it. --- studio/backend/core/inference/llama_cpp.py | 12 ++++++++++-- studio/backend/routes/inference.py | 15 +++++++++++++++ studio/backend/tests/test_anthropic_messages.py | 13 +++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 5af194cc9e..7371a2d8ea 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -8818,9 +8818,17 @@ class LlamaCppBackend: in provisional_started_tool_calls.values() ) # Later parallel cards only reconcile when parallel use is enabled. + # Suppress the early card whenever this call will be + # gated for confirmation, so a python/terminal call under + # confirm_code_execution never flashes "running" before the + # approve/deny prompt. _confirm_gated = ( - confirm_tool_calls and not bypass_permissions - ) + confirm_tool_calls + or ( + confirm_code_execution + and current_name in CODE_EXECUTION_TOOL_NAMES + ) + ) and not bypass_permissions # Keep small-argument tools on the normal path. _args_len = len( tool_calls_acc[idx]["function"].get("arguments", "") diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 52f52e33af..091edd768f 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -10378,6 +10378,21 @@ 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( diff --git a/studio/backend/tests/test_anthropic_messages.py b/studio/backend/tests/test_anthropic_messages.py index 170b456eac..1c265f7890 100644 --- a/studio/backend/tests/test_anthropic_messages.py +++ b/studio/backend/tests/test_anthropic_messages.py @@ -1718,6 +1718,19 @@ 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): + 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 == [] + def test_per_request_enable_tools_false_blocks_server_tool_alias(self, monkeypatch): backend = _mock_backend(monkeypatch) payload = _basic_payload( From 5f5eaef386dd6b4613fefa2266349c74bf1b4c29 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 09:01:45 +0000 Subject: [PATCH 04/11] 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. --- studio/backend/routes/inference.py | 66 ++++++++++++++----- .../tests/test_confirm_code_execution.py | 18 ----- .../tests/test_openai_tool_passthrough.py | 22 +++++++ 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 091edd768f..9487b11136 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -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", diff --git a/studio/backend/tests/test_confirm_code_execution.py b/studio/backend/tests/test_confirm_code_execution.py index ac8cfc1012..e6d41e5a25 100644 --- a/studio/backend/tests/test_confirm_code_execution.py +++ b/studio/backend/tests/test_confirm_code_execution.py @@ -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 diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index 07368976a0..7ca86984f4 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -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 From 0595b62f9ba4cf222a54011e61c71db175898931 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:02:33 +0000 Subject: [PATCH 05/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/routes/inference.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 9487b11136..be9a4a34f3 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -7009,11 +7009,7 @@ async def openai_chat_completions( param = "confirm_tool_calls", ), ) - if ( - payload.confirm_code_execution - and not payload.bypass_permissions - and not payload.stream - ): + if payload.confirm_code_execution and not payload.bypass_permissions and not payload.stream: raise _reject( 400, openai_error_body( From dbf503e9a55ad614af948fa30a04a64538e721d6 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 09:24:28 +0000 Subject: [PATCH 06/11] Studio: scope the confirm_code_execution stream requirement to code-execution tools The streaming requirement for confirm_code_execution now fires only when a local code-execution tool (python/terminal) could actually run, so a non-streaming request that enables only non-code tools (web_search, render_html, ...) is no longer rejected. This matches the documented behavior that confirm_code_execution leaves non-code tools unaffected. - Per-handler (GGUF and safetensors): gate on the resolved tool list intersecting python/terminal. - Pre-switch: gate on a payload-level predicate that mirrors _select_request_tools (built-ins off unless the tool loop is enabled; an explicit enabled_tools filter must list python/terminal). - External-provider and Anthropic server-tool rejections stay broad: the local confirm gate cannot apply there at all. Tests: predicate coverage for both the resolved and payload-level checks. --- studio/backend/routes/inference.py | 60 ++++++++++++++----- .../tests/test_confirm_code_execution.py | 51 ++++++++++++++++ 2 files changed, 96 insertions(+), 15 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index be9a4a34f3..1e06e76bee 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1763,6 +1763,32 @@ async def _select_request_tools( return tools +def _enables_code_execution_tool(tools: list[dict]) -> bool: + """True when a resolved tool list includes a local code-execution tool + (python/terminal). ``confirm_code_execution`` only gates those, so the + streaming requirement is scoped to requests that actually expose one.""" + 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 _payload_may_enable_code_execution(payload) -> bool: + """True when a request could resolve a local code-execution tool before the + tool list is built (used by the pre-switch guard). Mirrors + ``_select_request_tools``: built-ins are off unless the tool loop is enabled, + and an explicit ``enabled_tools`` filter must then list python/terminal + (MCP/client tools are never code execution).""" + from core.inference.tools import CODE_EXECUTION_TOOL_NAMES + + if not _effective_enable_tools(payload): + return False + if payload.enabled_tools is not None: + return bool(set(payload.enabled_tools) & CODE_EXECUTION_TOOL_NAMES) + return True + + 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 @@ -5772,26 +5798,22 @@ 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. + # Same pre-switch guard for confirm_code_execution, but scoped to requests + # that could actually run a local code-execution tool: the gate only + # applies to python/terminal, so a non-code tool request (e.g. web_search) + # must not be rejected, and a code-execution one 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) - ) + and _payload_may_enable_code_execution(payload) ): raise HTTPException( status_code = 400, detail = openai_error_body( - "confirm_code_execution requires stream=true for local tool execution.", + "confirm_code_execution requires stream=true when a " + "code-execution tool (python/terminal) is enabled.", status = 400, code = "invalid_request_error", param = "confirm_code_execution", @@ -6273,11 +6295,13 @@ 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 for local tool execution.", + "confirm_code_execution requires stream=true when a " + "code-execution tool (python/terminal) is enabled.", status = 400, code = "invalid_request_error", param = "confirm_code_execution", @@ -7009,11 +7033,17 @@ async def openai_chat_completions( param = "confirm_tool_calls", ), ) - if payload.confirm_code_execution and not payload.bypass_permissions and not payload.stream: + if ( + 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 for local tool execution.", + "confirm_code_execution requires stream=true when a " + "code-execution tool (python/terminal) is enabled.", status = 400, code = "invalid_request_error", param = "confirm_code_execution", diff --git a/studio/backend/tests/test_confirm_code_execution.py b/studio/backend/tests/test_confirm_code_execution.py index e6d41e5a25..18b23be5a8 100644 --- a/studio/backend/tests/test_confirm_code_execution.py +++ b/studio/backend/tests/test_confirm_code_execution.py @@ -179,3 +179,54 @@ 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 predicates used by the route streaming 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 + # A non-code tool list must not trip the streaming requirement. + 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 + + +def test_payload_may_enable_code_execution_predicate(monkeypatch): + import routes.inference as inf + + monkeypatch.setattr("state.tool_policy.get_tool_policy", lambda: None) + + class _P: + def __init__(self, enable_tools = None, enabled_tools = None): + self.enable_tools = enable_tools + self.enabled_tools = enabled_tools + + # Built-ins off -> never code execution, even if enabled_tools lists python. + assert inf._payload_may_enable_code_execution(_P(enable_tools = None)) is False + assert ( + inf._payload_may_enable_code_execution(_P(enable_tools = False, enabled_tools = ["python"])) + is False + ) + # Built-ins on, explicit filter without a code tool -> not code execution (the fix). + assert ( + inf._payload_may_enable_code_execution( + _P(enable_tools = True, enabled_tools = ["web_search"]) + ) + is False + ) + # Built-ins on, code tool in the filter -> code execution. + assert ( + inf._payload_may_enable_code_execution(_P(enable_tools = True, enabled_tools = ["terminal"])) + is True + ) + # Built-ins on, no filter -> all built-ins including python/terminal. + assert inf._payload_may_enable_code_execution(_P(enable_tools = True)) is True From 5e87eb714d58dabc04804e4d9d4e6e7b2031030a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:24:59 +0000 Subject: [PATCH 07/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- studio/backend/routes/inference.py | 1 - studio/backend/tests/test_confirm_code_execution.py | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 1e06e76bee..8eae0f7f3e 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1768,7 +1768,6 @@ def _enables_code_execution_tool(tools: list[dict]) -> bool: (python/terminal). ``confirm_code_execution`` only gates those, so the streaming requirement is scoped to requests that actually expose one.""" 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 []) ) diff --git a/studio/backend/tests/test_confirm_code_execution.py b/studio/backend/tests/test_confirm_code_execution.py index 18b23be5a8..f6776e53ab 100644 --- a/studio/backend/tests/test_confirm_code_execution.py +++ b/studio/backend/tests/test_confirm_code_execution.py @@ -206,7 +206,11 @@ def test_payload_may_enable_code_execution_predicate(monkeypatch): monkeypatch.setattr("state.tool_policy.get_tool_policy", lambda: None) class _P: - def __init__(self, enable_tools = None, enabled_tools = None): + def __init__( + self, + enable_tools = None, + enabled_tools = None, + ): self.enable_tools = enable_tools self.enabled_tools = enabled_tools @@ -218,9 +222,7 @@ def test_payload_may_enable_code_execution_predicate(monkeypatch): ) # Built-ins on, explicit filter without a code tool -> not code execution (the fix). assert ( - inf._payload_may_enable_code_execution( - _P(enable_tools = True, enabled_tools = ["web_search"]) - ) + inf._payload_may_enable_code_execution(_P(enable_tools = True, enabled_tools = ["web_search"])) is False ) # Built-ins on, code tool in the filter -> code execution. From 5237614e7a9f8d3254f72d020cc50d38f1be8df8 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 09:56:48 +0000 Subject: [PATCH 08/11] 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. --- studio/backend/routes/inference.py | 66 ++++++++++++------- .../backend/tests/test_anthropic_messages.py | 25 +++++-- .../tests/test_openai_tool_passthrough.py | 34 +++++++++- 3 files changed, 96 insertions(+), 29 deletions(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 8eae0f7f3e..9cda4417d7 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -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( diff --git a/studio/backend/tests/test_anthropic_messages.py b/studio/backend/tests/test_anthropic_messages.py index 1c265f7890..d1532eb245 100644 --- a/studio/backend/tests/test_anthropic_messages.py +++ b/studio/backend/tests/test_anthropic_messages.py @@ -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) diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index 7ca86984f4..0ef14ef752 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -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: From 718202c996b2a84b9b4a34740f9ac3eec7736ebc Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 10:37:30 +0000 Subject: [PATCH 09/11] Scope confirm_code_execution to local python/terminal only Make confirm_code_execution a purely local gate: it pauses only before local python/terminal execution and does not apply to code that runs in a provider sandbox. External-provider and Anthropic server-tool requests no longer reject when the flag is set; the flag is ignored there instead (the local gate cannot intercept server-side execution, and rejecting gave no added safety). This removes the pre-switch rejection blocks on both paths and the _anthropic_may_run_code_execution helper. Also skip the local streaming requirement when max_tool_calls_per_message is 0: with the tool budget disabled no code-execution tool can run, so a non-stream request must not be rejected. Add the budget check to both the pre-switch and per-handler GGUF guards. Update the field docs and tests: consolidate the two Anthropic server-tool tests into one asserting the flag is ignored, replace the external-provider rejection tests with a passthrough test, and add a GGUF budget-zero not-rejected test. --- studio/backend/models/inference.py | 2 +- studio/backend/routes/inference.py | 67 ++------------- .../backend/tests/test_anthropic_messages.py | 38 ++++----- .../tests/test_openai_tool_passthrough.py | 83 +++++++++++++------ 4 files changed, 81 insertions(+), 109 deletions(-) diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index d610a3e98a..db1d15387e 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -775,7 +775,7 @@ class ChatCompletionRequest(BaseModel): ) confirm_code_execution: Optional[bool] = Field( None, - description = "[x-unsloth] When true, pause only before local code-execution tool calls (python/terminal) and wait for the user to allow/deny each via POST /api/inference/tool-confirm; other tools (web_search, render_html, ...) still run without a prompt. Independent of confirm_tool_calls; requires stream=true when a code-execution tool is enabled; bypass_permissions still takes precedence.", + description = "[x-unsloth] When true, pause only before local code-execution tool calls (python/terminal) and wait for the user to allow/deny each via POST /api/inference/tool-confirm; other tools (web_search, render_html, ...) still run without a prompt. Applies to local python/terminal execution only, so it is ignored for external providers and Anthropic server tools (their code runs in the provider's sandbox, not locally). Independent of confirm_tool_calls; requires stream=true when a local code-execution tool is enabled; bypass_permissions still takes precedence.", ) bypass_permissions: Optional[bool] = Field( False, diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 9cda4417d7..035b50cb82 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1788,21 +1788,6 @@ 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 @@ -5713,30 +5698,9 @@ async def openai_chat_completions( param = "confirm_tool_calls", ), ) - # 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.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) - ) - ): - raise HTTPException( - status_code = 400, - detail = openai_error_body( - "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", - ), - ) + # confirm_code_execution guards only local python/terminal; an external + # provider runs any hosted code_execution in its own sandbox, so the flag + # simply does not apply here and is ignored (documented on the field). if _wants_multiple_choices(payload): _raise_unsupported_n("external provider chat completions") return await _proxy_to_external_provider(payload, request, current_subject) @@ -5822,6 +5786,7 @@ async def openai_chat_completions( payload.confirm_code_execution and not payload.bypass_permissions and not payload.stream + and payload.max_tool_calls_per_message != 0 and _payload_may_enable_code_execution(payload) ): raise HTTPException( @@ -6310,6 +6275,7 @@ async def openai_chat_completions( payload.confirm_code_execution and not payload.bypass_permissions and not payload.stream + and payload.max_tool_calls_per_message != 0 and _enables_code_execution_tool(tools_to_use) ): raise _reject( @@ -10261,26 +10227,9 @@ 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", - ), - ) + # confirm_code_execution guards only local python/terminal; Anthropic server + # tools (incl. hosted code execution) run server-side, so the flag does not + # apply here and is ignored (documented on the field). # 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 diff --git a/studio/backend/tests/test_anthropic_messages.py b/studio/backend/tests/test_anthropic_messages.py index d1532eb245..a0f55d9cb1 100644 --- a/studio/backend/tests/test_anthropic_messages.py +++ b/studio/backend/tests/test_anthropic_messages.py @@ -1718,31 +1718,23 @@ 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_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"}], - ) + def test_confirm_code_execution_ignored_for_server_tools(self, monkeypatch): + # confirm_code_execution guards only local python/terminal execution. + # Anthropic server tools run in the provider's sandbox, not locally, so + # the flag does not apply here: it is ignored (never rejects), whether + # the requested server tool is code execution or not. + for tool in ( + {"type": "python", "name": "python"}, + {"type": "web_search_20250305", "name": "web_search"}, + ): + backend = _mock_backend(monkeypatch) + payload = _basic_payload( + confirm_code_execution = True, + tools = [tool], + ) - 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"}], - ) - - _drive(anthropic_messages(payload, request = None, current_subject = "t")) - assert backend.calls[0][0] == "tools" + assert backend.calls[0][0] == "tools" def test_per_request_enable_tools_false_blocks_server_tool_alias(self, monkeypatch): backend = _mock_backend(monkeypatch) diff --git a/studio/backend/tests/test_openai_tool_passthrough.py b/studio/backend/tests/test_openai_tool_passthrough.py index 0ef14ef752..faec967c52 100644 --- a/studio/backend/tests/test_openai_tool_passthrough.py +++ b/studio/backend/tests/test_openai_tool_passthrough.py @@ -462,31 +462,10 @@ 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 "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. + def test_confirm_code_execution_ignored_for_provider_tools(self, monkeypatch): + # confirm_code_execution guards only local python/terminal. An external + # provider runs any hosted code_execution in its own sandbox, so the flag + # is ignored (not rejected) -- even when provider code_execution is enabled. import routes.inference as inference_route called = {"proxied": False} @@ -508,7 +487,7 @@ class TestChatCompletionRequestToolFields: "provider_type": "openai", "external_model": "gpt-4.1", "enable_tools": True, - "enabled_tools": ["web_search"], + "enabled_tools": ["code_execution"], "confirm_code_execution": True, }, ) @@ -1639,6 +1618,58 @@ class TestGgufVisionToolRouting: assert "confirm_code_execution requires stream=true" in entry["error"] assert monitor.active_count() == 0 + def test_confirm_code_execution_budget_zero_not_rejected_gguf(self, monkeypatch): + # max_tool_calls_per_message=0 disables tool execution (max_tool_iterations=0), + # so no code-execution tool can run and the stream requirement must not reject. + import routes.inference as inf_mod + + reached = {"tools": False} + + def _plain(**kwargs): + raise AssertionError("plain GGUF path should not be used") + + def _tools(**kwargs): + # Reaching here proves the request cleared both the pre-switch and the + # per-handler confirm_code_execution guards instead of being rejected. + reached["tools"] = True + raise RuntimeError("reached tool backend") + + backend = SimpleNamespace( + is_loaded = True, + is_vision = False, + supports_tools = True, + model_identifier = "test-gguf", + context_length = 4096, + generate_chat_completion = _plain, + generate_chat_completion_with_tools = _tools, + _maybe_recover_from_mtp_crash = lambda e: None, + ) + monkeypatch.setattr(inf_mod, "get_llama_cpp_backend", lambda: backend) + monkeypatch.setattr(inf_mod, "api_monitor", ApiMonitor(max_entries = 3)) + + payload = ChatCompletionRequest( + model = "default", + enable_tools = True, + enabled_tools = ["python"], + confirm_code_execution = True, + stream = False, + max_tool_calls_per_message = 0, + messages = [{"role": "user", "content": "run code"}], + ) + + with pytest.raises(HTTPException) as exc: + self._drive( + openai_chat_completions( + payload, + request = self._Request(), + current_subject = "test", + ) + ) + # Reaching the tool backend (and not a confirm_code_execution 400) proves the + # budget-zero request was not rejected by the stream requirement. + assert reached["tools"] is True + assert exc.value.status_code != 400 + def test_standard_gguf_stream_splits_reasoning_content(self, monkeypatch): def _generate(**_kwargs): yield " Date: Tue, 7 Jul 2026 11:03:42 +0000 Subject: [PATCH 10/11] Reject confirm_code_execution for local code tools on Anthropic path The Anthropic /v1/messages server-tool path maps a Studio tool alias such as {"type":"python"} to the local tool loop and runs python/terminal on the host. That path does not wire the confirmation prompt into its SSE translation (which is why confirm_tool_calls is already rejected there), so treating confirm_code_execution as ignored let local code run without the prompt the flag promises. Reject confirm_code_execution on this path when a local code-execution tool is actually selected, mirroring the confirm_tool_calls rejection. The check sits inside the server-tool branch and is gated on the resolved tool list, so a non-code request (e.g. web_search) is unaffected and a disabled request (enable_tools=false / --disable-tools) never reaches it. bypass_permissions still suppresses the gate. Update the field docs and split the Anthropic test into a code-tool rejection case and a non-code ignored case. --- studio/backend/models/inference.py | 2 +- studio/backend/routes/inference.py | 37 +++++++++++++++-- .../backend/tests/test_anthropic_messages.py | 41 ++++++++++++------- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index db1d15387e..9289385a0d 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -775,7 +775,7 @@ class ChatCompletionRequest(BaseModel): ) confirm_code_execution: Optional[bool] = Field( None, - description = "[x-unsloth] When true, pause only before local code-execution tool calls (python/terminal) and wait for the user to allow/deny each via POST /api/inference/tool-confirm; other tools (web_search, render_html, ...) still run without a prompt. Applies to local python/terminal execution only, so it is ignored for external providers and Anthropic server tools (their code runs in the provider's sandbox, not locally). Independent of confirm_tool_calls; requires stream=true when a local code-execution tool is enabled; bypass_permissions still takes precedence.", + description = "[x-unsloth] When true, pause only before local code-execution tool calls (python/terminal) and wait for the user to allow/deny each via POST /api/inference/tool-confirm; other tools (web_search, render_html, ...) still run without a prompt. Supported on the OpenAI-compatible local endpoints (/v1/chat/completions, /v1/responses). It is ignored for external providers (their hosted code runs in the provider's sandbox, not locally); on Anthropic /v1/messages it is rejected when a local code-execution tool is selected, since that path does not wire the confirmation prompt. Independent of confirm_tool_calls; requires stream=true when a local code-execution tool is enabled; bypass_permissions still takes precedence.", ) bypass_permissions: Optional[bool] = Field( False, diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 035b50cb82..970c11d8c4 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -10227,9 +10227,10 @@ async def anthropic_messages( ), ) - # confirm_code_execution guards only local python/terminal; Anthropic server - # tools (incl. hosted code execution) run server-side, so the flag does not - # apply here and is ignored (documented on the field). + # confirm_code_execution is handled below inside the server-tool branch (it is + # rejected when a local python/terminal tool is actually selected, mirroring + # confirm_tool_calls), so nothing to do pre-switch here: a non-code request is + # unaffected and a disabled request never enters that branch. # 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 @@ -10431,6 +10432,36 @@ async def anthropic_messages( payload.enabled_tools, ) + # confirm_code_execution guards local python/terminal execution. On this + # path a Studio tool alias like {"type":"python"} maps to the local tool + # loop and runs code on this host -- but the Anthropic Messages SSE + # translation does not wire the confirmation prompt (which is why + # confirm_tool_calls is rejected above). Silently ignoring the flag would + # run python/terminal without the promised prompt, so reject it when a + # local code-execution tool is actually selected. A non-code selection + # (e.g. web_search) is unaffected, and bypass_permissions suppresses the + # gate. Gated on server_tools above, so a disabled request never reaches + # here. + if ( + bool(getattr(payload, "confirm_code_execution", False)) + and not bool(getattr(payload, "bypass_permissions", False)) + and _enables_code_execution_tool(openai_tools) + ): + 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; it only guards local python/terminal execution on " + "the OpenAI-compatible endpoints (/v1/chat/completions).", + status = 400, + err_type = "invalid_request_error", + ), + ) + # Build tool-use system prompt nudge (same logic as /chat/completions) _nudge = _build_tool_action_nudge( tools = openai_tools, diff --git a/studio/backend/tests/test_anthropic_messages.py b/studio/backend/tests/test_anthropic_messages.py index a0f55d9cb1..cd64925494 100644 --- a/studio/backend/tests/test_anthropic_messages.py +++ b/studio/backend/tests/test_anthropic_messages.py @@ -1718,23 +1718,34 @@ class TestAnthropicMessagesToolRouting: assert "confirm_tool_calls is not supported" in exc.value.detail["error"]["message"] assert backend.calls == [] - def test_confirm_code_execution_ignored_for_server_tools(self, monkeypatch): - # confirm_code_execution guards only local python/terminal execution. - # Anthropic server tools run in the provider's sandbox, not locally, so - # the flag does not apply here: it is ignored (never rejects), whether - # the requested server tool is code execution or not. - for tool in ( - {"type": "python", "name": "python"}, - {"type": "web_search_20250305", "name": "web_search"}, - ): - backend = _mock_backend(monkeypatch) - payload = _basic_payload( - confirm_code_execution = True, - tools = [tool], - ) + def test_confirm_code_execution_rejected_for_code_server_tools(self, monkeypatch): + # A Studio {"type":"python"} alias runs the local python executor via the + # tool loop, but this path does not wire the confirmation prompt (like + # confirm_tool_calls above). Ignoring the flag would run code without the + # prompt, so it is rejected when a local code-execution tool is selected. + 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 backend.calls[0][0] == "tools" + assert exc.value.status_code == 400 + assert "confirm_code_execution is not supported" in exc.value.detail["error"]["message"] + assert backend.calls == [] + + def test_confirm_code_execution_ignored_for_non_code_server_tools(self, monkeypatch): + # web_search is not code execution, so the flag does not apply and the + # request proceeds normally rather than being rejected. + backend = _mock_backend(monkeypatch) + payload = _basic_payload( + confirm_code_execution = True, + tools = [{"type": "web_search_20250305", "name": "web_search"}], + ) + + _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) From 9d5e2f20d63327a8d6ae2e47287b49708e2e9e20 Mon Sep 17 00:00:00 2001 From: danielhanchen Date: Tue, 7 Jul 2026 11:09:33 +0000 Subject: [PATCH 11/11] Harden _enables_code_execution_tool against non-dict tool entries Verify each entry and its function field are dicts before reading the tool name, matching the defensive isinstance checks the payload.tools validation already uses. The callers pass resolved internal tool specs, so this is robustness rather than a reachable bug. --- studio/backend/routes/inference.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 970c11d8c4..9f92fabf84 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -1769,7 +1769,10 @@ def _enables_code_execution_tool(tools: list[dict]) -> bool: streaming requirement is scoped to requests that actually expose one.""" 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 []) + isinstance(t, dict) + and isinstance(t.get("function"), dict) + and t["function"].get("name") in CODE_EXECUTION_TOOL_NAMES + for t in (tools or []) )