Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ docs = [
"pymdown-extensions>=10.0",
"ghp-import>=2.1",
]
k8s = [
"kubernetes>=31.0.0",
]
dev = [
"pytest>=8.3.0",
"pytest-asyncio>=0.24.0",
Expand Down
27 changes: 27 additions & 0 deletions src/forge/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ def ignored_ci_checks(self) -> list[str]:
)

# Container Configuration
sandbox_driver: str = Field(
default="podman",
alias="forge_sandbox_driver",
description="Sandbox driver backend: podman or kubernetes",
)
container_image: str = Field(
default="localhost/forge-dev:latest",
description="Container image for task execution (local or registry URL)",
Expand All @@ -357,6 +362,28 @@ def ignored_ci_checks(self) -> list[str]:
description="Container CPU limit",
)

# Kubernetes Driver Configuration (only used when sandbox_driver=kubernetes)
k8s_namespace: str = Field(
default="forge",
description="Kubernetes namespace for sandbox Jobs",
)
k8s_workspace_pvc: str = Field(
default="",
description="PVC name for workspace storage shared between worker and sandbox pods",
)
k8s_workspace_base_path: str = Field(
default="",
description="Mount path on the worker host where the workspace PVC is accessible",
)
k8s_image_pull_secrets: str = Field(
default="",
description="Comma-separated image pull secret names for sandbox pods",
)
k8s_service_account: str = Field(
default="",
description="Kubernetes service account for sandbox pods",
)

# Auto Review Configuration
auto_review_poll_interval: float = Field(
default=5.0,
Expand Down
9 changes: 8 additions & 1 deletion src/forge/sandbox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
"""Sandbox module for container-based code execution."""

from forge.sandbox.driver import ExecutionResult, ExecutionSpec, SandboxDriver
from forge.sandbox.runner import ContainerResult, ContainerRunner

__all__ = ["ContainerRunner", "ContainerResult"]
__all__ = [
"ContainerResult",
"ContainerRunner",
"ExecutionResult",
"ExecutionSpec",
"SandboxDriver",
]
53 changes: 0 additions & 53 deletions src/forge/sandbox/config.py

This file was deleted.

73 changes: 73 additions & 0 deletions src/forge/sandbox/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Abstract sandbox driver interface for container runtime backends."""

from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class ExecutionSpec:
"""Runtime-agnostic specification for a container execution.

Built by the orchestration layer (ContainerRunner), consumed by a
SandboxDriver implementation.
"""

container_name: str
image: str
workspace_path: Path
task_file: Path
env_vars: dict[str, str]
memory_limit: str
cpu_limit: str
network_mode: str
timeout_seconds: int
skip_tests: bool
max_retries: int
volume_mounts: list[tuple[Path, str, str]] = field(default_factory=list)
remove_after: bool = True


@dataclass
class ExecutionResult:
"""Raw result from driver execution, before interpretation."""

exit_code: int
stdout: str
stderr: str


class SandboxDriver(ABC):
"""Abstract interface for container runtime backends.

Implementations handle the runtime-specific details of creating and
running containers or pods. The orchestration layer (ContainerRunner)
builds an ExecutionSpec and delegates to the driver.
"""

@abstractmethod
async def execute(self, spec: ExecutionSpec) -> ExecutionResult:
"""Execute a container/pod with the given specification.

Must handle starting the container/pod, waiting for completion
(with timeout), capturing stdout/stderr, and cleanup on timeout
or cancellation.
"""
...

@abstractmethod
def is_available(self) -> bool:
"""Check if this driver's runtime is available."""
...

async def build_image(self, containerfile_path: Path | None = None, tag: str = "") -> bool:
"""Build a container image. Optional — not all drivers support this."""
raise NotImplementedError(f"{type(self).__name__} does not support local image building")

async def image_exists(self, tag: str) -> bool: # noqa: ARG002
"""Check if an image exists locally."""
return False

async def pull_image(self, image: str) -> bool:
"""Pull a container image."""
raise NotImplementedError(f"{type(self).__name__} does not support image pulling")
44 changes: 44 additions & 0 deletions src/forge/sandbox/drivers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Sandbox driver registry and factory."""

from __future__ import annotations

from typing import TYPE_CHECKING

from forge.sandbox.driver import SandboxDriver

if TYPE_CHECKING:
from forge.config import Settings

__all__ = ["create_driver"]


def create_driver(settings: Settings) -> SandboxDriver:
"""Create a sandbox driver based on settings.

Raises:
ValueError: If the configured driver name is unknown.
RuntimeError: If the driver's runtime is not available.
"""
driver_name = settings.sandbox_driver

match driver_name:
case "podman":
from forge.sandbox.drivers.podman import PodmanDriver

driver = PodmanDriver(settings)
case "kubernetes":
from forge.sandbox.drivers.kubernetes import KubernetesDriver

driver = KubernetesDriver(settings)
case _:
raise ValueError(
f"Unknown sandbox driver: {driver_name!r}. Valid options: podman, kubernetes"
)

if not driver.is_available():
raise RuntimeError(
f"Sandbox driver {driver_name!r} is not available. "
f"Check that the required runtime is installed and accessible."
)

return driver
Loading
Loading