Problem
src/bmad_loop/workspace.py::_rmtree_confined resolves both the run worktree root and the candidate worktree before deciding whether shutil.rmtree is confined, but its refusal boundary catches only ValueError and OSError. On Python 3.11/3.12 a resolution loop can raise RuntimeError, so that provider failure escapes the cleanup helper instead of returning False.
This is a same-family follow-up from the #556 audit. The live callers are fallback cleanup paths reached from close_unit_workspace and discard_worktree; an escaping exception can abort worktree teardown even though uncertain containment should refuse deletion.
Reproduction
- Monkeypatch either
unit_worktrees_dir(run_dir).resolve() or wt.resolve() to raise RuntimeError("symlink loop").
- Call
_rmtree_confined(wt, run_dir).
- Observe the
RuntimeError escape because the current except (ValueError, OSError) does not own it.
The regression test should cover both resolution sites and assert that shutil.rmtree is never called when either path is uncertain.
Scope
- Catch
RuntimeError at this destructive confinement boundary alongside ValueError and OSError.
- Preserve the existing refusal contract: return
False without deleting anything.
- Keep the change limited to
_rmtree_confined and its direct tests; provisioning, mount opening, and other path-resolution families have separate ownership.
Problem
src/bmad_loop/workspace.py::_rmtree_confinedresolves both the run worktree root and the candidate worktree before deciding whethershutil.rmtreeis confined, but its refusal boundary catches onlyValueErrorandOSError. On Python 3.11/3.12 a resolution loop can raiseRuntimeError, so that provider failure escapes the cleanup helper instead of returningFalse.This is a same-family follow-up from the #556 audit. The live callers are fallback cleanup paths reached from
close_unit_workspaceanddiscard_worktree; an escaping exception can abort worktree teardown even though uncertain containment should refuse deletion.Reproduction
unit_worktrees_dir(run_dir).resolve()orwt.resolve()to raiseRuntimeError("symlink loop")._rmtree_confined(wt, run_dir).RuntimeErrorescape because the currentexcept (ValueError, OSError)does not own it.The regression test should cover both resolution sites and assert that
shutil.rmtreeis never called when either path is uncertain.Scope
RuntimeErrorat this destructive confinement boundary alongsideValueErrorandOSError.Falsewithout deleting anything._rmtree_confinedand its direct tests; provisioning, mount opening, and other path-resolution families have separate ownership.