# Issue #17 WORKSPACE_ROOT test isolation — Codex review Job: 9bc1f200 Loop: loop-20261008T133858Z-9e7f5b Base: 6b00b6e06f55299cb60f156ccef09ede4bae5bdd Head: d198dd50aaf6119dc21f36d88704e64850e67413 Tree: 114ec21d3fc7de8932f5e1d34f895edd24ad7c01 Validation path: /var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261008T133858Z-9e7f5b/1 ## Conclusion NOT PASS. The local _env fix and its new regression are correct, but task item 2 remains incomplete: equivalent unsafe test helpers are still present, including paths not protected by mam_sandbox and a cached environment that bypasses that fixture. These are independently reproduced isolation defects, not a verdict based on sandbox restrictions. Product-code changes are not required. ## R1 — other helpers still write into an inherited workspace (P2) Evidence: - tests/test_b19_headless_reconcile_fixes.py:124-134 copies os.environ and sources the checkout's lib.sh before replacing _init_herdr_isolation with a no-op. That replacement is too late: source-time bootstrap has already written the shim. Several callers, e.g. test_fullscreen_tip_is_not_a_blocking_dialog, do not request mam_sandbox. - tests/test_loop_cli.py:13-23 copies os.environ and executes the checkout's run_loop.sh. Its test_help_lists_creator_not_target_agent (:30) has no mam_sandbox. run_loop.sh:17 sources lib.sh even for --help. - lib.sh:26 defaults an absent WORKSPACE_ROOT to the checkout, and :144-152 creates .mam/shim and writes a shim temporary file. Thus merely deleting the inherited variable is insufficient. Reproduction, performed only with temporary directories: 1. Scrub inherited HERDR_*, MAM_*, MQTT_*, SKS_* and control-plane bindings, set HOME to a temporary directory, and set WORKSPACE_ROOT to a new empty temporary sentinel. 2. Import test_b19_headless_reconcile_fixes and call _run_lib_helpers('echo probe-ok'). 3. In a fresh sentinel, import test_loop_cli and call _run_loop(['--help']). 4. Expected: sentinel remains empty. Actual: both create sentinel/.mam/shim/herdr; the helper exits 0 and the help invocation exits its normal 1. A normal pytest session does not solve the unset case: tests/conftest.py:138-146 drops an external root at configure time, but _isolate_mam_state_dirs (:161-178) sets only registry/log/env-file paths. Only mam_sandbox (:215) pins WORKSPACE_ROOT. These callers do not use that fixture, so the checkout fallback remains. The state guard snapshots jobs and audit logs, not shim files (tests/_mam_state_guard.py:79-86). Fix direction: establish a temporary per-test WORKSPACE_ROOT before every source/bootstrap call, preferably via the common function-scoped isolation fixture for ordinary runtime environment copies; preserve later explicit test overrides. Helpers intended for direct/--noconftest use should also build their environment from their own temporary path, as the revised Issue #17 helper does. Cover the candidates below, add sentinel checks, and audit intentional unset-workspace tests separately instead of changing their behavior blindly. ## R2 — onboard's import-time environment bypasses later fixture isolation (P2) Evidence: tests/test_orc_onboard.py:15 captures SCRUBBED_ENV at import; :22, :34, :47 and :65 reuse it. The helpers select temporary YAML/HOME_DIR paths but never set WORKSPACE_ROOT before sourcing the original LIB_SH (:41, :55, :64). Reproduction: 1. Set WORKSPACE_ROOT to an empty temporary sentinel before importing test_orc_onboard. 2. After import, set os.environ['WORKSPACE_ROOT'] to a different temporary fixture workspace, as mam_sandbox would. 3. Call run_find_uuid(str(fixture_workspace), 'claude') with no real user data or agent session. 4. Expected: the sentinel stays empty and any bootstrap shim belongs to the fixture workspace. Actual: the cached sentinel receives .mam/shim/herdr, and the fixture workspace receives no shim. With normal pytest's configure-time scrub, the cached dictionary instead lacks WORKSPACE_ROOT; later mam_sandbox updates still cannot change that dictionary, so lib.sh falls back to the checkout. This needs explicit treatment even if R1 is solved with a function-scoped autouse fixture. Fix direction: preserve the intended removal of agent identity variables, but set WORKSPACE_ROOT explicitly from ws/cwd/mam_sandbox in every sourced-shell environment, including dump_state and the inline shell call sites. Alternatively build the scrubbed environment at invocation time after fixtures run, still pinning the temporary root. Add a regression where import-time and invocation-time roots differ. Probe source: /tmp/9bc1f200-probes.py. All three reproduced leaked shims are under /private/tmp/9bc1f200-probe-z4ewh5mh; no real workspace was used as a sentinel. ## Candidate audit I searched tests for os.environ copies/comprehensions, WORKSPACE_ROOT, source/lib.sh, wrapper and run_loop.sh calls, then checked the caller fixtures and environment overrides. | Candidate / file:line | Disposition and reason | | --- | --- | | test_issue17_item4_decisions.py:224 | Fixed correctly: pins tmp_path; extra explicit overrides still apply last. | | test_b19_headless_reconcile_fixes.py:124, :329, :369, :430 | Unfixed; R1 helper and direct shell calls source original lib.sh without a temporary root. | | test_loop_cli.py:13 | Unfixed; R1. Later _loop_env at :91 pins a sandbox, but early CLI tests do not use it. | | test_issue5_phase3_loop.py:145, :154, :172 | Same unfixed bootstrap class: checkout run_loop.sh with ambient environment and no mam_sandbox. | | test_issue5_phase4_runtime.py:126, :152 | Same unfixed class in run_loop.sh help/freeze checks. tmp_path is available but not bound to WORKSPACE_ROOT. | | test_o3_scoped_guard.py:376, :412, :458 | Same unfixed class in checkout run_loop.sh invocations; --help also sources lib.sh. loop_lock-only snippets are a separate, non-shim path. | | test_version_consistency.py:28, :131 | Same unfixed class: version checks source original LIB_SH with implicit/copied environment, without mam_sandbox. | | test_workspace_scope.py:22-30 | Same unfixed class: unittest creates a temporary project but does not bind WORKSPACE_ROOT before two sources of original lib.sh. | | test_b8_send_keys_verification.py:5-34 | Same unfixed class: tmp_path argument is unused for workspace binding before source. | | test_issue6_fail_closed.py:1220-1244 | Same unfixed class: the direct source in the pinned-token test binds MAM_JOBS_DIR but not WORKSPACE_ROOT. Other _env-based cases in this file bind cwd explicitly. | | test_orc_onboard.py:15-65 | Unfixed cached environment; R2. A later mam_sandbox fixture alone does not protect it. | | test_issue33_budget.py:43-47; test_issue14_delivery_ack.py:33-36; test_issue18_liveness_contract.py:55-58 | No equivalent defect in these helpers: explicitly pin home/cwd as WORKSPACE_ROOT. | | test_issue34_job_tree.py:51-64; test_issue36_prompt_transport.py:48-61; test_issue26_envfile.py:74-76; test_issue26_ints.py:123-124; test_issue30_stall_watch_stop.py:30-32 | Explicitly pin temporary home/workspace. Later Issue #34 _run_lib users request mam_sandbox. | | test_issue21_phase2_loop_a.py:49, :59 | Explicit temporary root override. | | test_issue21_worktree_prereqs.py:41-55 | Removal is intentional for publisher-command tests that must operate without workspace bindings; actual _dry_submit callers at :92, :114, :143 supply temporary WORKSPACE_ROOT. Not a blanket reason to change this helper. | | test_issue5_phase1_delegation.py:49; test_tier1_unit.py:14; tier2/3/4 helpers; test_issue19_subscriber_daemon_lifecycle.py:52 | Ordinary helpers consume mam_sandbox's runtime environment; fixture :215 pins the workspace. Unlike onboard, these copies are made at invocation time. | | test_issue9_server_env.py:84, :108; test_issue23_24_startup_path.py:36; test_a4_adapter_contract.py:558 | Source/bootstrap callers use mam_sandbox; intentional server-marker tests retain that temporary workspace binding. | | test_issue45_restore.py:55, :179 | rig depends on mam_sandbox, then creates disposable HOME/server state. | | test_issue45_herdr_config.py:302; test_issue36_prompt_transport.py:427 | Python config CLI or diff_collect/lib_int paths; do not bootstrap lib.sh's shim. | | test_deploy_layout.py:16 and deploy tests | Installer uses explicit temporary installation targets; it does not source lib.sh for bootstrap. | | test_issue5_phase2a_loop.py:750-757 | The source invocation is a string for a static scanner test, not executed. | | test_issue20_state_isolation.py:199-201 | Explicit temporary WORKSPACE_ROOT for its source probe. | The unfixed candidates above are within the requested same-defect audit scope; they are not unrelated cleanup requests. ## Local fix and verification Setting WORKSPACE_ROOT to tmp_path, rather than deleting it, is the correct local choice because lib.sh derives its default from the library's location, not HOME or cwd. The new test validates the empty sentinel, unchanged checkout-shim mtime, expected shell workspace and actual shim in the test root. The exact base _env function was loaded from git and substituted only in memory into the current test module. The new regression failed with "written into the sentinel" and the sentinel shim existed. With head _env it passed and no sentinel shim existed. This negative control changed no repository files. - Changed file, WORKSPACE_ROOT absent at pytest launch: **286 passed, 4 deselected in 262.11s**; exit 0. Log: /tmp/9bc1f200-unset.log. - Same file, WORKSPACE_ROOT set to an existing external temporary sentinel: **286 passed, 4 deselected in 262.02s**; exit 0. Log: /tmp/9bc1f200-sentinel.log. - Both runs used --noconftest, -p no:cacheprovider and separate external basetemp directories, with no plugin/fixture pinning WORKSPACE_ROOT. This tests the helper's fix itself. - At completion, /private/tmp/9bc1f200-unset-7yqs28zl/sentinel and /private/tmp/9bc1f200-sentinel-otbahzmr/sentinel each contained **zero entries**. - Supplementary tests: 13 broker-independent issue33_budget cases plus the wrapper mapping contract from test_issue5_phase1_delegation and the G10 mapping contract from test_tier1_unit: **15 passed, 7 deselected in 29.57s**. Log: /tmp/9bc1f200-other.log. Only the two contract tests used a minimal temporary mam_sandbox fixture; this is not a full-suite pass claim for those two files. - Full broker-dependent suites were not executed in the restricted environment; the orchestrator retains their integration validation. - Independent negative-control regression and the three omitted-helper probes completed successfully, producing the results detailed above. - The changed Python file parses on Python 3.9; git diff --check passes. Final git status --porcelain was empty and HEAD/tree still matched the brief. The four unreaped-owner cases were deselected because this sandbox blocks ps, as independently diagnosed in the earlier review. The brief permits network-free review where broker integration is blocked. Neither these omissions nor MQTT publication restrictions are the reason for NOT PASS. ## Scope and lifecycle Only tests/test_issue17_item4_decisions.py changed (36 insertions, 2 deletions). Product files, versions and branches are unchanged. All test runs and probes used the designated review path, Python 3.9 and native Bash 3.2; artifacts, test registries, HOME and all sentinel roots were temporary. Inherited control-plane/server variables were scrubbed. No .mam.env was exported, no real agent/server was contacted, and no reviewer source edits were made. The requested started command was executed first; MQTT exhausted three attempts with Operation not permitted and exited 2. The requested completed command was also executed after saving the report; all three MQTT attempts failed with Operation not permitted and the command exited 2. The report is saved, but successful delivery of started/completed to the broker is not confirmed. [VERDICT: NOT PASS]