diff --git a/tests/test_v1_harbor_cli.py b/tests/test_v1_harbor_cli.py index d996ccc4c..d02544aa8 100644 --- a/tests/test_v1_harbor_cli.py +++ b/tests/test_v1_harbor_cli.py @@ -22,6 +22,7 @@ terminus_2_agent_script, ) from verifiers.v1.packages.tasksets.harbor import harbor_reward +from verifiers.v1.utils.mcp_proxy_utils import MCP_PROXY_PYTHON_PATH from verifiers.v1.utils.program_utils import merge_task_program, merge_task_sandbox @@ -304,7 +305,10 @@ def test_pi_harness_writes_intercepted_model_and_mcp_config() -> None: assert provider["api"] == "openai-completions" assert provider["apiKey"] == "secret" assert provider["models"] == [{"id": "model", "name": "openai/gpt-5.4-mini"}] - assert mcp["mcpServers"]["verifiers-tools"]["command"] == "python3" + server = mcp["mcpServers"]["verifiers-tools"] + assert server["command"] == "/bin/sh" + assert server["args"][0] == "-lc" + assert MCP_PROXY_PYTHON_PATH in server["args"][1] def test_terminus_2_harness_builds_sandbox_program() -> None: diff --git a/tests/test_v1_runtime_lifecycle.py b/tests/test_v1_runtime_lifecycle.py index 04065d569..11e6109bb 100644 --- a/tests/test_v1_runtime_lifecycle.py +++ b/tests/test_v1_runtime_lifecycle.py @@ -22,6 +22,7 @@ from verifiers.v1.utils.endpoint_utils import endpoint_api_key from verifiers.v1.utils import mcp_utils from verifiers.v1.utils.mcp_proxy_utils import MCP_PROXY_CONFIG_PATH, MCP_PROXY_PATH +from verifiers.v1.utils.mcp_proxy_utils import MCP_PROXY_PYTHON_PATH from verifiers.v1.utils.mcp_proxy_utils import proxy_command, proxy_source from verifiers.v1.utils.program_utils import command_env from verifiers.v1.utils.sandbox_program_utils import ( @@ -38,6 +39,7 @@ from verifiers.v1.utils.sandbox_utils import ( VF_STATE_INPUT_PATH_KEY, collect_sandbox_artifacts, + python_package_install_command, run_sandbox_command, ) @@ -1019,6 +1021,30 @@ def test_sandbox_fn_program_installs_local_package( assert command[2].endswith(" /tmp/vf_program_runner.py fn local_program:run") +def test_python_package_install_command_ignores_image_pip_config() -> None: + command = python_package_install_command("'mcp>=1.14.1' requests") + + assert "export PIP_CONFIG_FILE=/dev/null" in command + assert ( + "unset PIP_INDEX_URL PIP_EXTRA_INDEX_URL PIP_FIND_LINKS PIP_NO_INDEX " + "PIP_REQUIRE_VIRTUALENV" + ) in command + assert "UV_TOOL_DIR=/tmp/vf-tools" in command + assert "UV_DEFAULT_INDEX=https://pypi.org/simple" in command + assert "uv --no-config tool install" in command + assert "--python 3.11 --with requests 'mcp>=1.14.1'" in command + assert "$UV_TOOL_DIR/mcp/bin/python" in command + assert MCP_PROXY_PYTHON_PATH in command + assert ( + "pip install --disable-pip-version-check --break-system-packages requests" + in command + ) + assert ( + "pip install --disable-pip-version-check --break-system-packages 'mcp>=1.14.1'" + not in command + ) + + def test_sandbox_fn_program_resolves_local_module_package( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -1208,7 +1234,11 @@ def test_program_channels_mcp_injects_proxy_into_sandbox_program() -> None: "tool_base_url": "http://127.0.0.1:1/rollout/test/vf/tools", "tool_api_key": harness.endpoint.secret, } - assert proxy_command() == ["python3", MCP_PROXY_PATH, MCP_PROXY_CONFIG_PATH] + command = proxy_command() + assert command[:2] == ["/bin/sh", "-lc"] + assert MCP_PROXY_PYTHON_PATH in command[2] + assert MCP_PROXY_PATH in command[2] + assert MCP_PROXY_CONFIG_PATH in command[2] packages = sandbox["packages"] assert isinstance(packages, list) assert "mcp>=1.14.1" in packages @@ -1417,6 +1447,7 @@ async def write_real_sandbox_file(text: str, sandbox, state) -> str: REAL_MCP_PROXY_SCRIPT = r""" import asyncio import json +import sys from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client @@ -1424,7 +1455,7 @@ async def write_real_sandbox_file(text: str, sandbox, state) -> str: async def main(): server = StdioServerParameters( - command="python3", + command=sys.executable, args=["/tmp/vf_mcp_tools.py", "/tmp/vf_mcp_tools.json"], ) async with stdio_client(server) as (read_stream, write_stream): @@ -1505,7 +1536,11 @@ async def test_real_sandbox_command_program_uses_mcp_tool_proxy() -> None: harness = make_harness( program={ "sandbox": True, - "command": ["python", "/tmp/call_mcp.py"], + "command": [ + "/bin/sh", + "-lc", + f'exec "$(cat {MCP_PROXY_PYTHON_PATH})" /tmp/call_mcp.py', + ], "channels": "mcp", "files": {"/tmp/call_mcp.py": REAL_MCP_PROXY_SCRIPT}, }, diff --git a/verifiers/v1/utils/mcp_proxy_utils.py b/verifiers/v1/utils/mcp_proxy_utils.py index 885774bca..10c364d78 100644 --- a/verifiers/v1/utils/mcp_proxy_utils.py +++ b/verifiers/v1/utils/mcp_proxy_utils.py @@ -7,6 +7,7 @@ MCP_PROXY_PATH = "/tmp/vf_mcp_tools.py" MCP_PROXY_CONFIG_PATH = "/tmp/vf_mcp_tools.json" +MCP_PROXY_PYTHON_PATH = "/tmp/vf_mcp_python" MCP_PACKAGE = "mcp>=1.14.1" REQUESTS_PACKAGE = "requests" @@ -69,7 +70,13 @@ def proxy_program( def proxy_command() -> list[str]: - return ["python3", MCP_PROXY_PATH, MCP_PROXY_CONFIG_PATH] + command = ( + f"PYTHON=$(cat {shlex.quote(MCP_PROXY_PYTHON_PATH)} 2>/dev/null || " + "command -v python3); " + f'exec "$PYTHON" {shlex.quote(MCP_PROXY_PATH)} ' + f"{shlex.quote(MCP_PROXY_CONFIG_PATH)}" + ) + return ["/bin/sh", "-lc", command] def proxy_sandbox(sandbox_config: ConfigMap) -> ConfigData: diff --git a/verifiers/v1/utils/sandbox_utils.py b/verifiers/v1/utils/sandbox_utils.py index 8124bea65..9e6d8e9b8 100644 --- a/verifiers/v1/utils/sandbox_utils.py +++ b/verifiers/v1/utils/sandbox_utils.py @@ -16,6 +16,7 @@ from .artifact_utils import artifact_format, artifact_key, artifact_optional from .artifact_utils import artifact_path +from .mcp_proxy_utils import MCP_PROXY_PYTHON_PATH from .program_utils import command_argv, command_env, float_config, int_config from .program_utils import program_option_mapping, program_channel_setup from .program_utils import resolve_program_value @@ -511,8 +512,45 @@ def sandbox_scope(sandbox_config: ConfigMap) -> str: def python_package_install_command(package_args: str) -> str: + packages = shlex.split(package_args) + pip_packages = [package for package in packages if not package.startswith("mcp")] + pip_package_args = " ".join(shlex.quote(package) for package in pip_packages) + mcp_python_setup = "" + if any(package.startswith("mcp") for package in packages): + mcp_packages = [package for package in packages if package.startswith("mcp")] + mcp_package_args = " ".join(shlex.quote(package) for package in mcp_packages) + requests_packages = [ + package for package in packages if package.startswith("requests") + ] + requests_package = requests_packages[0] if requests_packages else "requests" + mcp_python_setup = ( + "VF_UV_SITE_PACKAGES=/tmp/vf-uv-site-packages\n" + "export UV_TOOL_DIR=/tmp/vf-tools\n" + "export UV_PYTHON_INSTALL_DIR=/tmp/vf-python\n" + "export UV_CACHE_DIR=/tmp/vf-uv-cache\n" + "export UV_DEFAULT_INDEX=https://pypi.org/simple\n" + "unset UV_INDEX UV_INDEX_URL UV_EXTRA_INDEX_URL UV_FIND_LINKS UV_NO_INDEX UV_CONFIG_FILE\n" + 'mkdir -p "$VF_UV_SITE_PACKAGES" "$UV_TOOL_DIR" "$UV_PYTHON_INSTALL_DIR" "$UV_CACHE_DIR"\n' + '"$PYTHON" -m pip install --disable-pip-version-check --break-system-packages ' + ' --target "$VF_UV_SITE_PACKAGES" uv==0.11.7 || ' + '"$PYTHON" -m pip install --disable-pip-version-check ' + ' --target "$VF_UV_SITE_PACKAGES" uv==0.11.7\n' + 'env PYTHONPATH="$VF_UV_SITE_PACKAGES" "$PYTHON" -m uv --no-config tool install ' + f"--python 3.11 --with {shlex.quote(requests_package)} {mcp_package_args}\n" + f'printf "%s\\n" "$UV_TOOL_DIR/mcp/bin/python" > {shlex.quote(MCP_PROXY_PYTHON_PATH)}\n' + ) + pip_install = "" + if pip_package_args: + pip_install = ( + "$PYTHON -m pip install --disable-pip-version-check --break-system-packages " + f"{pip_package_args} || " + "$PYTHON -m pip install --disable-pip-version-check " + f"{pip_package_args}" + ) return ( "set -e\n" + "export PIP_CONFIG_FILE=/dev/null\n" + "unset PIP_INDEX_URL PIP_EXTRA_INDEX_URL PIP_FIND_LINKS PIP_NO_INDEX PIP_REQUIRE_VIRTUALENV\n" "if command -v python3 >/dev/null 2>&1; then PYTHON=python3; " "elif command -v python >/dev/null 2>&1; then PYTHON=python; " "elif command -v apt-get >/dev/null 2>&1; then " @@ -525,10 +563,8 @@ def python_package_install_command(package_args: str) -> str: "(command -v apt-get >/dev/null 2>&1 && " "apt-get -o Acquire::Retries=3 update && " "apt-get -o Acquire::Retries=3 install -y python3-pip)\n" - "$PYTHON -m pip install --disable-pip-version-check --break-system-packages " - f"{package_args} || " - "$PYTHON -m pip install --disable-pip-version-check " - f"{package_args}" + f"{mcp_python_setup}" + f"{pip_install}" )