sand-boxer/tests/test_bwrap.py
tegwick 76c38e758c
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Implement ext.bwrap: local bubblewrap namespace extension (SAND-WP-0013)
Adds the first local, same-host, kernel-namespace-only sandbox
extension: no SSH hop, no container runtime. Extends IsolationSpec.level
with "process", implements BwrapExtension (provision/wait_ready/
teardown) spawning bwrap with unshared user/mount/pid/ipc/uts/net
namespaces, registers ext.bwrap + profile.bwrap-local, and extends
manager._handle_from_status to carry pid/workspace_dir. Verified with a
live bwrap smoke run in addition to the mocked test suite.

T04 (reachability vs. the SSH-based glas-harness consumer contract)
deliberately left open pending glas-harness's harness contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 12:37:56 +02:00

166 lines
5.5 KiB
Python

"""ext.bwrap — local namespace isolation extension."""
from unittest.mock import MagicMock, patch
import pytest
from sandboxer.extensions.base import SandboxExtension
from sandboxer.extensions.bwrap import BwrapExtension
from sandboxer.models import IsolationSpec, Profile
def _profile() -> Profile:
return Profile.model_validate(
{
"id": "profile.bwrap-local",
"version": "1.0.0",
"extension": "ext.bwrap",
"isolation": {"level": "process"},
}
)
def test_is_sandbox_extension_subclass() -> None:
assert issubclass(BwrapExtension, SandboxExtension)
def test_process_is_a_valid_isolation_level() -> None:
assert IsolationSpec(level="process").level == "process"
def test_bwrap_bin_env_override(monkeypatch) -> None:
monkeypatch.setenv("SANDBOXER_BWRAP_BIN", "/custom/bwrap")
ext = BwrapExtension({"bwrap_bin": "bwrap"})
assert ext._bwrap_bin() == "/custom/bwrap"
def test_bwrap_argv_unshares_net_and_binds_workspace(tmp_path) -> None:
ext = BwrapExtension({"ro_binds": []})
argv = ext._bwrap_argv(str(tmp_path))
assert "--unshare-net" in argv
assert "--unshare-user" in argv
assert str(tmp_path) in argv
assert argv[-2:] == ["sleep", "infinity"]
def test_existing_ro_binds_filters_missing_paths(tmp_path) -> None:
real_dir = tmp_path / "real"
real_dir.mkdir()
ext = BwrapExtension({"ro_binds": [str(real_dir), "/definitely/does/not/exist"]})
assert ext._existing_ro_binds() == [str(real_dir)]
def test_provision_spawns_bwrap_and_returns_handle(tmp_path) -> None:
ext = BwrapExtension({"base_dir": str(tmp_path)})
fake_proc = MagicMock()
fake_proc.pid = 12345
with patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc) as popen:
handle = ext.provision(_profile(), {"sandbox_id": "abc12345"}, "localhost")
popen.assert_called_once()
assert handle["sandbox_id"] == "abc12345"
assert handle["host"] == "localhost"
assert handle["pid"] == "12345"
assert handle["workspace_dir"].endswith("abc12345")
def test_provision_copies_repo_into_workspace(tmp_path) -> None:
repo = tmp_path / "repo"
repo.mkdir()
(repo / "file.txt").write_text("hello")
base_dir = tmp_path / "sandboxes"
ext = BwrapExtension({"base_dir": str(base_dir)})
fake_proc = MagicMock()
fake_proc.pid = 1
with patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc):
handle = ext.provision(
_profile(), {"sandbox_id": "cafe1234", "repo": str(repo)}, "localhost"
)
assert (base_dir / "cafe1234" / "file.txt").read_text() == "hello"
assert handle["workspace_dir"] == str(base_dir / "cafe1234")
def test_provision_missing_repo_raises(tmp_path) -> None:
ext = BwrapExtension({"base_dir": str(tmp_path)})
with pytest.raises(FileNotFoundError):
ext.provision(
_profile(),
{"sandbox_id": "x", "repo": str(tmp_path / "missing")},
"localhost",
)
def test_wait_ready_checks_process_and_workspace(tmp_path) -> None:
workspace = tmp_path / "ws"
workspace.mkdir()
ext = BwrapExtension()
handle = {"pid": str(1), "workspace_dir": str(workspace), "host": "localhost"}
with patch.object(BwrapExtension, "_pid_alive", return_value=True):
result = ext.wait_ready(handle)
assert result["host"] == "localhost"
assert result["endpoint"] == "pid:1"
def test_wait_ready_raises_if_process_dead(tmp_path) -> None:
workspace = tmp_path / "ws"
workspace.mkdir()
ext = BwrapExtension()
handle = {"pid": "999999", "workspace_dir": str(workspace), "host": "localhost"}
with patch.object(BwrapExtension, "_pid_alive", return_value=False):
with pytest.raises(RuntimeError, match="not running"):
ext.wait_ready(handle)
def test_wait_ready_raises_if_workspace_missing(tmp_path) -> None:
ext = BwrapExtension()
handle = {"pid": "1", "workspace_dir": str(tmp_path / "gone"), "host": "localhost"}
with patch.object(BwrapExtension, "_pid_alive", return_value=True):
with pytest.raises(RuntimeError, match="workspace missing"):
ext.wait_ready(handle)
def test_teardown_kills_process_group_and_removes_workspace(tmp_path) -> None:
workspace = tmp_path / "ws"
workspace.mkdir()
ext = BwrapExtension()
handle = {"pid": "42", "workspace_dir": str(workspace), "host": "localhost"}
with (
patch.object(BwrapExtension, "_pid_alive", return_value=True),
patch("sandboxer.extensions.bwrap.os.getpgid", return_value=42),
patch("sandboxer.extensions.bwrap.os.killpg") as killpg,
):
result = ext.teardown(handle)
killpg.assert_called_once_with(42, __import__("signal").SIGKILL)
assert result["process_killed"] == "True"
assert result["workspace_removed"] == "True"
assert not workspace.exists()
def test_teardown_handles_already_dead_process(tmp_path) -> None:
workspace = tmp_path / "ws"
workspace.mkdir()
ext = BwrapExtension()
handle = {"pid": "42", "workspace_dir": str(workspace), "host": "localhost"}
with patch.object(BwrapExtension, "_pid_alive", return_value=False):
result = ext.teardown(handle)
assert result["process_killed"] == "False"
assert result["workspace_removed"] == "True"
def test_supports_snapshots_is_false() -> None:
ext = BwrapExtension()
assert ext.supports_snapshots() is False
with pytest.raises(NotImplementedError):
ext.snapshot({})