mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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}..."
|
||||
|
||||
@@ -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.<server>` 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
|
||||
|
||||
@@ -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']}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user