feat(runtime): pluggable sandbox backend registry

``STRIX_RUNTIME_BACKEND`` was already declared on ``Config`` but never
read — ``session_manager`` hard-coded ``StrixDockerSandboxClient`` plus
``DockerSandboxClientOptions`` plus ``docker.from_env()`` directly into
the call site. Adding a second backend would have meant retrofitting
every Docker-specific import.

Move all of that behind a registry:

- ``strix/runtime/backends.py``: maps backend names to async factories
  ``(image, manifest, exposed_ports) -> (client, session)``. Ships with
  ``"docker"``; ``register_backend`` lets downstream users plug in
  Daytona / K8s / Modal / etc. without forking.
- Each backend's deps are imported lazily inside its factory, so a
  K8s-only deployment doesn't need ``docker-py`` installed (and
  vice-versa).
- ``session_manager`` reads the config name, looks up the backend,
  calls it. Zero Docker imports remain.
- Unknown backend name raises ``ValueError`` with the supported list,
  so ``STRIX_RUNTIME_BACKEND=docke`` typos surface immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
0xallam
2026-04-25 15:02:51 -07:00
parent fe5f749e13
commit 6990fd4ef1
4 changed files with 118 additions and 12 deletions
+3
View File
@@ -204,6 +204,9 @@ ignore = [
"TC003", # collections.abc.AsyncIterator imported for return type "TC003", # collections.abc.AsyncIterator imported for return type
] ]
# Custom Docker subclass duplicates parent body; some imports are for annotations. # Custom Docker subclass duplicates parent body; some imports are for annotations.
# Backend factories import their backend's deps lazily so deployments
# that pick a different backend don't need every backend's libs installed.
"strix/runtime/backends.py" = ["PLC0415"]
"strix/runtime/docker_client.py" = [ "strix/runtime/docker_client.py" = [
"TC002", # Manifest, Container imported for annotations "TC002", # Manifest, Container imported for annotations
"TC003", # uuid imported for annotation "TC003", # uuid imported for annotation
+9 -4
View File
@@ -1,10 +1,15 @@
"""Strix runtime — Docker-backed sandbox lifecycle on top of the Agents SDK. """Strix runtime — pluggable sandbox lifecycle on top of the Agents SDK.
- :class:`strix.runtime.docker_client.StrixDockerSandboxClient` — - :mod:`.backends` — registry mapping ``STRIX_RUNTIME_BACKEND`` values
``DockerSandboxClient`` subclass that injects ``NET_ADMIN`` / to async factories that bring up a ``(client, session)`` pair. Ships
``NET_RAW`` capabilities and ``host.docker.internal`` extra-hosts. with ``"docker"`` out of the box; ``register_backend`` lets downstream
users add Daytona / K8s / Modal / etc. without forking.
- :mod:`.session_manager` — ``create_or_reuse`` / ``cleanup`` keyed - :mod:`.session_manager` — ``create_or_reuse`` / ``cleanup`` keyed
by scan id; bundles the SDK session with a ready Caido client. by scan id; bundles the SDK session with a ready Caido client.
- :mod:`.caido_bootstrap` — runtime-agnostic Caido auth dance via - :mod:`.caido_bootstrap` — runtime-agnostic Caido auth dance via
``session.exec``. ``session.exec``.
- :class:`strix.runtime.docker_client.StrixDockerSandboxClient` —
``DockerSandboxClient`` subclass that injects ``NET_ADMIN`` /
``NET_RAW`` capabilities and ``host.docker.internal`` extra-hosts
(used only by the Docker backend).
""" """
+93
View File
@@ -0,0 +1,93 @@
"""Sandbox backend registry — runtime-agnostic session bring-up.
A *backend* is an async callable that takes an image tag + an SDK
:class:`Manifest` + the ports to expose, and returns the matching
``(client, session)`` pair. The caller owns lifecycle from there
(``await client.delete(session)``).
This keeps :mod:`strix.runtime.session_manager` free of any
backend-specific imports — switching to Daytona / K8s / Modal /
whatever is one new factory function plus one registry entry.
Selection is driven by ``STRIX_RUNTIME_BACKEND`` (default: ``"docker"``).
Unknown values raise :class:`ValueError` rather than silently falling
back, so typos fail loudly.
"""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from agents.sandbox.manifest import Manifest
# A backend brings up a fresh session and returns the (client, session)
# pair. The client is whatever object exposes ``await client.delete(session)``
# for cleanup — typically an ``agents.sandbox.client.BaseSandboxClient``
# subclass, but the protocol is duck-typed so non-SDK backends could
# also plug in if they implement the same interface.
SandboxBackend = Callable[..., Awaitable[tuple[Any, Any]]]
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.
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.
"""
import docker
from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions
from strix.runtime.docker_client import StrixDockerSandboxClient
client = StrixDockerSandboxClient(docker.from_env())
options = DockerSandboxClientOptions(image=image, exposed_ports=exposed_ports)
session = await client.create(options=options, manifest=manifest)
return client, session
_BACKENDS: dict[str, SandboxBackend] = {
"docker": _docker_backend,
}
def get_backend(name: str) -> SandboxBackend:
"""Return the backend factory for ``name`` or raise.
Args:
name: Backend identifier (e.g. ``"docker"``). Match is exact;
no fallback. Unknown values raise so config typos surface
immediately instead of silently picking a default.
"""
backend = _BACKENDS.get(name)
if backend is None:
supported = ", ".join(sorted(_BACKENDS))
raise ValueError(
f"Unknown STRIX_RUNTIME_BACKEND: {name!r} (supported: {supported})",
)
return backend
def register_backend(name: str, backend: SandboxBackend) -> None:
"""Register a custom backend under ``name``.
Intended for downstream users who ship their own runtime — register
before any ``session_manager.create_or_reuse`` call. Re-registering
an existing name overwrites the prior entry.
"""
_BACKENDS[name] = backend
def supported_backends() -> list[str]:
"""Snapshot of registered backend names. Useful for ``--help`` text."""
return sorted(_BACKENDS)
+13 -8
View File
@@ -18,13 +18,12 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import docker
from agents.sandbox.entries import LocalDir from agents.sandbox.entries import LocalDir
from agents.sandbox.manifest import Environment, Manifest from agents.sandbox.manifest import Environment, Manifest
from agents.sandbox.sandboxes.docker import DockerSandboxClientOptions
from strix.config.config import Config
from strix.runtime.backends import get_backend
from strix.runtime.caido_bootstrap import bootstrap_caido from strix.runtime.caido_bootstrap import bootstrap_caido
from strix.runtime.docker_client import StrixDockerSandboxClient
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -84,15 +83,21 @@ async def create_or_reuse(
), ),
) )
client = StrixDockerSandboxClient(docker.from_env()) backend_name = Config.get("strix_runtime_backend") or "docker"
options = DockerSandboxClientOptions( backend = get_backend(backend_name)
logger.info(
"Creating sandbox session for scan %s (backend=%s, image=%s)",
scan_id,
backend_name,
image,
)
client, session = await backend(
image=image, image=image,
manifest=manifest,
exposed_ports=(_CONTAINER_CAIDO_PORT,), exposed_ports=(_CONTAINER_CAIDO_PORT,),
) )
logger.info("Creating sandbox session for scan %s (image=%s)", scan_id, image)
session = await client.create(options=options, manifest=manifest)
caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT) caido_endpoint = await session.resolve_exposed_port(_CONTAINER_CAIDO_PORT)
host_caido_url = f"http://{caido_endpoint.host}:{caido_endpoint.port}" host_caido_url = f"http://{caido_endpoint.host}:{caido_endpoint.port}"