From 4e6c99d79552a60265c555980ac13139b65bc74d Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Tue, 26 May 2026 14:31:30 +0800 Subject: [PATCH 1/3] [deployment,docs] fix local OCI published port --- docs/source/start/agent_env.md | 3 +- tests/deployment/test_local_deployment.py | 296 +++++++++++++++++++++- uni_agent/deployment/local/deployment.py | 36 ++- 3 files changed, 319 insertions(+), 16 deletions(-) diff --git a/docs/source/start/agent_env.md b/docs/source/start/agent_env.md index 2a649bf3..04e754ee 100644 --- a/docs/source/start/agent_env.md +++ b/docs/source/start/agent_env.md @@ -80,10 +80,11 @@ env_config = AgentEnvConfig(**{ - **`command`** runs inside the sandbox and should start `swerex.server`. It can use `{token}` and `{port}` placeholders. - **`container_runtime`** can be set to an Apptainer/Singularity binary path, `docker`, or `podman`. - **`published_port`** optionally pins the localhost port used by the `swerex` server. +- **`runtime_port`** is the port where `swerex.server` listens inside a Docker/Podman sandbox. The default is `8000`. - **`extra_run_args`** can pass additional runtime flags. For example, Apptainer bind mounts or GPU flags must appear before the image argument. - **`network`** is Docker/Podman-specific and useful when the current process is itself running inside Docker. -Apptainer launches the server with host networking, so the selected port is passed directly to `swerex.server`. Docker and Podman keep using port publishing. +Apptainer launches the server with host networking, so the selected port is passed directly to `swerex.server`. Docker and Podman use port publishing by default: `runtime_port` is the port inside the sandbox container, while `published_port` is the host-side port Uni-Agent connects to. When Uni-Agent connects over a shared container network, it connects to the sandbox IP and `runtime_port`. When Docker/Podman uses host networking, port publishing is skipped and Uni-Agent connects to `runtime_port`. Useful local overrides: diff --git a/tests/deployment/test_local_deployment.py b/tests/deployment/test_local_deployment.py index 4aeb1d44..44220b50 100644 --- a/tests/deployment/test_local_deployment.py +++ b/tests/deployment/test_local_deployment.py @@ -1,9 +1,12 @@ import asyncio +from pathlib import Path +from types import SimpleNamespace from uni_agent.deployment.local import deployment as local_deployment from uni_agent.deployment.local.deployment import ( LocalDeployment, _is_apptainer_runtime, + _is_running_in_container, _normalize_apptainer_image, ) @@ -51,6 +54,36 @@ def test_default_container_runtime_falls_back_to_apptainer_name(monkeypatch): assert local_deployment._default_container_runtime() == "apptainer" +def test_running_in_container_detects_docker_or_podman_markers(monkeypatch): + marker_paths = { + "/.dockerenv": True, + "/run/.containerenv": False, + } + + original_exists = Path.exists + + def fake_exists(path): + normalized_path = path.as_posix() + if normalized_path.endswith("/.dockerenv"): + return marker_paths["/.dockerenv"] + if normalized_path.endswith("/run/.containerenv"): + return marker_paths["/run/.containerenv"] + return original_exists(path) + + monkeypatch.setattr(Path, "exists", fake_exists) + + assert _is_running_in_container() + + marker_paths["/.dockerenv"] = False + marker_paths["/run/.containerenv"] = True + + assert _is_running_in_container() + + marker_paths["/run/.containerenv"] = False + + assert not _is_running_in_container() + + def test_apptainer_runtime_detection_accepts_paths_and_singularity_alias(): assert _is_apptainer_runtime("/opt/apptainer/bin/apptainer") assert _is_apptainer_runtime("singularity") @@ -113,9 +146,8 @@ def test_docker_command_keeps_published_to_runtime_port_mapping(): shell="/bin/bash", extra_run_args=["--cpus", "1"], ) - deployment._get_current_container_network = lambda: None - command = deployment._build_run_command("sandbox-name", 4567, "server") + command = deployment._build_run_command("sandbox-name", 4567, "server", network=None) assert command == [ "docker", @@ -136,6 +168,266 @@ def test_docker_command_keeps_published_to_runtime_port_mapping(): ] +class _CapturingRuntime: + configs = [] + + @classmethod + def from_config(cls, config, run_id=None): + cls.configs.append((config, run_id)) + return cls() + + +def _capture_exec(commands): + def fake_exec(args, check=True): + commands.append(args) + return _completed_container() + + return fake_exec + + +def _completed_container(container_id="container-id"): + return SimpleNamespace(stdout=f"{container_id}\n") + + +def test_oci_runtime_uses_published_port_when_connecting_from_host(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + deployment._get_current_container_network = lambda: None + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: False) + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://127.0.0.1" + assert runtime_config.port == 4567 + + +def test_oci_runtime_uses_published_port_with_explicit_host(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + host="http://docker-host.example", + runtime_port=8000, + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://docker-host.example" + assert runtime_config.port == 4567 + + +def test_oci_runtime_uses_container_port_with_explicit_host_and_host_network(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + host="http://docker-host.example", + runtime_port=8000, + network="host", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://docker-host.example" + assert runtime_config.port == 8000 + + +def test_oci_runtime_uses_container_port_with_host_network(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + network="host", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://127.0.0.1" + assert runtime_config.port == 8000 + + +def test_oci_command_omits_port_publish_with_host_network(): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + shell="/bin/bash", + ) + + command = deployment._build_run_command("sandbox-name", 4567, "server", network="host") + + assert command == [ + "docker", + "run", + "--rm", + "-d", + "--name", + "sandbox-name", + "--entrypoint", + "/bin/bash", + "--network", + "host", + "python:3.12", + "-lc", + "server", + ] + + +def test_oci_runtime_uses_container_port_when_connecting_over_container_network(monkeypatch): + commands = [] + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + network="agent-net", + ) + deployment._runtime_exec = _capture_exec(commands) + deployment._get_container_ip = lambda container_name: "172.18.0.9" + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://172.18.0.9" + assert runtime_config.port == 8000 + assert commands[0] == [ + "docker", + "run", + "--rm", + "-d", + "--name", + "sandbox-name", + "--entrypoint", + "/bin/bash", + "--network", + "agent-net", + "-p", + "4567:8000", + "python:3.12", + "-lc", + "python3 -m pip install -q swe-rex && python3 -m swerex.server --host 0.0.0.0 --port 8000 --auth-token secret", + ] + + +def test_oci_runtime_uses_container_port_when_inheriting_current_container_network(monkeypatch): + commands = [] + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + ) + deployment._runtime_exec = _capture_exec(commands) + deployment._get_current_container_network = lambda: "agent-net" + deployment._get_container_ip = lambda container_name: "172.18.0.9" + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://172.18.0.9" + assert runtime_config.port == 8000 + assert commands[0] == [ + "docker", + "run", + "--rm", + "-d", + "--name", + "sandbox-name", + "--entrypoint", + "/bin/bash", + "--network", + "agent-net", + "-p", + "4567:8000", + "python:3.12", + "-lc", + "python3 -m pip install -q swe-rex && python3 -m swerex.server --host 0.0.0.0 --port 8000 --auth-token secret", + ] + + +def test_oci_runtime_uses_container_port_when_inheriting_host_network(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + deployment._get_current_container_network = lambda: "host" + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://127.0.0.1" + assert runtime_config.port == 8000 + + +def test_oci_runtime_falls_back_to_published_port_when_container_ip_is_unavailable(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + network="agent-net", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + deployment._get_container_ip = lambda container_name: None + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://127.0.0.1" + assert runtime_config.port == 4567 + + class _FakeRuntime: def __init__(self): self.closed = False diff --git a/uni_agent/deployment/local/deployment.py b/uni_agent/deployment/local/deployment.py index 086e2e55..181a9f17 100644 --- a/uni_agent/deployment/local/deployment.py +++ b/uni_agent/deployment/local/deployment.py @@ -24,6 +24,7 @@ _APPTAINER_RUNTIMES = {"apptainer", "singularity"} _CONTAINER_RUNTIME_ENV_VARS = ("UNI_AGENT_CONTAINER_RUNTIME", "LOCAL_CONTAINER_RUNTIME") _DEFAULT_CONTAINER_RUNTIME_CANDIDATES = ("apptainer", "singularity", "docker", "podman") +_HOST_NETWORK = "host" _IMAGE_URI_PREFIXES = ( "docker://", "oras://", @@ -46,7 +47,7 @@ def _sanitize_name(value: str) -> str: def _is_running_in_container() -> bool: - return Path("/.dockerenv").exists() + return Path("/.dockerenv").exists() or Path("/run/.containerenv").exists() def _pick_free_port() -> int: @@ -183,23 +184,30 @@ def _get_container_ip(self, container_name: str) -> str | None: ip_address = result.stdout.strip() return ip_address or None - def _get_runtime_host(self, container_name: str) -> str: + def _get_oci_network(self) -> str | None: + return self._config.network or self._get_current_container_network() + + def _get_runtime_endpoint(self, container_name: str, published_port: int, network: str | None) -> tuple[str, int]: if self._config.host: - return self._config.host + port = self._config.runtime_port if network == _HOST_NETWORK else published_port + return self._config.host, port + + if network == _HOST_NETWORK: + return "http://127.0.0.1", self._config.runtime_port - if _is_running_in_container() or self._config.network: + if network or _is_running_in_container(): container_ip = self._get_container_ip(container_name) if container_ip: - return f"http://{container_ip}" + return f"http://{container_ip}", self._config.runtime_port - return "http://127.0.0.1" + return "http://127.0.0.1", published_port def _format_command(self, token: str, port: int) -> str: return self._config.command.format(token=token, port=port) - def _build_run_command(self, container_name: str, published_port: int, command: str) -> list[str]: - network = self._config.network or self._get_current_container_network() - + def _build_run_command( + self, container_name: str, published_port: int, command: str, network: str | None + ) -> list[str]: args = [ self._config.container_runtime, "run", @@ -212,7 +220,8 @@ def _build_run_command(self, container_name: str, published_port: int, command: ] if network: args.extend(["--network", network]) - args.extend(["-p", f"{published_port}:{self._config.runtime_port}"]) + if network != _HOST_NETWORK: + args.extend(["-p", f"{published_port}:{self._config.runtime_port}"]) args.extend(self._config.extra_run_args) args.extend([self._config.image, "-lc", command]) return args @@ -276,16 +285,17 @@ async def _start_apptainer(self, token: str, published_port: int) -> None: self._runtime = LocalRuntime.from_config(runtime_config, run_id=self.run_id) async def _start_oci_container(self, token: str, container_name: str, published_port: int) -> None: + network = await asyncio.to_thread(self._get_oci_network) command = self._format_command(token=token, port=self._config.runtime_port) result = await asyncio.to_thread( - self._runtime_exec, self._build_run_command(container_name, published_port, command) + self._runtime_exec, self._build_run_command(container_name, published_port, command, network) ) self._container_id = result.stdout.strip() - host = await asyncio.to_thread(self._get_runtime_host, container_name) + host, port = await asyncio.to_thread(self._get_runtime_endpoint, container_name, published_port, network) runtime_config = LocalRuntimeConfig( auth_token=token, host=host, - port=self._config.runtime_port, + port=port, timeout=self._config.timeout, ) self._runtime = LocalRuntime.from_config(runtime_config, run_id=self.run_id) From d4cd2315d073a4b36187b30c9153826a95d6e9ee Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Wed, 27 May 2026 16:33:40 +0800 Subject: [PATCH 2/3] [deployment] fix OCI endpoint for container network paths --- docs/source/start/agent_env.md | 2 +- tests/deployment/test_local_deployment.py | 49 +++++++++++++++++++++++ uni_agent/deployment/local/deployment.py | 6 ++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/docs/source/start/agent_env.md b/docs/source/start/agent_env.md index 04e754ee..6c735b92 100644 --- a/docs/source/start/agent_env.md +++ b/docs/source/start/agent_env.md @@ -84,7 +84,7 @@ env_config = AgentEnvConfig(**{ - **`extra_run_args`** can pass additional runtime flags. For example, Apptainer bind mounts or GPU flags must appear before the image argument. - **`network`** is Docker/Podman-specific and useful when the current process is itself running inside Docker. -Apptainer launches the server with host networking, so the selected port is passed directly to `swerex.server`. Docker and Podman use port publishing by default: `runtime_port` is the port inside the sandbox container, while `published_port` is the host-side port Uni-Agent connects to. When Uni-Agent connects over a shared container network, it connects to the sandbox IP and `runtime_port`. When Docker/Podman uses host networking, port publishing is skipped and Uni-Agent connects to `runtime_port`. +Apptainer launches the server with host networking, so the selected port is passed directly to `swerex.server`. Docker and Podman use port publishing by default: `runtime_port` is the port inside the sandbox container, while `published_port` is the host-side port Uni-Agent connects to. When Uni-Agent itself runs inside a container and connects over a shared container network, it connects to the sandbox IP and `runtime_port`. When Uni-Agent runs on the host, it connects through `published_port`, even if the sandbox container is attached to a custom Docker/Podman network. When Docker/Podman uses host networking, port publishing is skipped and Uni-Agent connects to `runtime_port`. Useful local overrides: diff --git a/tests/deployment/test_local_deployment.py b/tests/deployment/test_local_deployment.py index 44220b50..3f9641bd 100644 --- a/tests/deployment/test_local_deployment.py +++ b/tests/deployment/test_local_deployment.py @@ -232,6 +232,29 @@ def test_oci_runtime_uses_published_port_with_explicit_host(monkeypatch): assert runtime_config.port == 4567 +def test_oci_runtime_uses_container_port_with_explicit_host_from_container(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + host="http://sandbox", + runtime_port=8000, + network="agent-net", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://sandbox" + assert runtime_config.port == 8000 + + def test_oci_runtime_uses_container_port_with_explicit_host_and_host_network(monkeypatch): deployment = LocalDeployment( run_id="test", @@ -316,6 +339,7 @@ def test_oci_runtime_uses_container_port_when_connecting_over_container_network( ) deployment._runtime_exec = _capture_exec(commands) deployment._get_container_ip = lambda container_name: "172.18.0.9" + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) _CapturingRuntime.configs = [] @@ -344,6 +368,29 @@ def test_oci_runtime_uses_container_port_when_connecting_over_container_network( ] +def test_oci_runtime_uses_published_port_with_custom_network_from_host(monkeypatch): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + runtime_port=8000, + network="agent-net", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + deployment._get_container_ip = lambda container_name: "172.18.0.9" + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: False) + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == "http://127.0.0.1" + assert runtime_config.port == 4567 + + def test_oci_runtime_uses_container_port_when_inheriting_current_container_network(monkeypatch): commands = [] deployment = LocalDeployment( @@ -356,6 +403,7 @@ def test_oci_runtime_uses_container_port_when_inheriting_current_container_netwo deployment._runtime_exec = _capture_exec(commands) deployment._get_current_container_network = lambda: "agent-net" deployment._get_container_ip = lambda container_name: "172.18.0.9" + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) _CapturingRuntime.configs = [] @@ -417,6 +465,7 @@ def test_oci_runtime_falls_back_to_published_port_when_container_ip_is_unavailab ) deployment._runtime_exec = lambda args, check=True: _completed_container() deployment._get_container_ip = lambda container_name: None + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) _CapturingRuntime.configs = [] diff --git a/uni_agent/deployment/local/deployment.py b/uni_agent/deployment/local/deployment.py index 181a9f17..eea4768d 100644 --- a/uni_agent/deployment/local/deployment.py +++ b/uni_agent/deployment/local/deployment.py @@ -189,13 +189,15 @@ def _get_oci_network(self) -> str | None: def _get_runtime_endpoint(self, container_name: str, published_port: int, network: str | None) -> tuple[str, int]: if self._config.host: - port = self._config.runtime_port if network == _HOST_NETWORK else published_port + port = ( + self._config.runtime_port if network == _HOST_NETWORK or _is_running_in_container() else published_port + ) return self._config.host, port if network == _HOST_NETWORK: return "http://127.0.0.1", self._config.runtime_port - if network or _is_running_in_container(): + if _is_running_in_container(): container_ip = self._get_container_ip(container_name) if container_ip: return f"http://{container_ip}", self._config.runtime_port From 432bbbdab9a06e7678d1a3714bb2d3578f32ca22 Mon Sep 17 00:00:00 2001 From: liuyongkai Date: Thu, 28 May 2026 11:59:53 +0800 Subject: [PATCH 3/3] [deployment] handle host-gateway OCI endpoints --- tests/deployment/test_local_deployment.py | 24 +++++++++++++++++++++++ uni_agent/deployment/local/deployment.py | 12 +++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/deployment/test_local_deployment.py b/tests/deployment/test_local_deployment.py index 3f9641bd..a8b0e163 100644 --- a/tests/deployment/test_local_deployment.py +++ b/tests/deployment/test_local_deployment.py @@ -255,6 +255,30 @@ def test_oci_runtime_uses_container_port_with_explicit_host_from_container(monke assert runtime_config.port == 8000 +def test_oci_runtime_uses_published_port_with_host_gateway_from_container(monkeypatch): + for host in ("http://host.docker.internal", "http://host.containers.internal"): + deployment = LocalDeployment( + run_id="test", + type="local", + container_runtime="docker", + image="python:3.12", + host=host, + runtime_port=8000, + network="agent-net", + ) + deployment._runtime_exec = lambda args, check=True: _completed_container() + monkeypatch.setattr(local_deployment, "_is_running_in_container", lambda: True) + monkeypatch.setattr(local_deployment, "LocalRuntime", _CapturingRuntime) + _CapturingRuntime.configs = [] + + asyncio.run(deployment._start_oci_container(token="secret", container_name="sandbox-name", published_port=4567)) + + runtime_config, run_id = _CapturingRuntime.configs[-1] + assert run_id == "test" + assert runtime_config.host == host + assert runtime_config.port == 4567 + + def test_oci_runtime_uses_container_port_with_explicit_host_and_host_network(monkeypatch): deployment = LocalDeployment( run_id="test", diff --git a/uni_agent/deployment/local/deployment.py b/uni_agent/deployment/local/deployment.py index eea4768d..a7721ada 100644 --- a/uni_agent/deployment/local/deployment.py +++ b/uni_agent/deployment/local/deployment.py @@ -9,6 +9,7 @@ import uuid from pathlib import Path from typing import Any, Self +from urllib.parse import urlparse from swerex.deployment.abstract import AbstractDeployment from swerex.deployment.hooks.abstract import CombinedDeploymentHook, DeploymentHook @@ -24,6 +25,7 @@ _APPTAINER_RUNTIMES = {"apptainer", "singularity"} _CONTAINER_RUNTIME_ENV_VARS = ("UNI_AGENT_CONTAINER_RUNTIME", "LOCAL_CONTAINER_RUNTIME") _DEFAULT_CONTAINER_RUNTIME_CANDIDATES = ("apptainer", "singularity", "docker", "podman") +_HOST_GATEWAY_NAMES = {"host.docker.internal", "host.containers.internal"} _HOST_NETWORK = "host" _IMAGE_URI_PREFIXES = ( "docker://", @@ -72,6 +74,11 @@ def _runtime_basename(runtime: str) -> str: return Path(runtime).name.lower() +def _is_host_gateway_name(host: str) -> bool: + parsed = urlparse(host if "://" in host else f"http://{host}") + return (parsed.hostname or "").lower() in _HOST_GATEWAY_NAMES + + def _is_apptainer_runtime(runtime: str) -> bool: return _runtime_basename(runtime) in _APPTAINER_RUNTIMES @@ -190,7 +197,10 @@ def _get_oci_network(self) -> str | None: def _get_runtime_endpoint(self, container_name: str, published_port: int, network: str | None) -> tuple[str, int]: if self._config.host: port = ( - self._config.runtime_port if network == _HOST_NETWORK or _is_running_in_container() else published_port + self._config.runtime_port + if network == _HOST_NETWORK + or (_is_running_in_container() and not _is_host_gateway_name(self._config.host)) + else published_port ) return self._config.host, port