"""Scan ./scripts/** for any usage of the global %TEMP% directory. Used to verify the Tier 2 sandbox invariant: no production script under ./scripts/ may write to C:\\Users\\Ed\\AppData\\Local\\Temp\\ (or any other platform temp dir). All scratch / intermediate files must live in: - ./tests/artifacts/ (for test artifacts) - C:\\Users\\Ed\\AppData\\Local\\manual_slop\\tier2\\ (for app data) This script is the canonical audit. The persistent enforcement is tests/test_no_temp_writes.py (a default-on pytest test that calls this audit's main() and asserts the return code is 0). Exit codes: 0 CLEAN: no script emits to %TEMP% 1 FOUND: at least one script uses %TEMP% (printed to stdout) """ import argparse import json import re import sys from pathlib import Path # Patterns that indicate a script is using the global temp directory. # The patterns cover: # - Python: tempfile module, os.environ['TEMP'], etc. # - PowerShell: $env:TEMP, $env:TMP # - cmd: %TEMP%, %TMP% # - Unix-style: /tmp/ (sometimes used in cross-platform code) PATTERNS = [ r"tempfile\.", r"gettempdir", r"mkstemp", r"NamedTemporaryFile", r"TemporaryFile", r"os\.environ\[.TEMP", r"os\.environ\[.TMP", r"os\.environ\.get..TEMP", r"os\.environ\.get..TMP", r"\$env:TEMP", r"\$env:TMP", r"%TEMP%", r"%TMP%", r"/tmp/", r"\bTempDir\b", r"\btempfile\b", ] COMPILED = re.compile("|".join(PATTERNS), re.IGNORECASE) # Throw-away scripts from prior Tier 2 tracks live here. They are # archived for reference but are not part of the production code. # The audit excludes them. EXCLUDE_DIRS = {"scripts/tier2/artifacts"} # This audit script itself contains the patterns it searches for. # Exclude it so the audit can find its own pattern definitions. # Other audit scripts (e.g. audit_test_sandbox_violations.py) also # legitimately reference tempfile in their docstring/pattern definitions. EXCLUDE_FILES = { "scripts/audit_no_temp_writes.py", "scripts/audit_test_sandbox_violations.py", } def find_violations(root: str = "scripts") -> list[dict[str, object]]: """Return a list of violations: each is {path, line, content}.""" results: list[dict[str, object]] = [] for f in Path(root).rglob("*"): if not f.is_file(): continue if f.suffix not in {".py", ".ps1", ".sh", ".bat", ".cmd", ".psm1"}: continue rel = str(f).replace("\\", "/") if any(rel.startswith(d) for d in EXCLUDE_DIRS): continue if rel in EXCLUDE_FILES: continue try: content = f.read_text(encoding="utf-8", errors="ignore") except Exception: continue for i, line in enumerate(content.splitlines(), 1): if COMPILED.search(line): results.append({"path": rel, "line": i, "content": line.strip()}) return results def main() -> int: parser = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("--json", action="store_true", help="Output JSON instead of human-readable report") parser.add_argument("--strict", action="store_true", help="Exit 1 if any violations are found (for CI use; the convention's CI gate)") args = parser.parse_args() violations = find_violations() if args.json: print(json.dumps({"violations": violations, "count": len(violations)}, indent=2)) else: if not violations: print("CLEAN: no script under ./scripts/ emits to %TEMP%") else: print(f"FOUND {len(violations)} matches:") for v in violations: print(f" {v['path']}:{v['line']}: {v['content']}") return 1 if (args.strict and violations) else 0 if __name__ == "__main__": sys.exit(main())