from __future__ import annotations import builtins import importlib import io import sys import tarfile import types from pathlib import Path from typing import Any, Literal, cast import httpx import pytest from pydantic import BaseModel, PrivateAttr from agents.sandbox import Manifest, SandboxPathGrant from agents.sandbox.entries import File, InContainerMountStrategy, Mount, MountpointMountPattern from agents.sandbox.entries.mounts.base import InContainerMountAdapter from agents.sandbox.errors import ConfigurationError, InvalidManifestPathError from agents.sandbox.manifest import Environment from agents.sandbox.materialization import MaterializedFile from agents.sandbox.session.base_sandbox_session import BaseSandboxSession from agents.sandbox.session.dependencies import Dependencies from agents.sandbox.snapshot import NoopSnapshot, SnapshotBase from agents.sandbox.types import User from tests._fake_workspace_paths import resolve_fake_workspace_path class _FakeNetworkPolicyRule(BaseModel): pass class _FakeNetworkPolicySubnets(BaseModel): allow: list[str] | None = None deny: list[str] | None = None class _FakeNetworkPolicyCustom(BaseModel): allow: dict[str, list[_FakeNetworkPolicyRule]] | list[str] | None = None subnets: _FakeNetworkPolicySubnets | None = None NetworkPolicy = _FakeNetworkPolicyCustom NetworkPolicyCustom = _FakeNetworkPolicyCustom NetworkPolicyRule = _FakeNetworkPolicyRule NetworkPolicySubnets = _FakeNetworkPolicySubnets class Resources(BaseModel): memory: int | None = None class SnapshotSource(BaseModel): type: Literal["snapshot"] = "snapshot" snapshot_id: str class _FakeVercelSandboxError(Exception): pass class _FakeVercelAPIError(_FakeVercelSandboxError): def __init__(self, message: str, *, status_code: int, data: object | None = None) -> None: super().__init__(message) self.status_code = status_code self.response = types.SimpleNamespace(status_code=status_code) self.data = data class _FakeVercelSandboxAuthError(_FakeVercelAPIError): def __init__(self, message: str = "auth failed", *, data: object | None = None) -> None: super().__init__(message, status_code=401, data=data) class _FakeVercelSandboxNotFoundError(_FakeVercelAPIError): def __init__(self, message: str = "not found", *, data: object | None = None) -> None: super().__init__(message, status_code=404, data=data) class _FakeVercelSandboxPermissionError(_FakeVercelAPIError): def __init__(self, message: str = "permission denied", *, data: object | None = None) -> None: super().__init__(message, status_code=403, data=data) class _FakeVercelSandboxRateLimitError(_FakeVercelAPIError): def __init__(self, message: str = "rate limited", *, data: object | None = None) -> None: super().__init__(message, status_code=429, data=data) class _FakeVercelSandboxServerError(_FakeVercelAPIError): def __init__(self, message: str = "server error", *, data: object | None = None) -> None: super().__init__(message, status_code=500, data=data) class _FakeVercelSandboxValidationError(_FakeVercelSandboxError): def __init__(self, message: str = "validation failed") -> None: super().__init__(message) class _MemorySnapshot(SnapshotBase): type: Literal["test-vercel-memory"] = "test-vercel-memory" payload: bytes = b"" is_restorable: bool = False async def persist(self, data: io.IOBase, *, dependencies: Dependencies | None = None) -> None: _ = dependencies raw = data.read() if isinstance(raw, str): raw = raw.encode("utf-8") assert isinstance(raw, bytes | bytearray) object.__setattr__(self, "payload", bytes(raw)) object.__setattr__(self, "is_restorable", True) async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase: _ = dependencies return io.BytesIO(self.payload) async def restorable(self, *, dependencies: Dependencies | None = None) -> bool: _ = dependencies return self.is_restorable class _FakeCommandFinished: def __init__(self, *, stdout: str = "", stderr: str = "", exit_code: int = 0) -> None: self._stdout = stdout self._stderr = stderr self.exit_code = exit_code async def stdout(self) -> str: return self._stdout async def stderr(self) -> str: return self._stderr class _FakeClient: def __init__(self) -> None: self.closed = False async def aclose(self) -> None: self.closed = True class _FakeAsyncSnapshot: def __init__(self, snapshot_id: str) -> None: self.snapshot_id = snapshot_id class _FakeAsyncSandbox: create_calls: list[dict[str, object]] = [] get_calls: list[dict[str, object]] = [] snapshot_counter = 0 sandboxes: dict[str, _FakeAsyncSandbox] = {} snapshots: dict[str, dict[str, bytes]] = {} fail_get_ids: set[str] = set() create_failures: list[BaseException] = [] def __init__( self, *, sandbox_id: str, status: str = "running", routes: list[dict[str, object]] | None = None, files: dict[str, bytes] | None = None, ) -> None: self.sandbox_id = sandbox_id self.status = status self.routes = routes or [{"port": 3000, "url": "https://3000-sandbox.vercel.run"}] self.files = dict(files or {}) self.client = _FakeClient() self.next_command_result = _FakeCommandFinished() self.run_command_calls: list[tuple[str, list[str], str | None]] = [] self.refresh_calls = 0 self.read_file_calls: list[tuple[str, str | None]] = [] self.stop_calls = 0 self.wait_for_status_calls: list[tuple[object, float | None]] = [] self.wait_for_status_error: BaseException | None = None self.write_failures: list[BaseException] = [] self.write_files_calls: list[list[dict[str, object]]] = [] self.tar_create_result: _FakeCommandFinished | None = None self.tar_extract_result: _FakeCommandFinished | None = None self.symlinks: dict[str, str] = {} @classmethod def reset(cls) -> None: cls.create_calls = [] cls.get_calls = [] cls.snapshot_counter = 0 cls.sandboxes = {} cls.snapshots = {} cls.fail_get_ids = set() cls.create_failures = [] @classmethod async def create(cls, **kwargs: object) -> _FakeAsyncSandbox: cls.create_calls.append(dict(kwargs)) if cls.create_failures: raise cls.create_failures.pop(0) source = kwargs.get("source") sandbox_id = f"vercel-sandbox-{len(cls.create_calls)}" files: dict[str, bytes] = {} snapshot_id = getattr(source, "snapshot_id", None) if getattr(source, "type", None) == "snapshot" and isinstance(snapshot_id, str): files = dict(cls.snapshots.get(snapshot_id, {})) ports = cast(list[int] | None, kwargs.get("ports")) sandbox = cls( sandbox_id=sandbox_id, routes=[ {"port": port, "url": f"https://{port}-sandbox.vercel.run"} for port in (ports or [3000]) ], files=files, ) cls.sandboxes[sandbox_id] = sandbox return sandbox @classmethod async def get(cls, **kwargs: object) -> _FakeAsyncSandbox: cls.get_calls.append(dict(kwargs)) sandbox_id = kwargs["sandbox_id"] assert isinstance(sandbox_id, str) if sandbox_id in cls.fail_get_ids: raise RuntimeError("sandbox missing") sandbox = cls.sandboxes.get(sandbox_id) if sandbox is None: raise RuntimeError("sandbox missing") return sandbox async def refresh(self) -> None: self.refresh_calls += 1 async def wait_for_status(self, status: object, timeout: float | None = None) -> None: self.wait_for_status_calls.append((status, timeout)) if self.wait_for_status_error is not None: raise self.wait_for_status_error self.status = str(status) def domain(self, port: int) -> str: for route in self.routes: if route.get("port") == port: return str(route["url"]) raise ValueError("missing route") async def run_command( self, cmd: str, args: list[str] | None = None, *, cwd: str | None = None, env: dict[str, str] | None = None, sudo: bool = False, ) -> _FakeCommandFinished: _ = (env, sudo) args = args or [] self.run_command_calls.append((cmd, list(args), cwd)) resolved = resolve_fake_workspace_path( (cmd, *args), symlinks=self.symlinks, home_dir="/workspace", ) if resolved is not None: return _FakeCommandFinished( exit_code=resolved.exit_code, stdout=resolved.stdout, stderr=resolved.stderr, ) if cmd == "tar" and len(args) >= 3 and args[0] == "cf": if self.tar_create_result is not None: return self.tar_create_result archive_path = args[1] assert cwd is not None include_root = args[-1] == "." exclusions = { argument.removeprefix("--exclude=./") for argument in args[2:-1] if argument.startswith("--exclude=./") } buffer = io.BytesIO() with tarfile.open(fileobj=buffer, mode="w") as archive: for path, content in sorted(self.files.items()): if not path.startswith(cwd.rstrip("/") + "/"): continue rel_path = path[len(cwd.rstrip("/")) + 1 :] if any( rel_path == exclusion or rel_path.startswith(f"{exclusion}/") for exclusion in exclusions ): continue info = tarfile.TarInfo(name=rel_path if include_root else path) info.size = len(content) archive.addfile(info, io.BytesIO(content)) self.files[archive_path] = buffer.getvalue() return _FakeCommandFinished() if cmd == "tar" and len(args) >= 4 and args[0] == "xf": if self.tar_extract_result is not None: return self.tar_extract_result archive_path = args[1] destination = args[3] raw = self.files[archive_path] with tarfile.open(fileobj=io.BytesIO(raw), mode="r") as archive: for member in archive.getmembers(): if not member.isfile(): continue extracted = archive.extractfile(member) assert extracted is not None self.files[f"{destination.rstrip('/')}/{member.name}"] = extracted.read() return _FakeCommandFinished() if cmd == "rm" and args: target = args[-1] self.files.pop(target, None) return _FakeCommandFinished() return self.next_command_result async def read_file(self, path: str, *, cwd: str | None = None) -> bytes | None: self.read_file_calls.append((path, cwd)) resolved = path if path.startswith("/") or cwd is None else f"{cwd.rstrip('/')}/{path}" return self.files.get(resolved) async def write_files(self, files: list[dict[str, object]]) -> None: self.write_files_calls.append(files) if self.write_failures: raise self.write_failures.pop(0) for file in files: self.files[str(file["path"])] = bytes(cast(bytes, file["content"])) async def stop( self, *, blocking: bool = False, timeout: float = 30.0, poll_interval: float = 0.5 ) -> None: _ = (blocking, timeout, poll_interval) self.stop_calls += 1 self.status = "stopped" async def snapshot(self, *, expiration: int | None = None) -> _FakeAsyncSnapshot: _ = expiration type(self).snapshot_counter += 1 snapshot_id = f"vercel-snapshot-{type(self).snapshot_counter}" type(self).snapshots[snapshot_id] = dict(self.files) self.status = "stopped" return _FakeAsyncSnapshot(snapshot_id) class _RecordingMount(Mount): type: str = "test_vercel_recording_mount" bucket: str = "bucket" _events: list[tuple[str, str]] = PrivateAttr(default_factory=list) def supported_in_container_patterns( self, ) -> tuple[builtins.type[MountpointMountPattern], ...]: return (MountpointMountPattern,) def in_container_adapter(self) -> InContainerMountAdapter: mount = self class _Adapter(InContainerMountAdapter): def validate(self, strategy: InContainerMountStrategy) -> None: super().validate(strategy) async def activate( self, strategy: InContainerMountStrategy, session: BaseSandboxSession, dest: Path, base_dir: Path, ) -> list[MaterializedFile]: _ = (strategy, session, dest, base_dir) return [] async def deactivate( self, strategy: InContainerMountStrategy, session: BaseSandboxSession, dest: Path, base_dir: Path, ) -> None: _ = (strategy, session, dest, base_dir) async def teardown_for_snapshot( self, strategy: InContainerMountStrategy, session: BaseSandboxSession, path: Path, ) -> None: _ = strategy mount._events.append(("unmount", path.as_posix())) sandbox = cast(Any, session)._sandbox if sandbox is not None: sandbox.files.pop(f"{path.as_posix()}/mounted.txt", None) async def restore_after_snapshot( self, strategy: InContainerMountStrategy, session: BaseSandboxSession, path: Path, ) -> None: _ = strategy mount._events.append(("mount", path.as_posix())) sandbox = cast(Any, session)._sandbox if sandbox is not None: sandbox.files[f"{path.as_posix()}/mounted.txt"] = b"mounted-content" return _Adapter(self) def _load_vercel_module(monkeypatch: pytest.MonkeyPatch) -> Any: _FakeAsyncSandbox.reset() fake_vercel = types.ModuleType("vercel") fake_vercel_sandbox = cast(Any, types.ModuleType("vercel.sandbox")) fake_vercel_sandbox.AsyncSandbox = _FakeAsyncSandbox fake_vercel_sandbox.NetworkPolicy = NetworkPolicy fake_vercel_sandbox.NetworkPolicyCustom = NetworkPolicyCustom fake_vercel_sandbox.NetworkPolicyRule = NetworkPolicyRule fake_vercel_sandbox.NetworkPolicySubnets = NetworkPolicySubnets fake_vercel_sandbox.Resources = Resources fake_vercel_sandbox.SandboxAuthError = _FakeVercelSandboxAuthError fake_vercel_sandbox.SandboxNotFoundError = _FakeVercelSandboxNotFoundError fake_vercel_sandbox.SandboxPermissionError = _FakeVercelSandboxPermissionError fake_vercel_sandbox.SandboxRateLimitError = _FakeVercelSandboxRateLimitError fake_vercel_sandbox.SandboxServerError = _FakeVercelSandboxServerError fake_vercel_sandbox.SandboxStatus = types.SimpleNamespace(RUNNING="running") fake_vercel_sandbox.SandboxValidationError = _FakeVercelSandboxValidationError fake_vercel_sandbox.SnapshotSource = SnapshotSource cast(Any, fake_vercel).sandbox = fake_vercel_sandbox monkeypatch.setitem(sys.modules, "vercel", fake_vercel) monkeypatch.setitem(sys.modules, "vercel.sandbox", fake_vercel_sandbox) sys.modules.pop("agents.extensions.sandbox.vercel.sandbox", None) sys.modules.pop("agents.extensions.sandbox.vercel", None) return importlib.import_module("agents.extensions.sandbox.vercel.sandbox") async def _noop_sleep(*_args: object, **_kwargs: object) -> None: return None def test_vercel_package_re_exports_backend_symbols(monkeypatch: pytest.MonkeyPatch) -> None: vercel_module = _load_vercel_module(monkeypatch) package_module = importlib.import_module("agents.extensions.sandbox.vercel") assert package_module.VercelSandboxClient is vercel_module.VercelSandboxClient assert package_module.VercelSandboxSessionState is vercel_module.VercelSandboxSessionState def test_vercel_supports_pty_is_disabled_until_provider_methods_exist( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) noninteractive = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000000", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-noninteractive", interactive=False, ) interactive = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000001", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-interactive", interactive=True, ) assert not vercel_module.VercelSandboxSession.from_state(noninteractive).supports_pty() assert not vercel_module.VercelSandboxSession.from_state(interactive).supports_pty() @pytest.mark.asyncio async def test_vercel_create_passes_provider_options(monkeypatch: pytest.MonkeyPatch) -> None: vercel_module = _load_vercel_module(monkeypatch) network_policy = NetworkPolicyCustom( allow={ "api.openai.com": [NetworkPolicyRule()], }, subnets=NetworkPolicySubnets(allow=["10.0.0.0/8"]), ) client = vercel_module.VercelSandboxClient(token="token") session = await client.create( manifest=Manifest( environment=Environment(value={"FLAG": "manifest", "FROM_MANIFEST": "1"}) ), options=vercel_module.VercelSandboxClientOptions( project_id="project", team_id="team", timeout_ms=12_000, runtime="node22", resources={"memory": 1024}, env={"FLAG": "options", "HELLO": "world"}, exposed_ports=(3000, 4000), interactive=True, network_policy=network_policy, ), ) assert _FakeAsyncSandbox.create_calls == [ { "source": None, "ports": [3000, 4000], "timeout": 12_000, "resources": Resources(memory=1024), "runtime": "node22", "token": "token", "project_id": "project", "team_id": "team", "interactive": True, "env": {"FLAG": "manifest", "HELLO": "world", "FROM_MANIFEST": "1"}, "network_policy": network_policy, } ] assert _FakeAsyncSandbox.sandboxes["vercel-sandbox-1"].wait_for_status_calls == [ ("running", vercel_module.DEFAULT_VERCEL_WAIT_FOR_RUNNING_TIMEOUT_S) ] assert session._inner.state.sandbox_id == "vercel-sandbox-1" assert session._inner.state.manifest.root == vercel_module.DEFAULT_VERCEL_WORKSPACE_ROOT @pytest.mark.asyncio async def test_vercel_create_retries_transient_transport_error( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) monkeypatch.setattr("agents.sandbox.util.retry.asyncio.sleep", _noop_sleep) _FakeAsyncSandbox.create_failures = [httpx.ReadError("read failed")] client = vercel_module.VercelSandboxClient(token="token") session = await client.create( manifest=Manifest(), options=vercel_module.VercelSandboxClientOptions(), ) assert len(_FakeAsyncSandbox.create_calls) == 2 assert _FakeAsyncSandbox.sandboxes[session._inner.state.sandbox_id].wait_for_status_calls == [ ("running", vercel_module.DEFAULT_VERCEL_WAIT_FOR_RUNNING_TIMEOUT_S) ] @pytest.mark.asyncio async def test_vercel_create_does_not_retry_non_transient_error( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) monkeypatch.setattr("agents.sandbox.util.retry.asyncio.sleep", _noop_sleep) class _BadRequestError(Exception): status_code = 400 _FakeAsyncSandbox.create_failures = [_BadRequestError("bad request")] client = vercel_module.VercelSandboxClient() with pytest.raises(_BadRequestError): await client.create( manifest=Manifest(), options=vercel_module.VercelSandboxClientOptions(), ) assert len(_FakeAsyncSandbox.create_calls) == 1 @pytest.mark.asyncio async def test_vercel_exec_read_write_and_port_resolution(monkeypatch: pytest.MonkeyPatch) -> None: vercel_module = _load_vercel_module(monkeypatch) snapshot = NoopSnapshot(id="snapshot") state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000001", manifest=Manifest(), snapshot=snapshot, sandbox_id="sandbox-existing", exposed_ports=(3000,), ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-existing") sandbox.next_command_result = _FakeCommandFinished(stdout="hello\n", stderr="", exit_code=0) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.write(Path("notes.txt"), io.BytesIO(b"payload")) result = await session.exec("printf", "hello", shell=False) endpoint = await session.resolve_exposed_port(3000) payload = await session.read(Path("notes.txt")) assert result.ok() assert result.stdout == b"hello\n" assert endpoint == vercel_module.ExposedPortEndpoint( host="3000-sandbox.vercel.run", port=443, tls=True, ) assert payload.read() == b"payload" @pytest.mark.asyncio async def test_vercel_exec_marks_typed_not_found_non_retryable( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000120", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-exec-missing", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-exec-missing") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) async def _raise_not_found(*args: object, **kwargs: object) -> object: _ = (args, kwargs) raise vercel_module.vercel_sandbox.SandboxNotFoundError("sandbox missing") monkeypatch.setattr(sandbox, "run_command", _raise_not_found) with pytest.raises(vercel_module.ExecTransportError) as exc_info: await session.exec("pwd", shell=False) assert exc_info.value.retryable is False @pytest.mark.asyncio async def test_vercel_exec_marks_typed_rate_limit_retryable( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000121", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-exec-rate-limit", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-exec-rate-limit") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) async def _raise_rate_limit(*args: object, **kwargs: object) -> object: _ = (args, kwargs) raise vercel_module.vercel_sandbox.SandboxRateLimitError("rate limited") monkeypatch.setattr(sandbox, "run_command", _raise_rate_limit) with pytest.raises(vercel_module.ExecTransportError) as exc_info: await session.exec("pwd", shell=False) assert exc_info.value.retryable is True @pytest.mark.asyncio async def test_vercel_write_marks_typed_validation_error_non_retryable( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) monkeypatch.setattr("agents.sandbox.util.retry.asyncio.sleep", _noop_sleep) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000122", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-write-validation", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-write-validation") sandbox.write_failures = [vercel_module.vercel_sandbox.SandboxValidationError("invalid write")] session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) with pytest.raises(vercel_module.WorkspaceArchiveWriteError) as exc_info: await session.write(Path("hello.txt"), io.BytesIO(b"world")) assert len(sandbox.write_files_calls) == 1 assert exc_info.value.retryable is False @pytest.mark.parametrize( ("status", "expected_retryable"), [ (400, False), (401, False), (403, False), (404, False), (408, True), (425, True), (422, False), (429, True), (500, True), (502, True), (503, True), (504, True), ], ) def test_vercel_retryability_status_table( monkeypatch: pytest.MonkeyPatch, status: int, expected_retryable: bool, ) -> None: vercel_module = _load_vercel_module(monkeypatch) class FakeStatusError(Exception): status_code = status assert vercel_module._vercel_provider_retryability(FakeStatusError()) is expected_retryable @pytest.mark.asyncio async def test_vercel_start_uses_base_session_contract_and_materializes_workspace( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000012", manifest=Manifest(entries={"notes.txt": File(content=b"payload")}), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-start", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-start") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.start() payload = await session.read(Path("notes.txt")) assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/workspace"], None) assert ("mkdir", ["-p", "/workspace"], "/workspace") in sandbox.run_command_calls assert session.state.workspace_root_ready is True assert payload.read() == b"payload" @pytest.mark.asyncio async def test_vercel_start_materializes_entries_under_literal_manifest_root( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000013", manifest=Manifest( root="/workspace/my app", entries={"notes.txt": File(content=b"payload")} ), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-start-literal", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-start-literal") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.start() payload = await session.read(Path("notes.txt")) assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/workspace/my app"], None) assert ("mkdir", ["-p", "/workspace/my app"], "/workspace/my app") in sandbox.run_command_calls assert sandbox.write_files_calls == [ [{"path": "/workspace/my app/notes.txt", "content": b"payload"}] ] assert payload.read() == b"payload" @pytest.mark.asyncio async def test_vercel_start_bootstraps_arbitrary_absolute_root_before_using_it_as_cwd( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000014", manifest=Manifest(root="/tmp/outside", entries={"notes.txt": File(content=b"payload")}), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-start-outside", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-start-outside") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.start() payload = await session.read(Path("notes.txt")) assert sandbox.run_command_calls[0] == ("mkdir", ["-p", "--", "/tmp/outside"], None) assert ("mkdir", ["-p", "/tmp/outside"], "/tmp/outside") in sandbox.run_command_calls assert payload.read() == b"payload" @pytest.mark.asyncio async def test_vercel_create_allows_manifest_root_outside_provider_workspace( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(root="/tmp/outside"), options=vercel_module.VercelSandboxClientOptions(), ) assert session._inner.state.manifest.root == "/tmp/outside" @pytest.mark.asyncio async def test_vercel_create_allows_manifest_root_within_provider_workspace( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(root="/vercel/sandbox/my app"), options=vercel_module.VercelSandboxClientOptions(), ) assert session._inner.state.manifest.root == "/vercel/sandbox/my app" @pytest.mark.asyncio async def test_vercel_normalize_path_rejects_workspace_escape_and_allows_absolute_in_root( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(root="/vercel/sandbox/project"), options=vercel_module.VercelSandboxClientOptions(), ) inner = session._inner with pytest.raises(InvalidManifestPathError): inner.normalize_path("../outside.txt") with pytest.raises(InvalidManifestPathError): inner.normalize_path("/etc/passwd") assert inner.normalize_path("/vercel/sandbox/project/nested/file.txt") == Path( "/vercel/sandbox/project/nested/file.txt" ) @pytest.mark.asyncio async def test_vercel_read_and_write_reject_paths_outside_workspace_root( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(root="/vercel/sandbox/project"), options=vercel_module.VercelSandboxClientOptions(), ) with pytest.raises(InvalidManifestPathError): await session.read("../outside.txt") with pytest.raises(InvalidManifestPathError): await session.write("/etc/passwd", io.BytesIO(b"nope")) @pytest.mark.asyncio async def test_vercel_read_rejects_workspace_symlink_to_ungranted_path( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000016", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-read-escape-link", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-read-escape-link") sandbox.symlinks["/workspace/link"] = "/private" session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) with pytest.raises(InvalidManifestPathError) as exc_info: await session.read("link/secret.txt") assert sandbox.read_file_calls == [] assert str(exc_info.value) == "manifest path must not escape root: link/secret.txt" assert exc_info.value.context == { "rel": "link/secret.txt", "reason": "escape_root", "resolved_path": "workspace escape: /private/secret.txt", } @pytest.mark.asyncio async def test_vercel_write_rejects_workspace_symlink_to_read_only_extra_path_grant( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000015", manifest=Manifest( root="/workspace", extra_path_grants=(SandboxPathGrant(path="/tmp/protected", read_only=True),), ), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-readonly-link", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-readonly-link") sandbox.symlinks["/workspace/link"] = "/tmp/protected" session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) with pytest.raises(vercel_module.WorkspaceArchiveWriteError) as exc_info: await session.write("link/out.txt", io.BytesIO(b"blocked")) assert sandbox.write_files_calls == [] assert str(exc_info.value) == "failed to write archive for path: /workspace/link/out.txt" assert exc_info.value.context == { "path": "/workspace/link/out.txt", "reason": "read_only_extra_path_grant", "grant_path": "/tmp/protected", "resolved_path": "/tmp/protected/out.txt", } @pytest.mark.asyncio async def test_vercel_rejects_sandbox_local_user_arguments( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(root="/vercel/sandbox/project"), options=vercel_module.VercelSandboxClientOptions(), ) with pytest.raises(ConfigurationError, match="does not support sandbox-local users"): await session.exec("pwd", user="sandbox-user") with pytest.raises(ConfigurationError, match="does not support sandbox-local users"): await session.read("notes.txt", user=User(name="sandbox-user")) with pytest.raises(ConfigurationError, match="does not support sandbox-local users"): await session.write("notes.txt", io.BytesIO(b"payload"), user="sandbox-user") @pytest.mark.asyncio async def test_vercel_resume_reconnects_existing_running_sandbox( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing") _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000002", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(state) assert _FakeAsyncSandbox.get_calls == [ { "sandbox_id": "sandbox-existing", "token": None, "project_id": None, "team_id": None, } ] assert resumed._inner.state.sandbox_id == "sandbox-existing" assert _FakeAsyncSandbox.create_calls == [] # Sandbox is already RUNNING, so wait_for_status should not be called. assert existing.wait_for_status_calls == [] assert resumed._inner._workspace_state_preserved_on_start() is True # noqa: SLF001 assert resumed._inner._system_state_preserved_on_start() is True # noqa: SLF001 @pytest.mark.asyncio async def test_vercel_resume_waits_when_sandbox_pending( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing", status="pending") _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000200", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(state) assert resumed._inner.state.sandbox_id == "sandbox-existing" assert _FakeAsyncSandbox.create_calls == [] assert existing.wait_for_status_calls == [ ("running", vercel_module.DEFAULT_VERCEL_WAIT_FOR_RUNNING_TIMEOUT_S) ] assert resumed._inner._workspace_state_preserved_on_start() is True # noqa: SLF001 @pytest.mark.asyncio @pytest.mark.parametrize( "terminal_status", ["stopping", "stopped", "failed", "aborted", "snapshotting"] ) async def test_vercel_resume_recreates_sandbox_when_cannot_reach_running( monkeypatch: pytest.MonkeyPatch, terminal_status: str, ) -> None: """A sandbox in any state that cannot transition to RUNNING must be recreated immediately, without waiting for the wait_for_status timeout.""" vercel_module = _load_vercel_module(monkeypatch) existing = _FakeAsyncSandbox(sandbox_id="sandbox-terminal", status=terminal_status) _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000201", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(state) assert existing.wait_for_status_calls == [] assert existing.client.closed is True assert len(_FakeAsyncSandbox.create_calls) == 1 assert resumed._inner.state.sandbox_id != "sandbox-terminal" assert resumed._inner.state.workspace_root_ready is False assert resumed._inner._workspace_state_preserved_on_start() is False # noqa: SLF001 @pytest.mark.asyncio async def test_vercel_resume_falls_back_to_recreate_when_sandbox_missing( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) _FakeAsyncSandbox.fail_get_ids.add("sandbox-missing") state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000003", manifest=Manifest(environment=Environment(value={"FLAG": "manifest"})), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-missing", timeout_ms=90_000, runtime="python3.14", env={"FLAG": "options", "BASE": "1"}, exposed_ports=(3000,), ) client = vercel_module.VercelSandboxClient(token="token") resumed = await client.resume(state) assert resumed._inner.state.sandbox_id == "vercel-sandbox-1" assert resumed._inner.state.workspace_root_ready is False assert _FakeAsyncSandbox.create_calls[0]["runtime"] == "python3.14" assert _FakeAsyncSandbox.create_calls[0]["timeout"] == 90_000 assert _FakeAsyncSandbox.create_calls[0]["token"] == "token" assert _FakeAsyncSandbox.create_calls[0]["env"] == {"FLAG": "manifest", "BASE": "1"} assert resumed._inner._workspace_state_preserved_on_start() is False # noqa: SLF001 @pytest.mark.asyncio async def test_vercel_resume_recreates_sandbox_after_wait_timeout( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) # Use "pending" so that the code enters the wait path (not already RUNNING). existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing", status="pending") existing.wait_for_status_error = TimeoutError() _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000101", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(state) assert existing.client.closed is True assert resumed._inner.state.sandbox_id == "vercel-sandbox-1" assert len(_FakeAsyncSandbox.create_calls) == 1 assert resumed._inner.state.workspace_root_ready is False assert resumed._inner._workspace_state_preserved_on_start() is False # noqa: SLF001 @pytest.mark.asyncio async def test_vercel_create_does_not_read_token_or_scope_from_env( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("VERCEL_TOKEN", "env-token") monkeypatch.setenv("VERCEL_PROJECT_ID", "env-project") monkeypatch.setenv("VERCEL_TEAM_ID", "env-team") vercel_module = _load_vercel_module(monkeypatch) client = vercel_module.VercelSandboxClient() session = await client.create( manifest=Manifest(), options=vercel_module.VercelSandboxClientOptions(), ) assert _FakeAsyncSandbox.create_calls[-1]["token"] is None assert _FakeAsyncSandbox.create_calls[-1]["project_id"] is None assert _FakeAsyncSandbox.create_calls[-1]["team_id"] is None assert session._inner.state.project_id is None assert session._inner.state.team_id is None @pytest.mark.asyncio async def test_vercel_resume_uses_client_project_and_team_fallbacks( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing") _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000099", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient(project_id="client-project", team_id="client-team") resumed = await client.resume(state) assert _FakeAsyncSandbox.get_calls[-1]["project_id"] == "client-project" assert _FakeAsyncSandbox.get_calls[-1]["team_id"] == "client-team" assert resumed._inner.state.project_id == "client-project" assert resumed._inner.state.team_id == "client-team" @pytest.mark.asyncio async def test_vercel_resume_does_not_read_token_or_scope_from_env( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("VERCEL_TOKEN", "env-token") monkeypatch.setenv("VERCEL_PROJECT_ID", "env-project") monkeypatch.setenv("VERCEL_TEAM_ID", "env-team") vercel_module = _load_vercel_module(monkeypatch) existing = _FakeAsyncSandbox(sandbox_id="sandbox-existing") _FakeAsyncSandbox.sandboxes[existing.sandbox_id] = existing state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000100", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=existing.sandbox_id, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(state) assert _FakeAsyncSandbox.get_calls[-1]["token"] is None assert _FakeAsyncSandbox.get_calls[-1]["project_id"] is None assert _FakeAsyncSandbox.get_calls[-1]["team_id"] is None assert resumed._inner.state.project_id is None assert resumed._inner.state.team_id is None @pytest.mark.asyncio async def test_vercel_serialized_session_state_omits_token_and_resume_uses_live_client_token( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) network_policy = NetworkPolicyCustom( allow=["example.com"], subnets=NetworkPolicySubnets(deny=["192.168.0.0/16"]), ) client = vercel_module.VercelSandboxClient(token="token-from-client") session = await client.create( manifest=Manifest(), options=vercel_module.VercelSandboxClientOptions( project_id="project", network_policy=network_policy, ), ) payload = client.serialize_session_state(session.state) restored = client.deserialize_session_state(payload) resumed = await client.resume(restored) assert "token" not in payload assert restored.project_id == "project" assert payload["network_policy"] == { "allow": ["example.com"], "subnets": {"allow": None, "deny": ["192.168.0.0/16"]}, } assert restored.network_policy == network_policy assert _FakeAsyncSandbox.get_calls[-1]["token"] == "token-from-client" assert resumed._inner.state.sandbox_id == session._inner.state.sandbox_id @pytest.mark.asyncio async def test_vercel_tar_persistence_round_trip(monkeypatch: pytest.MonkeyPatch) -> None: vercel_module = _load_vercel_module(monkeypatch) snapshot = _MemorySnapshot(id="snapshot") state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000004", manifest=Manifest(), snapshot=snapshot, sandbox_id="sandbox-tar", workspace_persistence="tar", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-tar") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.write(Path("hello.txt"), io.BytesIO(b"world")) await session.stop() restored_state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000005", manifest=Manifest(), snapshot=snapshot, sandbox_id="sandbox-restored", workspace_persistence="tar", ) restored = vercel_module.VercelSandboxSession.from_state( restored_state, sandbox=_FakeAsyncSandbox(sandbox_id="sandbox-restored"), ) await restored.hydrate_workspace(await snapshot.restore()) payload = await restored.read(Path("hello.txt")) assert payload.read() == b"world" @pytest.mark.asyncio async def test_vercel_tar_persist_raises_archive_error_on_nonzero_exec( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000105", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-tar-fail", workspace_persistence="tar", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-tar-fail") sandbox.tar_create_result = _FakeCommandFinished(stderr="tar failed", exit_code=2) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) with pytest.raises(vercel_module.WorkspaceArchiveReadError) as exc_info: await session.persist_workspace() assert isinstance(exc_info.value.__cause__, vercel_module.ExecNonZeroError) assert exc_info.value.__cause__.exit_code == 2 assert sandbox.run_command_calls[-1] == ( "rm", ["/tmp/openai-agents-00000000000000000000000000000105.tar"], "/workspace", ) def test_vercel_validate_tar_bytes_rejects_unsafe_members( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000103", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-tar-validate", ) session = vercel_module.VercelSandboxSession.from_state(state) absolute_buf = io.BytesIO() with tarfile.open(fileobj=absolute_buf, mode="w") as archive: info = tarfile.TarInfo(name="/etc/passwd") info.size = 4 archive.addfile(info, io.BytesIO(b"root")) with pytest.raises(ValueError, match="absolute path"): session._validate_tar_bytes(absolute_buf.getvalue()) with pytest.raises(ValueError, match="invalid tar stream"): session._validate_tar_bytes(b"not a tar file") @pytest.mark.asyncio async def test_vercel_hydrate_workspace_rejects_unsafe_tar_before_upload( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000104", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-hydrate-unsafe", workspace_persistence="tar", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-hydrate-unsafe") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) unsafe_buf = io.BytesIO() with tarfile.open(fileobj=unsafe_buf, mode="w") as archive: info = tarfile.TarInfo(name="../escape.txt") info.size = 4 archive.addfile(info, io.BytesIO(b"data")) with pytest.raises(vercel_module.WorkspaceArchiveWriteError) as exc_info: await session.hydrate_workspace(io.BytesIO(unsafe_buf.getvalue())) assert "parent traversal" in str(exc_info.value.__cause__) assert sandbox.write_files_calls == [] assert not any( call for call in sandbox.run_command_calls if call[0] == "tar" and call[1][0] == "xf" ) @pytest.mark.asyncio async def test_vercel_hydrate_workspace_rejects_external_symlink_target_before_upload( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000105", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-hydrate-external-link", workspace_persistence="tar", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-hydrate-external-link") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) unsafe_buf = io.BytesIO() with tarfile.open(fileobj=unsafe_buf, mode="w") as archive: info = tarfile.TarInfo(name="leak") info.type = tarfile.SYMTYPE info.linkname = "/etc/passwd" archive.addfile(info) with pytest.raises(vercel_module.WorkspaceArchiveWriteError) as exc_info: await session.hydrate_workspace(io.BytesIO(unsafe_buf.getvalue())) assert "absolute symlink target not allowed" in str(exc_info.value.__cause__) assert sandbox.write_files_calls == [] assert not any( call for call in sandbox.run_command_calls if call[0] == "tar" and call[1][0] == "xf" ) @pytest.mark.asyncio async def test_vercel_hydrate_workspace_raises_archive_error_on_nonzero_tar_exec( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000106", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-hydrate-fail", workspace_persistence="tar", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-hydrate-fail") sandbox.tar_extract_result = _FakeCommandFinished(stderr="extract failed", exit_code=2) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) archive = io.BytesIO() with tarfile.open(fileobj=archive, mode="w") as tar: info = tarfile.TarInfo(name="hello.txt") info.size = 5 tar.addfile(info, io.BytesIO(b"hello")) with pytest.raises(vercel_module.WorkspaceArchiveWriteError) as exc_info: await session.hydrate_workspace(io.BytesIO(archive.getvalue())) assert isinstance(exc_info.value.__cause__, vercel_module.ExecNonZeroError) assert exc_info.value.__cause__.exit_code == 2 assert sandbox.run_command_calls[-1] == ( "rm", ["/tmp/openai-agents-00000000000000000000000000000106.tar"], "/workspace", ) @pytest.mark.asyncio async def test_vercel_write_retries_transient_transport_error( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) monkeypatch.setattr("agents.sandbox.util.retry.asyncio.sleep", _noop_sleep) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000102", manifest=Manifest(), snapshot=NoopSnapshot(id="snapshot"), sandbox_id="sandbox-write-retry", ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-write-retry") sandbox.write_failures = [httpx.ProtocolError("transient write failure")] session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.write(Path("notes.txt"), io.BytesIO(b"payload")) payload = await session.read(Path("notes.txt")) assert payload.read() == b"payload" assert len(sandbox.write_files_calls) == 2 @pytest.mark.asyncio async def test_vercel_snapshot_mode_resume_uses_native_snapshot_reference( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) snapshot = _MemorySnapshot(id="snapshot") state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000006", manifest=Manifest(), snapshot=snapshot, sandbox_id="sandbox-snapshot", workspace_persistence="snapshot", snapshot_expiration_ms=60_000, ) sandbox = _FakeAsyncSandbox(sandbox_id="sandbox-snapshot") session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.write(Path("config.json"), io.BytesIO(b'{"version":1}')) await session.stop() resumed_state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000007", manifest=Manifest(), snapshot=snapshot, sandbox_id="sandbox-snapshot", workspace_persistence="snapshot", snapshot_expiration_ms=60_000, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(resumed_state) payload = await resumed._inner.read(Path("config.json")) assert _FakeAsyncSandbox.create_calls[-1]["source"] == SnapshotSource( snapshot_id="vercel-snapshot-1" ) assert resumed._inner.state.sandbox_id == "vercel-sandbox-1" assert payload.read() == b'{"version":1}' @pytest.mark.asyncio async def test_vercel_tar_persistence_tears_down_ephemeral_mounts( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) snapshot = _MemorySnapshot(id="snapshot") mount = _RecordingMount( mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()) ) sandbox = _FakeAsyncSandbox( sandbox_id="sandbox-mount-tar", files={ "/workspace/kept.txt": b"kept", "/workspace/remote/mounted.txt": b"mounted-content", }, ) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000008", manifest=Manifest(root="/workspace", entries={"remote": mount}), snapshot=snapshot, sandbox_id=sandbox.sandbox_id, workspace_persistence="tar", ) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.stop() with tarfile.open(fileobj=io.BytesIO(snapshot.payload), mode="r") as archive: archived_names = sorted(member.name for member in archive.getmembers()) tar_calls = [ call for call in sandbox.run_command_calls if call[0] == "tar" and call[1][0] == "cf" ] assert mount._events == [("unmount", "/workspace/remote"), ("mount", "/workspace/remote")] assert tar_calls == [ ( "tar", [ "cf", "/tmp/openai-agents-00000000000000000000000000000008.tar", "--exclude=./remote", ".", ], "/workspace", ) ] assert archived_names == ["kept.txt"] assert sandbox.files["/workspace/remote/mounted.txt"] == b"mounted-content" @pytest.mark.asyncio async def test_vercel_snapshot_persistence_tears_down_ephemeral_mounts( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) snapshot = _MemorySnapshot(id="snapshot") mount = _RecordingMount( mount_strategy=InContainerMountStrategy(pattern=MountpointMountPattern()) ) sandbox = _FakeAsyncSandbox( sandbox_id="sandbox-mount-snapshot", files={ "/workspace/kept.txt": b"kept", "/workspace/remote/mounted.txt": b"mounted-content", }, ) state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000009", manifest=Manifest(root="/workspace", entries={"remote": mount}), snapshot=snapshot, sandbox_id=sandbox.sandbox_id, workspace_persistence="snapshot", snapshot_expiration_ms=60_000, ) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=sandbox) await session.stop() restored_state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000010", manifest=Manifest(root="/workspace", entries={"remote": mount}), snapshot=snapshot, sandbox_id="sandbox-mount-snapshot", workspace_persistence="snapshot", snapshot_expiration_ms=60_000, ) client = vercel_module.VercelSandboxClient() resumed = await client.resume(restored_state) assert mount._events == [("unmount", "/workspace/remote"), ("mount", "/workspace/remote")] assert "/workspace/remote/mounted.txt" not in _FakeAsyncSandbox.snapshots["vercel-snapshot-1"] with pytest.raises(vercel_module.WorkspaceReadNotFoundError): await resumed._inner.read(Path("remote/mounted.txt")) kept = await resumed._inner.read(Path("kept.txt")) assert kept.read() == b"kept" @pytest.mark.asyncio async def test_vercel_snapshot_hydrate_replaces_and_stops_superseded_sandbox( monkeypatch: pytest.MonkeyPatch, ) -> None: vercel_module = _load_vercel_module(monkeypatch) current = _FakeAsyncSandbox( sandbox_id="sandbox-current", files={"/workspace/current.txt": b"before"}, ) _FakeAsyncSandbox.snapshots["vercel-snapshot-1"] = {"/workspace/restored.txt": b"after"} state = vercel_module.VercelSandboxSessionState( session_id="00000000-0000-0000-0000-000000000011", manifest=Manifest(root="/workspace"), snapshot=NoopSnapshot(id="snapshot"), sandbox_id=current.sandbox_id, workspace_persistence="snapshot", ) session = vercel_module.VercelSandboxSession.from_state(state, sandbox=current) await session.hydrate_workspace( io.BytesIO(vercel_module._encode_snapshot_ref(snapshot_id="vercel-snapshot-1")) ) assert current.stop_calls == 1 assert current.client.closed is True assert session._sandbox is not current assert session.state.sandbox_id == "vercel-sandbox-1" restored = await session.read(Path("restored.txt")) assert restored.read() == b"after"