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.
This commit is contained in:
parent
0595b62f9b
commit
dbf503e9a5
2 changed files with 96 additions and 15 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue