feat(runtime): resolve sandbox ports over a shared Docker network (#775)

Co-authored-by: Ahmed Allam <ahmed39652003@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-07-15 11:57:42 -07:00
committed by GitHub
co-authored by Ahmed Allam
parent 40f4e67320
commit 914207ffb3
+69 -1
View File
@@ -24,18 +24,22 @@ from __future__ import annotations
import contextlib import contextlib
import logging import logging
import os
import uuid import uuid
from typing import Any from typing import Any, cast
from agents.sandbox.errors import ExposedPortUnavailableError
from agents.sandbox.manifest import Manifest from agents.sandbox.manifest import Manifest
from agents.sandbox.sandboxes.docker import ( from agents.sandbox.sandboxes.docker import (
DockerSandboxClient, DockerSandboxClient,
DockerSandboxSession,
_build_docker_volume_mounts, _build_docker_volume_mounts,
_docker_port_key, _docker_port_key,
_manifest_requires_fuse, _manifest_requires_fuse,
_manifest_requires_sys_admin, _manifest_requires_sys_admin,
) )
from agents.sandbox.session.sandbox_session import SandboxSession from agents.sandbox.session.sandbox_session import SandboxSession
from agents.sandbox.types import ExposedPortEndpoint
from docker import errors as docker_errors # type: ignore[import-untyped, unused-ignore] from docker import errors as docker_errors # type: ignore[import-untyped, unused-ignore]
from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore] from docker.models.containers import Container # type: ignore[import-untyped, unused-ignore]
from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore] from docker.types import Mount as DockerSDKMount # type: ignore[import-untyped, unused-ignore]
@@ -46,6 +50,59 @@ from requests.exceptions import RequestException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SANDBOX_NETWORK_ENV = "STRIX_DOCKER_SANDBOX_NETWORK"
def _sandbox_network() -> str | None:
value = os.environ.get(_SANDBOX_NETWORK_ENV, "").strip()
return value or None
def _apply_sandbox_network(create_kwargs: dict[str, Any]) -> None:
network = _sandbox_network()
if network:
create_kwargs["network"] = network
create_kwargs.pop("ports", None)
class StrixDockerSandboxSession(DockerSandboxSession):
sandbox_network: str = ""
async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint:
try:
self._container.reload()
except docker_errors.APIError as e:
raise ExposedPortUnavailableError(
port=port,
exposed_ports=self.state.exposed_ports,
reason="backend_unavailable",
context={
"backend": "docker",
"detail": "container_reload_failed",
"network": self.sandbox_network,
},
cause=e,
) from e
attrs = getattr(self._container, "attrs", {}) or {}
networks = attrs.get("NetworkSettings", {}).get("Networks", {})
endpoint = networks.get(self.sandbox_network) or {}
ip = endpoint.get("IPAddress") or endpoint.get("GlobalIPv6Address")
if not isinstance(ip, str) or not ip:
raise ExposedPortUnavailableError(
port=port,
exposed_ports=self.state.exposed_ports,
reason="backend_unavailable",
context={
"backend": "docker",
"detail": "container_not_on_network",
"network": self.sandbox_network,
},
)
host = f"[{ip}]" if ":" in ip else ip
return ExposedPortEndpoint(host=host, port=port, tls=False)
class StrixDockerSandboxClient(DockerSandboxClient): class StrixDockerSandboxClient(DockerSandboxClient):
# Host directories to bind-mount into the container, set by the docker # Host directories to bind-mount into the container, set by the docker
# backend before ``create()``. Each item is ``{source, target, read_only}``. # backend before ``create()``. Each item is ``{source, target, read_only}``.
@@ -117,6 +174,8 @@ class StrixDockerSandboxClient(DockerSandboxClient):
extra_hosts = create_kwargs.setdefault("extra_hosts", {}) extra_hosts = create_kwargs.setdefault("extra_hosts", {})
extra_hosts["host.docker.internal"] = "host-gateway" extra_hosts["host.docker.internal"] = "host-gateway"
_apply_sandbox_network(create_kwargs)
# Strix injection: host bind mounts (e.g. large repos passed via --mount) # Strix injection: host bind mounts (e.g. large repos passed via --mount)
# that bypass the SDK's file-by-file LocalDir copy. # that bypass the SDK's file-by-file LocalDir copy.
bind_mounts = getattr(self, "strix_bind_mounts", ()) bind_mounts = getattr(self, "strix_bind_mounts", ())
@@ -146,6 +205,15 @@ class StrixDockerSandboxClient(DockerSandboxClient):
) )
return container return container
async def create(self, **kwargs: Any) -> SandboxSession:
session = await super().create(**kwargs)
network = _sandbox_network()
inner = session._inner
if network and isinstance(inner, DockerSandboxSession):
inner.__class__ = StrixDockerSandboxSession
cast("StrixDockerSandboxSession", inner).sandbox_network = network
return session
async def delete(self, session: SandboxSession) -> SandboxSession: async def delete(self, session: SandboxSession) -> SandboxSession:
container_id = getattr(getattr(session._inner, "state", None), "container_id", None) container_id = getattr(getattr(session._inner, "state", None), "container_id", None)
if container_id: if container_id: