feat(tests): add FR1 Python runtime sandbox via sys.addaudithook

This commit is contained in:
ed
2026-06-19 07:36:59 -04:00
parent 1329723c20
commit e733e5247f
2 changed files with 147 additions and 6 deletions
+82 -4
View File
@@ -21,6 +21,68 @@ thirdparty_dir = os.path.join(os.path.dirname(__file__), "..", "thirdparty")
if thirdparty_dir not in sys.path:
sys.path.insert(0, thirdparty_dir)
_SANDBOX_PROJECT_ROOT: Path | None = None
_SANDBOX_ALLOWLIST_PATH_PARTS: tuple[str, ...] = (
".pytest_cache",
"__pycache__",
".coverage",
".slop_cache",
".ruff_cache",
)
def _is_sandbox_path_allowed(resolved: Path, original: str) -> bool:
if _SANDBOX_PROJECT_ROOT is None:
return True
parts = set(resolved.parts)
if any(allowed in parts for allowed in _SANDBOX_ALLOWLIST_PATH_PARTS):
return True
orig_lower = original.lower()
if orig_lower.startswith("\\\\.\\") or orig_lower.startswith("//./"):
return True
if orig_lower.startswith("/dev/"):
return True
try:
resolved.relative_to(_SANDBOX_PROJECT_ROOT / "tests")
return True
except ValueError:
pass
return False
def _sandbox_audit_hook(event: str, args: tuple[object, ...]) -> None:
"""
sys.addaudithook target. Blocks writes outside ./tests/ + cache dirs.
Reads pass through. Per FR1 of test_sandbox_hardening_20260619 spec.
[C: tests/conftest.py:pytest_configure, tests/test_test_sandbox.py]
"""
if event != "open":
return
if not args:
return
path_obj = args[0]
mode = args[1] if len(args) > 1 else ""
if not isinstance(mode, str):
return
if not any(m in mode for m in ("w", "a", "x", "+")):
return
try:
path_str = os.fspath(path_obj)
except (TypeError, ValueError):
return
try:
resolved = Path(path_str).resolve()
except (OSError, ValueError, RuntimeError):
return
if _is_sandbox_path_allowed(resolved, path_str):
return
raise RuntimeError(
f"TEST_SANDBOX_VIOLATION: attempted to write to {resolved} "
f"(outside <project_root>/tests/). Use tmp_path or fixture-provided paths. "
f"See conductor/code_styleguides/test_sandbox.md for guidance."
)
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if project_root not in sys.path:
sys.path.insert(0, project_root)
@@ -118,11 +180,16 @@ _pytest_finished_event: threading.Event = threading.Event()
def pytest_configure(config: object) -> None:
"""
Pytest session-start hook. Runs required-dependency check before any
test is collected so the user sees a clear, actionable error if the
test environment is incomplete.
[C: tests/test_required_test_dependencies.py:test_check_succeeds_when_deps_present, tests/test_required_test_dependencies.py:test_check_raises_on_missing_sentence_transformers]
Pytest session-start hook. Installs the runtime sandbox audit hook
(FR1 of test_sandbox_hardening_20260619) BEFORE any test module
imports so misbehaving tests cannot write outside ./tests/. Then
runs the required-dependency check so the user sees a clear,
actionable error if the test environment is incomplete.
[C: tests/test_required_test_dependencies.py:test_check_succeeds_when_deps_present, tests/test_required_test_dependencies.py:test_check_raises_on_missing_sentence_transformers, tests/test_test_sandbox.py]
"""
global _SANDBOX_PROJECT_ROOT
_SANDBOX_PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.addaudithook(_sandbox_audit_hook)
_check_required_test_dependencies()
@@ -256,6 +323,17 @@ class VerificationLogger:
f.write(f"{status} {self.test_name} ({result_msg})\n\n")
print(f"[FINAL] {self.test_name}: {status} - {result_msg}")
@pytest.fixture(autouse=True)
def _enforce_test_sandbox() -> Generator[None, None, None]:
"""
Default-on runtime guard (FR1 of test_sandbox_hardening_20260619 spec).
The actual sys.addaudithook is installed in pytest_configure (session-
scoped, BEFORE any test module imports). This autouse fixture exists
as a marker so the contract is visible in fixture introspection.
[C: tests/conftest.py:pytest_configure, tests/test_test_sandbox.py]
"""
yield
@pytest.fixture(autouse=True)
def isolate_workspace(tmp_path_factory, monkeypatch) -> Generator[None, None, None]:
"""