fix(bash-guard): block package-env mutations targeting /app (all providers)

An agent that ran `uv sync` / `pip install` against /app rebuilt the
orchestrator's MCP-gateway venv (/app/.venv) — breaking its own gateway tools
(every roboco-flow/-do/-git verb), stranding it, and getting its task reaped.
Deny package-mutation verbs (uv sync/lock/add/remove, uv pip, pip install)
whose target resolves to /app (cd /app, --project/--directory /app, /app/.venv,
UV_PROJECT_ENVIRONMENT=/app). Placed outside the ROBOCO_GUARD_SKIP_GIT gate so
it fires for every provider — the Claude PreToolUse hook and the grok exfil
hook alike. Reads of /app and workspace installs are untouched.
This commit is contained in:
Renn F
2026-06-20 22:51:02 +02:00
parent b342de2913
commit 6f3ba2f42e
2 changed files with 85 additions and 0 deletions
+17
View File
@@ -321,4 +321,21 @@ if echo "$low" | grep -qE '(^|[[:space:];&|])rm[[:space:]]+[^|;&]*-[[:alpha:]]*[
fi
fi
# --- package-environment mutations targeting /app (ALL providers) -------------
# /app holds the orchestrator code and the MCP-gateway venv (/app/.venv). An
# agent that `uv sync` / `pip install`s into /app rebuilds that venv and breaks
# its OWN gateway tools (every roboco-flow / -do / -git verb) — stranding the
# agent and getting its task reaped. Agents manage dependencies in their
# workspace clone under /data/workspaces, never in /app. Two-step: a
# package-mutation verb AND a target that resolves to /app's environment
# (cd /app, --project/--directory /app, /app/.venv, UV_PROJECT_ENVIRONMENT=/app).
# Reads of /app (cat/ls/grep) and workspace installs are untouched. Deliberately
# NOT gated by ROBOCO_GUARD_SKIP_GIT, so it fires for every provider — the Claude
# PreToolUse hook AND the grok exfil hook (and future provider hooks).
if echo "$low" | grep -qE '(^|[[:space:];&|])(uv[[:space:]]+(sync|lock|add|remove)|uv[[:space:]]+pip[[:space:]]+(install|uninstall)|pip3?[[:space:]]+(install|uninstall))' && \
echo "$low" | grep -qE '(/app/\.venv|--project[[:space:]=]+"?/app([^a-z]|$)|--directory[[:space:]=]+"?/app([^a-z]|$)|uv_project_environment="?/app([^a-z]|$)|(^|[[:space:];&|])cd[[:space:]]+"?/app([^a-z]|$))'; then
echo "Denied: installing or syncing packages into /app rebuilds the orchestrator / MCP-gateway venv (/app/.venv) and breaks your own gateway tools. Manage dependencies in your workspace clone under /data/workspaces, never /app. If /app's environment looks broken, report it via your blocked / escalation verb — don't try to repair it." >&2
exit 2
fi
exit 0
+68
View File
@@ -381,3 +381,71 @@ def test_allows_pytest_even_if_suite_uses_requests() -> None:
"""The command string is just the runner — no http-client token and
no internal host literal — so it must pass."""
assert _run("uv run python -m pytest tests/unit/ -q") == _ALLOWED
# ---------------------------------------------------------------------------
# Package-environment mutations targeting /app (the orchestrator + MCP-gateway
# venv). Must be blocked for EVERY provider so an agent can't corrupt its own
# gateway by `uv sync` / `pip install`ing into /app.
# ---------------------------------------------------------------------------
def test_blocks_uv_sync_cd_app() -> None:
assert _run("cd /app && uv sync") == _DENIED
def test_blocks_pip_install_cd_app() -> None:
assert _run("cd /app && pip install httpcore httpx") == _DENIED
def test_blocks_uv_sync_project_app() -> None:
assert _run("uv sync --project /app") == _DENIED
def test_blocks_uv_pip_install_app_venv() -> None:
assert _run("uv pip install --python /app/.venv/bin/python httpx") == _DENIED
def test_blocks_uv_project_environment_app() -> None:
assert _run("UV_PROJECT_ENVIRONMENT=/app/.venv uv sync --no-dev") == _DENIED
def test_blocks_app_mutation_even_in_grok_mode() -> None:
"""Grok runs the hook with ROBOCO_GUARD_SKIP_GIT=1; the /app rule is NOT a
git rule, so it must STILL fire — every provider is protected."""
payload = json.dumps(
{"tool_name": "Bash", "tool_input": {"command": "cd /app && uv sync"}}
)
result = subprocess.run(
[str(GUARD)],
input=payload,
capture_output=True,
text=True,
check=False,
env={**os.environ, "ROBOCO_GUARD_SKIP_GIT": "1"},
)
assert result.returncode == _DENIED
def test_allows_uv_sync_in_workspace() -> None:
"""Legit dependency sync in the agent's own workspace clone must pass."""
assert (
_run("cd /data/workspaces/roboco/backend/be-dev-1 && uv sync --extra dev")
== _ALLOWED
)
def test_allows_pip_install_in_workspace() -> None:
assert _run("pip install -r requirements.txt") == _ALLOWED
def test_allows_reading_files_under_app() -> None:
"""Reads of /app (not env mutations) are untouched."""
assert _run("cat /app/pyproject.toml") == _ALLOWED
assert _run("ls -la /app/.venv/bin") == _ALLOWED
def test_allows_uv_sync_for_app_named_workspace_project() -> None:
"""A workspace path that merely contains 'app' (e.g. .../myapp/...) must not
trip the rule — the boundary requires /app to be its own path segment."""
assert _run("cd /data/workspaces/myapp/backend/be-dev-1 && uv sync") == _ALLOWED