Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
242 lines
7.7 KiB
Python
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
|