refactor(de-sidecar): remove the per-bottle companion-container architecture
The per-agent companion container (the egress/git-gate/supervise data plane run once per bottle) is the pre-consolidation architecture. Remove it and disable the backends that still depend on it, per the #385 thread. - Delete `backend/docker/sidecar_bundle.py`; docker's live path uses the consolidated shared gateway, not a per-bottle bundle. - Disable the firecracker and macos-container backends: their `launch()` fails closed (they launched a per-bottle companion; firecracker's consolidated relaunch is #354, macos follows). Their `enumerate` return empty and `cleanup` drop the companion-container discovery (firecracker keeps VMM/run-dir cleanup). - Fail-close both backends' `egress_apply` reload (it signalled the per-bottle container); consolidated egress policy resolves per-request against the orchestrator, so gateway-side apply is a follow-up. - Rename `egress_sidecar_env_entries` → `egress_gateway_env_entries`, `SIDECAR_PORTS` → `GATEWAY_PORTS`. - Move the shared DockerBottlePlan fixture to `tests/unit/_docker_bottle_plan.py`; delete tests for the removed launch paths; update cleanup/egress-apply tests. Docker consolidated launch verified end-to-end (multitenant isolation integration test passes). macos/firecracker are intentionally disabled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""Shared in-memory `DockerBottlePlan` fixture for docker-backend tests.
|
||||
|
||||
A fully-resolved plan with toggles for the conditional-service matrix
|
||||
(git-gate / egress / supervise / canary). Consumed by the consolidated
|
||||
compose tests; kept here (rather than in a test module) so it survives
|
||||
independent of any one test file.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from bot_bottle.agent_provider import AgentProvisionPlan
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.docker.bottle_plan import DockerBottlePlan
|
||||
from bot_bottle.egress import EgressPlan, EgressRoute
|
||||
from bot_bottle.git_gate import GitGatePlan, GitGateUpstream
|
||||
from bot_bottle.manifest import ManifestIndex
|
||||
from bot_bottle.supervise import SupervisePlan
|
||||
|
||||
|
||||
SLUG = "demo-abc12"
|
||||
STAGE = Path("/tmp/cb-stage")
|
||||
STATE = Path("/tmp/cb-state")
|
||||
|
||||
|
||||
def _manifest(*, supervise: bool, with_git: bool, with_egress: bool) -> ManifestIndex:
|
||||
"""Minimal manifest with the toggles the matrix needs. The renderer
|
||||
only reads from the plan, not the manifest, so this is just here to
|
||||
back BottleSpec."""
|
||||
bottle: dict[str, object] = {}
|
||||
if supervise:
|
||||
bottle["supervise"] = True
|
||||
if with_git:
|
||||
bottle["git-gate"] = {"repos": {
|
||||
"upstream": {
|
||||
"url": "ssh://git@example.com:22/x/y.git",
|
||||
"key": {"provider": "static", "path": "/etc/hostname"},
|
||||
},
|
||||
}}
|
||||
if with_egress:
|
||||
bottle["egress"] = {
|
||||
"routes": [{
|
||||
"host": "api.example",
|
||||
"auth": {"scheme": "Bearer", "token_ref": "TOK"},
|
||||
}],
|
||||
}
|
||||
return ManifestIndex.from_json_obj({
|
||||
"bottles": {"dev": bottle},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
})
|
||||
|
||||
|
||||
def _git_gate_plan(upstreams: tuple[GitGateUpstream, ...] = ()) -> GitGatePlan:
|
||||
return GitGatePlan(
|
||||
slug=SLUG,
|
||||
entrypoint_script=STATE / "git-gate" / "entrypoint.sh",
|
||||
hook_script=STATE / "git-gate" / "pre-receive",
|
||||
access_hook_script=STATE / "git-gate" / "access-hook",
|
||||
upstreams=upstreams,
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
egress_network=f"bot-bottle-egress-{SLUG}",
|
||||
)
|
||||
|
||||
|
||||
def _egress_plan(
|
||||
routes: tuple[EgressRoute, ...] = (),
|
||||
*,
|
||||
canary: bool = False,
|
||||
) -> EgressPlan:
|
||||
token_env_map = {
|
||||
r.token_env: r.token_ref
|
||||
for r in routes
|
||||
if r.token_env
|
||||
}
|
||||
return EgressPlan(
|
||||
slug=SLUG,
|
||||
routes_path=STATE / "egress" / "routes.yaml",
|
||||
routes=routes,
|
||||
token_env_map=token_env_map,
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
egress_network=f"bot-bottle-egress-{SLUG}",
|
||||
mitmproxy_ca_host_path=STATE / "egress-ca" / "mitmproxy-ca.pem",
|
||||
mitmproxy_ca_cert_only_host_path=STATE / "egress-ca" / "ca.pem",
|
||||
canary="fake-canary-value" if canary else "",
|
||||
canary_env="CANON_ALPHA_SECRET" if canary else "",
|
||||
)
|
||||
|
||||
|
||||
def _supervise_plan() -> SupervisePlan:
|
||||
return SupervisePlan(
|
||||
slug=SLUG,
|
||||
db_path=STATE / "bot-bottle.db",
|
||||
internal_network=f"bot-bottle-net-{SLUG}",
|
||||
)
|
||||
|
||||
|
||||
def _plan(
|
||||
*,
|
||||
with_git: bool = False,
|
||||
with_egress: bool = False,
|
||||
supervise: bool = False,
|
||||
canary: bool = False,
|
||||
) -> DockerBottlePlan:
|
||||
"""Build a fully-resolved DockerBottlePlan. Toggles cover the
|
||||
matrix the renderer's conditional-service logic branches on."""
|
||||
upstreams: tuple[GitGateUpstream, ...] = ()
|
||||
if with_git:
|
||||
upstreams = (GitGateUpstream(
|
||||
name="upstream",
|
||||
upstream_url="ssh://git@example.com:22/x/y.git",
|
||||
upstream_host="example.com",
|
||||
upstream_port="22",
|
||||
identity_file="/etc/hostname",
|
||||
known_host_key="",
|
||||
known_hosts_file=STATE / "git-gate" / "upstream-known_hosts",
|
||||
),)
|
||||
routes: tuple[EgressRoute, ...] = ()
|
||||
if with_egress:
|
||||
routes = (EgressRoute(
|
||||
host="api.example",
|
||||
auth_scheme="Bearer",
|
||||
token_env="EGRESS_TOKEN_0",
|
||||
token_ref="TOK",
|
||||
roles=(),
|
||||
),)
|
||||
|
||||
index = _manifest(supervise=supervise, with_git=with_git, with_egress=with_egress)
|
||||
spec = BottleSpec(
|
||||
manifest=index,
|
||||
agent_name="demo",
|
||||
copy_cwd=False,
|
||||
user_cwd="/tmp/x",
|
||||
)
|
||||
return DockerBottlePlan(
|
||||
spec=spec,
|
||||
manifest=index.load_for_agent("demo"),
|
||||
stage_dir=STAGE,
|
||||
slug=SLUG,
|
||||
forwarded_env={"CLAUDE_CODE_OAUTH_TOKEN": "x"},
|
||||
git_gate_plan=_git_gate_plan(upstreams),
|
||||
egress_plan=_egress_plan(routes, canary=canary),
|
||||
supervise_plan=_supervise_plan() if supervise else None,
|
||||
use_runsc=False,
|
||||
agent_provision=AgentProvisionPlan(
|
||||
template="claude",
|
||||
command="claude",
|
||||
prompt_mode="append_file",
|
||||
image="bot-bottle-claude:latest",
|
||||
dockerfile="",
|
||||
guest_home="/home/node",
|
||||
instance_name=f"bot-bottle-{SLUG}",
|
||||
prompt_file=STAGE / "prompt",
|
||||
guest_env={},
|
||||
),
|
||||
)
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import unittest
|
||||
|
||||
from bot_bottle.backend.docker.consolidated_compose import consolidated_agent_compose
|
||||
from tests.unit.test_compose import _plan
|
||||
from tests.unit._docker_bottle_plan import _plan
|
||||
|
||||
_GW = "172.18.0.2"
|
||||
_IP = "172.18.0.5"
|
||||
|
||||
@@ -16,7 +16,7 @@ from bot_bottle.egress import (
|
||||
egress_render_routes,
|
||||
egress_resolve_token_values,
|
||||
egress_routes_for_bottle,
|
||||
egress_sidecar_env_entries,
|
||||
egress_gateway_env_entries,
|
||||
egress_token_env_map,
|
||||
)
|
||||
from bot_bottle.errors import MissingEnvVarError
|
||||
@@ -603,7 +603,7 @@ class TestEgressEnvEntries(unittest.TestCase):
|
||||
"CANON_ALPHA_SECRET=fake-canary-value",
|
||||
"BOT_BOTTLE_SENSITIVE_PREFIXES=CANON_ALPHA_SECRET",
|
||||
),
|
||||
egress_sidecar_env_entries(plan),
|
||||
egress_gateway_env_entries(plan),
|
||||
)
|
||||
|
||||
def test_agent_entries_include_only_canary_bait(self):
|
||||
@@ -630,7 +630,7 @@ class TestEgressEnvEntries(unittest.TestCase):
|
||||
canary="fake-canary-value",
|
||||
)
|
||||
|
||||
self.assertEqual((), egress_sidecar_env_entries(plan))
|
||||
self.assertEqual((), egress_gateway_env_entries(plan))
|
||||
self.assertEqual((), egress_agent_env_entries(plan))
|
||||
|
||||
|
||||
|
||||
@@ -70,32 +70,16 @@ class TestApplyRoutesChange(unittest.TestCase):
|
||||
self.addCleanup(self._tmp.cleanup)
|
||||
self.addCleanup(use_bottle_root(Path(self._tmp.name) / ".bot-bottle"))
|
||||
|
||||
def test_writes_live_routes_and_signals_reload(self):
|
||||
calls: list[list[str]] = []
|
||||
|
||||
def fake_run(argv: list[str], **kwargs: object) -> SimpleNamespace:
|
||||
calls.append(list(argv))
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
with patch(
|
||||
"bot_bottle.backend.docker.egress_apply.subprocess.run",
|
||||
side_effect=fake_run,
|
||||
):
|
||||
before, after = applicator.apply_routes_change(
|
||||
def test_apply_routes_change_fails_closed_after_companion_removal(self):
|
||||
# The per-bottle companion container that live route-apply used to
|
||||
# signal was removed in the de-sidecar cleanup (#385); apply now
|
||||
# fails closed until the gateway-side apply lands.
|
||||
with self.assertRaises(EgressApplyError) as cm:
|
||||
applicator.apply_routes_change(
|
||||
"dev",
|
||||
"routes:\n - host: google.com\n",
|
||||
)
|
||||
|
||||
self.assertEqual("", before)
|
||||
self.assertEqual("routes:\n - host: google.com\n", after)
|
||||
self.assertEqual(
|
||||
"routes:\n - host: google.com\n",
|
||||
(Path(self._tmp.name) / ".bot-bottle/state/dev/egress/routes.yaml").read_text(encoding="utf-8"),
|
||||
)
|
||||
self.assertEqual(
|
||||
["docker", "kill", "--signal", "HUP", "bot-bottle-sidecars-dev"],
|
||||
calls[0],
|
||||
)
|
||||
self.assertIn("consolidated gateway", str(cm.exception))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -38,18 +38,6 @@ class TestOrphanEnumeration(unittest.TestCase):
|
||||
with patch.object(fc_cleanup.subprocess, "run", return_value=_proc(returncode=1)):
|
||||
self.assertEqual([], fc_cleanup._orphan_vm_pids())
|
||||
|
||||
def test_sidecar_containers_sorted(self):
|
||||
with patch.object(fc_cleanup.subprocess, "run",
|
||||
return_value=_proc("bot-bottle-sidecars-b\nbot-bottle-sidecars-a\n")):
|
||||
self.assertEqual(
|
||||
["bot-bottle-sidecars-a", "bot-bottle-sidecars-b"],
|
||||
fc_cleanup._sidecar_containers(),
|
||||
)
|
||||
|
||||
def test_sidecar_containers_empty_on_failure(self):
|
||||
with patch.object(fc_cleanup.subprocess, "run", return_value=_proc(returncode=1)):
|
||||
self.assertEqual([], fc_cleanup._sidecar_containers())
|
||||
|
||||
def test_run_dirs_empty_when_absent(self):
|
||||
with patch.object(fc_cleanup.util, "cache_dir") as cache:
|
||||
cache.return_value.__truediv__.return_value.is_dir.return_value = False
|
||||
@@ -57,28 +45,23 @@ class TestOrphanEnumeration(unittest.TestCase):
|
||||
|
||||
def test_prepare_cleanup_assembles_plan(self):
|
||||
with patch.object(fc_cleanup, "_orphan_vm_pids", return_value=[7]), \
|
||||
patch.object(fc_cleanup, "_sidecar_containers", return_value=["c1"]), \
|
||||
patch.object(fc_cleanup, "_run_dirs", return_value=["/run/x"]):
|
||||
plan = fc_cleanup.prepare_cleanup()
|
||||
self.assertEqual((7,), plan.vm_pids)
|
||||
self.assertEqual(("c1",), plan.containers)
|
||||
self.assertEqual(("/run/x",), plan.run_dirs)
|
||||
|
||||
|
||||
class TestCleanupRemoval(unittest.TestCase):
|
||||
def test_cleanup_kills_removes_and_rmtrees(self):
|
||||
def test_cleanup_kills_and_rmtrees(self):
|
||||
plan = FirecrackerBottleCleanupPlan(
|
||||
vm_pids=(101,), containers=("bot-bottle-sidecars-x",),
|
||||
vm_pids=(101,),
|
||||
run_dirs=("/run/dev-x",),
|
||||
)
|
||||
with patch.object(fc_cleanup.os, "kill") as kill, \
|
||||
patch.object(fc_cleanup.subprocess, "run") as run, \
|
||||
patch.object(fc_cleanup.shutil, "rmtree") as rmtree, \
|
||||
patch.object(fc_cleanup, "info"):
|
||||
fc_cleanup.cleanup(plan)
|
||||
kill.assert_called_once()
|
||||
run.assert_called_once()
|
||||
self.assertIn("bot-bottle-sidecars-x", run.call_args.args[0])
|
||||
rmtree.assert_called_once_with("/run/dev-x", ignore_errors=True)
|
||||
|
||||
def test_cleanup_tolerates_dead_pid(self):
|
||||
@@ -101,13 +84,12 @@ class TestCleanupPlan(unittest.TestCase):
|
||||
|
||||
def test_print_lists_resources(self):
|
||||
plan = FirecrackerBottleCleanupPlan(
|
||||
vm_pids=(5,), containers=("c",), run_dirs=("/r",),
|
||||
vm_pids=(5,), run_dirs=("/r",),
|
||||
)
|
||||
with patch("bot_bottle.backend.firecracker.bottle_cleanup_plan.info") as info:
|
||||
plan.print()
|
||||
joined = " ".join(c.args[0] for c in info.call_args_list)
|
||||
self.assertIn("pid 5", joined)
|
||||
self.assertIn("container: c", joined)
|
||||
self.assertIn("run dir: /r", joined)
|
||||
|
||||
|
||||
|
||||
@@ -43,27 +43,10 @@ class TestMacosContainerCleanup(unittest.TestCase):
|
||||
|
||||
|
||||
class TestMacosContainerEnumerate(unittest.TestCase):
|
||||
def test_enumerate_active_reads_metadata(self):
|
||||
completed = enum_mod.subprocess.CompletedProcess(
|
||||
args=[],
|
||||
returncode=0,
|
||||
stdout="bot-bottle-a\nbot-bottle-sidecars-a\nother\n",
|
||||
stderr="",
|
||||
)
|
||||
|
||||
class _Metadata:
|
||||
agent_name = "impl"
|
||||
started_at = "2026-06-10T00:00:00Z"
|
||||
label = "Implement"
|
||||
color = "blue"
|
||||
|
||||
with patch.object(enum_mod.subprocess, "run", return_value=completed), \
|
||||
patch.object(enum_mod, "read_metadata", return_value=_Metadata()):
|
||||
agents = enum_mod.enumerate_active()
|
||||
self.assertEqual(1, len(agents))
|
||||
self.assertEqual("macos-container", agents[0].backend_name)
|
||||
self.assertEqual("a", agents[0].slug)
|
||||
self.assertEqual("impl", agents[0].agent_name)
|
||||
def test_enumerate_active_is_empty_while_disabled(self):
|
||||
# The macOS backend is disabled during the de-sidecar cleanup
|
||||
# (#385); it launches nothing, so there is nothing to enumerate.
|
||||
self.assertEqual([], enum_mod.enumerate_active())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
"""Unit: Apple Container launch argv construction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import patch
|
||||
|
||||
from bot_bottle.agent_provider import AgentProvisionPlan
|
||||
from bot_bottle.backend import BottleSpec
|
||||
from bot_bottle.backend.macos_container import launch
|
||||
from bot_bottle.backend.macos_container.bottle_plan import MacosContainerBottlePlan
|
||||
from bot_bottle.egress import EgressPlan
|
||||
from bot_bottle.git_gate import GitGatePlan
|
||||
from bot_bottle.manifest import ManifestIndex
|
||||
|
||||
_MANIFEST = ManifestIndex.from_json_obj({
|
||||
"bottles": {"dev": {}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
}).load_for_agent("demo")
|
||||
|
||||
|
||||
def _plan(
|
||||
*,
|
||||
stage_dir: Path,
|
||||
git: bool = False,
|
||||
supervise: bool = False,
|
||||
agent_git_gate_url: str = "",
|
||||
agent_supervise_url: str = "",
|
||||
canary: bool = False,
|
||||
) -> MacosContainerBottlePlan:
|
||||
routes_path = stage_dir / "routes.yaml"
|
||||
routes_path.write_text("routes: []\n", encoding="utf-8")
|
||||
ca_dir = stage_dir / "egress-ca"
|
||||
ca_dir.mkdir(exist_ok=True)
|
||||
ca_path = ca_dir / "mitmproxy-ca.pem"
|
||||
ca_path.write_text("ca\n", encoding="utf-8")
|
||||
egress_plan = SimpleNamespace(
|
||||
mitmproxy_ca_host_path=ca_path,
|
||||
routes_path=routes_path,
|
||||
routes=("route",),
|
||||
token_env_map={"EGRESS_TOKEN_0": "HOST_TOKEN"},
|
||||
canary="fake-canary-value" if canary else "",
|
||||
canary_env="CANON_ALPHA_SECRET" if canary else "",
|
||||
)
|
||||
if git:
|
||||
key_path = stage_dir / "origin-key"
|
||||
key_path.write_text("key\n", encoding="utf-8")
|
||||
known_hosts_path = stage_dir / "origin-known-hosts"
|
||||
known_hosts_path.write_text("example.com ssh-ed25519 AAAA\n", encoding="utf-8")
|
||||
entrypoint = stage_dir / "git_gate_entrypoint.sh"
|
||||
entrypoint.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
hook = stage_dir / "git_gate_pre_receive.sh"
|
||||
hook.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
access_hook = stage_dir / "git_gate_access_hook.sh"
|
||||
access_hook.write_text("#!/bin/sh\n", encoding="utf-8")
|
||||
upstream = SimpleNamespace(
|
||||
name="origin",
|
||||
identity_file=str(key_path),
|
||||
known_hosts_file=known_hosts_path,
|
||||
)
|
||||
git_gate_plan = SimpleNamespace(
|
||||
upstreams=(upstream,),
|
||||
entrypoint_script=entrypoint,
|
||||
hook_script=hook,
|
||||
access_hook_script=access_hook,
|
||||
)
|
||||
else:
|
||||
git_gate_plan = SimpleNamespace(upstreams=())
|
||||
supervise_plan = (
|
||||
SimpleNamespace(
|
||||
db_path=Path("/state/bot-bottle.db"),
|
||||
)
|
||||
if supervise else None
|
||||
)
|
||||
agent_provision = SimpleNamespace(
|
||||
guest_env={"LITERAL": "value"},
|
||||
provisioned_env={"CODEX_HOME": "/run/codex-home"},
|
||||
)
|
||||
return cast(MacosContainerBottlePlan, SimpleNamespace(
|
||||
spec=SimpleNamespace(),
|
||||
manifest=_MANIFEST,
|
||||
stage_dir=stage_dir,
|
||||
slug="dev-abc",
|
||||
container_name="bot-bottle-dev-abc",
|
||||
image="bot-bottle-agent:latest",
|
||||
forwarded_env={"OAUTH_TOKEN": "host-value"},
|
||||
egress_plan=egress_plan,
|
||||
git_gate_plan=git_gate_plan,
|
||||
supervise_plan=supervise_plan,
|
||||
agent_provision=agent_provision,
|
||||
agent_git_gate_url=agent_git_gate_url,
|
||||
agent_supervise_url=agent_supervise_url,
|
||||
))
|
||||
|
||||
|
||||
class TestMacosContainerLaunchArgv(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.stage_dir = Path(self._tmp.name)
|
||||
|
||||
def tearDown(self):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_sidecar_argv_uses_egress_network_first_and_explicit_dns(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, supervise=True)
|
||||
with patch.object(launch.os, "environ", {
|
||||
"BOT_BOTTLE_MACOS_CONTAINER_DNS": "9.9.9.9",
|
||||
}):
|
||||
argv = launch._sidecar_run_argv(
|
||||
plan,
|
||||
"bot-bottle-sidecars-dev-abc",
|
||||
"bot-bottle-net-dev-abc",
|
||||
"bot-bottle-egress-dev-abc",
|
||||
)
|
||||
self.assertEqual(
|
||||
[
|
||||
"--network", "bot-bottle-egress-dev-abc",
|
||||
"--network", "bot-bottle-net-dev-abc",
|
||||
],
|
||||
argv[argv.index("--network"):argv.index("--dns")],
|
||||
)
|
||||
self.assertIn("--dns", argv)
|
||||
self.assertEqual("9.9.9.9", argv[argv.index("--dns") + 1])
|
||||
self.assertIn(
|
||||
"BOT_BOTTLE_SIDECAR_DAEMONS=egress,supervise",
|
||||
argv,
|
||||
)
|
||||
self.assertIn("EGRESS_TOKEN_0", argv)
|
||||
self.assertIn(
|
||||
f"type=bind,source={self.stage_dir / 'egress-ca'},target=/home/mitmproxy/.mitmproxy",
|
||||
argv,
|
||||
)
|
||||
self.assertIn(
|
||||
f"type=bind,source={self.stage_dir},target=/etc/egress,readonly",
|
||||
argv,
|
||||
)
|
||||
self.assertIn(
|
||||
"type=bind,source=/state,target=/run/supervise",
|
||||
argv,
|
||||
)
|
||||
|
||||
def test_sidecar_argv_registers_canary_env_as_sensitive(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, canary=True)
|
||||
argv = launch._sidecar_run_argv(
|
||||
plan,
|
||||
"bot-bottle-sidecars-dev-abc",
|
||||
"bot-bottle-net-dev-abc",
|
||||
"bot-bottle-egress-dev-abc",
|
||||
)
|
||||
self.assertIn("CANON_ALPHA_SECRET=fake-canary-value", argv)
|
||||
self.assertIn("BOT_BOTTLE_SENSITIVE_PREFIXES=CANON_ALPHA_SECRET", argv)
|
||||
|
||||
def test_agent_argv_receives_canary_env(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, canary=True)
|
||||
argv = launch._agent_run_argv(
|
||||
plan,
|
||||
"bot-bottle-net-dev-abc",
|
||||
"192.0.2.10",
|
||||
)
|
||||
self.assertIn("CANON_ALPHA_SECRET=fake-canary-value", argv)
|
||||
|
||||
def test_agent_env_points_proxy_at_sidecar_ip(self):
|
||||
plan = _plan(
|
||||
stage_dir=self.stage_dir,
|
||||
agent_git_gate_url="http://192.168.128.2:9420",
|
||||
agent_supervise_url="http://192.168.128.2:9100/",
|
||||
)
|
||||
env = launch._agent_env_entries(plan, "192.168.128.2")
|
||||
self.assertIn("HTTPS_PROXY=http://192.168.128.2:9099", env)
|
||||
self.assertIn("HTTP_PROXY=http://192.168.128.2:9099", env)
|
||||
self.assertIn("https_proxy=http://192.168.128.2:9099", env)
|
||||
self.assertIn("http_proxy=http://192.168.128.2:9099", env)
|
||||
self.assertIn("NO_PROXY=localhost,127.0.0.1,192.168.128.2", env)
|
||||
self.assertIn("NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/bot-bottle-mitm-ca.crt", env)
|
||||
self.assertIn("SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", env)
|
||||
self.assertIn("REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt", env)
|
||||
self.assertIn("GIT_GATE_URL=http://192.168.128.2:9420", env)
|
||||
self.assertIn("MCP_SUPERVISE_URL=http://192.168.128.2:9100/", env)
|
||||
self.assertIn("LITERAL=value", env)
|
||||
self.assertIn("OAUTH_TOKEN", env)
|
||||
self.assertNotIn("CODEX_HOME", env)
|
||||
|
||||
def test_agent_run_uses_internal_network_only(self):
|
||||
plan = _plan(stage_dir=self.stage_dir)
|
||||
argv = launch._agent_run_argv(
|
||||
plan, "bot-bottle-net-dev-abc", "192.168.128.2",
|
||||
)
|
||||
self.assertIn("--network", argv)
|
||||
self.assertEqual("bot-bottle-net-dev-abc", argv[argv.index("--network") + 1])
|
||||
self.assertNotIn("bot-bottle-egress-dev-abc", argv)
|
||||
self.assertEqual(["bot-bottle-agent:latest", "sleep", "2147483647"], argv[-3:])
|
||||
|
||||
def test_git_gate_daemons_are_ready_gated(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, git=True)
|
||||
self.assertEqual(
|
||||
("egress", "git-gate", "git-http"),
|
||||
launch._sidecar_daemons(plan),
|
||||
)
|
||||
self.assertIn(
|
||||
"BOT_BOTTLE_GIT_GATE_READY_FILE=/run/git-gate/ready",
|
||||
launch._sidecar_env_entries(plan),
|
||||
)
|
||||
|
||||
def test_stamp_agent_urls_includes_git_http_when_git_gate_exists(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, git=True, supervise=True)
|
||||
with patch.object(launch.dataclasses, "replace") as replace:
|
||||
launch._stamp_agent_urls(plan, "192.168.128.2")
|
||||
replace.assert_called_once_with(
|
||||
plan,
|
||||
agent_proxy_url="http://192.168.128.2:9099",
|
||||
agent_git_gate_url="http://192.168.128.2:9420",
|
||||
agent_supervise_url="http://192.168.128.2:9100/",
|
||||
)
|
||||
|
||||
def test_macos_plan_uses_http_git_gate_rewrites(self):
|
||||
base = _plan(
|
||||
stage_dir=self.stage_dir,
|
||||
git=True,
|
||||
agent_git_gate_url="http://192.168.128.2:9420",
|
||||
)
|
||||
plan = MacosContainerBottlePlan(
|
||||
spec=base.spec,
|
||||
manifest=base.manifest,
|
||||
stage_dir=base.stage_dir,
|
||||
git_gate_plan=base.git_gate_plan,
|
||||
egress_plan=base.egress_plan,
|
||||
supervise_plan=base.supervise_plan,
|
||||
agent_provision=base.agent_provision,
|
||||
slug=base.slug,
|
||||
forwarded_env=base.forwarded_env,
|
||||
agent_git_gate_url=base.agent_git_gate_url,
|
||||
)
|
||||
self.assertEqual(
|
||||
"192.168.128.2:9420",
|
||||
plan.git_gate_insteadof_host,
|
||||
)
|
||||
self.assertEqual("http", plan.git_gate_insteadof_scheme)
|
||||
|
||||
def test_stage_git_gate_copies_files_and_releases_ready_marker(self):
|
||||
plan = _plan(stage_dir=self.stage_dir, git=True)
|
||||
with (
|
||||
patch.object(launch.container_mod, "exec_container") as exec_container,
|
||||
patch.object(launch.container_mod, "copy_into_container") as copy_in,
|
||||
):
|
||||
launch._stage_git_gate(plan, "sidecar")
|
||||
|
||||
exec_container.assert_any_call(
|
||||
"sidecar",
|
||||
[
|
||||
"mkdir",
|
||||
"-p",
|
||||
"/etc/git-gate",
|
||||
"/git-gate/creds",
|
||||
"/git",
|
||||
"/run/git-gate",
|
||||
],
|
||||
)
|
||||
copied = [call.args for call in copy_in.call_args_list]
|
||||
self.assertIn(
|
||||
(
|
||||
"sidecar",
|
||||
str(self.stage_dir / "git_gate_entrypoint.sh"),
|
||||
"/git-gate-entrypoint.sh",
|
||||
),
|
||||
copied,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
"sidecar",
|
||||
str(self.stage_dir / "origin-key"),
|
||||
"/git-gate/creds/origin-key",
|
||||
),
|
||||
copied,
|
||||
)
|
||||
self.assertIn(
|
||||
(
|
||||
"sidecar",
|
||||
str(self.stage_dir / "origin-known-hosts"),
|
||||
"/git-gate/creds/origin-known_hosts",
|
||||
),
|
||||
copied,
|
||||
)
|
||||
self.assertIn(
|
||||
"touch /run/git-gate/ready",
|
||||
exec_container.call_args_list[-1].args[1][-1],
|
||||
)
|
||||
|
||||
|
||||
def _build_plan(stage_dir: Path) -> MacosContainerBottlePlan:
|
||||
return MacosContainerBottlePlan(
|
||||
spec=cast(BottleSpec, SimpleNamespace()),
|
||||
manifest=_MANIFEST,
|
||||
stage_dir=stage_dir,
|
||||
git_gate_plan=cast(GitGatePlan, SimpleNamespace(upstreams=())),
|
||||
egress_plan=cast(EgressPlan, SimpleNamespace(canary="")),
|
||||
supervise_plan=None,
|
||||
agent_provision=AgentProvisionPlan(
|
||||
template="claude",
|
||||
command="claude",
|
||||
prompt_mode="append_file",
|
||||
image="bot-bottle-agent:latest",
|
||||
dockerfile="/repo/Dockerfile",
|
||||
guest_home="/home/node",
|
||||
instance_name="bot-bottle-dev-abc",
|
||||
prompt_file=stage_dir / "prompt.txt",
|
||||
guest_env={},
|
||||
),
|
||||
slug="dev-abc",
|
||||
forwarded_env={},
|
||||
)
|
||||
|
||||
|
||||
class TestMacosContainerLaunchCommittedImage(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
self.stage_dir = Path(self._tmp.name)
|
||||
|
||||
def tearDown(self):
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_build_images_uses_committed_image_when_present(self):
|
||||
plan = _build_plan(self.stage_dir)
|
||||
calls = []
|
||||
|
||||
def fake_build(image: str, context: str, *, dockerfile: str = "") -> None:
|
||||
calls.append((image, context, dockerfile))
|
||||
|
||||
with patch.object(
|
||||
launch, "read_committed_image",
|
||||
return_value="bot-bottle-committed-dev-abc:latest",
|
||||
), patch.object(
|
||||
launch.container_mod, "image_exists", return_value=True,
|
||||
), patch.object(
|
||||
launch.container_mod, "build_image", side_effect=fake_build,
|
||||
), patch.object(launch, "info"):
|
||||
updated = launch._build_images(plan)
|
||||
|
||||
self.assertEqual("bot-bottle-committed-dev-abc:latest", updated.image)
|
||||
self.assertEqual(1, len(calls))
|
||||
self.assertEqual(launch.SIDECAR_BUNDLE_IMAGE, calls[0][0])
|
||||
|
||||
def test_build_images_builds_agent_when_committed_image_missing(self):
|
||||
plan = _build_plan(self.stage_dir)
|
||||
calls = []
|
||||
|
||||
def fake_build(image: str, context: str, *, dockerfile: str = "") -> None:
|
||||
calls.append((image, context, dockerfile))
|
||||
|
||||
with patch.object(
|
||||
launch, "read_committed_image",
|
||||
return_value="bot-bottle-committed-dev-abc:latest",
|
||||
), patch.object(
|
||||
launch.container_mod, "image_exists", return_value=False,
|
||||
), patch.object(
|
||||
launch.container_mod, "build_image", side_effect=fake_build,
|
||||
):
|
||||
updated = launch._build_images(plan)
|
||||
|
||||
self.assertEqual("bot-bottle-agent:latest", updated.image)
|
||||
self.assertEqual(2, len(calls))
|
||||
self.assertEqual("bot-bottle-agent:latest", calls[1][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user