litellm/tests/e2e/cli_harness/lockdown.py
ryan-crabbe-berri 3af23024bc
test(e2e): share the coding-CLI harness between suites (#49370)
* test(e2e): share the CLI version rule, env lockdown and rate limiter in tests/e2e/cli_harness

Moves the npm release rule (now taking the package name), the env allowlist plus fresh HOME, and the per-upstream rate limiter out of claude_code so the Codex suite can reuse them. Claude Code behavior is unchanged, its CI, cron and Dockerfile paths now call the shared resolver with @anthropic-ai/claude-code

* test(e2e): freeze the packument map and note the rate limiter self-test

* test(e2e): race real processes on one rate limiter bucket so a missing lock fails

Eight spawned processes start together and drain a bucket that never refills. With the flock they get exactly its burst between them, without it every attempt is granted

* test(e2e): keep the old Claude Code version resolver path for the Buildkite runner images

The runner Dockerfiles still run tests/e2e/claude_code/pr_gate_version_resolver.py and silently install @latest when it is missing, which drops the three day review window. The old path now forwards to cli_harness/npm_version_resolver.py
2026-11-02 13:50:56 -08:00

49 lines
1.4 KiB
Python

"""What a coding-agent CLI under test may see of the host.
The CI job holds the proxy's upstream credentials (provider keys, AWS, Azure, the database URL, GitHub tokens) in its
environment, and the cron VM runs as a real user with a populated home. A compromised CLI release, or a model told to
read a file, must reach neither, so the CLI gets only the allowlisted runtime variables plus what its suite passes in,
and a fresh empty HOME that is deleted when it exits.
"""
from __future__ import annotations
import os
import shutil
import tempfile
from collections.abc import Generator, Mapping
from contextlib import contextmanager
from pathlib import Path
from types import MappingProxyType
from typing import Final
CLI_ENV_ALLOWLIST: Final = (
"PATH",
"USER",
"LOGNAME",
"SHELL",
"TERM",
"TMPDIR",
"LANG",
"LC_ALL",
"LC_CTYPE",
"NODE_PATH",
"NVM_DIR",
"NVM_BIN",
)
@contextmanager
def isolated_home(prefix: str) -> Generator[Path]:
home: Final = Path(tempfile.mkdtemp(prefix=prefix))
try:
yield home
finally:
shutil.rmtree(home, ignore_errors=True)
def locked_down_env(
*, home: Path, overrides: Mapping[str, str], source: Mapping[str, str] = os.environ
) -> Mapping[str, str]:
allowed: Final = {name: source[name] for name in CLI_ENV_ALLOWLIST if name in source}
return MappingProxyType({**allowed, "HOME": str(home), **overrides})