mirror of
https://github.com/usestrix/strix.git
synced 2026-10-03 02:24:24 +00:00
fix: wire STRIX_RUNTIME_BACKEND for podman support
Add Podman as a runtime backend alongside Docker. The backend registry now includes "podman", auto-detecting the Podman socket (rootless first, rootful fallback) or respecting STRIX_RUNTIME_SOCKET / DOCKER_HOST. Startup checks (CLI presence, daemon connectivity, host-gateway hostname) are all backend-aware so setting STRIX_RUNTIME_BACKEND=podman works end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
3bd9d56814
commit
29e2987187
6 changed files with 185 additions and 33 deletions
|
|
@ -47,6 +47,7 @@ class RuntimeSettings(BaseSettings):
|
|||
alias="STRIX_IMAGE",
|
||||
)
|
||||
backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND")
|
||||
socket_path: str | None = Field(default=None, alias="STRIX_RUNTIME_SOCKET")
|
||||
|
||||
|
||||
class TelemetrySettings(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -48,12 +48,10 @@ from strix.telemetry import posthog, scarf
|
|||
from strix.telemetry.logging import configure_dependency_logging
|
||||
|
||||
|
||||
HOST_GATEWAY_HOSTNAME = "host.docker.internal"
|
||||
|
||||
from strix.runtime.backends import get_host_gateway
|
||||
|
||||
import logging # noqa: E402
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -183,6 +181,33 @@ def validate_environment() -> None:
|
|||
|
||||
|
||||
def check_docker_installed() -> None:
|
||||
settings = load_settings()
|
||||
backend = settings.runtime.backend
|
||||
|
||||
if backend == "podman":
|
||||
if shutil.which("podman") is None:
|
||||
logger.error("Podman CLI not found in PATH")
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("PODMAN NOT INSTALLED", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("The 'podman' CLI was not found in your PATH.\n", style="white")
|
||||
error_text.append(
|
||||
"Please install Podman and ensure the 'podman' command is available.\n\n",
|
||||
style="white",
|
||||
)
|
||||
panel = Panel(
|
||||
error_text,
|
||||
title="[bold white]STRIX",
|
||||
title_align="left",
|
||||
border_style="red",
|
||||
padding=(1, 2),
|
||||
)
|
||||
console.print("\n", panel, "\n")
|
||||
sys.exit(1)
|
||||
logger.debug("Podman CLI present")
|
||||
return
|
||||
|
||||
if shutil.which("docker") is None:
|
||||
logger.error("Docker CLI not found in PATH")
|
||||
console = Console()
|
||||
|
|
@ -456,7 +481,8 @@ Examples:
|
|||
parser.error(f"Invalid target '{target}'")
|
||||
|
||||
assign_workspace_subdirs(args.targets_info)
|
||||
rewrite_localhost_targets(args.targets_info, HOST_GATEWAY_HOSTNAME)
|
||||
host_gateway = get_host_gateway(load_settings().runtime.backend)
|
||||
rewrite_localhost_targets(args.targets_info, host_gateway)
|
||||
|
||||
return args
|
||||
|
||||
|
|
|
|||
|
|
@ -1329,18 +1329,38 @@ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None)
|
|||
|
||||
|
||||
def check_docker_connection() -> Any:
|
||||
from strix.config import load_settings
|
||||
from strix.runtime.backends import create_docker_client
|
||||
|
||||
settings = load_settings()
|
||||
backend = settings.runtime.backend
|
||||
|
||||
try:
|
||||
return docker.from_env()
|
||||
return create_docker_client(backend)
|
||||
except DockerException:
|
||||
console = Console()
|
||||
error_text = Text()
|
||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
||||
error_text.append(
|
||||
"Please ensure Docker Desktop is installed and running, and try running strix again.\n",
|
||||
style="white",
|
||||
)
|
||||
|
||||
if backend == "podman":
|
||||
error_text.append("PODMAN NOT AVAILABLE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Cannot connect to Podman daemon.\n", style="white")
|
||||
error_text.append(
|
||||
"Please ensure Podman is installed and running, and try running strix again.\n\n",
|
||||
style="white",
|
||||
)
|
||||
error_text.append(
|
||||
"Tip: set STRIX_RUNTIME_SOCKET to your Podman socket path if auto-detection fails.\n",
|
||||
style="dim",
|
||||
)
|
||||
else:
|
||||
error_text.append("DOCKER NOT AVAILABLE", style="bold red")
|
||||
error_text.append("\n\n", style="white")
|
||||
error_text.append("Cannot connect to Docker daemon.\n", style="white")
|
||||
error_text.append(
|
||||
"Please ensure Docker Desktop is installed and running, and try running strix again.\n",
|
||||
style="white",
|
||||
)
|
||||
|
||||
panel = Panel(
|
||||
error_text,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from strix.config import load_settings
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from agents.sandbox.manifest import Manifest
|
||||
|
|
@ -17,40 +20,131 @@ logger = logging.getLogger(__name__)
|
|||
SandboxBackend = Callable[..., Awaitable[tuple[Any, Any]]]
|
||||
|
||||
|
||||
async def _docker_backend(
|
||||
def get_host_gateway(backend_name: str) -> str:
|
||||
"""Return the host-gateway hostname for *backend_name*.
|
||||
|
||||
Docker uses ``host.docker.internal``; Podman uses
|
||||
``host.containers.internal`` (resolved automatically by Podman's
|
||||
built-in DNS, no ``--add-host`` needed).
|
||||
"""
|
||||
if backend_name == "podman":
|
||||
return "host.containers.internal"
|
||||
return "host.docker.internal"
|
||||
|
||||
|
||||
def create_docker_client(backend_name: str) -> Any:
|
||||
"""Create a ``docker.DockerClient`` pointed at the right daemon.
|
||||
|
||||
Resolution order:
|
||||
1. ``STRIX_RUNTIME_SOCKET`` env var / config (explicit)
|
||||
2. ``DOCKER_HOST`` env var (standard docker-py mechanism)
|
||||
3. Per-backend auto-detection (e.g. Podman socket probing)
|
||||
4. ``docker.from_env()`` default
|
||||
"""
|
||||
import docker
|
||||
|
||||
settings = load_settings()
|
||||
socket_path = settings.runtime.socket_path
|
||||
if socket_path:
|
||||
logger.debug("Using explicit runtime socket: %s", socket_path)
|
||||
return docker.DockerClient(base_url=socket_path)
|
||||
|
||||
if os.environ.get("DOCKER_HOST"):
|
||||
return docker.from_env()
|
||||
|
||||
if backend_name == "podman":
|
||||
for candidate in _podman_socket_candidates():
|
||||
path = candidate.replace("unix://", "")
|
||||
if os.path.exists(path):
|
||||
logger.debug("Auto-detected podman socket: %s", candidate)
|
||||
return docker.DockerClient(base_url=candidate)
|
||||
|
||||
return docker.from_env()
|
||||
|
||||
|
||||
def _podman_socket_candidates() -> list[str]:
|
||||
"""Return Podman socket URI candidates (rootless first, then rootful)."""
|
||||
candidates: list[str] = []
|
||||
xdg_runtime = os.environ.get("XDG_RUNTIME_DIR")
|
||||
if xdg_runtime:
|
||||
candidates.append(f"unix://{xdg_runtime}/podman/podman.sock")
|
||||
else:
|
||||
try:
|
||||
candidates.append(f"unix:///run/user/{os.getuid()}/podman/podman.sock")
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
candidates.append("unix:///run/podman/podman.sock")
|
||||
return candidates
|
||||
|
||||
|
||||
# -- backend factories --------------------------------------------------
|
||||
|
||||
|
||||
async def _create_sandbox(
|
||||
*,
|
||||
image: str,
|
||||
manifest: Manifest,
|
||||
exposed_ports: tuple[int, ...],
|
||||
docker_client: Any,
|
||||
host_gateway_hostname: str,
|
||||
) -> tuple[Any, Any]:
|
||||
"""Bring up a session backed by the local Docker daemon.
|
||||
|
||||
Uses :class:`StrixDockerSandboxClient` to inject NET_ADMIN /
|
||||
NET_RAW caps + ``host.docker.internal`` host-gateway. Imports
|
||||
``docker`` lazily so deployments that target a non-Docker
|
||||
backend don't need the docker-py library installed.
|
||||
|
||||
``session.start()`` is what materializes the manifest entries
|
||||
(LocalDir copies, mount setup, etc.) into the running container —
|
||||
the SDK's ``client.create()`` only builds the inner session object
|
||||
without applying the manifest. ``async with session:`` would call it
|
||||
too, but Strix manages session lifetime explicitly via
|
||||
``client.delete()`` so we trigger ``start()`` ourselves.
|
||||
"""
|
||||
import docker
|
||||
from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions
|
||||
|
||||
from strix.runtime.docker_client import StrixDockerSandboxClient
|
||||
|
||||
client = StrixDockerSandboxClient(docker.from_env())
|
||||
client = StrixDockerSandboxClient(
|
||||
docker_client, host_gateway_hostname=host_gateway_hostname
|
||||
)
|
||||
options = DockerSandboxClientOptions(image=image, exposed_ports=exposed_ports)
|
||||
session = await client.create(options=options, manifest=manifest)
|
||||
await session.start()
|
||||
return client, session
|
||||
|
||||
|
||||
async def _docker_backend(
|
||||
*,
|
||||
image: str,
|
||||
manifest: Manifest,
|
||||
exposed_ports: tuple[int, ...],
|
||||
) -> tuple[Any, Any]:
|
||||
"""Bring up a session backed by the local Docker daemon."""
|
||||
docker_client = create_docker_client("docker")
|
||||
return await _create_sandbox(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=exposed_ports,
|
||||
docker_client=docker_client,
|
||||
host_gateway_hostname=get_host_gateway("docker"),
|
||||
)
|
||||
|
||||
|
||||
async def _podman_backend(
|
||||
*,
|
||||
image: str,
|
||||
manifest: Manifest,
|
||||
exposed_ports: tuple[int, ...],
|
||||
) -> tuple[Any, Any]:
|
||||
"""Bring up a session backed by a local Podman daemon.
|
||||
|
||||
Uses the Docker-compatible API socket — the same ``docker-py``
|
||||
library drives it, just pointed at the Podman socket.
|
||||
"""
|
||||
docker_client = create_docker_client("podman")
|
||||
return await _create_sandbox(
|
||||
image=image,
|
||||
manifest=manifest,
|
||||
exposed_ports=exposed_ports,
|
||||
docker_client=docker_client,
|
||||
host_gateway_hostname=get_host_gateway("podman"),
|
||||
)
|
||||
|
||||
|
||||
# -- registry -----------------------------------------------------------
|
||||
|
||||
|
||||
_BACKENDS: dict[str, SandboxBackend] = {
|
||||
"docker": _docker_backend,
|
||||
"podman": _podman_backend,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,15 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class StrixDockerSandboxClient(DockerSandboxClient):
|
||||
def __init__(
|
||||
self,
|
||||
docker_client: Any,
|
||||
*,
|
||||
host_gateway_hostname: str = "host.docker.internal",
|
||||
) -> None:
|
||||
super().__init__(docker_client)
|
||||
self._host_gateway_hostname = host_gateway_hostname
|
||||
|
||||
async def _create_container(
|
||||
self,
|
||||
image: str,
|
||||
|
|
@ -106,7 +115,7 @@ class StrixDockerSandboxClient(DockerSandboxClient):
|
|||
cap_add.append(cap)
|
||||
|
||||
extra_hosts = create_kwargs.setdefault("extra_hosts", {})
|
||||
extra_hosts["host.docker.internal"] = "host-gateway"
|
||||
extra_hosts[self._host_gateway_hostname] = "host-gateway"
|
||||
|
||||
logger.debug(
|
||||
"Creating sandbox container: image=%s caps=%s exposed_ports=%s",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from agents.sandbox.entries import BaseEntry, LocalDir
|
|||
from agents.sandbox.manifest import Environment, Manifest
|
||||
|
||||
from strix.config import load_settings
|
||||
from strix.runtime.backends import get_backend
|
||||
from strix.runtime.backends import get_backend, get_host_gateway
|
||||
from strix.runtime.caido_bootstrap import bootstrap_caido
|
||||
|
||||
|
||||
|
|
@ -48,6 +48,9 @@ async def create_or_reuse(
|
|||
continue
|
||||
entries[ws_subdir] = LocalDir(src=Path(host_path).expanduser().resolve())
|
||||
|
||||
backend_name = load_settings().runtime.backend
|
||||
host_gateway = get_host_gateway(backend_name)
|
||||
|
||||
# Caido runs as an in-container sidecar; HTTP(S) traffic from any
|
||||
# process started via ``session.exec`` (the SDK's Shell tool, etc.)
|
||||
# picks up these env vars automatically. ``NO_PROXY`` keeps the
|
||||
|
|
@ -59,7 +62,7 @@ async def create_or_reuse(
|
|||
environment=Environment(
|
||||
value={
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"HOST_GATEWAY": "host.docker.internal",
|
||||
"HOST_GATEWAY": host_gateway,
|
||||
"http_proxy": container_caido_url,
|
||||
"https_proxy": container_caido_url,
|
||||
"ALL_PROXY": container_caido_url,
|
||||
|
|
@ -68,7 +71,6 @@ async def create_or_reuse(
|
|||
),
|
||||
)
|
||||
|
||||
backend_name = load_settings().runtime.backend
|
||||
backend = get_backend(backend_name)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue