Add owner-mediated bwrap execution boundary
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a06def-6490-7033-8448-2eab2d12ed44
This commit is contained in:
parent
877676d1f1
commit
d79e3fe358
23 changed files with 1321 additions and 86 deletions
|
|
@ -2,15 +2,23 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
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, SandboxState, SandboxStatus
|
||||
from sandboxer.models import (
|
||||
ActorType,
|
||||
Consumer,
|
||||
SandboxCreateRequest,
|
||||
SandboxExecRequest,
|
||||
SandboxState,
|
||||
SandboxStatus,
|
||||
)
|
||||
|
||||
|
||||
class FakeBackend:
|
||||
|
|
@ -40,6 +48,26 @@ class FakeBackend:
|
|||
}
|
||||
|
||||
|
||||
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")
|
||||
|
|
@ -82,4 +110,133 @@ def test_destroy_idempotent(store: SandboxStore) -> None:
|
|||
store.save(status)
|
||||
manager = SandboxManager(store=store)
|
||||
result = manager.destroy("gone1234")
|
||||
assert result.state == SandboxState.DESTROYED
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue