diff --git a/studio/backend/core/inference/tools.py b/studio/backend/core/inference/tools.py index efcd712bc8..57da12cc13 100644 --- a/studio/backend/core/inference/tools.py +++ b/studio/backend/core/inference/tools.py @@ -2953,16 +2953,19 @@ WEB_SEARCH_TOOL = { # nonexistent /mnt/data or cd/grep-ing a guessed local path for a repo the # user only mentioned but never uploaded. _SANDBOX_PATHS_NOTE = ( - " The working directory is an isolated scratch space that starts empty " - "except for files created here or explicitly uploaded to this " - "conversation, and it persists only for this conversation. This sandbox " - "cannot reach other machines or remote hosts. A repository, folder, or " - "file the user refers to is not present here unless it was uploaded or " - "created in the sandbox, so do not assume a mentioned path exists or guess " - "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." + " 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. It can reach only 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. 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." ) PYTHON_TOOL = { diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index da5b2fa218..1aaf20eeaf 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -2405,11 +2405,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 and " - "that cannot reach other machines or remote hosts, so do not assume a file, " - "folder, or repository the user mentions is already present. If it has not " - "been uploaded to this conversation, ask the user to upload it or give an " - "exact path rather than running commands against a guessed one." + "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." ) _TOOL_ARTIFACT_TIP = ( "For HTML, CSS, or JavaScript canvas requests, call render_html once when " diff --git a/studio/backend/tests/test_sandbox_paths_note.py b/studio/backend/tests/test_sandbox_paths_note.py new file mode 100644 index 0000000000..40e31e184e --- /dev/null +++ b/studio/backend/tests/test_sandbox_paths_note.py @@ -0,0 +1,50 @@ +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +"""Accuracy guards for the sandbox-paths note appended to the python/terminal +tool descriptions (#7242). + +The note must not misdirect the model with claims that are false for the real +sandbox: (1) the sandbox is not network-isolated -- the python tool can reach an +allowlist of public hosts (github.com / huggingface.co / pypi.org), so it must +not claim it "cannot reach ... remote hosts" at all; (2) a project sandbox is a +shared per-project directory, so a new thread can inherit files from prior +threads -- the note must not claim the workdir "starts empty" or "persists only +for this conversation". +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +_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 + + +def test_note_does_not_claim_full_network_isolation(): + lowered = _SANDBOX_PATHS_NOTE.lower() + # The old absolute claim is false for the python tool (allowlisted egress). + assert "cannot reach other machines or remote hosts" not in lowered + # It should instead scope the block to private/arbitrary hosts and name the + # allowlist so the model still fetches valid public URLs. + assert "allowlist" in lowered + assert "github.com" in lowered and "huggingface.co" in lowered + assert "arbitrary" in lowered or "private" in lowered + + +def test_note_does_not_claim_project_sandbox_starts_empty(): + lowered = _SANDBOX_PATHS_NOTE.lower() + # Project sandboxes are shared across threads, so these are inaccurate. + assert "starts empty" not in lowered + assert "persists only for this conversation" not in lowered + # It should acknowledge that project files may already be present. + assert "project" in lowered + + +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)