diff --git a/studio/backend/core/inference/tools.py b/studio/backend/core/inference/tools.py index cfb2fd1a6c..ddf8d450e9 100644 --- a/studio/backend/core/inference/tools.py +++ b/studio/backend/core/inference/tools.py @@ -2955,18 +2955,21 @@ WEB_SEARCH_TOOL = { _SANDBOX_PATHS_NOTE = ( " The working directory is an isolated scratch space that may already hold " "files from earlier work in this conversation or project, plus anything you " - "create here or that is uploaded; it persists across this conversation and, " - "for a project, across the project's threads. Code execution can fetch from a " - "fixed allowlist of public sites (such as github.com, huggingface.co, and " - "pypi.org); shell network commands are blocked, and neither can reach the " - "user's own machines, private hosts, or arbitrary addresses. A repository, " - "folder, or file the user refers to is not present " - "here unless it was uploaded, created here, or already part of this project, " - "so list the working directory to see what is available rather than assuming " - "a mentioned path exists or guessing where it lives. Read and write files " - "using relative paths in the working directory; absolute paths like " - "/mnt/data or /tmp/outputs do not exist. If the files you need are not here, " - "ask the user to upload them or provide an exact path instead of guessing one." + "create here; it persists across this conversation and, for a project, " + "across the project's threads. It cannot see the user's own computer or the " + "files stored on it. Internet access is limited: the python tool can fetch " + "only from a fixed allowlist of public sites (such as github.com, " + "huggingface.co, and pypi.org), not the user's own machines, private hosts, " + "or arbitrary addresses, and the terminal blocks direct download commands " + "like curl and wget. Documents the user attaches to the chat are retrieved " + "separately and are not listed here. A repository, folder, or file the user " + "refers to is not present here unless you created it here or it is already " + "part of this project, so list the working directory to see what is " + "available rather than assuming a mentioned path exists or guessing where it " + "lives. Read and write files using relative paths in the working directory; " + "absolute paths like /mnt/data or /tmp/outputs do not exist. If the files " + "you need are not here, ask the user to provide them or an exact path " + "instead of guessing one." ) PYTHON_TOOL = { @@ -5849,11 +5852,12 @@ def _bash_exec( base = f"Blocked command(s) for safety: {', '.join(sorted(blocked))}." if blocked & _NETWORK_BLOCKED_COMMANDS: return ( - base + " This sandbox cannot reach other machines or remote " - "hosts over the network, so files that live on another " - "machine are not accessible from here. Do not retry with " - "other remote-access commands; ask the user to upload the " - "files they want you to work with." + base + " Direct network commands are blocked in the " + "terminal. Public files on allowlisted sites (such as " + "github.com, huggingface.co, or pypi.org) can be downloaded " + "from Python code instead; the user's own machine and other " + "private or arbitrary hosts are not reachable, so for those " + "ask the user to upload the files they want you to work with." ) return base elif not _harden_parent_against_proc_env_leak(): diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index 1aaf20eeaf..6a8385bfe9 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -2404,14 +2404,13 @@ _TOOL_WEB_EXPANDED_TIP = ( ) _TOOL_CODE_TIP = ( "Use code execution for math, calculations, data processing, or to parse " - "and analyze information from tool results. The python and terminal tools " - "run in a sandbox whose working directory is an isolated scratch space that " - "may already hold files from earlier work and can reach only a fixed " - "allowlist of public sites, not the user's own machines or private hosts, so " - "do not assume a file, folder, or repository the user mentions is already " - "present. List the working directory to see what is there; if what you need " - "is not present, ask the user to upload it or give an exact path rather than " - "running commands against a guessed one." + "and analyze information from tool results. It runs in a sandbox whose " + "working directory is an isolated scratch space that may already hold files " + "from earlier work; it cannot access the user's own computer, and its " + "internet access is limited, so do not assume a file, folder, or repository " + "the user mentions is already present. List the working directory to see " + "what is there; if what you need is not present, ask the user to provide it " + "or give an exact path rather than running commands against a guessed one." ) _TOOL_ARTIFACT_TIP = ( "For HTML, CSS, or JavaScript canvas requests, call render_html once when " diff --git a/studio/backend/tests/test_anthropic_messages.py b/studio/backend/tests/test_anthropic_messages.py index 9ccc3f44dd..f53dbbd7fb 100644 --- a/studio/backend/tests/test_anthropic_messages.py +++ b/studio/backend/tests/test_anthropic_messages.py @@ -173,6 +173,25 @@ class TestToolActionNudge: def test_balanced_nudge_empty_without_known_tool_categories(self): assert _build_tool_action_nudge(tools = [], model_name = "Llama-3.1-8B-Instruct") == "" + def test_code_tip_does_not_name_a_disabled_sibling_code_tool(self): + # has_code fires when either python or terminal is present, so the code tip + # must stay tool-neutral: a request that enabled only one must never name + # the other as an available tool (the missing-tool hallucination the + # render_html gate already avoids). + py_only = _build_tool_action_nudge( + tools = [{"type": "function", "function": {"name": "python"}}], + model_name = "Llama-3.1-8B-Instruct", + ) + assert "Use code execution for math" in py_only + assert "terminal" not in py_only.lower() + + term_only = _build_tool_action_nudge( + tools = [{"type": "function", "function": {"name": "terminal"}}], + model_name = "Llama-3.1-8B-Instruct", + ) + assert "Use code execution for math" in term_only + assert "python" not in term_only.lower() + # ===================================================================== # Pydantic model tests diff --git a/studio/backend/tests/test_sandbox_paths_note.py b/studio/backend/tests/test_sandbox_paths_note.py index 40e31e184e..0026dfae40 100644 --- a/studio/backend/tests/test_sandbox_paths_note.py +++ b/studio/backend/tests/test_sandbox_paths_note.py @@ -22,7 +22,12 @@ _BACKEND_DIR = str(Path(__file__).resolve().parent.parent) if _BACKEND_DIR not in sys.path: sys.path.insert(0, _BACKEND_DIR) -from core.inference.tools import PYTHON_TOOL, TERMINAL_TOOL, _SANDBOX_PATHS_NOTE +from core.inference.tools import ( + PYTHON_TOOL, + TERMINAL_TOOL, + _SANDBOX_PATHS_NOTE, + _bash_exec, +) def test_note_does_not_claim_full_network_isolation(): @@ -48,3 +53,34 @@ def test_note_does_not_claim_project_sandbox_starts_empty(): def test_note_is_appended_to_both_tool_descriptions(): assert PYTHON_TOOL["function"]["description"].endswith(_SANDBOX_PATHS_NOTE) assert TERMINAL_TOOL["function"]["description"].endswith(_SANDBOX_PATHS_NOTE) + + +def test_note_scopes_network_block_to_the_terminal_not_all_shell_commands(): + # The terminal leaves the network namespace intact and does not block git/pip, + # so the note must not claim shell network is fully blocked; it names the + # commands that are blocked (curl / wget) and attributes the host allowlist to + # the python tool. + lowered = _SANDBOX_PATHS_NOTE.lower() + assert "shell network commands are blocked" not in lowered + assert "curl" in lowered and "wget" in lowered + assert "python tool can fetch" in lowered + + +def test_note_distinguishes_attachments_from_sandbox_uploads(): + # Chat/project document uploads land in the RAG store, not the sandbox workdir, + # so the note must not tell the model that uploaded files appear via `ls`. + lowered = _SANDBOX_PATHS_NOTE.lower() + assert "was uploaded" not in lowered + assert "or that is uploaded" not in lowered + assert "retrieved separately" in lowered + assert "attach" in lowered + + +def test_blocked_network_command_message_points_to_python_egress(): + # A blocked curl/wget must not tell the model the whole sandbox is offline: the + # python tool can still fetch allowlisted public hosts. + msg = _bash_exec("curl https://raw.githubusercontent.com/foo/bar/main/x.py").lower() + assert "blocked command" in msg + assert "cannot reach other machines or remote hosts" not in msg + assert "python" in msg + assert "github.com" in msg