diff --git a/nanobot/agent/tools/sandbox.py b/nanobot/agent/tools/sandbox.py index 25f869daa..459ce16a3 100644 --- a/nanobot/agent/tools/sandbox.py +++ b/nanobot/agent/tools/sandbox.py @@ -8,14 +8,19 @@ and register it in _BACKENDS below. import shlex from pathlib import Path +from nanobot.config.paths import get_media_dir + def _bwrap(command: str, workspace: str, cwd: str) -> str: """Wrap command in a bubblewrap sandbox (requires bwrap in container). Only the workspace is bind-mounted read-write; its parent dir (which holds - config.json) is hidden behind a fresh tmpfs. + config.json) is hidden behind a fresh tmpfs. The media directory is + bind-mounted read-only so exec commands can read uploaded attachments. """ ws = Path(workspace).resolve() + media = get_media_dir().resolve() + try: sandbox_cwd = str(ws / Path(cwd).resolve().relative_to(ws)) except ValueError: @@ -33,6 +38,7 @@ def _bwrap(command: str, workspace: str, cwd: str) -> str: "--tmpfs", str(ws.parent), # mask config dir "--dir", str(ws), # recreate workspace mount point "--bind", str(ws), str(ws), + "--ro-bind-try", str(media), str(media), # read-only access to media "--chdir", sandbox_cwd, "--", "sh", "-c", command, ] diff --git a/tests/tools/test_sandbox.py b/tests/tools/test_sandbox.py index 315bcf7c8..82232d83e 100644 --- a/tests/tools/test_sandbox.py +++ b/tests/tools/test_sandbox.py @@ -92,6 +92,22 @@ class TestBwrapBackend: assert "/bin" in try_targets assert "/etc/ssl/certs" in try_targets + def test_media_dir_ro_bind(self, tmp_path, monkeypatch): + """Media directory should be read-only mounted inside the sandbox.""" + fake_media = tmp_path / "media" + fake_media.mkdir() + monkeypatch.setattr( + "nanobot.agent.tools.sandbox.get_media_dir", + lambda: fake_media, + ) + ws = str(tmp_path / "project") + result = wrap_command("bwrap", "ls", ws, ws) + tokens = _parse(result) + + try_indices = [i for i, t in enumerate(tokens) if t == "--ro-bind-try"] + try_pairs = {(tokens[i + 1], tokens[i + 2]) for i in try_indices} + assert (str(fake_media), str(fake_media)) in try_pairs + class TestUnknownBackend: def test_raises_value_error(self, tmp_path):