diff --git a/sandboxes/__init__.py b/sandboxes/__init__.py index d2cb014..dd3af96 100644 --- a/sandboxes/__init__.py +++ b/sandboxes/__init__.py @@ -8,6 +8,7 @@ from .base import ( ExecutionResult, + ProviderCapabilities, SandboxConfig, SandboxProvider, SandboxState, @@ -40,6 +41,7 @@ "SandboxConfig", "ExecutionResult", "SandboxState", + "ProviderCapabilities", # Manager "SandboxManager", "Manager", # Alias diff --git a/sandboxes/providers/cloudflare.py b/sandboxes/providers/cloudflare.py index 5f2bdb4..f8e48f4 100644 --- a/sandboxes/providers/cloudflare.py +++ b/sandboxes/providers/cloudflare.py @@ -15,7 +15,14 @@ import httpx -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError from ..security import validate_download_path, validate_upload_path @@ -26,6 +33,12 @@ class CloudflareProvider(SandboxProvider): """Interact with a Cloudflare Sandbox Worker deployment via HTTP API.""" + CAPABILITIES = ProviderCapabilities( + persistent=True, + streaming=True, + file_upload=True, + ) + def __init__( self, *, diff --git a/sandboxes/providers/daytona.py b/sandboxes/providers/daytona.py index 665f819..9883ff8 100644 --- a/sandboxes/providers/daytona.py +++ b/sandboxes/providers/daytona.py @@ -5,7 +5,14 @@ import os from typing import Any -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError from ..security import validate_download_path, validate_upload_path @@ -34,6 +41,12 @@ class DaytonaProvider(SandboxProvider): """Daytona sandbox provider implementation.""" + CAPABILITIES = ProviderCapabilities( + persistent=True, + snapshot=True, + file_upload=True, + ) + def __init__(self, api_key: str | None = None, **config): """Initialize Daytona provider.""" super().__init__(**config) diff --git a/sandboxes/providers/e2b.py b/sandboxes/providers/e2b.py index 246904e..f489cb6 100644 --- a/sandboxes/providers/e2b.py +++ b/sandboxes/providers/e2b.py @@ -8,7 +8,14 @@ from datetime import datetime from typing import Any -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError from ..security import validate_download_path, validate_upload_path @@ -27,6 +34,12 @@ class E2BProvider(SandboxProvider): """E2B sandbox provider using the official SDK.""" + CAPABILITIES = ProviderCapabilities( + persistent=True, + streaming=True, + file_upload=True, + ) + def __init__(self, api_key: str | None = None, **config): """ Initialize E2B provider. diff --git a/sandboxes/providers/hopx.py b/sandboxes/providers/hopx.py index 9bbd60f..feebb9b 100644 --- a/sandboxes/providers/hopx.py +++ b/sandboxes/providers/hopx.py @@ -8,7 +8,14 @@ from datetime import datetime from typing import Any -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError from ..security import validate_download_path, validate_upload_path @@ -27,6 +34,12 @@ class HopxProvider(SandboxProvider): """Hopx sandbox provider using the official hopx-ai SDK.""" + CAPABILITIES = ProviderCapabilities( + persistent=True, + streaming=True, + file_upload=True, + ) + def __init__(self, api_key: str | None = None, **config): """ Initialize Hopx provider. diff --git a/sandboxes/providers/modal.py b/sandboxes/providers/modal.py index 7a77fc5..d711e2d 100644 --- a/sandboxes/providers/modal.py +++ b/sandboxes/providers/modal.py @@ -7,7 +7,14 @@ from datetime import datetime from typing import Any -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError logger = logging.getLogger(__name__) @@ -27,6 +34,11 @@ class ModalProvider(SandboxProvider): """Modal sandbox provider implementation.""" + CAPABILITIES = ProviderCapabilities( + persistent=True, + streaming=True, + ) + def __init__(self, **config): """Initialize Modal provider. diff --git a/sandboxes/providers/sprites.py b/sandboxes/providers/sprites.py index 16f513e..10cf1d9 100644 --- a/sandboxes/providers/sprites.py +++ b/sandboxes/providers/sprites.py @@ -11,7 +11,14 @@ from datetime import datetime from typing import Any -from ..base import ExecutionResult, Sandbox, SandboxConfig, SandboxProvider, SandboxState +from ..base import ( + ExecutionResult, + ProviderCapabilities, + Sandbox, + SandboxConfig, + SandboxProvider, + SandboxState, +) from ..exceptions import ProviderError, SandboxError, SandboxNotFoundError logger = logging.getLogger(__name__) @@ -51,6 +58,12 @@ class SpritesProvider(SandboxProvider): - CLI mode: Uses sprite CLI with existing login (sprite login) """ + CAPABILITIES = ProviderCapabilities( + persistent=True, + streaming=True, + interactive_shell=True, + ) + def __init__(self, token: str | None = None, use_cli: bool = False, **config): """Initialize Sprites provider. diff --git a/tests/test_provider_capabilities.py b/tests/test_provider_capabilities.py new file mode 100644 index 0000000..932d2fb --- /dev/null +++ b/tests/test_provider_capabilities.py @@ -0,0 +1,83 @@ +"""Tests for provider capability declarations.""" + +from sandboxes.providers.cloudflare import CloudflareProvider +from sandboxes.providers.daytona import DaytonaProvider +from sandboxes.providers.e2b import E2BProvider +from sandboxes.providers.hopx import HopxProvider +from sandboxes.providers.modal import ModalProvider +from sandboxes.providers.sprites import SpritesProvider +from sandboxes.providers.vercel import VercelProvider + + +def test_provider_capability_matrix_contract(): + """Providers should declare capabilities that match implemented features.""" + expected = { + "e2b": { + "persistent": True, + "snapshot": False, + "streaming": True, + "file_upload": True, + "interactive_shell": False, + "gpu": False, + }, + "modal": { + "persistent": True, + "snapshot": False, + "streaming": True, + "file_upload": False, + "interactive_shell": False, + "gpu": False, + }, + "daytona": { + "persistent": True, + "snapshot": True, + "streaming": False, + "file_upload": True, + "interactive_shell": False, + "gpu": False, + }, + "hopx": { + "persistent": True, + "snapshot": False, + "streaming": True, + "file_upload": True, + "interactive_shell": False, + "gpu": False, + }, + "sprites": { + "persistent": True, + "snapshot": False, + "streaming": True, + "file_upload": False, + "interactive_shell": True, + "gpu": False, + }, + "cloudflare": { + "persistent": True, + "snapshot": False, + "streaming": True, + "file_upload": True, + "interactive_shell": False, + "gpu": False, + }, + "vercel": { + "persistent": True, + "snapshot": True, + "streaming": True, + "file_upload": True, + "interactive_shell": True, + "gpu": False, + }, + } + + observed = { + "e2b": E2BProvider.get_capabilities().as_dict(), + "modal": ModalProvider.get_capabilities().as_dict(), + "daytona": DaytonaProvider.get_capabilities().as_dict(), + "hopx": HopxProvider.get_capabilities().as_dict(), + "sprites": SpritesProvider.get_capabilities().as_dict(), + "cloudflare": CloudflareProvider.get_capabilities().as_dict(), + "vercel": VercelProvider.get_capabilities().as_dict(), + } + + assert observed == expected