8.3 KiB
Test Sandbox Hardening — Hard Rule
TL;DR
The Manual Slop test suite runs under a 4-layer sandbox that prevents any pytest invocation from writing files outside ./tests/. The root-cause fix removes the historical SLOP_CONFIG env-var fallback in favor of an explicit --config CLI flag. Any test that needs a config file must point at one inside ./tests/artifacts/.
The 4-Layer Model
| Layer | Mechanism | Where | Default-on? |
|---|---|---|---|
| Layer 1 | Python runtime file-I/O guard (sys.addaudithook) |
tests/conftest.py:_sandbox_audit_hook |
Yes |
| Layer 2 | isolate_workspace autouse + pyproject.toml --basetemp |
tests/conftest.py + pyproject.toml |
Yes |
| Layer 3 | OS-level restricted-token PowerShell wrapper | scripts/run_tests_sandboxed.ps1 |
Opt-in |
| Layer 4 | Static audit script (CI gate) | scripts/audit_test_sandbox_violations.py |
Yes (informational) / opt-in (--strict) |
Layer 1 + Layer 2 + Layer 4 are file-presence-on = enabled (delete the relevant file to disable). Layer 3 requires explicit invocation.
The --config CLI Flag (replaces SLOP_CONFIG)
The historical SLOP_CONFIG env var has been removed from src/paths.py. The CLI flag --config <path> is now the ONLY supported mechanism for overriding the default <project_root>/config.toml location.
sloppy.py
# Use the default <project_root>/config.toml
uv run python sloppy.py
# Override
uv run python sloppy.py --config /path/to/your/config.toml
sloppy.py calls paths.set_config_override(Path(args.config).resolve()) AFTER parse_args() and BEFORE any from src.gui_2 import App import. This is the only way to override the config path in production.
tests/conftest.py
tests/conftest.py parses sys.argv for --config at MODULE BODY (BEFORE any src/ import). If --config is not passed, conftest auto-defaults to tests/artifacts/_isolation_workspace_<RUN_ID>/config_overrides.toml (which lives inside ./tests/artifacts/, so the Layer 1 guard allows writes to it).
# Module body in tests/conftest.py (BEFORE any src/ import)
def _parse_config_arg(argv: list[str]) -> Path | None:
for i in range(1, len(argv)):
arg = argv[i]
if arg == "--config" and i + 1 < len(argv):
return Path(argv[i + 1]).resolve()
if arg.startswith("--config="):
return Path(arg.split("=", 1)[1]).resolve()
return None
_config_override_arg = _parse_config_arg(sys.argv)
if _config_override_arg is None:
_config_override_arg = _ISOLATION_WORKSPACE / "config_overrides.toml"
from src import paths as _paths # noqa: E402
_paths.set_config_override(_config_override_arg)
The fixture also auto-generates a placeholder config_overrides.toml (with ai.provider, projects, gui.show_windows) so src/ code that reads the config at startup does not crash.
The --basetemp Rule
pyproject.toml sets addopts = "--basetemp=tests/artifacts/_pytest_tmp". This redirects pytest's tmp_path and tmp_path_factory fixtures (which default to %TEMP%\pytest-of-<user>\ on Windows) into ./tests/artifacts/. This is what allows the Layer 1 allowlist to be a single rule: "anything under ./tests/ is allowed."
Layer 1 Audit Hook Contract
tests/conftest.py:_sandbox_audit_hook is a sys.addaudithook callback. It fires on every open() call. Behavior:
- Reads (mode
r,rb): pass through, no check - Writes (mode contains
w,a,x,+): check path - Allowed if path resolves under:
<project_root>/tests/- Path contains
.pytest_cache,__pycache__,.coverage,.slop_cache, or.ruff_cache - Original path string starts with
\\.\(Windows device namespace) or/dev/(Unix device namespace)
- Blocked otherwise: raises
RuntimeError("TEST_SANDBOX_VIOLATION: attempted to write to <path>...")
How to fix a violation:
- Move the write under
<project_root>/tests/(usetmp_path,tests/artifacts/_<name>/, etc.) - For pytest internal files (cache, log): check if the path is in the allowlist; if not, open an issue to add it
Layer 2 Workspace Convention (config_overrides.toml)
Tests that need a config.toml should use the auto-generated tests/artifacts/_isolation_workspace_<RUN_ID>/config_overrides.toml. The naming convention config_overrides.toml (instead of config.toml) signals that this file is an override for tests, not the production config.
Tests CAN pass --config /some/other/path.toml explicitly; conftest will honor it. But the default is fine for most cases.
Layer 3 Opt-in OS-Level Wrapper
scripts/run_tests_sandboxed.ps1 is the Windows-only restricted-token + Job Object wrapper for paranoid users. It mirrors scripts/tier2/run_tier2_sandboxed.ps1:
# Dry-run (no actual sandbox; just prints what would happen)
pwsh -File scripts/run_tests_sandboxed.ps1 -WhatIf
# Run the full suite in the sandbox
pwsh -File scripts/run_tests_sandboxed.ps1
# Run a specific test path
pwsh -File scripts/run_tests_sandboxed.ps1 -TestPath tests/test_paths.py
# Override config explicitly
pwsh -File scripts/run_tests_sandboxed.ps1 -ConfigPath /some/path/config.toml
The wrapper:
- Acquires a restricted token via .NET DuplicateTokenEx
- Sets cwd to
<project_root> - Invokes
uv run python -m pytest $TestPath --basetemp=tests/artifacts/_pytest_tmp [--config=...] - Forwards pytest exit code
Layer 4 Static Audit
scripts/audit_test_sandbox_violations.py scans tests/test_*.py for hardcoded paths that would corrupt user files:
Path("manual_slop.toml"),Path("config.toml"),Path("credentials.toml"),Path("presets.toml"), etc.open("manual_slop.toml", "w")and similar write-mode callsPath("C:/projects/...")andPath("C:\\projects\\...")Path("tests/artifacts/...")literal (violates workspace_paths.md; should use a fixture)tempfile.mkdtemp(),tempfile.mkstemp()(withoutdir=)
Default mode (informational) exits 0 and lists violations. --strict mode (CI gate) exits 1 on any violation.
# Informational
uv run python scripts/audit_test_sandbox_violations.py
# CI gate
uv run python scripts/audit_test_sandbox_violations.py --strict
Why This Rule Exists
The user has lost "important sample data" multiple times over the past month because tests have written to manual_slop.toml, manual_slop_history.toml, personas.toml, presets.toml, tool_presets.toml, or credentials.toml at the top of the repo. The root cause was the silent SLOP_CONFIG env-var fallback in src/paths.py — any test could set the env var and have paths.get_config_path() return a project-root file.
This track fixes that and adds defense in depth.
Forbidden Patterns (Hard Bans)
1. SLOP_CONFIG env var
Setting SLOP_CONFIG no longer affects paths.get_config_path(). Use --config instead.
2. tempfile.mkdtemp() / tempfile.mkstemp() without dir=
These default to %TEMP%, which the Layer 1 guard blocks. Use:
tempfile.mkdtemp(dir="tests/artifacts/")(explicit under tests)tmp_pathpytest fixture (resolves under--basetemp)tmp_path_factory.mktemp("name")(same)
3. Writing to <project_root>/*.toml or <project_root>/*.ini
The Layer 1 guard raises TEST_SANDBOX_VIOLATION on any write to a top-level TOML/INI file. Move the file under tests/artifacts/.
4. Path(__file__).parent.parent / "config.toml"
This pattern is a .. traversal to the project root. Flagged by Layer 4 static audit.
Audit Enforcement
- Layer 4 runs as a pre-commit hook + CI gate (
--strictmode) - Layer 1 fires at pytest runtime; cannot be bypassed without deleting
tests/conftest.py:_sandbox_audit_hook - Layer 2 is enforced by
pyproject.tomladdopts; cannot be overridden per-invocation
See Also
conductor/code_styleguides/workspace_paths.md— the existing test-workspace rule (extended by this track)conductor/code_styleguides/feature_flags.md— file-presence = enabled conventionconductor/tech-stack.md§"pyproject.toml pytest addopts" — dated note explaining--basetempscripts/audit_no_temp_writes.py— pattern reference for Layer 4 auditscripts/tier2/run_tier2_sandboxed.ps1— pattern reference for Layer 3 wrapperconductor/tracks/test_sandbox_hardening_20260619/— this track's spec + plan + stateconductor/tracks/workspace_path_finalize_20260609/— prior track that establishedtests/artifacts/workspace pattern