from dataclasses import dataclass, field from datetime import datetime from typing import ( Any, Callable, Dict, List, Literal, Mapping, Optional, TypedDict, Union, cast, ) from typing_extensions import NotRequired, Unpack from e2b.api.client.models import ( ListedSandbox, SandboxDetail, SandboxState, ) from e2b.api.client.models import ( SandboxLifecycle as ClientSandboxLifecycle, ) from e2b.api.client.models import ( SandboxNetworkConfig as ClientSandboxNetworkConfig, ) from e2b.api.client.models import ( SandboxNetworkConfigRules, ) from e2b.api.client.models import ( SandboxNetworkRule as ClientSandboxNetworkRule, ) from e2b.api.client.models import ( SandboxNetworkTransform as ClientSandboxNetworkTransform, ) from e2b.api.client.models import ( SandboxNetworkTransformHeaders as ClientSandboxNetworkTransformHeaders, ) from e2b.api.client.models import ( SandboxNetworkUpdateConfig, ) from e2b.api.client.models import ( SandboxNetworkUpdateConfigRules, ) from e2b.api.client.types import Unset from e2b.connection_config import ApiParams from e2b.sandbox.mcp import McpServer as BaseMcpServer from e2b.sandbox.network import ALL_TRAFFIC from e2b.paginator import PaginatorBase class GitHubMcpServerConfig(TypedDict): """ Configuration for a GitHub-based MCP server. """ run_cmd: str """ Command to run the MCP server. Must start a stdio-compatible server. """ install_cmd: NotRequired[str] """ Command to install dependencies for the MCP server. Working directory is the root of the github repository. """ envs: NotRequired[Dict[str, str]] """ Environment variables to set in the MCP process. """ # Extended MCP server configuration that includes base servers # and allows dynamic GitHub-based MCP servers with custom run and install commands. # For GitHub servers, use keys in the format "github/owner/repo" GitHubMcpServer = Dict[str, Union[GitHubMcpServerConfig, Any]] # Union type that combines base MCP servers with GitHub-based servers McpServer = Union[BaseMcpServer, GitHubMcpServer] class SandboxNetworkTransform(TypedDict): """ Transform applied to egress requests matching a :class:`SandboxNetworkRule`. """ headers: NotRequired[Dict[str, str]] """ Headers to inject into the outbound request. Values override any headers already present on the request. """ class SandboxNetworkRule(TypedDict): """ Per-domain rule applied to egress requests. """ transform: NotRequired[SandboxNetworkTransform] """ Transform applied to requests matching this rule. """ SandboxNetworkRules = Dict[str, List[SandboxNetworkRule]] """ Map of host (or CIDR / IP) to ordered list of rules applied to outbound requests for that host. Registering a host here does not allow egress on its own — the host must also appear in ``SandboxNetworkOpts.allow_out``. """ class SandboxNetworkRuleInfo(TypedDict): """ Per-domain rule as returned by the sandbox info endpoint. Mirrors :class:`SandboxNetworkRule` but with ``transform`` always materialized to the static :class:`SandboxNetworkTransform` shape — no callable variant. """ transform: NotRequired[SandboxNetworkTransform] @dataclass(frozen=True) class SandboxNetworkSelectorContext: """ Context passed to ``allow_out``/``deny_out`` callables. """ all_traffic: str """All traffic sentinel — equivalent to ``"0.0.0.0/0"``.""" rules: Mapping[str, List[SandboxNetworkRule]] """Rules registered in :attr:`SandboxNetworkOpts.rules`.""" SandboxNetworkSelector = Union[ List[str], Callable[[SandboxNetworkSelectorContext], List[str]], ] """ Egress rule list, either a static list of CIDR blocks / IP addresses / hostnames, or a callable that receives a :class:`SandboxNetworkSelectorContext` and returns the same. """ class SandboxNetworkOpts(TypedDict): """ Sandbox network configuration options. """ allow_out: NotRequired[SandboxNetworkSelector] """ Allow outbound traffic from the sandbox to the specified addresses. If ``allow_out`` is not specified, all outbound traffic is allowed. Accepts either a static list of CIDR blocks / IP addresses / hostnames, or a callable that receives a :class:`SandboxNetworkSelectorContext` and returns the same. ``ctx.all_traffic`` is ``"0.0.0.0/0"``; ``ctx.rules`` is a read-only view of :attr:`rules`. Examples: - Static list: ``["1.1.1.1", "8.8.8.0/24"]`` - Allow only rule-registered hosts: ``lambda ctx: list(ctx.rules.keys())`` """ deny_out: NotRequired[SandboxNetworkSelector] """ Deny outbound traffic from the sandbox to the specified addresses. Accepts the same shapes as ``allow_out``. Examples: - Static list: ``["1.1.1.1", "8.8.8.0/24"]`` - Block all egress: ``lambda ctx: [ctx.all_traffic]`` """ rules: NotRequired[SandboxNetworkRules] """ Per-domain transform rules applied to matching egress HTTP/HTTPS requests. Keys are domains (e.g. ``"api.example.com"``); values are ordered lists of :class:`SandboxNetworkRule`. Registering a host here does not allow egress on its own — the host must also appear in ``allow_out``. Hosts registered here are exposed to the ``allow_out``/``deny_out`` callables via ``ctx.rules``. """ allow_public_traffic: NotRequired[bool] """ Controls whether sandbox URLs should be publicly accessible or require authentication. Defaults to True. """ mask_request_host: NotRequired[str] """ Allows specifying a custom host mask for all sandbox requests. Supports ${PORT} variable. Defaults to "${PORT}-sandboxid.e2b.app". Examples: - Custom subdomain: `"${PORT}-myapp.example.com"` """ class SandboxNetworkUpdate(TypedDict, total=False): """ Subset of :class:`SandboxNetworkOpts` accepted by ``Sandbox.update_network``. The update endpoint replaces all egress rules atomically — fields that are omitted are cleared on the server. """ allow_out: SandboxNetworkSelector """See :attr:`SandboxNetworkOpts.allow_out`.""" deny_out: SandboxNetworkSelector """See :attr:`SandboxNetworkOpts.deny_out`.""" rules: SandboxNetworkRules """See :attr:`SandboxNetworkOpts.rules`.""" allow_internet_access: bool """ Allow sandbox to access the internet. When set to ``False``, it behaves the same as specifying ``deny_out=["0.0.0.0/0"]`` in the network config. """ class SandboxNetworkInfo(TypedDict, total=False): """ Network configuration as returned by the sandbox info endpoint. Mirrors :class:`SandboxNetworkOpts` but with ``allow_out``/``deny_out`` always materialized to plain string lists. """ allow_out: List[str] deny_out: List[str] rules: Dict[str, List[SandboxNetworkRuleInfo]] allow_public_traffic: bool mask_request_host: str class SandboxOnTimeoutPause(TypedDict): """ Object form of `on_timeout` that auto-pauses the sandbox when the timeout is reached, optionally controlling the pause snapshot kind via `keep_memory`. """ action: Literal["pause"] """Auto-pause the sandbox when the timeout is reached.""" keep_memory: NotRequired[bool] """ Whether the timeout auto-pause keeps a full memory snapshot. Defaults to `True`. When `False`, the auto-pause drops the in-memory state and persists only the filesystem (a filesystem-only snapshot); resuming such a sandbox cold-boots (reboots) it from disk, losing running processes and open connections. Cannot be combined with `auto_resume`: auto-resume wakes a paused sandbox on inbound traffic by restoring its memory snapshot in place, so the request that woke it hits an already-running process. A filesystem-only snapshot has no memory to restore — resuming cold-boots it — so it can't be woken transparently by traffic and must be resumed explicitly via `connect()`. """ class SandboxOnTimeoutKill(TypedDict): """ Object form of `on_timeout` that kills the sandbox when the timeout is reached. """ action: Literal["kill"] """Kill the sandbox when the timeout is reached.""" SandboxOnTimeout = Union[ Literal["pause", "kill"], SandboxOnTimeoutPause, SandboxOnTimeoutKill ] """ What should happen to the sandbox when the timeout is reached. Either the bare action (`"pause"` / `"kill"`) or the object form. The object form is a discriminated union on `action`: `keep_memory` is only accepted alongside `action: "pause"`. Passing `keep_memory` with `action: "kill"` is a static type error. """ class SandboxLifecycle(TypedDict): """ Sandbox lifecycle configuration; defines post-timeout behavior and auto-resume settings. Defaults to `on_timeout="kill"` and `auto_resume=False`. """ on_timeout: SandboxOnTimeout """ What should happen to the sandbox when timeout is reached. `"kill"` terminates the sandbox; `"pause"` pauses it for later resume. Accepts either the bare action or an object `{"action": "pause", "keep_memory": ...}` / `{"action": "kill"}` to also control the pause snapshot kind. Defaults to `"kill"`. """ auto_resume: NotRequired[bool] """ Whether activity should cause the sandbox to resume when paused. Defaults to `False`. Can be `True` only when `on_timeout` is `pause`. Not supported when `keep_memory` is `False` (a filesystem-only snapshot must be resumed explicitly via `connect()`). """ class SandboxInfoLifecycle(TypedDict): """ Sandbox lifecycle configuration returned by sandbox info. """ on_timeout: Literal["pause", "kill"] """ What should happen to the sandbox when timeout is reached. """ auto_resume: bool """ Whether activity should cause the sandbox to resume when paused. """ def _resolve_network_selector( selector: Optional[SandboxNetworkSelector], rules: Mapping[str, List[SandboxNetworkRule]], ) -> Optional[List[str]]: if selector is None: return None if callable(selector): ctx = SandboxNetworkSelectorContext(all_traffic=ALL_TRAFFIC, rules=rules) return list(selector(ctx)) return list(selector) def _build_client_rules(rules: SandboxNetworkRules) -> SandboxNetworkConfigRules: client_rules = SandboxNetworkConfigRules() for host, host_rules in rules.items(): converted: List[ClientSandboxNetworkRule] = [] for rule in host_rules: transform = rule.get("transform") if transform is None: converted.append(ClientSandboxNetworkRule()) continue client_transform = ClientSandboxNetworkTransform() headers = transform.get("headers") if headers: client_headers = ClientSandboxNetworkTransformHeaders() client_headers.additional_properties = dict(headers) client_transform.headers = client_headers converted.append(ClientSandboxNetworkRule(transform=client_transform)) client_rules.additional_properties[host] = converted return client_rules def _build_network_egress( network: Mapping[str, Any], ) -> Dict[str, Any]: """ Resolve the shared egress fields (``allow_out`` / ``deny_out`` / per-host ``rules``) used by both the create and update endpoints. ``rules`` in the returned dict is the inner ``Dict[host, List[ClientSandboxNetworkRule]]`` — callers wrap it in their endpoint-specific rules attrs class. """ rules = network.get("rules") or {} allow_out = _resolve_network_selector(network.get("allow_out"), rules) deny_out = _resolve_network_selector(network.get("deny_out"), rules) body: Dict[str, Any] = {} if allow_out is not None: body["allow_out"] = allow_out if deny_out is not None: body["deny_out"] = deny_out if "rules" in network and network["rules"] is not None: body["rules"] = _build_client_rules(network["rules"]).additional_properties return body def build_network_config( network: Optional[SandboxNetworkOpts], ) -> Optional[Dict[str, Any]]: """Resolve a :class:`SandboxNetworkOpts` into the dict the API expects.""" if network is None: return None body = _build_network_egress(network) if "rules" in body: client_rules = SandboxNetworkConfigRules() client_rules.additional_properties = body["rules"] body["rules"] = client_rules if "allow_public_traffic" in network: body["allow_public_traffic"] = network["allow_public_traffic"] if "mask_request_host" in network: body["mask_request_host"] = network["mask_request_host"] return body def build_network_update_body( network: SandboxNetworkUpdate, ) -> SandboxNetworkUpdateConfig: """Resolve a :class:`SandboxNetworkUpdate` into the API client body.""" egress = _build_network_egress(network) body = SandboxNetworkUpdateConfig() if "allow_out" in egress: body.allow_out = egress["allow_out"] if "deny_out" in egress: body.deny_out = egress["deny_out"] if "rules" in egress: rules = SandboxNetworkUpdateConfigRules() rules.additional_properties = egress["rules"] body.rules = rules if "allow_internet_access" in network: body.allow_internet_access = network["allow_internet_access"] return body def from_client_network_config( network: Union[Unset, ClientSandboxNetworkConfig], ) -> Optional[SandboxNetworkInfo]: if isinstance(network, Unset): return None result: SandboxNetworkInfo = {} if not isinstance(network.allow_out, Unset): result["allow_out"] = list(network.allow_out) if not isinstance(network.deny_out, Unset): result["deny_out"] = list(network.deny_out) if not isinstance(network.rules, Unset): result["rules"] = cast( Dict[str, List[SandboxNetworkRuleInfo]], network.rules.to_dict() ) if not isinstance(network.allow_public_traffic, Unset): result["allow_public_traffic"] = network.allow_public_traffic if not isinstance(network.mask_request_host, Unset): result["mask_request_host"] = network.mask_request_host return result def from_client_lifecycle( lifecycle: Union[Unset, ClientSandboxLifecycle], ) -> Optional[SandboxInfoLifecycle]: if isinstance(lifecycle, Unset): return None result: SandboxInfoLifecycle = { "on_timeout": cast(Literal["pause", "kill"], lifecycle.on_timeout), "auto_resume": lifecycle.auto_resume, } return result @dataclass class SandboxInfo: """Information about a sandbox.""" sandbox_id: str """Sandbox ID.""" sandbox_domain: Optional[str] """Domain where the sandbox is hosted.""" template_id: str """Template ID.""" name: Optional[str] """Template name.""" metadata: Dict[str, str] """Saved sandbox metadata.""" started_at: datetime """Sandbox start time.""" end_at: datetime """Sandbox expiration date.""" state: SandboxState """Sandbox state.""" cpu_count: int """Sandbox CPU count.""" memory_mb: int """Sandbox Memory size in MiB.""" envd_version: str """Envd version.""" allow_internet_access: Optional[bool] = None """Whether internet access was explicitly enabled or disabled for the sandbox.""" network: Optional[SandboxNetworkInfo] = None """Sandbox network configuration.""" lifecycle: Optional[SandboxInfoLifecycle] = None """Sandbox lifecycle configuration.""" volume_mounts: List[Dict[str, str]] = field(default_factory=list) """Volume mounts for the sandbox.""" @classmethod def _from_sandbox_data( cls, sandbox: Union[ListedSandbox, SandboxDetail], sandbox_domain: Optional[str] = None, allow_internet_access: Optional[bool] = None, network: Optional[SandboxNetworkInfo] = None, lifecycle: Optional[SandboxInfoLifecycle] = None, ): return cls( sandbox_domain=sandbox_domain, sandbox_id=sandbox.sandbox_id, template_id=sandbox.template_id, name=(sandbox.alias if isinstance(sandbox.alias, str) else None), metadata=cast( Dict[str, str], sandbox.metadata if isinstance(sandbox.metadata, dict) else {}, ), started_at=sandbox.started_at, end_at=sandbox.end_at, state=sandbox.state, cpu_count=sandbox.cpu_count, memory_mb=sandbox.memory_mb, envd_version=sandbox.envd_version, volume_mounts=[ {"name": vm.name, "path": vm.path} for vm in sandbox.volume_mounts ] if not isinstance(sandbox.volume_mounts, Unset) else [], allow_internet_access=allow_internet_access, network=network, lifecycle=lifecycle, ) @classmethod def _from_listed_sandbox(cls, listed_sandbox: ListedSandbox): return cls._from_sandbox_data(listed_sandbox) @classmethod def _from_sandbox_detail(cls, sandbox_detail: SandboxDetail): return cls._from_sandbox_data( sandbox_detail, sandbox_domain=( sandbox_detail.domain if isinstance(sandbox_detail.domain, str) else None ), allow_internet_access=( sandbox_detail.allow_internet_access if isinstance(sandbox_detail.allow_internet_access, bool) else None ), network=from_client_network_config(sandbox_detail.network), lifecycle=from_client_lifecycle(sandbox_detail.lifecycle), ) @dataclass class SandboxQuery: """Query parameters for listing sandboxes.""" metadata: Optional[dict[str, str]] = None """Filter sandboxes by metadata.""" state: Optional[list[SandboxState]] = None """Filter sandboxes by state.""" @dataclass class SandboxMetrics: """Sandbox metrics.""" cpu_count: int """Number of CPUs.""" cpu_used_pct: float """CPU usage percentage.""" disk_total: int """Total disk space in bytes.""" disk_used: int """Disk used in bytes.""" mem_total: int """Total memory in bytes.""" mem_used: int """Memory used in bytes.""" mem_cache: int """Cached memory (page cache) in bytes.""" timestamp: datetime """Timestamp of the metric entry.""" @dataclass class SnapshotInfo: """Information about a snapshot.""" snapshot_id: str """Snapshot identifier — template ID with tag, or namespaced name with tag (e.g. my-snapshot:latest). Can be used with Sandbox.create() to create a new sandbox from this snapshot.""" names: List[str] = field(default_factory=list) """Full names of the snapshot template including team namespace and tag (e.g. team-slug/my-snapshot:v2).""" class SnapshotPaginatorBase(PaginatorBase[SnapshotInfo, ApiParams]): def __init__( self, sandbox_id: Optional[str] = None, limit: Optional[int] = None, next_token: Optional[str] = None, **opts: Unpack[ApiParams], ): super().__init__(limit=limit, next_token=next_token, **opts) self.sandbox_id = sandbox_id class SandboxPaginatorBase(PaginatorBase[SandboxInfo, ApiParams]): def __init__( self, query: Optional[SandboxQuery] = None, limit: Optional[int] = None, next_token: Optional[str] = None, **opts: Unpack[ApiParams], ): super().__init__(limit=limit, next_token=next_token, **opts) self.query = query