fix(agent): pin uv to baked /app/.venv so MCP/SDK servers start instantly (#179)

Every agent MCP server is launched as `uv run python -m roboco.mcp.<server>`
(via the orchestrator-generated mcp-config.json) and the SDK server via
`uv run python -m roboco.agent_sdk.server` (sdk-startup-hook.sh) — both with
cwd = the agent's WORKSPACE, not /app. `uv run` then resolves a cwd-relative
`.venv` (≠ the image's 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 every spawn.

A warm host uv wheel cache masks this (fast re-resolve from cached wheels —
earlier runs this session opened PR #26/#28/#29 fine). On a COLD cache (first
spawn after an image rebuild — exactly when deploying new fixes) the download
takes minutes, the MCP servers never register, and the agent burns its whole
budget with "No such tool available: mcp__roboco-*" before reaping. Observed
this session: be-dev-1 never claimed; /tmp/sdk-server.log showed the live
torch/lancedb download + the `VIRTUAL_ENV ... will be ignored` warning.

Fix: set UV_PROJECT_ENVIRONMENT=/app/.venv in (1) every MCP server's env in
the generated mcp-config.json (one place — shared mcp_env dict) and (2) the
SDK startup hook. uv then reuses the pre-baked image venv instantly,
regardless of cwd or cache state. Not a regression from this session's code
(none of #172b/#175/#176/#177/#178 touched the launch/venv path — verified);
a pre-existing launch-cwd fragility that rebuilding to deploy exposed.

Test: _generate_mcp_config asserts every server env pins
UV_PROJECT_ENVIRONMENT=/app/.venv. make quality green.
This commit is contained in:
Renn F
2026-05-23 01:23:18 +02:00
parent 879b8cb991
commit 3742483e1c
3 changed files with 49 additions and 0 deletions
@@ -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']}"
)