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:
Carsten Meininger 2026-05-28 12:44:49 +02:00
parent 3bd9d56814
commit 29e2987187
6 changed files with 185 additions and 33 deletions

View file

@ -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):

View file

@ -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

View file

@ -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,

View file

@ -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,
}

View file

@ -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",

View file

@ -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(