fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
92 lines
3.7 KiB
Python
92 lines
3.7 KiB
Python
"""Factory + process-wide singleton selecting a sandbox backend from settings."""
|
|
|
|
from typing import Callable, Dict, Optional
|
|
|
|
from application.core.settings import settings
|
|
from application.sandbox.base import CodeSandbox
|
|
from application.sandbox.jupyter_gateway import JupyterKernelGatewaySandbox
|
|
from application.sandbox.manager import SandboxManager
|
|
|
|
|
|
def _make_jupyter() -> CodeSandbox:
|
|
"""Build the Jupyter Kernel Gateway backend from ``SANDBOX_*`` settings."""
|
|
return JupyterKernelGatewaySandbox(
|
|
gateway_url=settings.SANDBOX_GATEWAY_URL,
|
|
auth_token=settings.SANDBOX_GATEWAY_AUTH_TOKEN,
|
|
kernel_name=settings.SANDBOX_KERNEL_NAME,
|
|
default_timeout=float(settings.SANDBOX_EXEC_TIMEOUT),
|
|
http_timeout=float(settings.SANDBOX_HTTP_TIMEOUT),
|
|
max_output_bytes=int(settings.SANDBOX_MAX_OUTPUT_BYTES),
|
|
max_file_bytes=int(settings.SANDBOX_MAX_FILE_BYTES),
|
|
)
|
|
|
|
|
|
def _make_daytona() -> CodeSandbox:
|
|
"""Build the Daytona Cloud backend from ``DAYTONA_*``/``SANDBOX_*`` settings."""
|
|
from application.sandbox.daytona import DaytonaSandbox
|
|
|
|
# Auto-delete is the only backstop against orphaned (paid) sandboxes, so a
|
|
# never-expiring value (<= 0) is rejected and clamped to a safe default.
|
|
auto_delete_interval = int(settings.DAYTONA_AUTO_DELETE_INTERVAL)
|
|
if auto_delete_interval <= 0:
|
|
auto_delete_interval = 60
|
|
|
|
return DaytonaSandbox(
|
|
api_key=settings.DAYTONA_API_KEY,
|
|
api_url=settings.DAYTONA_API_URL,
|
|
target=settings.DAYTONA_TARGET,
|
|
snapshot=settings.DAYTONA_SNAPSHOT,
|
|
language=settings.DAYTONA_LANGUAGE,
|
|
default_timeout=float(settings.SANDBOX_EXEC_TIMEOUT),
|
|
create_timeout=float(settings.SANDBOX_HTTP_TIMEOUT) * 6,
|
|
auto_stop_interval=int(settings.DAYTONA_AUTO_STOP_INTERVAL),
|
|
auto_delete_interval=auto_delete_interval,
|
|
# Bounds only what the app propagates from a response: the Daytona SDK still
|
|
# buffers the whole HTTP response before we see it, so a full in-sandbox cap
|
|
# would need runner-side support. This is a partial but real mitigation.
|
|
max_output_bytes=int(settings.SANDBOX_MAX_OUTPUT_BYTES),
|
|
max_file_bytes=int(settings.SANDBOX_MAX_FILE_BYTES),
|
|
max_sandboxes=int(settings.DAYTONA_MAX_SANDBOXES),
|
|
)
|
|
|
|
|
|
class SandboxCreator:
|
|
"""Resolves ``SANDBOX_BACKEND`` to a backend and caches a single manager."""
|
|
|
|
backends: Dict[str, Callable[[], CodeSandbox]] = {
|
|
"jupyter": _make_jupyter,
|
|
"daytona": _make_daytona,
|
|
}
|
|
|
|
_instance = None
|
|
|
|
@classmethod
|
|
def get_manager(cls) -> SandboxManager:
|
|
"""Return the process-wide ``SandboxManager``, building it on first use."""
|
|
if cls._instance is None:
|
|
backend = cls.create_backend(getattr(settings, "SANDBOX_BACKEND", "jupyter"))
|
|
cls._instance = SandboxManager(
|
|
backend=backend,
|
|
max_ttl=float(settings.SANDBOX_MAX_TTL),
|
|
max_sessions=int(settings.SANDBOX_MAX_SESSIONS),
|
|
)
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def peek_manager(cls) -> Optional[SandboxManager]:
|
|
"""Return the process-wide manager if already built, else None (never constructs one)."""
|
|
return cls._instance
|
|
|
|
@classmethod
|
|
def create_backend(cls, type_name: str) -> CodeSandbox:
|
|
"""Instantiate the backend registered under ``type_name`` (case-insensitive)."""
|
|
factory = cls.backends.get(type_name.lower())
|
|
if not factory:
|
|
raise ValueError(f"No sandbox backend found for type {type_name}")
|
|
return factory()
|
|
|
|
@classmethod
|
|
def reset(cls) -> None:
|
|
"""Drop the cached singleton (test/teardown hook)."""
|
|
cls._instance = None
|