"""Live-Mac acceptance spike for guest-local rootless Docker (issue #392). Run explicitly on an Apple Silicon/macOS 26 host: BOT_BOTTLE_ROOTLESS_DOCKER_SPIKE=1 \ python3 -m unittest tests.integration.test_macos_rootless_docker_spike -v The opt-in is deliberate: ordinary Linux CI cannot execute Apple Container. """ from __future__ import annotations import os import platform import shutil import tempfile import unittest from pathlib import Path from bot_bottle.backend import BottleSpec, get_bottle_backend from bot_bottle.manifest import ManifestIndex @unittest.skipUnless( platform.system() == "Darwin" and os.environ.get("BOT_BOTTLE_ROOTLESS_DOCKER_SPIKE") == "1", "requires an explicit live-Mac rootless-Docker spike run", ) class TestMacosRootlessDockerSpike(unittest.TestCase): def test_compose_stays_inside_registered_bottle(self) -> None: workspace = Path(tempfile.mkdtemp(prefix="rootless-docker-spike.")) stage = Path(tempfile.mkdtemp(prefix="rootless-docker-stage.")) try: (workspace / "index.html").write_text("bottle-compose-ok\n") (workspace / "compose.yaml").write_text( "services:\n" " web:\n" " image: python:3.12-alpine\n" " working_dir: /workspace\n" " command: python -m http.server 8000\n" " volumes: ['.:/workspace']\n" " ports: ['18080:8000']\n", encoding="utf-8", ) manifest = ManifestIndex.from_json_obj({ "bottles": {"dev": { "docker_access": True, "egress": {"routes": [ {"host": "auth.docker.io"}, {"host": "registry-1.docker.io"}, {"host": "production.cloudflare.docker.com"}, ]}, }}, "agents": {"spike": { "bottle": "dev", "skills": [], "prompt": "", }}, }) spec = BottleSpec( manifest=manifest, agent_name="spike", copy_cwd=True, user_cwd=str(workspace), ) backend = get_bottle_backend("macos-container") plan = backend.prepare(spec, stage_dir=stage) with backend.launch(plan) as bottle: workdir = plan.workspace_plan.workdir checks = ( "docker info >/dev/null && docker compose version && " f"cd {workdir} && docker compose up -d --wait && " "curl --fail --silent http://127.0.0.1:18080/ | " "grep -q bottle-compose-ok" ) result = bottle.exec(checks) self.assertEqual( 0, result.returncode, f"stdout={result.stdout!r}\nstderr={result.stderr!r}", ) inspect = bottle.exec( "docker info --format '{{json .SecurityOptions}}'" ) self.assertIn("rootless", inspect.stdout.lower()) self.assertNotEqual( 0, bottle.exec("test -S /var/run/docker.sock").returncode, "spike must never expose a host/rootful Docker socket", ) direct = bottle.exec( "docker run --rm --env HTTP_PROXY= --env HTTPS_PROXY= " "--env http_proxy= --env https_proxy= python:3.12-alpine " "wget -T 4 -qO- https://evil.example.com/" ) self.assertNotEqual( 0, direct.returncode, "an inner container obtained direct, unproxied egress", ) finally: shutil.rmtree(workspace, ignore_errors=True) shutil.rmtree(stage, ignore_errors=True) if __name__ == "__main__": unittest.main()