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
|
|
@ -5,7 +5,14 @@ from unittest.mock import patch
|
|||
from fastapi.testclient import TestClient
|
||||
|
||||
from sandboxer.api.app import app
|
||||
from sandboxer.models import ActorType, Consumer, SandboxState, SandboxStatus, SnapshotRecord
|
||||
from sandboxer.models import (
|
||||
ActorType,
|
||||
Consumer,
|
||||
SandboxExecResult,
|
||||
SandboxState,
|
||||
SandboxStatus,
|
||||
SnapshotRecord,
|
||||
)
|
||||
|
||||
|
||||
def test_list_sandboxes_empty() -> None:
|
||||
|
|
@ -148,4 +155,59 @@ def test_expire_sandboxes() -> None:
|
|||
client = TestClient(app)
|
||||
resp = client.post("/v1/sandboxes/expire")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()[0]["action"] == "dry-run"
|
||||
assert resp.json()[0]["action"] == "dry-run"
|
||||
|
||||
|
||||
def test_exec_api_fails_closed_when_owner_token_unconfigured(monkeypatch) -> None:
|
||||
monkeypatch.delenv("SANDBOXER_EXEC_TOKEN", raising=False)
|
||||
client = TestClient(app)
|
||||
resp = client.post(
|
||||
"/v1/sandboxes/abc12345/exec",
|
||||
json={
|
||||
"command": ["true"],
|
||||
"consumer": {"actor": "agt", "project": "glas-harness", "run_id": "run-1"},
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
def test_exec_api_requires_bearer_and_returns_execution(monkeypatch) -> None:
|
||||
from datetime import UTC, datetime
|
||||
|
||||
monkeypatch.setenv("SANDBOXER_EXEC_TOKEN", "owner-capability")
|
||||
now = datetime.now(UTC)
|
||||
consumer = Consumer(actor="agt", project="glas-harness", run_id="run-1")
|
||||
result = SandboxExecResult(
|
||||
sandbox_id="abc12345",
|
||||
profile_id="profile.bwrap-local",
|
||||
extension_id="ext.bwrap",
|
||||
consumer=consumer,
|
||||
command_name="true",
|
||||
exit_code=0,
|
||||
duration_seconds=0.1,
|
||||
workspace_dir="/tmp/sandboxer-bwrap/abc12345",
|
||||
network_default="deny",
|
||||
started_at=now,
|
||||
completed_at=now,
|
||||
)
|
||||
payload = {
|
||||
"command": ["true"],
|
||||
"consumer": consumer.model_dump(mode="json"),
|
||||
}
|
||||
with patch("sandboxer.api.app._manager") as mgr:
|
||||
mgr.execute.return_value = result
|
||||
client = TestClient(app)
|
||||
denied = client.post(
|
||||
"/v1/sandboxes/abc12345/exec",
|
||||
json=payload,
|
||||
headers={"Authorization": "Bearer wrong"},
|
||||
)
|
||||
allowed = client.post(
|
||||
"/v1/sandboxes/abc12345/exec",
|
||||
json=payload,
|
||||
headers={"Authorization": "Bearer owner-capability"},
|
||||
)
|
||||
|
||||
assert denied.status_code == 401
|
||||
assert allowed.status_code == 200
|
||||
assert allowed.json()["exit_code"] == 0
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
"""ext.bwrap — local namespace isolation extension."""
|
||||
|
||||
import json
|
||||
import signal
|
||||
import socket
|
||||
import stat
|
||||
import subprocess
|
||||
import threading
|
||||
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
|
||||
from sandboxer.extensions.bwrap_runner import _run
|
||||
from sandboxer.models import IsolationSpec, Profile, Reachability
|
||||
from sandboxer.reachability.enrich import enrich_reachability
|
||||
|
||||
|
||||
def _profile() -> Profile:
|
||||
|
|
@ -40,7 +48,13 @@ def test_bwrap_argv_unshares_net_and_binds_workspace(tmp_path) -> None:
|
|||
assert "--unshare-net" in argv
|
||||
assert "--unshare-user" in argv
|
||||
assert str(tmp_path) in argv
|
||||
assert argv[-2:] == ["sleep", "infinity"]
|
||||
assert "/run/sandboxer/bwrap_runner.py" in argv
|
||||
assert argv[-4:] == [
|
||||
"/usr/bin/python3",
|
||||
"/run/sandboxer/bwrap_runner.py",
|
||||
str(tmp_path),
|
||||
f"{tmp_path}/.sandboxer-owner.sock",
|
||||
]
|
||||
|
||||
|
||||
def test_existing_ro_binds_filters_missing_paths(tmp_path) -> None:
|
||||
|
|
@ -55,13 +69,17 @@ def test_provision_spawns_bwrap_and_returns_handle(tmp_path) -> None:
|
|||
fake_proc = MagicMock()
|
||||
fake_proc.pid = 12345
|
||||
|
||||
with patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc) as popen:
|
||||
with (
|
||||
patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc) as popen,
|
||||
patch.object(BwrapExtension, "_read_child_pid", return_value=23456),
|
||||
):
|
||||
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["pid"] == "23456"
|
||||
assert handle["supervisor_pid"] == "12345"
|
||||
assert handle["workspace_dir"].endswith("abc12345")
|
||||
|
||||
|
||||
|
|
@ -75,13 +93,17 @@ def test_provision_copies_repo_into_workspace(tmp_path) -> None:
|
|||
fake_proc = MagicMock()
|
||||
fake_proc.pid = 1
|
||||
|
||||
with patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc):
|
||||
with (
|
||||
patch("sandboxer.extensions.bwrap.subprocess.Popen", return_value=fake_proc),
|
||||
patch.object(BwrapExtension, "_read_child_pid", return_value=2),
|
||||
):
|
||||
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")
|
||||
assert stat.S_IMODE((base_dir / "cafe1234").stat().st_mode) == 0o700
|
||||
|
||||
|
||||
def test_provision_missing_repo_raises(tmp_path) -> None:
|
||||
|
|
@ -100,7 +122,10 @@ def test_wait_ready_checks_process_and_workspace(tmp_path) -> None:
|
|||
ext = BwrapExtension()
|
||||
handle = {"pid": str(1), "workspace_dir": str(workspace), "host": "localhost"}
|
||||
|
||||
with patch.object(BwrapExtension, "_pid_alive", return_value=True):
|
||||
with (
|
||||
patch.object(BwrapExtension, "_pid_alive", return_value=True),
|
||||
patch("sandboxer.extensions.bwrap.Path.is_socket", return_value=True),
|
||||
):
|
||||
result = ext.wait_ready(handle)
|
||||
|
||||
assert result["host"] == "localhost"
|
||||
|
|
@ -113,18 +138,22 @@ def test_wait_ready_raises_if_process_dead(tmp_path) -> None:
|
|||
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)
|
||||
with (
|
||||
patch.object(BwrapExtension, "_pid_alive", return_value=False),
|
||||
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)
|
||||
with (
|
||||
patch.object(BwrapExtension, "_pid_alive", return_value=True),
|
||||
pytest.raises(RuntimeError, match="workspace missing"),
|
||||
):
|
||||
ext.wait_ready(handle)
|
||||
|
||||
|
||||
def test_teardown_kills_process_group_and_removes_workspace(tmp_path) -> None:
|
||||
|
|
@ -166,10 +195,7 @@ def test_supports_snapshots_is_false() -> None:
|
|||
ext.snapshot({})
|
||||
|
||||
|
||||
def test_reachability_local_exec_hint_for_bwrap_handle() -> None:
|
||||
from sandboxer.reachability.enrich import enrich_reachability, local_exec_hint
|
||||
from sandboxer.models import Reachability
|
||||
|
||||
def test_reachability_enriches_bwrap_handle_without_direct_exec_hint() -> None:
|
||||
handle = {"pid": "555", "workspace_dir": "/tmp/sandboxer-bwrap/abc", "host": "localhost"}
|
||||
reach = {"host": "localhost", "endpoint": "pid:555"}
|
||||
profile = _profile()
|
||||
|
|
@ -179,7 +205,147 @@ def test_reachability_local_exec_hint_for_bwrap_handle() -> None:
|
|||
|
||||
assert reachability.pid == "555"
|
||||
assert reachability.workspace_dir == "/tmp/sandboxer-bwrap/abc"
|
||||
assert local_exec_hint(reachability) == (
|
||||
"nsenter --target 555 --mount --pid --net --uts --ipc "
|
||||
"-- sh -c 'cd /tmp/sandboxer-bwrap/abc && exec $SHELL'"
|
||||
)
|
||||
|
||||
|
||||
def test_execute_sends_bounded_request_to_in_namespace_owner(tmp_path) -> None:
|
||||
base_dir = tmp_path / "sandboxes"
|
||||
workspace = base_dir / "abc12345"
|
||||
workspace.mkdir(parents=True)
|
||||
ext = BwrapExtension({"base_dir": str(base_dir)})
|
||||
handle = {
|
||||
"sandbox_id": "abc12345",
|
||||
"pid": "42",
|
||||
"workspace_dir": str(workspace),
|
||||
}
|
||||
control = workspace / ".sandboxer-owner.sock"
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
server.bind(str(control))
|
||||
server.listen(1)
|
||||
received = {}
|
||||
|
||||
def serve_once() -> None:
|
||||
connection, _ = server.accept()
|
||||
with connection:
|
||||
raw = b""
|
||||
while chunk := connection.recv(65_536):
|
||||
raw += chunk
|
||||
received.update(json.loads(raw))
|
||||
connection.sendall(
|
||||
json.dumps(
|
||||
{
|
||||
"exit_code": 0,
|
||||
"timed_out": False,
|
||||
"stdout": "ok\n",
|
||||
"stderr": "",
|
||||
"output_truncated": False,
|
||||
"workspace_dir": str(workspace),
|
||||
}
|
||||
).encode()
|
||||
)
|
||||
|
||||
thread = threading.Thread(target=serve_once)
|
||||
thread.start()
|
||||
|
||||
with patch.object(BwrapExtension, "_pid_alive", return_value=True):
|
||||
result = ext.execute(
|
||||
handle,
|
||||
["python3", "-V"],
|
||||
credential_route_refs=["rein-openweights-openrouter-approle"],
|
||||
execution_context={"actor": "agt", "run_id": "run-1"},
|
||||
timeout_seconds=30,
|
||||
max_output_bytes=1024,
|
||||
)
|
||||
thread.join(timeout=2)
|
||||
server.close()
|
||||
|
||||
assert received["command"] == ["python3", "-V"]
|
||||
assert received["execution_context"] == {"actor": "agt", "run_id": "run-1"}
|
||||
assert received["credential_route_refs"] == ["rein-openweights-openrouter-approle"]
|
||||
assert received["timeout_seconds"] == 30
|
||||
assert received["max_output_bytes"] == 1024
|
||||
assert result["stdout"] == "ok\n"
|
||||
assert result["exit_code"] == 0
|
||||
|
||||
|
||||
def test_in_namespace_runner_uses_sanitized_environment(tmp_path) -> None:
|
||||
process = MagicMock()
|
||||
process.returncode = 0
|
||||
process.communicate.return_value = (b"ok\n", b"")
|
||||
payload = {
|
||||
"command": ["python3", "-V"],
|
||||
"credential_route_refs": ["rein-openweights-openrouter-approle"],
|
||||
"execution_context": {"actor": "agt", "run_id": "run-1"},
|
||||
"timeout_seconds": 30,
|
||||
"max_output_bytes": 1024,
|
||||
}
|
||||
with patch(
|
||||
"sandboxer.extensions.bwrap_runner.subprocess.Popen", return_value=process
|
||||
) as popen:
|
||||
result = _run(payload, tmp_path)
|
||||
|
||||
assert popen.call_args.args[0] == ["python3", "-V"]
|
||||
assert popen.call_args.kwargs["cwd"] == tmp_path
|
||||
child_env = popen.call_args.kwargs["env"]
|
||||
assert child_env["SANDBOXER_ACTOR"] == "agt"
|
||||
assert child_env["SANDBOXER_RUN_ID"] == "run-1"
|
||||
assert "rein-openweights-openrouter-approle" in child_env[
|
||||
"SANDBOXER_CREDENTIAL_ROUTE_REFS"
|
||||
]
|
||||
assert set(child_env) == {
|
||||
"HOME",
|
||||
"LANG",
|
||||
"PATH",
|
||||
"SANDBOXER_CREDENTIAL_ROUTE_REFS",
|
||||
"SANDBOXER_ACTOR",
|
||||
"SANDBOXER_RUN_ID",
|
||||
}
|
||||
assert result["stdout"] == "ok\n"
|
||||
|
||||
|
||||
def test_execute_refuses_workspace_outside_owner_base(tmp_path) -> None:
|
||||
outside = tmp_path / "source-checkout"
|
||||
outside.mkdir()
|
||||
ext = BwrapExtension({"base_dir": str(tmp_path / "sandboxes")})
|
||||
handle = {"sandbox_id": "abc12345", "pid": "42", "workspace_dir": str(outside)}
|
||||
|
||||
with (
|
||||
patch.object(BwrapExtension, "_pid_alive", return_value=True),
|
||||
pytest.raises(RuntimeError, match="outside owner-managed"),
|
||||
):
|
||||
ext.execute(
|
||||
handle,
|
||||
["true"],
|
||||
credential_route_refs=[],
|
||||
execution_context={},
|
||||
timeout_seconds=30,
|
||||
max_output_bytes=1024,
|
||||
)
|
||||
|
||||
|
||||
def test_runner_bounds_output_and_normalizes_timeout(tmp_path) -> None:
|
||||
process = MagicMock()
|
||||
process.pid = 99
|
||||
process.communicate.side_effect = [
|
||||
subprocess.TimeoutExpired("cmd", 1),
|
||||
(b"123456", b"abcdef"),
|
||||
]
|
||||
payload = {
|
||||
"command": ["sleep", "2"],
|
||||
"credential_route_refs": [],
|
||||
"execution_context": {},
|
||||
"timeout_seconds": 1,
|
||||
"max_output_bytes": 3,
|
||||
}
|
||||
|
||||
with (
|
||||
patch("sandboxer.extensions.bwrap_runner.subprocess.Popen", return_value=process),
|
||||
patch("sandboxer.extensions.bwrap_runner.os.killpg") as killpg,
|
||||
):
|
||||
result = _run(payload, tmp_path)
|
||||
|
||||
killpg.assert_called_once_with(99, signal.SIGKILL)
|
||||
assert result["exit_code"] == 124
|
||||
assert result["timed_out"] is True
|
||||
assert result["stdout"] == "123"
|
||||
assert result["stderr"] == "abc"
|
||||
assert result["output_truncated"] is True
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -75,4 +75,30 @@ def test_build_reachability_report() -> None:
|
|||
report = build_reachability_report(status)
|
||||
assert report["sandbox_id"] == "abc12345"
|
||||
assert report["ssh_one_liner"] is not None
|
||||
assert "ops_bridge" in report
|
||||
assert "ops_bridge" in report
|
||||
|
||||
|
||||
def test_bwrap_report_points_to_owner_execution_not_nsenter() -> None:
|
||||
now = datetime.now(UTC)
|
||||
status = SandboxStatus(
|
||||
sandbox_id="bwrap123",
|
||||
profile_id="profile.bwrap-local",
|
||||
extension_id="ext.bwrap",
|
||||
state=SandboxState.READY,
|
||||
consumer=Consumer(actor=ActorType.AGT, project="glas-harness", run_id="run-1"),
|
||||
host="localhost",
|
||||
reachability=Reachability(
|
||||
host="localhost",
|
||||
endpoint="pid:42",
|
||||
pid="42",
|
||||
workspace_dir="/tmp/sandboxer-bwrap/bwrap123",
|
||||
),
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
|
||||
report = build_reachability_report(status)
|
||||
|
||||
assert report["execution"]["mode"] == "owner-mediated"
|
||||
assert "nsenter" not in str(report)
|
||||
assert "local_exec_hint" not in report
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ def test_resolve_initial_ttl() -> None:
|
|||
|
||||
|
||||
def test_extend_expires_at_caps_at_max() -> None:
|
||||
anchor = datetime(2026, 6, 24, 10, 0, tzinfo=UTC)
|
||||
anchor = datetime.now(UTC)
|
||||
current = anchor + timedelta(hours=23)
|
||||
new_expires, applied = extend_expires_at(
|
||||
current,
|
||||
|
|
@ -262,4 +262,4 @@ def test_manager_expire_dry_run_and_apply(store: SandboxStore) -> None:
|
|||
applied = manager.expire(apply=True, now=now)
|
||||
|
||||
assert applied[0].action == "destroyed"
|
||||
assert manager.get("gone5678").state == SandboxState.DESTROYED
|
||||
assert manager.get("gone5678").state == SandboxState.DESTROYED
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue