[bug] agent image: stop baking VIRTUAL_ENV=/app/.venv (silences uv run warning)

The agent-base image baked VIRTUAL_ENV=/app/.venv globally (since 8e201901,
conventional venv-baking, no targeted rationale), so every bare uv run in a
workspace clone warned 'VIRTUAL_ENV=/app/.venv does not match project .venv
and will be ignored' on every gate run. uv used the workspace .venv correctly
(the warning was noise), but it flooded every agent transcript.

The load-bearing MCP/SDK pin is UV_PROJECT_ENVIRONMENT=/app/.venv + --no-sync
(added in #179, the actual startup-stall fix), NOT VIRTUAL_ENV — so removing
VIRTUAL_ENV does not regress #179. The gateway tools stay on PATH.

Verified by building the image (roboco-agent-base:venv-verify) and smoke-
testing in a real container: (1) VIRTUAL_ENV is empty, python resolves to
/app/.venv/bin/python via PATH; (2) bare uv run from /app imports a roboco
module with no warning; (3) the MCP-launch path — cwd with its OWN .venv —
UV_PROJECT_ENVIRONMENT=/app/.venv uv run --no-sync resolves to /app/.venv/
bin/python, overriding the cwd .venv (the exact MCP-server invariant), no
warning; (4) grep for 'does not match|will be ignored' across all output is
empty. bash-guard-tests.sh 64/0, ruff clean, test_workspace_uv_resolves_clone_venv 3/3.

Ships on next agent-image rebuild + redeploy.
This commit is contained in:
Renn F
2026-06-30 09:58:36 +02:00
parent 0051339915
commit 7be10057d0
4 changed files with 14 additions and 12 deletions
+4 -1
View File
@@ -95,8 +95,11 @@ RUN echo '{}' > /home/agent/.claude.json
# PYTHONUNBUFFERED: flush stdout/stderr immediately so the SDK driver's logs
# (e.g. the intake agent's turn-received / streamed lines) reach `docker logs` in
# real time instead of block-buffering until the container is reaped.
# VIRTUAL_ENV is intentionally NOT baked: it made bare `uv run` in a workspace
# clone warn ("VIRTUAL_ENV=/app/.venv does not match project .venv") on every
# gate run. MCP/SDK pin to /app/.venv via UV_PROJECT_ENVIRONMENT (set in the
# orchestrator + sdk-startup-hook), not VIRTUAL_ENV. PATH keeps gateway tools.
ENV PATH="/app/.venv/bin:$PATH" \
VIRTUAL_ENV=/app/.venv \
PYTHONUNBUFFERED=1
# Claude Code uses mounted ~/.claude for auth.
+5 -6
View File
@@ -338,15 +338,14 @@ if echo "$low" | grep -qE '(^|[[:space:];&|])(uv[[:space:]]+(sync|lock|add|remov
exit 2
fi
# `uv run` retargeting onto /app/.venv — same brick as the package-mutation
# block above, via a verb that block doesn't list. In the agent container
# VIRTUAL_ENV=/app/.venv is baked globally, so `uv run --active` ALWAYS
# resolves onto /app/.venv and uv rebuilds it (be-dev-1 root cause,
# 2026-06-29). Also catch `uv run`/`uvx` with an explicit /app target.
# `uv run --active` is denied as a footgun: the contract is bare `uv run`
# (workspace .venv, cwd-relative). VIRTUAL_ENV is no longer image-baked (it
# leaked into every workspace `uv run` as a warning), so --active has no active
# env and errors; an explicit /app target still bricks the gateway (next block).
# Bare `uv run` (workspace .venv, cwd-relative) is untouched.
if echo "$low" | grep -qE '(^|[[:space:];&|])uv[[:space:]]+run([[:space:]]|$)' && \
echo "$low" | grep -qE '(^|[[:space:]=])--active([[:space:]]|$)'; then
echo "Denied: \`uv run --active\` retargets onto VIRTUAL_ENV=/app/.venv (the image-baked MCP-gateway venv) and rebuilds it, bricking your own gateway tools. Use bare \`uv run\` (it uses your workspace .venv under /data/workspaces, never /app). If /app's environment looks broken, report it via your blocked / escalation verb." >&2
echo "Denied: \`uv run --active\` is not the contract — use bare \`uv run\` (it uses your workspace .venv under /data/workspaces, never /app). If /app's environment looks broken, report it via your blocked / escalation verb." >&2
exit 2
fi
if echo "$low" | grep -qE '(^|[[:space:];&|])(uv[[:space:]]+run|uvx)([[:space:]]|$)' && \
+3 -2
View File
@@ -100,8 +100,9 @@ run_case "deny rm -rf /etc" 2 "rm -rf /etc"
run_case "deny uv sync --project /app" 2 "uv sync --project /app"
run_case "deny uv pip install /app venv" 2 "uv pip install --python /app/.venv/bin/python foo"
run_case "deny cd /app && uv sync" 2 "cd /app && uv sync"
# uv run --active: in the agent VIRTUAL_ENV=/app/.venv is baked globally, so
# --active ALWAYS retargets onto /app/.venv → uv rebuilds it → bricked gateway.
# uv run --active: not the contract (bare `uv run` uses the workspace .venv).
# VIRTUAL_ENV is no longer image-baked, so --active has no active env and
# errors; the guard still denies it with a clear remediation message.
run_case "deny uv run --active" 2 "uv run --active pytest"
run_case "deny uv run --active ruff" 2 "uv run --active ruff check ."
run_case "deny env venv /app uv run active" 2 "VIRTUAL_ENV=/app/.venv uv run --active pytest"