Skip to main content
Esprit CLI supports two runtime modes for executing scans: Cloud and Docker. Each mode has different requirements and use cases.

Runtime Selection

Esprit automatically selects the runtime based on your configuration:
esprit/interface/main.py
def _should_use_cloud_runtime() -> bool:
    """Check if scan runtime should be routed to Esprit Cloud."""
    from esprit.auth.credentials import get_user_plan, is_authenticated, verify_subscription
    
    if not is_authenticated():
        return False
    
    model_name = Config.get("esprit_llm")
    if not _is_cloud_subscription_model(model_name):
        return False
    
    # Verify subscription with server
    verification = verify_subscription(force_refresh=True)
    if verification.get("valid", False):
        cloud_enabled = verification.get("cloud_enabled")
        return bool(cloud_enabled) if cloud_enabled is not None else True
    
    # Fallback to local plan data if server unreachable
    error = str(verification.get("error", ""))
    return error.startswith("Subscription verification failed:") and _is_paid_subscription_plan(get_user_plan())

Cloud Runtime

Overview

Cloud runtime executes scans in managed sandboxes on Esprit Cloud infrastructure.
esprit/runtime/cloud_runtime.py
class CloudRuntime(AbstractRuntime):
    """Runtime that executes scans in Esprit cloud sandboxes."""
    
    def __init__(self, access_token: str, api_base: str) -> None:
        if not access_token:
            raise SandboxInitializationError(
                "Esprit Cloud authentication required.",
                "Run `esprit login` and try again.",
            )
        
        self.access_token = access_token
        self.api_base = api_base.rstrip("/")
        self._sandboxes: dict[str, dict[str, Any]] = {}

Requirements

1

Esprit Subscription

Active subscription with cloud-enabled plan (Pro, Team, Enterprise)
esprit provider login esprit
2

Cloud-Compatible Model

Model that routes through Esprit Cloud:
export ESPRIT_LLM="esprit/default"
export ESPRIT_LLM="esprit/kimi-k2.5"
export ESPRIT_LLM="bedrock/us.anthropic.claude-sonnet-4"
3

Internet Connection

Stable connection to Esprit Cloud API

When Cloud Runtime is Used

esprit/interface/main.py
def _is_cloud_subscription_model(model_name: str | None) -> bool:
    if not model_name:
        return True
    model_lower = model_name.strip().lower()
    return model_lower.startswith("esprit/") or model_lower.startswith("bedrock/")

Esprit Models

esprit/default esprit/kimi-k2.5 esprit/haiku

Bedrock Models

bedrock/us.anthropic.claude-sonnet-4 bedrock/us.anthropic.claude-opus-4

Cloud Sandbox Lifecycle

esprit/runtime/cloud_runtime.py
async def create_sandbox(self, agent_id: str) -> SandboxInfo:
    """Create a new cloud sandbox."""
    headers = {"Authorization": f"Bearer {self.access_token}"}
    payload = {"agent_id": agent_id}
    
    async with httpx.AsyncClient(timeout=30.0) as client:
        response = await client.post(
            f"{self.api_base}/sandboxes",
            json=payload,
            headers=headers,
        )
        response.raise_for_status()
        data = response.json()
    
    sandbox_id = data["sandbox_id"]
    tool_server_url = data["tool_server_url"]
    tool_server_token = data["tool_server_token"]
    
    # Wait for sandbox to be ready
    await self._wait_for_sandbox_ready(sandbox_id)
Cloud sandboxes automatically shut down after the scan completes or times out.

Configuration

# Esprit API URL (default: https://esprit.dev/api/v1)
export ESPRIT_API_URL="https://esprit.dev/api/v1"

# Runtime backend
export ESPRIT_RUNTIME_BACKEND="cloud"

# Model
export ESPRIT_LLM="esprit/default"

Sandbox State Tracking

esprit/runtime/cloud_runtime.py
_STATE_FILE_ENV = "ESPRIT_CLOUD_SANDBOX_STATE_FILE"
_DEFAULT_STATE_FILE = Path.home() / ".esprit" / "state" / "cloud_sandboxes.json"

@classmethod
def _load_tracked_sandboxes(cls) -> list[dict[str, str]]:
    """Load tracked sandbox IDs for cleanup."""
    path = cls._state_file_path()
    try:
        if not path.exists():
            return []
        payload = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return []
    
    return payload.get("sandboxes", [])
