Files
roboco/tests/unit/runtime/test_bash_guard_makefile_guardrail.py
5b27a443e9 chore(agnosticism): close the audit residue — B6/B8/B10 + three MAJORs (#587)
Thread the deployer's product name through the X reply + feature-spotlight
prompts (B6 leftover; release/video paths shipped in #570); make the
docs-site repo/URL config (ROBOCO_DOCS_SITE_*, defaults unchanged) instead
of a roboco-website hardcode (B8); de-assert our repo from the Main PM
prompt (B10); derive PR labels from the real target branch instead of
literal to-master/to-slave; drop the stale headcount from base.md; and
make the bash-guard's Makefile check require an actual quality/gate/lint/
test target before denying raw package-manager commands (no more
false-remediation loop on Go/Rust Makefiles).

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-19 17:52:17 +02:00

107 lines
3.5 KiB
Python

"""bash-guard Makefile guardrail — deny raw uv/pip/conda/poetry, point at make.
CEO item: force agents to the Makefile. The existing hook deliberately allowed
bare ``uv run`` (workspace .venv, cwd-relative); this guard overrides that by
CEO direction when a ``Makefile`` is present AND declares at least one of the
quality/gate/lint/test targets, denying raw package-manager / test-runner
commands and remediating to the make targets. Skipped when no Makefile exists,
or when one exists but declares none of those targets (a Go/Rust Makefile with
only build/run — existence alone would remediate into a dead end). On the grok
path (``ROBOCO_GUARD_SKIP_PM=1``) a deny cancels the whole run, so it nudges
(exit 0) instead.
"""
from __future__ import annotations
import json
import os
import subprocess
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
HOOK = REPO_ROOT / "docker" / "scripts" / "bash-guard-hook.sh"
# Hook exits 2 to deny, 0 to allow. Named (not magic) for ruff PLR2004.
_DENIED = 2
_ALLOWED = 0
def _run_hook(
command: str, cwd: Path, env_over: dict[str, str] | None = None
) -> tuple[int, str]:
payload = json.dumps({"tool_input": {"command": command}})
env = dict(os.environ)
if env_over:
env.update(env_over)
proc = subprocess.run(
["bash", str(HOOK)],
input=payload,
capture_output=True,
text=True,
cwd=str(cwd),
env=env,
check=False,
)
return proc.returncode, proc.stderr
def test_denies_uv_run_when_makefile_present() -> None:
rc, err = _run_hook("uv run pytest", REPO_ROOT)
assert rc == _DENIED
assert "make" in err.lower()
def test_denies_pip_install() -> None:
rc, _ = _run_hook("pip install requests", REPO_ROOT)
assert rc == _DENIED
def test_denies_compound_uv_run() -> None:
rc, _ = _run_hook("cd svc && uv run ruff check .", REPO_ROOT)
assert rc == _DENIED
def test_denies_conda_and_poetry() -> None:
assert _run_hook("conda install numpy", REPO_ROOT)[0] == _DENIED
assert _run_hook("poetry run pytest", REPO_ROOT)[0] == _DENIED
def test_allows_make_quality() -> None:
rc, _ = _run_hook("make quality", REPO_ROOT)
assert rc != _DENIED
def test_allows_pnpm() -> None:
rc, _ = _run_hook("pnpm lint", REPO_ROOT)
assert rc != _DENIED
def test_skips_deny_without_makefile(tmp_path: Path) -> None:
rc, _ = _run_hook("uv run pytest", tmp_path)
assert rc != _DENIED
def test_skips_deny_when_makefile_lacks_remediation_targets(tmp_path: Path) -> None:
"""A Go/Rust-style Makefile with only build/run targets — existence alone
must not deny+remediate to a `make quality`/`gate`/`lint`/`test` that
doesn't exist (the false-remediation dead-end loop the content check
closes)."""
(tmp_path / "Makefile").write_text("build:\n\tgo build ./...\nrun:\n\tgo run .\n")
rc, _ = _run_hook("uv run pytest", tmp_path)
assert rc != _DENIED
def test_denies_when_makefile_has_only_one_remediation_target(tmp_path: Path) -> None:
"""Just one of quality/gate/lint/test is enough to arm the deny — the
guard doesn't require all four."""
(tmp_path / "Makefile").write_text("lint:\n\truff check .\n")
rc, _ = _run_hook("uv run pytest", tmp_path)
assert rc == _DENIED
def test_grok_path_nudges_not_denies() -> None:
"""ROBOCO_GUARD_SKIP_PM=1 (grok) -> exit 0 nudge, not run-canceling exit 2."""
rc, err = _run_hook("uv run pytest", REPO_ROOT, {"ROBOCO_GUARD_SKIP_PM": "1"})
assert rc == _ALLOWED
assert "make" in err.lower()