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:
parent
772ae0146d
commit
5f5eaef386
3 changed files with 72 additions and 34 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue