mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""Lint guard: no new raw ``os.environ.copy()`` spawn-env sites.
|
|
|
|
Every child-process env in the codebase must be built through
|
|
``tools.environments.local.build_subprocess_env`` (or its sibling
|
|
``hermes_subprocess_env`` / ``_sanitize_subprocess_env``, which the factory
|
|
wraps) so profile-home propagation and secret-scrubbing have a single owner.
|
|
History: ~11 commits over 6 months each fixed one more spawn site that missed
|
|
``HERMES_HOME`` or secret-scrub propagation.
|
|
|
|
This test greps the source tree for ``os.environ.copy()`` appearing within
|
|
``PROXIMITY_LINES`` lines of a spawn call (``Popen`` / ``subprocess.run`` /
|
|
``create_subprocess*`` / ``PtyProcess.spawn`` / ``execvpe``) and asserts every
|
|
hit is in the explicit allowlist below. If you are adding a new spawn site:
|
|
|
|
* use ``build_subprocess_env(...)`` — with ``scrub_secrets=False,
|
|
inherit_profile_home=False`` if you need today's exact-inherit behavior; or
|
|
* consciously add the file to ``ALLOWED_RAW_SPAWN_ENV_FILES`` with a comment
|
|
explaining why the factory cannot be used.
|
|
"""
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# Directories that make up the shipped source tree.
|
|
SCAN_DIRS = ("agent", "hermes_cli", "tools", "gateway", "cron", "tui_gateway")
|
|
SCAN_ROOT_FILES = ("cli.py", "hermes_constants.py")
|
|
|
|
# How many lines around an `os.environ.copy()` we look for a spawn call.
|
|
PROXIMITY_LINES = 20
|
|
|
|
SPAWN_RE = re.compile(
|
|
r"\bPopen\b|\bsubprocess\.run\b|\bcreate_subprocess|\bPtyProcess\.spawn\b"
|
|
r"|\bexecvpe\b|\bptyprocess\.PtyProcess\b|\bspawn\("
|
|
)
|
|
COPY_RE = re.compile(r"\bos\.environ\.copy\(\)")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ALLOWLIST — intentionally-raw sites. Each entry is a relative posix path.
|
|
# Adding to this list is a conscious decision: document WHY inline here.
|
|
# ---------------------------------------------------------------------------
|
|
ALLOWED_RAW_SPAWN_ENV_FILES = {
|
|
# THE owner module: _sanitize_subprocess_env / hermes_subprocess_env /
|
|
# build_subprocess_env legitimately snapshot os.environ — everything else
|
|
# delegates to them.
|
|
"tools/environments/local.py",
|
|
}
|
|
|
|
|
|
def _iter_source_files():
|
|
for d in SCAN_DIRS:
|
|
root = REPO_ROOT / d
|
|
if root.is_dir():
|
|
yield from root.rglob("*.py")
|
|
for f in SCAN_ROOT_FILES:
|
|
p = REPO_ROOT / f
|
|
if p.is_file():
|
|
yield p
|
|
|
|
|
|
def _raw_spawn_env_sites():
|
|
"""Return [(relpath, lineno)] of os.environ.copy() near a spawn call."""
|
|
sites = []
|
|
for path in _iter_source_files():
|
|
rel = path.relative_to(REPO_ROOT).as_posix()
|
|
try:
|
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
except OSError:
|
|
continue
|
|
for i, line in enumerate(lines):
|
|
stripped = line.lstrip()
|
|
if stripped.startswith("#"):
|
|
continue # comments referencing the old pattern are fine
|
|
if not COPY_RE.search(line.split("#", 1)[0]):
|
|
continue
|
|
lo = max(0, i - PROXIMITY_LINES)
|
|
hi = min(len(lines), i + PROXIMITY_LINES + 1)
|
|
window = "\n".join(lines[lo:hi])
|
|
if SPAWN_RE.search(window):
|
|
sites.append((rel, i + 1))
|
|
return sites
|
|
|
|
|
|
def test_no_new_raw_environ_copy_spawn_sites():
|
|
sites = _raw_spawn_env_sites()
|
|
offenders = [
|
|
f"{rel}:{lineno}"
|
|
for rel, lineno in sites
|
|
if rel not in ALLOWED_RAW_SPAWN_ENV_FILES
|
|
]
|
|
assert not offenders, (
|
|
"New raw os.environ.copy() spawn-env site(s) found:\n "
|
|
+ "\n ".join(offenders)
|
|
+ "\nUse tools.environments.local.build_subprocess_env() instead "
|
|
"(scrub_secrets=False, inherit_profile_home=False preserves exact "
|
|
"legacy behavior), or consciously extend "
|
|
"ALLOWED_RAW_SPAWN_ENV_FILES in this test with a justification."
|
|
)
|
|
|
|
|
|
def test_allowlist_entries_still_exist():
|
|
"""Prune the allowlist when files are removed/renamed."""
|
|
for rel in ALLOWED_RAW_SPAWN_ENV_FILES:
|
|
assert (REPO_ROOT / rel).is_file(), (
|
|
f"Allowlist entry {rel} no longer exists — remove it from "
|
|
"ALLOWED_RAW_SPAWN_ENV_FILES."
|
|
)
|