open-swe/agent/utils/sandbox.py
Adam Moussa cfb5624663
Some checks are pending
CI / Lint (push) Waiting to run
CI / Format check (push) Waiting to run
CI / Typecheck (push) Waiting to run
CI / Unit tests (push) Waiting to run
CI / Playwright E2E (push) Waiting to run
CI / Docker build smoke (push) Waiting to run
CI / Triage ledger up to date (push) Waiting to run
CI / ui bun.lock in sync (push) Waiting to run
feat(open-swe): add E2B sandbox provider (port of upstream 48217b68, #1489) (#199)
Additive sandbox provider adapted to this fork's synchronous create_sandbox
factory: agent/integrations/e2b.py registered as a lazy-import entry in
SANDBOX_FACTORIES, so the e2b/langchain-e2b imports only load when
SANDBOX_TYPE=e2b (dark-safe on unset env; unset E2B_API_KEY raises a clean
ValueError only when the provider is selected). Supports reconnect-by-id and
optional E2B_TEMPLATE. Non-langsmith providers skip the GitHub proxy step,
so the GitHub-App token flow is untouched.

Upstream: langchain-ai/open-swe 48217b68 (#1489), re-implemented against the
fork's sync sandbox lifecycle rather than cherry-picked (upstream ships on
the deferred async sandbox.py base).

Docs: provider tables/lists in README, CUSTOMIZATION, INSTALLATION; the
CUSTOMIZATION registration example now shows the lazy-tuple form the code
actually uses. uv.lock refreshed (adds e2b, langchain-e2b, dockerfile-parse).
2026-07-16 18:23:42 -04:00

67 lines
2.6 KiB
Python

import os
from collections.abc import Callable
from importlib import import_module
from typing import Any
SandboxFactory = Callable[..., Any]
SANDBOX_FACTORIES: dict[str, tuple[str, str]] = {
"langsmith": ("agent.integrations.langsmith", "create_langsmith_sandbox"),
"daytona": ("agent.integrations.daytona", "create_daytona_sandbox"),
"modal": ("agent.integrations.modal", "create_modal_sandbox"),
"runloop": ("agent.integrations.runloop", "create_runloop_sandbox"),
"e2b": ("agent.integrations.e2b", "create_e2b_sandbox"),
"local": ("agent.integrations.local", "create_local_sandbox"),
}
def _load_sandbox_factory(sandbox_type: str) -> SandboxFactory:
factory_path = SANDBOX_FACTORIES.get(sandbox_type)
if factory_path is None:
supported = ", ".join(sorted(SANDBOX_FACTORIES))
raise ValueError(f"Invalid sandbox type: {sandbox_type}. Supported types: {supported}")
module_name, function_name = factory_path
factory = getattr(import_module(module_name), function_name)
if not callable(factory):
raise TypeError(f"Sandbox factory {module_name}.{function_name} is not callable")
return factory
def create_sandbox(
sandbox_id: str | None = None,
*,
snapshot_id: str | None = None,
) -> Any:
"""Create or reconnect to a sandbox using the configured provider.
The provider is selected via the SANDBOX_TYPE environment variable.
Supported values: langsmith (default), daytona, modal, runloop, e2b, local.
Args:
sandbox_id: Optional existing sandbox ID to reconnect to.
snapshot_id: Optional snapshot to boot a new sandbox from. Only the
langsmith provider honors this; others ignore it. When omitted the
langsmith provider falls back to DEFAULT_SANDBOX_SNAPSHOT_ID.
Returns:
A sandbox backend implementing SandboxBackendProtocol.
"""
sandbox_type = os.getenv("SANDBOX_TYPE", "langsmith")
factory = _load_sandbox_factory(sandbox_type)
if sandbox_type == "langsmith" and snapshot_id is not None:
return factory(sandbox_id, snapshot_id=snapshot_id)
return factory(sandbox_id)
def validate_sandbox_startup_config() -> None:
"""Validate the configured sandbox provider's env vars at server startup.
Raises ValueError if the active provider's configuration is invalid.
Called from the FastAPI lifespan hook so errors surface at boot rather
than on the first sandbox creation.
"""
sandbox_type = os.getenv("SANDBOX_TYPE", "langsmith")
if sandbox_type == "langsmith":
from agent.integrations.langsmith import LangSmithProvider
LangSmithProvider.validate_startup_config()