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.
This commit is contained in:
parent
5237614e7a
commit
718202c996
4 changed files with 81 additions and 109 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 "<thi"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue