From 3e7ecdd152e5161f02fde38ca00c289ccb529b5b Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Tue, 19 May 2026 06:40:17 +0000 Subject: [PATCH] =?UTF-8?q?fix(cron=5Fvm):=20greptile=20=E2=80=94=20scrub?= =?UTF-8?q?=20provider=20secrets=20from=20pytest=20invocation=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wrap the pytest call in run_daily.sh in `env -i` with the same minimal allowlist the PR-gate already uses, mirroring the CircleCI step. The systemd EnvironmentFile injects ANTHROPIC_API_KEY / AWS_BEARER_TOKEN_BEDROCK / VERTEXAI_* / AZURE_FOUNDRY_* / AGENT_SHIN_GITHUB_TOKEN / GITHUB_TOKEN into the script for the proxy to consume; pytest inherits them by default but only needs the loopback proxy URL/key. Scrubbing them closes two paths: PR-controlled test code reading them out of os.environ, and model-directed Read tool calls reaching /proc//environ during PDF/vision cells. Add a pin test mirroring the existing version-probe one. --- .../test_run_daily_pytest_scrubs_env.py | 95 +++++++++++++++++++ tests/claude_code/cron_vm/run_daily.sh | 33 ++++++- 2 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 tests/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py diff --git a/tests/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py b/tests/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py new file mode 100644 index 00000000000..8ca51890cb5 --- /dev/null +++ b/tests/claude_code/_publisher_unit_tests/test_run_daily_pytest_scrubs_env.py @@ -0,0 +1,95 @@ +"""Pin: the cron `pytest` invocation must run under `env -i`. + +The systemd service `litellm-compat-matrix.service` loads provider +credentials (`ANTHROPIC_API_KEY`, `AWS_BEARER_TOKEN_BEDROCK`, +`AZURE_FOUNDRY_API_KEY`, `VERTEXAI_*`) and the agent-shin GitHub token +(`AGENT_SHIN_GITHUB_TOKEN`) into `run_daily.sh`'s environment from +`/etc/litellm-compat-matrix.env`. Pytest only needs to talk to the +loopback proxy at `127.0.0.1:${PROXY_PORT}` and has no legitimate reason +to see provider creds in its own `os.environ`. Leaving them in would +let a test under `tests/claude_code/` read them via `os.environ` and +exfiltrate them, and would also let a model-directed `Read` tool call +during a PDF/vision cell reach `/proc//environ`. The +PR-gate's pytest step in `.circleci/config.yml` already runs under +`env -i`; this pin enforces the same scrub on the cron path. +""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[3] +RUN_DAILY = REPO_ROOT / "tests" / "claude_code" / "cron_vm" / "run_daily.sh" + + +def _pytest_invocation_block() -> str: + """Return only the executable lines around the pytest invocation. + + Comment text in run_daily.sh explains *why* certain credential + names must not appear, so a naïve substring scan over the whole + region would false-positive on the rationale itself. Strip lines + whose first non-space character is `#`. + """ + body = RUN_DAILY.read_text() + start = body.index('log "running pytest"') + end = body.index("PYTEST_EXIT=$?", start) + return "\n".join( + line for line in body[start:end].splitlines() + if line.lstrip()[:1] != "#" + ) + + +def test_pytest_invocation_wraps_in_env_i() -> None: + block = _pytest_invocation_block() + assert "env -i" in block, ( + "run_daily.sh: the pytest invocation must run under `env -i` so " + "PR-controlled test code under tests/claude_code/ cannot read " + "provider/agent-shin credentials out of the systemd service " + "environment, and so a model-directed `Read` tool call cannot " + "reach /proc//environ to pull them out." + ) + assert block.index("env -i") < block.index('"${WORKTREE_UV}" run pytest'), ( + "run_daily.sh: `env -i` must precede the pytest invocation; " + "otherwise pytest inherits the full credential-bearing env." + ) + + +def test_pytest_invocation_env_i_excludes_provider_secrets() -> None: + block = _pytest_invocation_block() + for forbidden in ( + "ANTHROPIC_API_KEY", + "AWS_BEARER_TOKEN_BEDROCK", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "VERTEXAI_CREDENTIALS", + "VERTEXAI_PROJECT", + "VERTEXAI_LOCATION", + "AZURE_FOUNDRY_API_KEY", + "AZURE_FOUNDRY_API_BASE", + "GITHUB_TOKEN", + "AGENT_SHIN_GITHUB_TOKEN", + ): + assert forbidden not in block, ( + f"run_daily.sh: the pytest-step `env -i` allowlist must not " + f"pass {forbidden} through. Found it inside the pytest " + f"invocation block." + ) + + +def test_pytest_invocation_passes_proxy_url_and_key_explicitly() -> None: + block = _pytest_invocation_block() + assert "LITELLM_PROXY_BASE_URL=" in block, ( + "run_daily.sh: the pytest `env -i` block must still pass " + "LITELLM_PROXY_BASE_URL so the test suite knows where to find " + "the loopback proxy." + ) + assert "LITELLM_PROXY_API_KEY=" in block, ( + "run_daily.sh: the pytest `env -i` block must still pass " + "LITELLM_PROXY_API_KEY so the test suite can authenticate to " + "the loopback proxy." + ) + assert "COMPAT_RESULTS_PATH=" in block, ( + "run_daily.sh: the pytest `env -i` block must still pass " + "COMPAT_RESULTS_PATH so the conftest writes the per-cell " + "tagged-union artifact to the script-managed path." + ) diff --git a/tests/claude_code/cron_vm/run_daily.sh b/tests/claude_code/cron_vm/run_daily.sh index a70c9678d9b..42060535379 100755 --- a/tests/claude_code/cron_vm/run_daily.sh +++ b/tests/claude_code/cron_vm/run_daily.sh @@ -351,9 +351,40 @@ fi log "running pytest" set +e +# Pytest only needs to talk to the loopback proxy at 127.0.0.1:${PROXY_PORT} +# — it has no legitimate reason to see ANTHROPIC_API_KEY / +# AWS_BEARER_TOKEN_BEDROCK / VERTEXAI_* / AZURE_FOUNDRY_* / +# AGENT_SHIN_GITHUB_TOKEN / GITHUB_TOKEN in its own env. The systemd +# unit's EnvironmentFile injects all of those into this script for the +# proxy to consume, and pytest inherits them by default. Wrap the +# invocation in `env -i` so: +# +# 1. test code under tests/claude_code/ (or anything it imports) +# cannot read provider/agent-shin creds out of `os.environ` and +# exfiltrate them via an outbound call from inside a conftest hook +# or a fixture (a sibling vector to the model-controlled Bash/Read +# concern handled by `cli_driver.py`'s own env scrub); +# 2. a model-directed `Read` tool call during a PDF/vision cell +# cannot reach /proc//environ and pull the creds out +# of the parent process the way it can today; +# 3. this matches the PR-gate pytest step in `.circleci/config.yml`, +# which already runs under `env -i` with the same minimal +# allowlist. +# +# `cli_driver.py` re-allowlists its own subset (PATH/USER/LOGNAME/etc.) +# when spawning the `claude` binary, so the CLI still finds Node + the +# claude shim on PATH and gets a fresh isolated HOME per invocation. ( cd "${WORKTREE}" \ - && LITELLM_PROXY_BASE_URL="http://127.0.0.1:${PROXY_PORT}" \ + && env -i \ + PATH="${PATH}" \ + HOME="${HOME}" \ + USER="${USER:-mateo}" \ + TERM="${TERM:-dumb}" \ + LANG="${LANG:-C.UTF-8}" \ + LC_ALL="${LC_ALL:-}" \ + TMPDIR="${TMPDIR:-/tmp}" \ + LITELLM_PROXY_BASE_URL="http://127.0.0.1:${PROXY_PORT}" \ LITELLM_PROXY_API_KEY="${PROXY_API_KEY}" \ COMPAT_RESULTS_PATH="${RESULTS_JSON}" \ "${WORKTREE_UV}" run pytest "${PYTEST_ARGS[@]}"