5-part fix to prevent test data loss outside ./tests/: 1. FR2 (root-cause): remove SLOP_CONFIG env var fallback from src/paths.py 2. --config CLI flag at entry point (sloppy.py for prod, conftest.py for tests) 3. FR1: sys.addaudithook runtime guard blocks writes outside ./tests/ 4. FR3: pytest --basetemp + isolate_workspace migration under ./tests/ 5. FR4: static audit (scripts/audit_test_sandbox_violations.py) + --strict CI gate Opt-in: FR5 Windows restricted-token wrapper (scripts/run_tests_sandboxed.ps1). 13 regression tests in tests/test_test_sandbox.py. Baseline: 1288 passed + 4 xdist-skipped (per result_migration_small_files_20260617). User directive: NO ENV VARS for config path. Use --config CLI flag. Test workspace file naming: config_overrides.toml (per user direction). Hard fail on any sandbox violation. Tests should never need AppData temp. Co-Authored-By: Claude <noreply@anthropic.com>
29 KiB
Track Specification: Test Sandbox Hardening (2026-06-19)
Overview
This track adds a hard file-I/O sandbox for the test suite so that a misbehaving
test (missing fixture, broken monkeypatch, direct open() to a hardcoded path)
cannot corrupt user-owned files in the project root. 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 fix has 5 parts:
- Eliminate the silent
SLOP_CONFIGenv-var fallback insrc/paths.py. Replace it with a module-level override set explicitly by the CLI flag--configat the entry point (sloppy.py for production, conftest.py for tests). This is the root-cause fix — without it, every other defense is a band-aid. - Add a Python runtime file-I/O guard (
sys.addaudithookonopenwrites). Default-on for every pytest invocation. - Migrate the test workspace off
tmp_path_factory.mktemp(which lives in%TEMP%) ontotests/artifacts/_isolation_workspace_<run_id>/so the Layer 2 allowlist can be a single rule. Add pytest--basetempto pyproject.toml so pytest's own tmp dirs also live under./tests/. - Add an OS-level restricted-token wrapper (Windows-only, opt-in via
scripts/run_tests_sandboxed.ps1) for users who want defense in depth on top of the Python guard. - Extend the static audit to flag any test source code that could try
to write to a top-level TOML file, plus
tempfile.mkdtemp()/tempfile.mkstemp()calls withoutdir=pointing under./tests/.
Out of scope (per user directive): the OTHER SLOP_* env vars
(SLOP_GLOBAL_PRESETS, SLOP_GLOBAL_TOOL_PRESETS, SLOP_GLOBAL_PERSONAS,
SLOP_GLOBAL_WORKSPACE_PROFILES, SLOP_CREDENTIALS, SLOP_MCP_ENV,
SLOP_LOGS_DIR, SLOP_SCRIPTS_DIR) remain env-var-driven for now. The user
considers them a separate "mess" to be addressed in follow-up tracks. The
test workspace still uses these env vars to redirect to per-run paths under
./tests/artifacts/.
After this track, the rule is: any pytest or run_tests_batched.py
invocation cannot write a single byte outside ./tests/, and the static audit
flags any test source code that could try.
Current State Audit (as of 2026-06-19)
Already Implemented (DO NOT re-implement)
isolate_workspaceautouse fixture (tests/conftest.py:259-281)- Sets
SLOP_CONFIG,SLOP_GLOBAL_PRESETS,SLOP_GLOBAL_TOOL_PRESETS,SLOP_GLOBAL_PERSONAS,SLOP_GLOBAL_WORKSPACE_PROFILESto a per-test path viatmp_path_factory.mktemp("isolated_workspace"). - Provides partial protection for tests that go through
src.paths.get_*_path().
- Sets
live_guifixture workspace (tests/conftest.py:484-525)- Creates
tests/artifacts/live_gui_workspace_<TIMESTAMP>/per pytest invocation with freshmanual_slop.toml+config.toml(perworkspace_path_finalize_20260609). - Sets
SLOP_CREDENTIALS+SLOP_MCP_ENVto the real project-rootcredentials.toml/mcp_env.toml(read-only intent).
- Creates
scripts/check_test_toml_paths.py(perconductor/tracks.md:395)- Static audit detects tests with hardcoded references to TOML basenames
(
manual_slop.toml,config.toml,credentials.toml, etc.) or toPath("C:/projects/...")andPath("tests/artifacts/")literals. - CI gate that exits 1 on violation.
- Static audit detects tests with hardcoded references to TOML basenames
(
conductor/code_styleguides/workspace_paths.md(148 lines)- Hard rule: test workspaces must live under
tests/artifacts/. Banned:tmp_path_factory.mktempfor test infrastructure workspaces, env vars for test paths, CLI args for test paths.
- Hard rule: test workspaces must live under
scripts/audit_no_temp_writes.py(108 lines)- Audits
scripts/for%TEMP%usage. Pattern reference for the new audit script in Layer 4.
- Audits
Gaps to Fill (This Track's Scope)
| # | Gap | Risk | Where |
|---|---|---|---|
| G1 | isolate_workspace uses tmp_path_factory.mktemp which lives in %TEMP%, violating the existing styleguide |
Path allowlist for Layer 1 has to include %TEMP% = widened blast radius |
tests/conftest.py:265 |
| G2 | isolate_workspace doesn't set SLOP_CREDENTIALS or SLOP_MCP_ENV |
Non-live_gui tests that go through src.paths.get_credentials_path() read the real credentials.toml |
tests/conftest.py:259-281 |
| G3 | No runtime file-I/O guard. Tests can Path("manual_slop.toml").write_text(...) with no consequence |
Direct cause of user's data loss | New: tests/conftest.py:_enforce_test_sandbox |
| G4 | Pytest's tmp_path / tmp_path_factory default to %TEMP%\pytest-of-<user>\ |
Same widening issue as G1 | New: pyproject.toml addopts + tests/conftest.py:pytest_configure |
| G5 | check_test_toml_paths.py doesn't catch non-TOML writes (e.g., Path("manualslop_layout.ini").write_text, Path("manualslop_history.toml").write_text) |
Hidden test paths slip through the static audit | New: scripts/audit_test_sandbox_violations.py (extends existing) |
| G6 | No OS-level hard sandbox option for paranoid users (Tier 2 has run_tier2_sandboxed.ps1; tests have no equivalent) |
Same risk for users running pytest interactively without the Python guard |
New: scripts/run_tests_sandboxed.ps1 (opt-in) |
| G7 | AppController() is initialized at tests/conftest.py:65 at module import (line 65-66: _warmup_app_controller = AppController(); _warmup_app_controller.wait_for_warmup(timeout=60.0)), BEFORE the autouse isolate_workspace fixture applies |
MOSTLY OK but no invariant. Per the call chain audit: AppController.__init__ (src/app_controller.py:787) only sets up state + starts warmup background thread; it does NOT call init_state() or load_config(). init_state() (which reads config) is called from App.__init__() in src/gui_2.py, AFTER fixtures apply. But there's no test asserting this invariant — a future refactor could accidentally move init_state() into init and silently break the safety |
New FR8 regression test: assert AppController.__init__ does not call init_state() or load_config() |
Goals
- Make it impossible for any test invocation to write outside
./tests/at the Python layer (Layer 1) and at the OS layer when the opt-in wrapper is used (Layer 3). - Make every test see a fully-sandboxed project root (Layer 2): config, presets, personas, tool_presets, credentials, mcp_env, AND pytest's own tmp dirs all live under
./tests/. - Catch sandbox violations statically (Layer 4): a developer adding a bad path to a test source file gets a CI failure before the test ever runs.
- No regression in test pass rate. All 11 tiers must continue to pass clean after this track ships.
- No new
@pytest.mark.skipmarkers. Per the user directive (perconductor/workflow.mdSkip-Marker Policy), in-session fixes only.
Functional Requirements
FR1. Python runtime file-I/O sandbox (Layer 1 — DEFAULT ON)
WHERE: New _enforce_test_sandbox autouse fixture in tests/conftest.py (registered alongside isolate_workspace at line ~258).
WHAT: Install a sys.addaudithook() callable that intercepts the open audit event when the mode is 'w', 'a', 'x', or '+', or when the call is to os.makedirs / shutil.rmtree / tempfile.mkdtemp / tempfile.mkstemp.
HOW:
- Allowlist (writes ALLOWED):
- Any path under
<project_root>/tests/(resolved absolute; case-normalized on Windows). - Any path under
<project_root>/tests/artifacts/(already covered by above; explicit for clarity). - Any path under the per-run
_RUN_WORKSPACE(which lives inside./tests/artifacts/). - The pyproject.toml
pytest --basetemptarget (also inside./tests/).
- Any path under
- Denylist (writes REJECTED):
- Anything outside
./tests/.
- Anything outside
- On violation: raise
RuntimeError("TEST_SANDBOX_VIOLATION: <test_name> attempted to write to <absolute_path> which is outside <project_root>/tests/. Use tmp_path or fixture-provided paths.")and let pytest report the failure with the test name. - The hook is installed in
pytest_configure(so it's in place before any test module imports), uninstalled inpytest_unconfigure.
SAFETY:
- Reads are NOT blocked. Tests legitimately need to read the source tree (
src/,pyproject.toml,mcp_env.toml,credentials.toml, etc.) for fixtures and mocks. - The hook must be thread-safe (pytest may run tests in xdist workers).
- The hook must not break pytest's own internals (
.pytest_cache,_pytest_tmp_path_factorycleanup). The basetemp migration (FR3) handles this. - Allowlist resolution must NOT block legitimate pytest cache writes (
<project_root>/.pytest_cache/,<project_root>/tests/.pytest_cache/,<project_root>/tests/artifacts/__pycache__/). Add.pytest_cache,__pycache__,.coverage,.slop_cache,.ruff_cacheto the allowlist.
FR2. CLI flag --config replaces SLOP_CONFIG env var (ROOT-CAUSE FIX)
WHERE: src/paths.py:42-46 (the silent fallback). sloppy.py (CLI parser). tests/conftest.py (CLI parser at module body BEFORE any src/ import).
WHAT: Remove the os.environ.get("SLOP_CONFIG", ...) fallback from src/paths.py:get_config_path(). Replace with a module-level _CONFIG_OVERRIDE: Path | None that is set ONLY by explicit CLI flag parsing at the entry point.
HOW:
# src/paths.py — REPLACE the existing get_config_path with:
_CONFIG_OVERRIDE: Path | None = None
def set_config_override(path: Path | None) -> None:
"""CLI flag is the ONLY way to override. Pass None to use default.
[C: sloppy.py:main, tests/conftest.py:_setup_test_paths]"""
global _CONFIG_OVERRIDE
_CONFIG_OVERRIDE = path
_RESOLVED.clear()
def get_config_path() -> Path:
"""Returns the active config.toml. If override is set, returns it.
Otherwise returns the default <project_root>/config.toml.
[C: src/app_controller.py:AppController.load_config,
src/app_controller.py:AppController.init_state,
src/models.py:_load_config_from_disk]"""
if _CONFIG_OVERRIDE is not None:
return _CONFIG_OVERRIDE
root_dir = Path(__file__).resolve().parent.parent
return root_dir / "config.toml"
# sloppy.py — ADD argparse flag (BEFORE any src/ import):
parser.add_argument("--config", help="Path to config.toml (default: <project_root>/config.toml)")
args = parser.parse_args()
if args.config:
from src import paths
paths.set_config_override(Path(args.config).resolve())
# tests/conftest.py — PARSE sys.argv at module body BEFORE any src/ import:
import sys as _sys
from pathlib import Path as _Path
def _parse_config_arg() -> _Path | None:
"""Manual sys.argv parse for --config. Returns resolved Path or None."""
args = _sys.argv[1:]
for i, arg in enumerate(args):
if arg == "--config" and i + 1 < len(args):
return _Path(args[i + 1]).resolve()
if arg.startswith("--config="):
return _Path(arg.split("=", 1)[1]).resolve()
return None
_config_arg = _parse_config_arg()
if _config_arg is None:
# Default for tests: sandboxed config_overrides.toml
config_arg = _Path(f"tests/artifacts/_isolation_workspace_{_RUN_ID}/config_overrides.toml")
else:
config_arg = _config_arg
# Set override BEFORE any src/ import
from src import paths # noqa: E402
paths.set_config_override(config_arg)
# ALSO register with pytest so it doesn't warn about unknown flag:
def pytest_addoption(parser):
parser.addoption("--config", action="store", default=None,
help="Manual Slop: override config.toml path for tests")
Test workspace contents (auto-generated by _setup_test_paths helper in conftest):
tests/artifacts/_isolation_workspace_<RUN_ID>/
├── config_overrides.toml # the AppController config (per user naming)
├── credentials.toml # placeholder for SLOP_CREDENTIALS (env var stays)
├── mcp_env.toml # placeholder for SLOP_MCP_ENV (env var stays)
├── presets.toml # placeholder for SLOP_GLOBAL_PRESETS
├── tool_presets.toml # placeholder for SLOP_GLOBAL_TOOL_PRESETS
└── personas.toml # placeholder for SLOP_GLOBAL_PERSONAS
SAFETY:
- The new
get_config_path()raisesKeyError-like behavior if no override AND no default exists. This is intentional — better to fail fast than silently use a wrong path. - The desktop GUI (
sloppy.pywithout--config) uses the default<project_root>/config.toml, which is unchanged behavior. - Tests ALWAYS use a path inside
./tests/(either from--configor the auto-generated default), so the Layer 1 audit hook's allowlist catches any stray writes. - Conftest's sys.argv parse happens BEFORE pytest's own argparse (which is too late for src/ imports).
- The
config_overrides.tomlnaming is a convention; tests CAN pass--config /some/other/path.tomland it will work.
FR3. Pytest tmp paths + isolate_workspace migration (Layer 2 — DEFAULT ON)
WHERE:
pyproject.toml— addaddopts = "--basetemp=tests/artifacts/_pytest_tmp"to[tool.pytest.ini_options].tests/conftest.py:isolate_workspace(lines 259-281) — replacetmp_path_factory.mktemp("isolated_workspace")withPath("tests/artifacts/_isolation_workspace") / _RUN_ID.tests/conftest.py:pytest_configure— defensive normalization: ifconfig._tmp_path_factory._basetempresolves outside./tests/, override totests/artifacts/_pytest_tmp.conductor/tech-stack.md— dated note explaining the--basetempchoice.
WHAT:
- All pytest
tmp_path/tmp_path_factoryfixtures create temp dirs under<project_root>/tests/artifacts/_pytest_tmp/. - The
isolate_workspaceautouse fixture's workspace lives under<project_root>/tests/artifacts/_isolation_workspace_<RUN_ID>/. - Both the
--basetemppath AND theisolate_workspacepath are inside./tests/, so the Layer 1 audit hook's allowlist can be a single rule: "anything under./tests/is allowed."
HOW:
pyproject.toml: standardaddoptsentry.isolate_workspace: replacetmp_path_factory.mktemp("isolated_workspace")withPath("tests/artifacts/_isolation_workspace") / _RUN_ID. AddSLOP_CREDENTIALS,SLOP_MCP_ENV,SLOP_GLOBAL_PRESETS,SLOP_GLOBAL_TOOL_PRESETS,SLOP_GLOBAL_PERSONAS,SLOP_GLOBAL_WORKSPACE_PROFILESenv vars pointing inside the isolation workspace. (Note: these are the OTHERSLOP_*env vars that the user is punting; they stay env-var-driven for now.)conftest.py:pytest_configure: defensive check for_tmp_path_factory._basetemp.tech-stack.md: dated note.
SAFETY: Update .gitignore to ensure tests/artifacts/_pytest_tmp/ and tests/artifacts/_isolation_workspace/ are covered (already covered by tests/artifacts/ pattern).
FR4. Extended static audit (Layer 4 — DEFAULT ON as CI gate)
WHERE: New file scripts/audit_test_sandbox_violations.py (extends scripts/check_test_toml_paths.py).
WHAT: Detect test source files that contain hardcoded write operations targeting paths outside ./tests/. Patterns:
Path("manual_slop.toml"),Path("config.toml"),Path("credentials.toml"),Path("presets.toml"),Path("personas.toml"),Path("tool_presets.toml"),Path("workspace_profiles.toml"),Path("manualslop_layout.ini"),Path("manual_slop_history.toml")Path("project.toml"),Path("manual_slop_history.toml")(top-level TOMLs)open("manual_slop.toml", "w")and similarPath("C:/projects/...")andPath("C:\\projects\\...")(project root references)Path("tests/artifacts/...")literal (violates workspace_paths.md; should use a fixture instead)Path(__file__).parent.parent / "config.toml"(and similar..traversal)tempfile.mkdtemp(),tempfile.mkstemp()(without adir=kwarg pointing to./tests/)
HOW: Mirror the existing check_test_toml_paths.py structure: list of compiled regexes + find_violations(root_dir) + main() with --strict CI mode.
SAFETY: The audit is INFORMATIONAL by default (exits 0). --strict mode (CI) exits 1 on any violation. This matches the precedent set by audit_no_temp_writes.py and check_test_toml_paths.py.
FR5. OS-level sandbox wrapper (Layer 3 — OPT IN)
WHERE: New file scripts/run_tests_sandboxed.ps1 (analogous to scripts/tier2/run_tier2_sandboxed.ps1).
WHAT: A PowerShell launcher that:
- Acquires a Windows restricted token (drops
SeDebugPrivilege,SeBackupPrivilege,SeRestorePrivilege,SeTakeOwnershipPrivilege, etc.) — same pattern asrun_tier2_sandboxed.ps1. - Sets the current directory to the project root.
- Wraps the pytest process tree in a Job Object so it cannot escape.
- Invokes
uv run python -m pytest(oruv run python scripts/run_tests_batched.py) under the restricted token with--basetemp=tests/artifacts/_pytest_tmp(Layer 2 + FR3 ensure tmp dirs are inside the sandbox). - Forwards
--config tests/artifacts/_isolation_workspace_<RUN_ID>/config_overrides.tomlso the test config is inside the sandbox. - Reports the exit code.
HOW: Copy the structure of scripts/tier2/run_tier2_sandboxed.ps1 (100 lines). Replace the OpenCode launch with a pytest launch. Keep the Job Object + restricted token machinery.
SAFETY: This is OPT-IN. Users who don't need OS-level enforcement continue to use uv run pytest or uv run python scripts/run_tests_batched.py directly and still get Layer 1 + Layer 2 + Layer 4 protection. The wrapper is for paranoid scenarios.
Note on Windows ACLs: The Tier 2 wrapper uses restricted-token; it does NOT use file ACLs. For tests, this is sufficient because (a) tests run as the same user, (b) the Python guard (Layer 1) is the primary defense, (c) restricted-token catches native code paths that bypass Python.
FR6. Regression tests for the guard
WHERE: New file tests/test_test_sandbox.py.
WHAT: Tests that verify:
test_sandbox_blocks_writes_outside_tests_dir: write to a hardcodedPath("manual_slop.toml")from a test →RuntimeErrorwith theTEST_SANDBOX_VIOLATIONprefix.test_sandbox_allows_writes_inside_tests_dir: write totmp_path / "foo.txt"→ succeeds.test_sandbox_allows_writes_inside_tests_artifacts: write toPath("tests/artifacts/_pytest_tmp_xxx/foo.txt")→ succeeds.test_sandbox_does_not_block_reads: read fromPath("pyproject.toml")→ succeeds.test_audit_test_sandbox_violations_flags_known_bad_pattern: create a temp test file withPath("manual_slop.toml").write_text(...), run the audit script as a subprocess, assert exit 1.test_audit_test_sandbox_violations_passes_clean_test: create a temp test file using onlytmp_path, assert exit 0.test_pyproject_toml_basetemp_is_under_tests: parsepyproject.toml, assertaddoptscontains--basetemp=tests/artifacts/_pytest_tmp.test_isolate_workspace_does_not_use_tmp_path_factory_for_infra: parsetests/conftest.py, assert notmp_path_factory.mktempcalls inisolate_workspace.test_appcontroller_init_does_not_load_config: parsesrc/app_controller.py, assertAppController.__init__does NOT callinit_state()orload_config(). Per the G7 audit: this guards the invariant that config reads happen AFTER fixtures apply.test_audit_flags_tempfile_mkdtemp_without_tests_dir: create a test file withtempfile.mkdtemp(), run audit, assert exit 1. Per user directive: tests should never need AppData temp.test_config_override_via_cli_flag: invokepython -c "..."with--config <path>and verifypaths.get_config_path()returns the override.test_paths_get_config_path_no_env_fallback: monkeypatch-deleteSLOP_CONFIGenv var, importsrc.paths, assertget_config_path()returns default (no env var lookup).test_sloppy_py_parses_config_flag: parsesloppy.pyAST, assert--configargparse argument exists and triggerspaths.set_config_override().
HOW: Standard pytest. Use subprocess.run for the audit script invocations to test the CLI surface. Use ast.parse for static checks on conftest.py, app_controller.py, and sloppy.py.
SAFETY: The test_sandbox_blocks_writes_outside_tests_dir test will raise RuntimeError — pytest must catch it as a pass. Use pytest.raises(RuntimeError, match="TEST_SANDBOX_VIOLATION").
FR7. Documentation update
WHERE: New file conductor/code_styleguides/test_sandbox.md. Update conductor/code_styleguides/workspace_paths.md. Update docs/guide_testing.md.
WHAT: Document:
- The
--configCLI flag convention (replacesSLOP_CONFIGenv var). - The
config_overrides.tomlnaming convention for test workspace configs. - The 4-layer enforcement model (Python guard, conftest isolation, OS-level wrapper, static audit).
- The
--basetemprule (why pytest tmp paths must live under./tests/). - The Layer 1 audit hook contract: writes outside
./tests/raiseRuntimeError. - The opt-in
scripts/run_tests_sandboxed.ps1wrapper. - The audit script and CI gate.
Non-Functional Requirements
- NFR1. Performance: The audit hook adds < 5% overhead to pytest run time (measured on the existing 11-tier suite). Conftest fixtures are unchanged in scope; only env-var setup is added.
- NFR2. Maintainability: No new
src/files (perAGENTS.mdFile Size and Naming Convention rule). The Python guard lives intests/conftest.py(test infrastructure). The audit script lives inscripts/(project infrastructure). The PowerShell wrapper lives inscripts/. - NFR3. Cross-platform: The Python guard (Layer 1) and the static audit (Layer 4) work on Windows, macOS, and Linux. The PowerShell wrapper (Layer 3) is Windows-only; on non-Windows it's a documented no-op (
Write-Host "OS-level sandbox requires Windows"and exit 0). - NFR4. Discoverability: The audit script's
--helpexplains what it checks, how to fix violations, and how to run in--strictmode. TheRuntimeErrorraised by Layer 1 includes a "How to fix" line pointing atconductor/code_styleguides/test_sandbox.md.
Architecture Reference
conductor/code_styleguides/workspace_paths.md— existing rule: test workspaces live undertests/artifacts/. This track extends it to ALL test infrastructure (including pytest'stmp_path).conductor/code_styleguides/feature_flags.md— Layer 1 + Layer 2 + Layer 4 are file-presence-on = enabled (matches the project's "delete to turn off" convention for cross-cutting concerns). Layer 3 is opt-in via the explicit PowerShell wrapper.scripts/audit_no_temp_writes.py— pattern reference for the newscripts/audit_test_sandbox_violations.py.scripts/tier2/run_tier2_sandboxed.ps1— pattern reference for the newscripts/run_tests_sandboxed.ps1.docs/guide_testing.md(existing) — test infrastructure deep-dive. Add a new "Sandbox Hardening" section that summarizes Layers 1-4 and links to the styleguide.conductor/tracks/workspace_path_finalize_20260609/— prior track that established thetests/artifacts/workspace pattern. This track extends it.
Out of Scope
- Reading protection. Tests still need to read the source tree (
src/,pyproject.toml, etc.) for fixtures. Reads are intentionally NOT blocked. If a future track wants read isolation, it's a separate scope. - Network sandboxing. Tests that hit the live Gemini/Anthropic/etc. APIs continue to do so. The user's data loss is filesystem, not network.
- Migrating existing tests to the new patterns. The audit (Layer 4) catches new violations; existing tests that already pass continue to pass. If the audit catches a currently-passing test, that's a bug to fix in the test (separate, in-session fixes).
- Converting the OTHER
SLOP_*env vars to CLI flags (SLOP_GLOBAL_PRESETS,SLOP_GLOBAL_TOOL_PRESETS,SLOP_GLOBAL_PERSONAS,SLOP_GLOBAL_WORKSPACE_PROFILES,SLOP_CREDENTIALS,SLOP_MCP_ENV,SLOP_LOGS_DIR,SLOP_SCRIPTS_DIR). Per user directive, this is the "mess" to address in follow-up tracks. This track only eliminatesSLOP_CONFIG. The test workspace still uses the other env vars to redirect to per-run paths under./tests/artifacts/. - A cross-platform equivalent of
run_tests_sandboxed.ps1. macOS/Linux users get Layer 1 + Layer 2 + Layer 4. Adding arun_tests_sandboxed.shwould requirebwraporunsharesetup; defer to a future track if needed. - Conftest fixture-level enforcement (e.g.,
@pytest.fixture(sandbox_strict=True)for tests that need full OS isolation). The blanket autouse fixture is the v1. Per-fixture tuning is a v2 feature.
Verification Criteria
For the track to be marked complete, ALL of the following must be true:
- VC1.
tests/test_test_sandbox.pyexists and all 13 tests pass. - VC2.
scripts/audit_test_sandbox_violations.pyruns in both modes:- Default (informational): exit 0, lists violations (or says "clean").
--strict: exit 1 on violation, exit 0 on clean.
- VC3.
pyproject.tomlcontainsaddopts = "--basetemp=tests/artifacts/_pytest_tmp"under[tool.pytest.ini_options]. - VC4.
tests/conftest.py:isolate_workspaceno longer callstmp_path_factory.mktemp(perworkspace_paths.md). All env-var redirects point to paths inside./tests/artifacts/. - VC5.
scripts/run_tests_sandboxed.ps1exists, parses cleanly, and on Windows can be invoked (-WhatIfmode for dry-run). - VC6.
conductor/tech-stack.mdhas a dated note explaining the--basetempchoice. - VC7.
conductor/code_styleguides/workspace_paths.md(or newtest_sandbox.md) documents the 3-layer model. - VC8. Full test suite:
uv run python scripts/run_tests_batched.py --tiers 1,2,3,4,5,6,7,8,9,10,11runs to completion; no regression in pass rate vs. the pre-track baseline (1288 passed + 4 xdist-skipped perresult_migration_small_files_20260617). - VC9. No new
@pytest.mark.skipmarkers added (perconductor/workflow.mdSkip-Marker Policy + user directive). - VC10. End-of-track report at
docs/reports/TRACK_COMPLETION_test_sandbox_hardening_20260619.md(per Tier 2 conventions).
Risk Assessment
| Risk | Likelihood | Mitigation |
|---|---|---|
Layer 1 audit hook breaks a test that legitimately writes outside ./tests/ (e.g., a test that writes to a tempfile.mkdtemp default location) |
medium | FR1 allowlist includes pytest's --basetemp; if a new path is needed, add it. The hook's RuntimeError includes the test name so the offending test is obvious. |
| Layer 1 audit hook slows down the test suite | low | sys.addaudithook is a thin C-level callback; overhead measured in <2% per Python docs. |
| Layer 4 audit flags a currently-passing test as a false positive | medium | The audit is INFORMATIONAL by default; --strict is opt-in for CI. If a real test is flagged, fix the test (don't suppress the audit). |
| Layer 3 PowerShell wrapper breaks on a Windows version without the required privileges | low | Wrapper is opt-in; default invocation stays uv run pytest. Wrapper docs explain the privilege requirements. |
Existing tests that don't go through isolate_workspace still read real config files |
high (known gap) | Reads are out of scope per the Out of Scope section. Layer 1 still blocks writes, which is the user's primary concern. |
pytest_configure setting _tmp_path_factory._basetemp uses a private API that changes between versions |
medium | The --basetemp addopts is the primary mechanism. The _basetemp assignment is defensive only; if it breaks, addopts still works. |
Execution Plan (high-level — see plan.md for worker-ready tasks)
- Phase 1: Investigation + baseline — capture current pass count, confirm
isolate_workspace+ audit script work as documented. - Phase 2: Layer 4 (static audit) + tests — write
audit_test_sandbox_violations.py, writetest_test_sandbox.pyaudit-tests (parts 5-8), commit. - Phase 3: Layer 1 (Python guard) + tests — implement
_enforce_test_sandboxfixture, write guard-specific regression tests (parts 1-4), commit. - Phase 4: Layer 2 (
isolate_workspacemigration + FR3 basetemp) — refactorisolate_workspace, addaddoptstopyproject.toml, updatetech-stack.md, commit. - Phase 5: Layer 3 (PowerShell wrapper) — write
scripts/run_tests_sandboxed.ps1, write a smoke test, commit. - Phase 6: Documentation — update
workspace_paths.md(or writetest_sandbox.md), updatedocs/guide_testing.md, commit. - Phase 7: Full suite verification — run all 11 tiers, verify no regression, commit.
- Phase 8: End-of-track report — write
docs/reports/TRACK_COMPLETION_test_sandbox_hardening_20260619.md, commit.
See Also
conductor/tracks.md:395— prior "Test Consolidation & TOML Sandboxing" track (addedcheck_test_toml_paths.py).conductor/archive/workspace_path_finalize_20260609/— prior track that established thetests/artifacts/workspace pattern.conductor/tracks/tier2_autonomous_sandbox_20260616/— meta-tooling track that this sandbox mirrors.conductor/code_styleguides/workspace_paths.md— existing test-workspace rule.conductor/code_styleguides/feature_flags.md— file-presence = enabled convention.conductor/workflow.mdSkip-Marker Policy + Skip-Marker review checklist.docs/guide_testing.md— existing test infrastructure deep-dive (add a "Sandbox Hardening" section).scripts/audit_no_temp_writes.py— pattern reference for the new audit script.scripts/tier2/run_tier2_sandboxed.ps1— pattern reference for the new PowerShell wrapper.