diff --git a/docker/scripts/sdk-startup-hook.sh b/docker/scripts/sdk-startup-hook.sh index 222042c1..c5ffc5bd 100644 --- a/docker/scripts/sdk-startup-hook.sh +++ b/docker/scripts/sdk-startup-hook.sh @@ -12,6 +12,17 @@ LOG_FILE="/tmp/sdk-server.log" BRIEFING_FILE="/app/briefing.md" PRECOMPACT_FILE="/tmp/roboco-precompact-${AGENT_ID}.md" +# #179: this hook runs with cwd = the agent's workspace, so a bare +# `uv run` resolves a cwd-relative `.venv` (≠ the baked /app/.venv), +# ignores VIRTUAL_ENV with a warning, and RE-SYNCS the full dependency +# set (torch/lancedb/pyarrow/scipy, ~350MB) into a fresh venv. On a cold +# uv wheel cache (first spawn after an image rebuild) that download takes +# minutes and the SDK/MCP layer never comes up before the agent reaps. +# Pin uv to the pre-baked image venv so it starts instantly regardless +# of cwd. (The orchestrator sets the same var in every MCP server's env +# in the generated mcp-config.json — keep both in sync.) +export UV_PROJECT_ENVIRONMENT=/app/.venv + # --- SDK bring-up --------------------------------------------------------- if ! curl -sf "http://localhost:${SDK_PORT}/health" >/dev/null 2>&1; then echo "[SDK] Starting for agent ${AGENT_ID} on port ${SDK_PORT}..." diff --git a/roboco/runtime/orchestrator.py b/roboco/runtime/orchestrator.py index 0996cfa9..d82af0f8 100644 --- a/roboco/runtime/orchestrator.py +++ b/roboco/runtime/orchestrator.py @@ -1878,6 +1878,19 @@ class AgentOrchestrator: "ROBOCO_ORCHESTRATOR_URL": api_url, "ROBOCO_AGENT_ID": agent_uuid, "ROBOCO_AGENT_ROLE": agent_role, + # #179: every MCP server is launched as `uv run python -m + # roboco.mcp.` by Claude Code, with cwd = the agent's + # WORKSPACE (not /app). Without this, `uv run` resolves a + # cwd-relative `.venv` (≠ the baked /app/.venv), ignores the + # image's VIRTUAL_ENV with a warning, and RE-SYNCS the full + # dependency set (torch/lancedb/pyarrow/scipy, ~350MB) into a + # fresh venv on every spawn — masked by a warm uv wheel cache, + # but on a cold cache (first spawn after an image rebuild) the + # download takes minutes and the MCP servers never come up + # before the agent burns its budget. Pinning the project env + # to the pre-baked venv makes `uv run` reuse it instantly, + # regardless of cwd. + "UV_PROJECT_ENVIRONMENT": "/app/.venv", } # Add git context if available diff --git a/tests/unit/runtime/test_spawn_strict_mcp.py b/tests/unit/runtime/test_spawn_strict_mcp.py index 0645279e..aee8cb3a 100644 --- a/tests/unit/runtime/test_spawn_strict_mcp.py +++ b/tests/unit/runtime/test_spawn_strict_mcp.py @@ -9,9 +9,11 @@ etc. The flag tells the CLI to load ONLY the servers from --mcp-config. from __future__ import annotations +import json from pathlib import Path from unittest.mock import patch +import pytest from roboco.models.runtime import OrchestratorAgentConfig, SpawnGitContext from roboco.runtime.orchestrator import AgentOrchestrator @@ -67,3 +69,26 @@ class TestSpawnStrictMcpConfig: f"--strict-mcp-config should appear near --mcp-config; " f"strict_idx={strict_idx}, mcp_idx={mcp_idx}. Cmd: {cmd}" ) + + +class TestMcpConfigPinsBakedVenv: + """#179: every generated MCP server launch must pin uv to the baked + image venv so `uv run` (cwd = workspace) reuses /app/.venv instead of + re-syncing the full dependency set (~350MB) on every spawn.""" + + @pytest.mark.asyncio + async def test_every_mcp_server_env_pins_uv_project_environment(self) -> None: + orch = AgentOrchestrator.__new__(AgentOrchestrator) + # be-dev-1 is a known agent (resolves role + uuid); generation is + # otherwise pure (writes a json file and returns its path). + config_path = await orch._generate_mcp_config("be-dev-1") + config = json.loads(Path(config_path).read_text()) + servers = config["mcpServers"] + assert servers, "expected at least the four core MCP servers" + for name, spec in servers.items(): + assert spec["command"] == "uv", f"{name} should launch via uv" + assert spec["env"].get("UV_PROJECT_ENVIRONMENT") == "/app/.venv", ( + f"MCP server {name!r} is missing UV_PROJECT_ENVIRONMENT=" + f"/app/.venv — without it `uv run` re-downloads deps into a " + f"cwd-relative venv on every spawn (#179). env={spec['env']}" + )