diff --git a/bot_bottle/backend/base.py b/bot_bottle/backend/base.py index 32eb016e..d9b4ad30 100644 --- a/bot_bottle/backend/base.py +++ b/bot_bottle/backend/base.py @@ -272,6 +272,12 @@ PlanT = TypeVar("PlanT", bound=BottlePlan) CleanupT = TypeVar("CleanupT", bound=BottleCleanupPlan) +class InfraLaunchError(RuntimeError): + """A per-host infra (gateway + orchestrator) launch/reconcile step could not + complete. Shared across backends so the base backend can raise + catch one + type; each backend's ``infra_launch`` module re-exports it.""" + + @dataclass(frozen=True) class BottleImages: """Resolved image references (or artifact paths) for a bottle launch. diff --git a/bot_bottle/backend/docker/backend.py b/bot_bottle/backend/docker/backend.py index bb3b3c19..c4f821ba 100644 --- a/bot_bottle/backend/docker/backend.py +++ b/bot_bottle/backend/docker/backend.py @@ -142,5 +142,5 @@ class DockerBottleBackend(BottleBackend["DockerBottlePlan", "DockerBottleCleanup return _enumerate.enumerate_active() def attach_bottled_agents_to_gateway(self) -> None: - from .consolidated_launch import attach_bottled_agents_to_gateway + from .infra_launch import attach_bottled_agents_to_gateway attach_bottled_agents_to_gateway() diff --git a/bot_bottle/backend/docker/consolidated_launch.py b/bot_bottle/backend/docker/infra_launch.py similarity index 95% rename from bot_bottle/backend/docker/consolidated_launch.py rename to bot_bottle/backend/docker/infra_launch.py index f562aaab..144a8cbd 100644 --- a/bot_bottle/backend/docker/consolidated_launch.py +++ b/bot_bottle/backend/docker/infra_launch.py @@ -25,16 +25,13 @@ from ...gateway import GATEWAY_NETWORK from .infra import INFRA_NAME, DockerInfraService from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME from ...orchestrator.reprovision import reprovision_bottles +from ..base import InfraLaunchError from ..provision_bottle import deprovision_bottle, provision_bottle from ..util import AGENT_CA_PATH from .gateway_transport import DockerGatewayTransport from .gateway_net import next_free_ip -class ConsolidatedLaunchError(RuntimeError): - """The consolidated register/provision sequence could not complete.""" - - @dataclass(frozen=True) class LaunchContext: """What the agent container needs to join the shared gateway.""" @@ -56,7 +53,7 @@ def _network_cidr(network: str) -> str: ]) cidr = proc.stdout.strip() if proc.returncode != 0 or not cidr: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"gateway network {network} has no subnet: {proc.stderr.strip()}" ) return cidr @@ -85,13 +82,13 @@ def _push_ca_to_container(container: str, ca_pem: str) -> None: """Replace one running agent container's trusted gateway CA with `ca_pem` and rebuild its trust store via `docker cp` + `docker exec`. Unconditional install — there is one gateway, so no fingerprint match is needed. Raises - `ConsolidatedLaunchError` on failure.""" + `InfraLaunchError` on failure.""" mkdir = run_docker( ["docker", "exec", container, "mkdir", "-p", "/usr/local/share/ca-certificates"] ) if mkdir.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {container} failed (mkdir): {mkdir.stderr.strip()}" ) with tempfile.NamedTemporaryFile("w", suffix=".crt") as tmp: @@ -99,7 +96,7 @@ def _push_ca_to_container(container: str, ca_pem: str) -> None: tmp.flush() cp = run_docker(["docker", "cp", tmp.name, f"{container}:{AGENT_CA_PATH}"]) if cp.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {container} failed (cp): {cp.stderr.strip()}" ) ex = run_docker( @@ -107,7 +104,7 @@ def _push_ca_to_container(container: str, ca_pem: str) -> None: f"chmod 644 {AGENT_CA_PATH} && update-ca-certificates"] ) if ex.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {container} failed (update-ca-certificates): " f"{ex.stderr.strip()}" ) @@ -143,7 +140,7 @@ def attach_bottled_agents_to_gateway( try: _push_ca_to_container(name, ca_pem) reconciled += 1 - except (OSError, ConsolidatedLaunchError) as exc: + except (OSError, InfraLaunchError) as exc: log.info(f"CA reconcile skipped for {name}: {exc}") if reconciled: log.info( @@ -260,5 +257,5 @@ __all__ = [ "launch_consolidated", "attach_bottled_agents_to_gateway", "deprovision_consolidated", - "ConsolidatedLaunchError", + "InfraLaunchError", ] diff --git a/bot_bottle/backend/docker/launch.py b/bot_bottle/backend/docker/launch.py index 1b96f01d..5cc212bf 100644 --- a/bot_bottle/backend/docker/launch.py +++ b/bot_bottle/backend/docker/launch.py @@ -61,7 +61,7 @@ from .compose import ( ) from .consolidated_compose import consolidated_agent_compose from ...orchestrator.store.config_store import resolve_teardown_timeout -from .consolidated_launch import launch_consolidated, deprovision_consolidated +from .infra_launch import launch_consolidated, deprovision_consolidated from .infra import INFRA_NAME from .gateway import DockerGateway from ... import resources diff --git a/bot_bottle/backend/firecracker/backend.py b/bot_bottle/backend/firecracker/backend.py index 44dab0e5..3200891a 100644 --- a/bot_bottle/backend/firecracker/backend.py +++ b/bot_bottle/backend/firecracker/backend.py @@ -120,7 +120,7 @@ class FirecrackerBottleBackend( return _enumerate.enumerate_active() def attach_bottled_agents_to_gateway(self) -> None: - from .consolidated_launch import attach_bottled_agents_to_gateway + from .infra_launch import attach_bottled_agents_to_gateway attach_bottled_agents_to_gateway() def supervise_mcp_url(self, plan: FirecrackerBottlePlan) -> str: diff --git a/bot_bottle/backend/firecracker/consolidated_launch.py b/bot_bottle/backend/firecracker/infra_launch.py similarity index 96% rename from bot_bottle/backend/firecracker/consolidated_launch.py rename to bot_bottle/backend/firecracker/infra_launch.py index fd5c7659..c2af3391 100644 --- a/bot_bottle/backend/firecracker/consolidated_launch.py +++ b/bot_bottle/backend/firecracker/infra_launch.py @@ -41,6 +41,7 @@ from ...orchestrator.lifecycle import ( ) from ...orchestrator.reprovision import reprovision_bottles from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME +from ..base import InfraLaunchError from ..provision_bottle import deprovision_bottle, provision_bottle from ..util import AGENT_CA_PATH from . import cleanup, util @@ -50,10 +51,6 @@ from .infra import FirecrackerInfraService _ENV_VAR_SECRET_PATH = "/run/bot-bottle/env-var-secret" -class ConsolidatedLaunchError(RuntimeError): - """The consolidated register/provision sequence could not complete.""" - - @dataclass(frozen=True) class LaunchContext: """What the Firecracker launch needs from the consolidated sequence.""" @@ -85,7 +82,7 @@ def persist_env_var_secret(private_key: Path, guest_ip: str, secret: str) -> Non input=secret, capture_output=True, text=True, check=False, ) if proc.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"failed to persist {ENV_VAR_SECRET_NAME} in agent VM: " f"{proc.stderr.strip() or ''}" ) @@ -95,7 +92,7 @@ def _push_gateway_ca_to_agent(private_key: Path, guest_ip: str, ca_pem: str) -> """Replace one running agent VM's trusted gateway CA with `ca_pem` and rebuild its trust store over SSH. Unconditional install — there is one gateway, so no fingerprint match is needed. Bare-pipe input keeps the cert - off argv. Raises `ConsolidatedLaunchError` on failure.""" + off argv. Raises `InfraLaunchError` on failure.""" install = ( "umask 022; mkdir -p /usr/local/share/ca-certificates && " f"cat > {AGENT_CA_PATH} && chmod 644 {AGENT_CA_PATH} && " @@ -106,7 +103,7 @@ def _push_gateway_ca_to_agent(private_key: Path, guest_ip: str, ca_pem: str) -> input=ca_pem, capture_output=True, text=True, check=False, ) if proc.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {guest_ip} failed: " f"{proc.stderr.strip() or ''}" ) @@ -135,7 +132,7 @@ def attach_bottled_agents_to_gateway() -> None: try: _push_gateway_ca_to_agent(private_key, guest_ip, ca_pem) reconciled += 1 - except (OSError, ConsolidatedLaunchError) as exc: + except (OSError, InfraLaunchError) as exc: info(f"CA reconcile skipped for {guest_ip}: {exc}") if reconciled: info(f"reconciled gateway CA into {reconciled} running Firecracker bottle(s)") @@ -215,6 +212,6 @@ __all__ = [ "launch_consolidated", "attach_bottled_agents_to_gateway", "deprovision_consolidated", - "ConsolidatedLaunchError", + "InfraLaunchError", "OrchestratorStartError", ] diff --git a/bot_bottle/backend/firecracker/launch.py b/bot_bottle/backend/firecracker/launch.py index 6fa0d0b8..4e078d00 100644 --- a/bot_bottle/backend/firecracker/launch.py +++ b/bot_bottle/backend/firecracker/launch.py @@ -51,7 +51,7 @@ from .bottle import FirecrackerBottle from .bottle_plan import FirecrackerBottlePlan from ...orchestrator.store.config_store import resolve_teardown_timeout from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME -from .consolidated_launch import ( +from .infra_launch import ( launch_consolidated, persist_env_var_secret, deprovision_consolidated, diff --git a/bot_bottle/backend/macos_container/backend.py b/bot_bottle/backend/macos_container/backend.py index a186ede7..53df3c8d 100644 --- a/bot_bottle/backend/macos_container/backend.py +++ b/bot_bottle/backend/macos_container/backend.py @@ -118,7 +118,7 @@ class MacosContainerBottleBackend( return _enumerate.enumerate_active() def attach_bottled_agents_to_gateway(self) -> None: - from .consolidated_launch import attach_bottled_agents_to_gateway + from .infra_launch import attach_bottled_agents_to_gateway attach_bottled_agents_to_gateway() def supervise_mcp_url(self, plan: MacosContainerBottlePlan) -> str: diff --git a/bot_bottle/backend/macos_container/consolidated_launch.py b/bot_bottle/backend/macos_container/infra_launch.py similarity index 96% rename from bot_bottle/backend/macos_container/consolidated_launch.py rename to bot_bottle/backend/macos_container/infra_launch.py index 88a386a5..5ca3a13d 100644 --- a/bot_bottle/backend/macos_container/consolidated_launch.py +++ b/bot_bottle/backend/macos_container/infra_launch.py @@ -43,6 +43,7 @@ from ...log import info from ...orchestrator.client import OrchestratorClient, OrchestratorClientError from ...orchestrator.reprovision import reprovision_bottles from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME +from ..base import InfraLaunchError from ..provision_bottle import deprovision_bottle, provision_bottle from ..util import AGENT_CA_PATH from . import util as container_mod @@ -52,10 +53,6 @@ from .gateway_transport import MacosGatewayTransport from .infra import MacosInfraService, OrchestratorStartError -class ConsolidatedLaunchError(RuntimeError): - """The consolidated register/provision sequence could not complete.""" - - @dataclass(frozen=True) class GatewayEndpoint: """What the agent `container run` needs to reach the shared gateway. @@ -104,13 +101,13 @@ def _push_ca_to_container(name: str, ca_pem: str) -> None: """Replace one running agent container's trusted gateway CA with `ca_pem` and rebuild its trust store via `container cp` + `container exec`. Unconditional install — there is one gateway, so no fingerprint match is - needed. Raises `ConsolidatedLaunchError` on failure.""" + needed. Raises `InfraLaunchError` on failure.""" mkdir = container_mod.run_container_argv( ["container", "exec", name, "mkdir", "-p", "/usr/local/share/ca-certificates"] ) if mkdir.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {name} failed (mkdir): {(mkdir.stderr or '').strip()}" ) with tempfile.NamedTemporaryFile("w", suffix=".crt") as tmp: @@ -120,7 +117,7 @@ def _push_ca_to_container(name: str, ca_pem: str) -> None: ["container", "cp", tmp.name, f"{name}:{AGENT_CA_PATH}"] ) if cp.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {name} failed (cp): {(cp.stderr or '').strip()}" ) ex = container_mod.run_container_argv( @@ -128,7 +125,7 @@ def _push_ca_to_container(name: str, ca_pem: str) -> None: f"chmod 644 {AGENT_CA_PATH} && update-ca-certificates"] ) if ex.returncode != 0: - raise ConsolidatedLaunchError( + raise InfraLaunchError( f"CA push to {name} failed (update-ca-certificates): " f"{(ex.stderr or '').strip()}" ) @@ -154,7 +151,7 @@ def attach_bottled_agents_to_gateway(ca_pem: str | None = None) -> None: try: _push_ca_to_container(name, ca_pem) reconciled += 1 - except (OSError, ConsolidatedLaunchError) as exc: + except (OSError, InfraLaunchError) as exc: info(f"CA reconcile skipped for {name}: {exc}") if reconciled: info(f"reconciled gateway CA into {reconciled} running macOS bottle(s)") @@ -266,7 +263,7 @@ __all__ = [ "live_source_ips", "register_agent", "deprovision_consolidated", - "ConsolidatedLaunchError", + "InfraLaunchError", "OrchestratorStartError", "GATEWAY_NETWORK", ] diff --git a/bot_bottle/backend/macos_container/launch.py b/bot_bottle/backend/macos_container/launch.py index 702dcff3..114d252f 100644 --- a/bot_bottle/backend/macos_container/launch.py +++ b/bot_bottle/backend/macos_container/launch.py @@ -5,7 +5,7 @@ proxies egress through the one per-host gateway, replacing the per-bottle companion container removed in #385. The order differs from docker's, forced by Apple Container 1.0.0 having no -`--ip` (see `consolidated_launch`): the agent is started *before* it is +`--ip` (see `infra_launch`): the agent is started *before* it is registered, because its DHCP-assigned address — the attribution key — does not exist until then. @@ -64,7 +64,7 @@ from . import nested_containers as nested_containers_mod from .bottle_plan import MacosContainerBottlePlan from ...orchestrator.store.config_store import resolve_teardown_timeout from ...orchestrator.store.secret_store import ENV_VAR_SECRET_NAME, new_env_var_secret -from .consolidated_launch import ( +from .infra_launch import ( GatewayEndpoint, ensure_gateway, register_agent, diff --git a/bot_bottle/backend/provision_bottle.py b/bot_bottle/backend/provision_bottle.py index 85edc15c..8adc6226 100644 --- a/bot_bottle/backend/provision_bottle.py +++ b/bot_bottle/backend/provision_bottle.py @@ -2,7 +2,7 @@ Register a bottle with the orchestrator and provision its git-gate state into the shared gateway (`provision_bottle`), and the inverse teardown -(`deprovision_bottle`). Backend-neutral — each backend's consolidated_launch +(`deprovision_bottle`). Backend-neutral — each backend's infra_launch drives these through its own `GatewayTransport` rather than re-implementing them. The git-gate half of the work lives in `provision_gateway`. """ diff --git a/docs/glossary.md b/docs/glossary.md index 52ee4be5..bff8ab88 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -55,3 +55,13 @@ agent has two observable states: Shorthand for an active (running) Bottled Agent. Used in the supervisor TUI and discovery layer to mean "a bottle whose agent runtime is currently up." + +## Infra + +The per-host pair of shared services every bottle attaches to: the +**orchestrator** (control plane — holds the signing key, owns the registry DB, +mints tokens) and the **gateway** (data plane — TLS-inspecting egress proxy, +git-gate, supervise). Brought up as an idempotent singleton pair by each +backend's `InfraService` (two containers on docker/macOS, two microVMs on +Firecracker). The per-backend `infra_launch` module composes the bring-up and +bottle register/provision sequence against this pair. diff --git a/tests/integration/test_multitenant_isolation.py b/tests/integration/test_multitenant_isolation.py index eb5ad609..f1ecb522 100644 --- a/tests/integration/test_multitenant_isolation.py +++ b/tests/integration/test_multitenant_isolation.py @@ -21,7 +21,7 @@ import subprocess import time import unittest -from bot_bottle.backend.docker.consolidated_launch import ( +from bot_bottle.backend.docker.infra_launch import ( _network_cidr, _network_container_ips, ) diff --git a/tests/unit/test_backend_secret_reprovision.py b/tests/unit/test_backend_secret_reprovision.py index e88cffa9..03a17623 100644 --- a/tests/unit/test_backend_secret_reprovision.py +++ b/tests/unit/test_backend_secret_reprovision.py @@ -11,9 +11,9 @@ from types import SimpleNamespace from unittest.mock import Mock, patch from bot_bottle.orchestrator.reprovision import reprovision_bottles -from bot_bottle.backend.firecracker import consolidated_launch as fc -from bot_bottle.backend.macos_container import consolidated_launch as mac -from bot_bottle.backend.docker import consolidated_launch as docker +from bot_bottle.backend.firecracker import infra_launch as fc +from bot_bottle.backend.macos_container import infra_launch as mac +from bot_bottle.backend.docker import infra_launch as docker from bot_bottle.orchestrator.client import OrchestratorClientError @@ -132,7 +132,7 @@ class TestFirecrackerReprovision(unittest.TestCase): def test_persist_failure_is_fatal_to_launch(self) -> None: with patch.object(fc.util, "ssh_base_argv", return_value=["ssh", "guest"]), \ patch.object(fc.subprocess, "run", return_value=_proc(1, stderr="denied")): - with self.assertRaisesRegex(fc.ConsolidatedLaunchError, "denied"): + with self.assertRaisesRegex(fc.InfraLaunchError, "denied"): fc.persist_env_var_secret(Path("/key"), "10.0.0.1", "secret") def test_reads_live_vm_key_and_reprovisions(self) -> None: diff --git a/tests/unit/test_docker_launch_committed_image.py b/tests/unit/test_docker_launch_committed_image.py index 05180f6d..6bb71d56 100644 --- a/tests/unit/test_docker_launch_committed_image.py +++ b/tests/unit/test_docker_launch_committed_image.py @@ -16,7 +16,7 @@ from bot_bottle.agent_provider import AgentProvisionPlan from bot_bottle.backend import BottleSpec from bot_bottle.backend.docker import launch as launch_mod from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan -from bot_bottle.backend.docker.consolidated_launch import LaunchContext +from bot_bottle.backend.docker.infra_launch import LaunchContext from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan from bot_bottle.log import Die diff --git a/tests/unit/test_docker_launch_teardown.py b/tests/unit/test_docker_launch_teardown.py index d80b3c42..0d0c1e14 100644 --- a/tests/unit/test_docker_launch_teardown.py +++ b/tests/unit/test_docker_launch_teardown.py @@ -19,7 +19,7 @@ from bot_bottle.agent_provider import AgentProvisionPlan from bot_bottle.backend import BottleImages, BottleSpec from bot_bottle.backend.docker import launch as launch_mod from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan -from bot_bottle.backend.docker.consolidated_launch import LaunchContext +from bot_bottle.backend.docker.infra_launch import LaunchContext from bot_bottle.egress import EgressPlan from bot_bottle.git_gate import GitGatePlan from bot_bottle.manifest import ManifestIndex diff --git a/tests/unit/test_consolidated_launch.py b/tests/unit/test_infra_launch.py similarity index 94% rename from tests/unit/test_consolidated_launch.py rename to tests/unit/test_infra_launch.py index 8c9d03d0..bd853f11 100644 --- a/tests/unit/test_consolidated_launch.py +++ b/tests/unit/test_infra_launch.py @@ -6,8 +6,8 @@ import unittest from pathlib import Path from unittest.mock import MagicMock, Mock, patch -from bot_bottle.backend.docker.consolidated_launch import ( - ConsolidatedLaunchError, +from bot_bottle.backend.docker.infra_launch import ( + InfraLaunchError, _network_container_ips, launch_consolidated, deprovision_consolidated, @@ -16,7 +16,7 @@ from bot_bottle.egress import EgressPlan, EgressRoute from bot_bottle.git_gate import GitGatePlan from bot_bottle.orchestrator.client import RegisteredBottle -_MOD = "bot_bottle.backend.docker.consolidated_launch" +_MOD = "bot_bottle.backend.docker.infra_launch" _UTIL = "bot_bottle.backend.provision_bottle" @@ -93,7 +93,7 @@ class TestNetworkContainerIps(unittest.TestCase): result = Mock(returncode=1, stdout="", stderr="daemon unavailable") with ( patch(f"{_MOD}.run_docker", return_value=result), - self.assertRaisesRegex(ConsolidatedLaunchError, "daemon unavailable"), + self.assertRaisesRegex(InfraLaunchError, "daemon unavailable"), ): _network_container_ips("bot-bottle-gateway") diff --git a/tests/unit/test_macos_container_launch_wiring.py b/tests/unit/test_macos_container_launch_wiring.py index 81a427be..9ec934be 100644 --- a/tests/unit/test_macos_container_launch_wiring.py +++ b/tests/unit/test_macos_container_launch_wiring.py @@ -16,7 +16,7 @@ from unittest.mock import ANY, patch from bot_bottle.backend.macos_container.bottle import MacosContainerBottle from bot_bottle.backend.macos_container.bottle_plan import MacosContainerBottlePlan -from bot_bottle.backend.macos_container.consolidated_launch import GatewayEndpoint +from bot_bottle.backend.macos_container.infra_launch import GatewayEndpoint from bot_bottle.backend.macos_container.gateway_hosts import GATEWAY_HOSTNAME from bot_bottle.backend.macos_container.launch import ( _agent_run_argv, diff --git a/tests/unit/test_macos_consolidated_launch.py b/tests/unit/test_macos_infra_launch.py similarity index 95% rename from tests/unit/test_macos_consolidated_launch.py rename to tests/unit/test_macos_infra_launch.py index c340f1b7..f3e13ef2 100644 --- a/tests/unit/test_macos_consolidated_launch.py +++ b/tests/unit/test_macos_infra_launch.py @@ -6,7 +6,7 @@ import unittest from pathlib import Path from unittest.mock import MagicMock, Mock, patch -from bot_bottle.backend.macos_container.consolidated_launch import ( +from bot_bottle.backend.macos_container.infra_launch import ( GatewayEndpoint, ensure_gateway, register_agent, @@ -16,7 +16,7 @@ from bot_bottle.egress import EgressPlan, EgressRoute from bot_bottle.git_gate import GitGatePlan from bot_bottle.orchestrator.client import RegisteredBottle -_MOD = "bot_bottle.backend.macos_container.consolidated_launch" +_MOD = "bot_bottle.backend.macos_container.infra_launch" _UTIL = "bot_bottle.backend.provision_bottle" @@ -145,7 +145,7 @@ class TestLiveSourceIps(unittest.TestCase): return [Mock(slug=s) for s in slugs] def test_maps_slugs_to_container_addresses(self) -> None: - from bot_bottle.backend.macos_container.consolidated_launch import live_source_ips + from bot_bottle.backend.macos_container.infra_launch import live_source_ips with patch(f"{_MOD}.enumerate_active", return_value=self._agents("a", "b")), \ patch(f"{_MOD}.container_mod.inspect_container_network_ip", side_effect=["10.0.0.1", "10.0.0.2"]) as ip: @@ -156,7 +156,7 @@ class TestLiveSourceIps(unittest.TestCase): def test_containers_without_an_address_are_skipped(self) -> None: """A container that hasn't been given a DHCP address yet contributes nothing — the reap's grace window, not this list, protects it.""" - from bot_bottle.backend.macos_container.consolidated_launch import live_source_ips + from bot_bottle.backend.macos_container.infra_launch import live_source_ips with patch(f"{_MOD}.enumerate_active", return_value=self._agents("a", "b")), \ patch(f"{_MOD}.container_mod.inspect_container_network_ip", side_effect=["", "10.0.0.2"]): @@ -165,7 +165,7 @@ class TestLiveSourceIps(unittest.TestCase): def test_container_list_failure_raises(self) -> None: """If container list fails, the live set is not authoritative and reconciliation must be skipped.""" - from bot_bottle.backend.macos_container.consolidated_launch import live_source_ips + from bot_bottle.backend.macos_container.infra_launch import live_source_ips from bot_bottle.backend.macos_container.enumerate import EnumerationError with patch(f"{_MOD}.enumerate_active", side_effect=EnumerationError("container list failed")): @@ -174,7 +174,7 @@ class TestLiveSourceIps(unittest.TestCase): def test_per_container_inspect_failure_raises(self) -> None: """If any individual inspect fails, the live set is not authoritative.""" - from bot_bottle.backend.macos_container.consolidated_launch import live_source_ips + from bot_bottle.backend.macos_container.infra_launch import live_source_ips from bot_bottle.backend.macos_container.enumerate import EnumerationError with patch(f"{_MOD}.enumerate_active", return_value=self._agents("a", "b")), \ patch(f"{_MOD}.container_mod.inspect_container_network_ip",