State file location: ~/.esprit/state/cloud_sandboxes.json

Cleanup

esprit/interface/main.py
def _cleanup_orphaned_cloud_sandboxes(console: Console) -> None:
    """Best-effort cleanup for cloud sandboxes left by abrupt previous exits."""
    from esprit.auth.credentials import get_auth_token
    from esprit.runtime.cloud_runtime import CloudRuntime
    
    access_token = get_auth_token()
    if not access_token:
        return
    
    api_base = os.getenv("ESPRIT_API_URL", "https://esprit.dev/api/v1")
    try:
        cleaned = asyncio.run(CloudRuntime.cleanup_stale_sandboxes(access_token, api_base))
    except Exception:
        logging.debug("Failed to run stale cloud sandbox cleanup", exc_info=True)
        return
    
    if cleaned > 0:
        suffix = "sandbox" if cleaned == 1 else "sandboxes"
        console.print(f"[dim]Cleaned up {cleaned} stale cloud {suffix} from previous runs.[/]")
Orphaned sandboxes are automatically cleaned up on the next run.

Docker Runtime

Overview

Docker runtime executes scans in local Docker containers.
esprit/runtime/docker_runtime.py
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
CONTAINER_TOOL_SERVER_PORT = 48081

class DockerRuntime(AbstractRuntime):
    def __init__(self) -> None:
        try:
            docker_timeout = int(Config.get("esprit_sandbox_timeout") or "60")
            self.client = docker.from_env(timeout=docker_timeout)
        except (DockerException, RequestsConnectionError, RequestsTimeout) as e:
            raise SandboxInitializationError(
                "Docker is not available",
                "Please ensure Docker Desktop is installed and running.",
            ) from e
        
        self._scan_container: Container | None = None
        self._tool_server_port: int | None = None
        self._tool_server_token: str | None = None

Requirements

1

Docker Installed

Install Docker Desktop or Docker Engine:
# Verify Docker
docker --version
2

Docker Running

Ensure Docker daemon is running:
docker ps
3

Sandbox Image

Esprit automatically pulls the sandbox image on first run:
export ESPRIT_IMAGE="improdead/esprit-sandbox:latest"
4

Disk Space

At least 2GB free space on Docker data root

Docker Health Check

esprit/interface/main.py
def _docker_health_check(console: Console, config: type) -> bool:
    """Consolidated pre-scan Docker health check."""
    import docker as docker_lib
    
    failures: list[tuple[str, str]] = []
    
    # 1. Docker daemon running
    try:
        client = docker_lib.from_env()
        client.ping()
    except Exception:
        failures.append(
            ("Docker daemon is not running", "Start Docker Desktop or run: sudo systemctl start docker"),
        )
    
    # 2. Sandbox image configured
    if not failures:
        image_name = str(config.get("esprit_image") or "").strip()
        if not image_name:
            failures.append(
                (
                    "Sandbox image is not configured",
                    "Set ESPRIT_IMAGE to a valid image (for example: improdead/esprit-sandbox:latest)",
                ),
            )
    
    # 3. Sufficient disk space (≥ 2 GB)
    if not failures:
        try:
            info = client.info()
            docker_root = info.get("DockerRootDir", "/var/lib/docker")
            usage = shutil.disk_usage(docker_root)
            min_free = 2 * 1024 * 1024 * 1024
            if usage.free < min_free:
                free_gb = usage.free / (1024 ** 3)
                failures.append(
                    (
                        f"Low disk space on Docker data-root ({free_gb:.1f} GB free, need ≥ 2 GB)",
                        f"Free up disk space in {docker_root}",
                    ),
                )
        except OSError:
            pass

Platform Detection

esprit/interface/main.py
def pull_docker_image() -> None:
    console = Console()
    client = check_docker_connection()
    image_name = Config.get("esprit_image")
    preferred_platform = (Config.get("esprit_docker_platform") or "").strip() or None
    
    # On macOS, always request linux/amd64 unless explicitly overridden.
    if preferred_platform is None and platform.system() == "Darwin":
        preferred_platform = "linux/amd64"
    
    # Proactively use linux/amd64 on ARM hosts
    if preferred_platform is None and platform.machine() in ("arm64", "aarch64"):
        preferred_platform = "linux/amd64"
Esprit automatically selects linux/amd64 on macOS and ARM hosts since the sandbox image only publishes AMD64 manifests.

