diff --git a/apodex/sandbox.py b/apodex/sandbox.py index 9a501c9..4b0a933 100644 --- a/apodex/sandbox.py +++ b/apodex/sandbox.py @@ -211,7 +211,7 @@ def set_active_strategy(strategy: Strategy) -> None: # ── execution ──────────────────────────────────────────────────────────── -_bwrap_sandbox = None # one jail per process; commands are cheap, setup is not +_bwrap_sandbox: tuple[str, Any] | None = None # one-entry cache keyed by workspace def _get_bwrap_sandbox(cwd: str) -> Any: @@ -223,11 +223,14 @@ def _get_bwrap_sandbox(cwd: str) -> Any: into it. """ global _bwrap_sandbox - if _bwrap_sandbox is None: + real = str(Path(cwd).expanduser().resolve()) + if _bwrap_sandbox is None or _bwrap_sandbox[0] != real: from plugins.tools._sandbox import BwrapSandbox - real = str(Path(cwd).expanduser().resolve()) - _bwrap_sandbox = BwrapSandbox(workspace=real, binds=((real, real, False),)) - return _bwrap_sandbox + sandbox = BwrapSandbox(workspace=real, binds=((real, real, False),)) + if _bwrap_sandbox is not None: + _bwrap_sandbox[1].kill() + _bwrap_sandbox = (real, sandbox) + return _bwrap_sandbox[1] async def run_shell( diff --git a/apodex/tests/test_native.py b/apodex/tests/test_native.py index f2b8890..20c1a88 100644 --- a/apodex/tests/test_native.py +++ b/apodex/tests/test_native.py @@ -4,7 +4,7 @@ import os from pathlib import Path -from apodex import cli, docker +from apodex import cli, docker, sandbox from apodex.native import prepare_native_runtime from apodex.sandbox import BWRAP, CONTAINER, NATIVE, Strategy, resolve_strategy from plugins.tools._sandbox import resolve_runtime_path @@ -168,6 +168,37 @@ def test_configured_bwrap_backend_remains_explicit(monkeypatch) -> None: assert strategy.reason == "SANDBOX_BACKEND=bwrap" +def test_bwrap_sandbox_rebuilds_when_workspace_changes( + tmp_path, monkeypatch, +) -> None: + class FakeBwrapSandbox: + def __init__(self, *, workspace, binds): + self.workspace = workspace + self.binds = binds + self.killed = False + + def kill(self): + self.killed = True + + first_workspace = tmp_path / "first" + second_workspace = tmp_path / "second" + first_workspace.mkdir() + second_workspace.mkdir() + monkeypatch.setattr("plugins.tools._sandbox.BwrapSandbox", FakeBwrapSandbox) + monkeypatch.setattr(sandbox, "_bwrap_sandbox", None) + + first = sandbox._get_bwrap_sandbox(str(first_workspace)) + same = sandbox._get_bwrap_sandbox(str(first_workspace / ".." / "first")) + second = sandbox._get_bwrap_sandbox(str(second_workspace)) + + assert same is first + assert second is not first + assert first.killed is True + assert second.killed is False + assert second.workspace == str(second_workspace.resolve()) + assert second.binds == ((str(second_workspace.resolve()),) * 2 + (False,),) + + def test_macos_falls_back_to_native_when_docker_is_unavailable( tmp_path, monkeypatch, capsys, ) -> None: