open-swe/agent/utils/sandbox.py
Johannes du Plessis 3992d3ef5d
feat: repo-scoped dynamic sandbox snapshots (#1595)
* feat: repo-scoped dynamic sandbox snapshots

Let admins build a per-repo sandbox image from a custom Dockerfile so runs
targeting that repo boot from a snapshot with its deps pre-baked. Snapshot
selection is purely additive: repos without a `ready` repo-scoped snapshot
always fall back to the configured DEFAULT_SANDBOX_SNAPSHOT_ID.

Backend adds a repo_snapshots store module (Dockerfile + build status keyed by
owner/name), threads the resolved repo through the LangSmith sandbox creation
path, runs builds via SandboxClient.create_snapshot_from_dockerfile in a
throwaway builder sandbox, and exposes admin-only CRUD + build endpoints. The
UI adds an admin-only Agents-tab page (repo picker + Monaco Dockerfile editor +
build status/logs).

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: harden repo snapshot builds

Require REPO_SNAPSHOT_BASE_IMAGE for generated Dockerfile templates so admins
cannot accidentally build a repo snapshot from a bare Python image that lacks
Open SWE's sandbox tools. Allow stale building records to be retried by tracking
build_started_at and treating old or missing timestamps as stale.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* fix: document repo snapshot base image config

Document REPO_SNAPSHOT_BASE_IMAGE alongside sandbox snapshot setup and convert
missing base-image configuration into a handled dashboard API error so admins see
a clear configuration message instead of an unhandled template-generation error.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-23 12:24:11 -07:00

67 lines
2.7 KiB
Python

import os
from collections.abc import Callable
from importlib import import_module
from deepagents.backends.protocol import SandboxBackendProtocol
SandboxFactory = Callable[[str | None], SandboxBackendProtocol]
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"),
"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,
) -> SandboxBackendProtocol:
"""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, 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()