Container Lifecycle

esprit/runtime/docker_runtime.py
async def create_sandbox(self, agent_id: str) -> SandboxInfo:
    image_name = Config.get("esprit_image")
    if not image_name:
        raise SandboxInitializationError(
            "Sandbox image not configured",
            "Set ESPRIT_IMAGE environment variable",
        )
    
    # Verify image is available
    self._verify_image_available(image_name)
    
    # Generate tool server token
    self._tool_server_token = secrets.token_hex(32)
    self._tool_server_port = self._find_available_port()
    
    # Create container
    container = self.client.containers.run(
        image_name,
        detach=True,
        environment={
            "TOOL_SERVER_TOKEN": self._tool_server_token,
        },
        ports={
            f"{CONTAINER_TOOL_SERVER_PORT}/tcp": self._tool_server_port,
        },
        name=f"esprit-scan-{scan_id}",
        remove=False,
    )
    
    self._scan_container = container
    
    # Wait for tool server to be ready
    self._wait_for_tool_server()

Configuration

# Sandbox image
export ESPRIT_IMAGE="improdead/esprit-sandbox:latest"

# Platform (auto-detected)
export ESPRIT_DOCKER_PLATFORM="linux/amd64"

# Runtime backend
export ESPRIT_RUNTIME_BACKEND="docker"

# Timeouts
export ESPRIT_SANDBOX_TIMEOUT="60"              # Startup timeout
export ESPRIT_SANDBOX_EXECUTION_TIMEOUT="120"   # Tool execution timeout
export ESPRIT_SANDBOX_CONNECT_TIMEOUT="10"      # Connection timeout

Networking

HOST_GATEWAY_HOSTNAME = "host.docker.internal"
Use host.docker.internal to access services running on the host from inside the container.

Auto-Start on macOS

esprit/interface/main.py
def ensure_docker_running() -> None:
    """Check if Docker daemon is running; auto-start on macOS if possible."""
    import subprocess
    import time
    
    console = Console()
    
    # Try to start Docker on macOS
    if sys.platform == "darwin":
        console.print("[dim]Docker daemon not running. Starting Docker Desktop...[/]")
        
        try:
            subprocess.Popen(
                ["open", "-a", "Docker"],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
            )
        except Exception:
            console.print("[red]Could not start Docker Desktop.[/]")
            sys.exit(1)
        
        # Wait for Docker to become available
        for i in range(60):
            time.sleep(1)
            try:
                import docker as docker_lib
                docker_lib.from_env()
                console.print("[green]✓ Docker started.[/]")
                return
            except Exception:
                pass

Comparing Runtimes

Pros:
  • No Docker installation required
  • Managed infrastructure
  • Automatic cleanup
  • Better for CI/CD
Cons:
  • Requires Esprit subscription (Pro/Team/Enterprise)
  • Internet connection required
  • Limited to esprit/* and bedrock/* models
Best for:
  • Users without Docker
  • CI/CD pipelines
  • Team environments

Troubleshooting

Cloud Runtime Issues

Authentication Failed

error: Esprit Cloud authentication required
Solution: Login to Esprit
esprit provider login esprit

Subscription Not Valid

error: Esprit Cloud access is currently disabled for this account
Solution: Upgrade to Pro/Team/Enterprise plan

Model Not Supported

Selected model requires Esprit login. Run `esprit provider login esprit`.
Solution: Use an esprit/* or bedrock/* model, or switch to Docker runtime

Docker Runtime Issues

Docker Not Installed

error: The 'docker' CLI was not found in your PATH
Solution: Install Docker Desktop
# macOS/Windows
https://docs.docker.com/get-docker/

# Linux
curl -fsSL https://get.docker.com | sh

Docker Not Running

error: Docker daemon is not running
Solution: Start Docker
# macOS
open -a Docker

# Linux
sudo systemctl start docker

Low Disk Space

error: Low disk space on Docker data-root (0.5 GB free, need ≥ 2 GB)
Solution: Free up disk space
# Remove unused images
docker system prune -a

# Check disk usage
du -sh /var/lib/docker

Image Pull Failed

error: Failed to pull image improdead/esprit-sandbox:latest
Solution: Check network and retry
docker pull improdead/esprit-sandbox:latest

Next Steps

Providers

Configure LLM providers

Environment Variables

Set runtime configuration

Build docs developers (and LLMs) love