sand-boxer/tests/test_manager.py
tegwick d79e3fe358 Add owner-mediated bwrap execution boundary
Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
2026-09-04 22:12:19 +02:00

242 lines
7.7 KiB
Python

"""SandboxManager unit tests with mocked backend."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
from threading import Lock
from unittest.mock import patch
import pytest
from sandboxer.core.manager import SandboxManager
from sandboxer.lifecycle.store import SandboxStore
from sandboxer.models import (
ActorType,
Consumer,
SandboxCreateRequest,
SandboxExecRequest,
SandboxState,
SandboxStatus,
)
class FakeBackend:
def provision(self, profile, inputs, host):
return {
"sandbox_id": "test1234",
"host": host,
"remote_dir": "/tmp/sandboxer/test1234",
"compose_project": "sbx-e2e-test1234",
"compose_file": "docker-compose.yml",
"ssh_user": "root",
}
def wait_ready(self, handle):
return {
"ssh": f"root@{handle['host']}",
"remote_dir": handle["remote_dir"],
"compose_project": handle["compose_project"],
"host": handle["host"],
}
def teardown(self, handle):
return {
"compose_removed": "True",
"remote_dir_removed": "True",
"remote_dir": handle["remote_dir"],
}
class FakeExecBackend:
def supports_execution(self):
return True
def execute(self, handle, command, **kwargs):
return {
"exit_code": 0,
"timed_out": False,
"stdout": "inside\n",
"stderr": "",
"output_truncated": False,
"workspace_dir": handle["workspace_dir"],
}
class FakeNonExecBackend:
def supports_execution(self):
return False
@pytest.fixture
def store(tmp_path: Path) -> SandboxStore:
return SandboxStore(path=tmp_path / "sandboxes.json")
def test_create_and_destroy(store: SandboxStore) -> None:
manager = SandboxManager(store=store)
request = SandboxCreateRequest(
profile="profile.compose-e2e",
inputs={"repo": "/tmp/repo"},
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
)
fake = FakeBackend()
with (
patch("sandboxer.core.manager.resolve_backend", return_value=fake),
patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None),
patch("sandboxer.core.manager.resolve_host", return_value="coulombcore"),
):
status = manager.create(request)
assert status.state == SandboxState.READY
assert status.sandbox_id == "test1234"
destroyed = manager.destroy(status.sandbox_id)
assert destroyed.state == SandboxState.DESTROYED
def test_destroy_idempotent(store: SandboxStore) -> None:
now = datetime.now(UTC)
status = SandboxStatus(
sandbox_id="gone1234",
profile_id="profile.compose-e2e",
extension_id="ext.compose-ssh",
state=SandboxState.DESTROYED,
consumer=Consumer(actor=ActorType.ADM, project="sand-boxer"),
created_at=now,
updated_at=now,
destroyed_at=now,
)
store.save(status)
manager = SandboxManager(store=store)
result = manager.destroy("gone1234")
assert result.state == SandboxState.DESTROYED
def _ready_bwrap_status(*, state: SandboxState = SandboxState.READY) -> SandboxStatus:
now = datetime.now(UTC)
return SandboxStatus(
sandbox_id="bwrap123",
profile_id="profile.bwrap-local",
extension_id="ext.bwrap",
state=state,
consumer=Consumer(
actor=ActorType.AGT,
project="glas-harness",
session_id="session-1",
run_id="run-1",
),
host="localhost",
inputs={"pid": "42", "workspace_dir": "/tmp/sandboxer-bwrap/bwrap123"},
expires_at=now + timedelta(hours=1),
created_at=now,
updated_at=now,
ready_at=now,
)
def _exec_request(**consumer_overrides) -> SandboxExecRequest:
consumer = {
"actor": "agt",
"project": "glas-harness",
"session_id": "session-1",
"run_id": "run-1",
**consumer_overrides,
}
return SandboxExecRequest(
command=["python3", "-V"],
consumer=Consumer.model_validate(consumer),
credential_route_refs=["rein-openweights-openrouter-approle"],
timeout_seconds=30,
)
def test_execute_binds_identity_and_restores_ready_state(store: SandboxStore) -> None:
store.save(_ready_bwrap_status())
manager = SandboxManager(store=store)
backend = FakeExecBackend()
with (
patch("sandboxer.core.manager.resolve_backend", return_value=backend),
patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None),
):
result = manager.execute("bwrap123", _exec_request())
assert result.exit_code == 0
assert result.stdout == "inside\n"
assert result.network_default == "deny"
assert result.network_egress == []
assert result.credential_route_refs == ["rein-openweights-openrouter-approle"]
assert store.get("bwrap123").state == SandboxState.READY
def test_execute_refuses_identity_mismatch(store: SandboxStore) -> None:
store.save(_ready_bwrap_status())
manager = SandboxManager(store=store)
with pytest.raises(PermissionError, match="exactly match"):
manager.execute("bwrap123", _exec_request(run_id="another-run"))
def test_execute_refuses_non_ready_and_expired_sandboxes(store: SandboxStore) -> None:
active = _ready_bwrap_status(state=SandboxState.ACTIVE)
store.save(active)
manager = SandboxManager(store=store)
with pytest.raises(RuntimeError, match="must be ready"):
manager.execute("bwrap123", _exec_request())
def test_execute_refuses_concurrent_command(store: SandboxStore) -> None:
store.save(_ready_bwrap_status())
manager = SandboxManager(store=store)
lock = manager._exec_locks.setdefault("bwrap123", Lock())
lock.acquire()
try:
with pytest.raises(RuntimeError, match="already has an active"):
manager.execute("bwrap123", _exec_request())
finally:
lock.release()
expired = _ready_bwrap_status()
expired.expires_at = datetime.now(UTC) - timedelta(seconds=1)
store.save(expired)
with pytest.raises(RuntimeError, match="TTL has expired"):
manager.execute("bwrap123", _exec_request())
def test_execute_refuses_invalid_credential_route_ref(store: SandboxStore) -> None:
store.save(_ready_bwrap_status())
request = _exec_request()
request.credential_route_refs = ["looks like a secret value"]
with pytest.raises(ValueError, match="value-free catalog"):
SandboxManager(store=store).execute("bwrap123", request)
def test_execute_refuses_extension_without_owner_boundary(store: SandboxStore) -> None:
status = _ready_bwrap_status()
status.profile_id = "profile.compose-e2e"
status.extension_id = "ext.compose-ssh"
store.save(status)
with (
patch("sandboxer.core.manager.resolve_backend", return_value=FakeNonExecBackend()),
pytest.raises(RuntimeError, match="no owner-mediated execution boundary"),
):
SandboxManager(store=store).execute("bwrap123", _exec_request())
def test_execute_restores_ready_after_backend_failure(store: SandboxStore) -> None:
store.save(_ready_bwrap_status())
manager = SandboxManager(store=store)
backend = FakeExecBackend()
backend.execute = lambda *args, **kwargs: (_ for _ in ()).throw(RuntimeError("boom"))
with (
patch("sandboxer.core.manager.resolve_backend", return_value=backend),
patch("sandboxer.core.manager.emit_lifecycle_event", return_value=None),
pytest.raises(RuntimeError, match="boom"),
):
manager.execute("bwrap123", _exec_request())
assert store.get("bwrap123").state == SandboxState.READY