feat(macos): run containers inside a bottle via guest-local podman
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 51s
test / integration-docker (pull_request) Successful in 40s
test / unit (pull_request) Successful in 46s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 43s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Successful in 12s
lint / lint (push) Successful in 51s
test / integration-docker (pull_request) Successful in 40s
test / unit (pull_request) Successful in 46s
test / integration-firecracker (pull_request) Successful in 3m35s
test / coverage (pull_request) Successful in 43s
test / publish-infra (pull_request) Has been skipped
Adds the `nested_containers` bottle flag. On the macOS backend it starts a rootless podman service inside the bottle and exposes its Docker-compatible API socket, so the agent still runs `docker` and `docker compose`. No host daemon socket is mounted and the guest gains no capabilities; backends that cannot run a guest-local engine reject the flag in the shared prepare template rather than ignoring it. Podman rather than rootless Docker because Apple Container's capability bounding set omits CAP_SYS_ADMIN, which the kernel requires to write a multi-range uid_map via newuidmap. With no subordinate UID range podman falls back to a single-UID self-mapping an unprivileged process may write itself, so the image build strips /etc/subuid and /etc/subgid entries rather than adding them. That mapping is also why nested containers are not an isolation layer: root inside one is the agent user outside it. They are a build/test convenience; the bottle remains the boundary. Ports the spike branch onto main, renaming docker_access — it implied Docker and granted access to nothing on the host — and drops podman from the derived layer now that every built-in image ships it (#451). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,7 +21,7 @@ from bot_bottle.backend.firecracker import FirecrackerBottleBackend
|
||||
from bot_bottle.manifest import ManifestIndex
|
||||
|
||||
|
||||
def _manifest() -> ManifestIndex:
|
||||
def _manifest(*, nested_containers: bool = False) -> ManifestIndex:
|
||||
return ManifestIndex.from_json_obj({
|
||||
"bottles": {
|
||||
"dev": {
|
||||
@@ -29,6 +29,7 @@ def _manifest() -> ManifestIndex:
|
||||
"LITERAL_ENV": "literal-value",
|
||||
"FORWARDED_ENV": "${HOST_SECRET_ENV}",
|
||||
},
|
||||
"nested_containers": nested_containers,
|
||||
},
|
||||
},
|
||||
"agents": {
|
||||
@@ -41,9 +42,11 @@ def _manifest() -> ManifestIndex:
|
||||
})
|
||||
|
||||
|
||||
def _spec(tmp: Path, *, identity: str) -> BottleSpec:
|
||||
def _spec(
|
||||
tmp: Path, *, identity: str, nested_containers: bool = False,
|
||||
) -> BottleSpec:
|
||||
return BottleSpec(
|
||||
manifest=_manifest(),
|
||||
manifest=_manifest(nested_containers=nested_containers),
|
||||
agent_name="demo",
|
||||
copy_cwd=False,
|
||||
user_cwd=str(tmp),
|
||||
@@ -113,6 +116,42 @@ class TestFirecrackerPrepare(_FakeStateMixin, unittest.TestCase):
|
||||
self.assertEqual({"FORWARDED_ENV": "secret-value"}, plan.forwarded_env)
|
||||
|
||||
|
||||
class TestNestedContainersRejection(_FakeStateMixin, unittest.TestCase):
|
||||
"""A backend with no guest-local engine must refuse the flag outright.
|
||||
|
||||
Ignoring it would leave the agent without `docker`, and the only ways to
|
||||
fake it here (a host daemon socket, a privileged container) are what
|
||||
issue #392 rules out.
|
||||
"""
|
||||
|
||||
def test_docker_backend_refuses_the_flag(self) -> None:
|
||||
backend = DockerBottleBackend()
|
||||
spec = _spec(
|
||||
Path(self.tmp.name), identity="demo-docker", nested_containers=True,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"bot_bottle.backend.resolve_common.die", side_effect=RuntimeError,
|
||||
) as die,
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
backend.prepare(spec, Path(self.tmp.name) / "stage")
|
||||
self.assertIn("nested_containers", die.call_args.args[0])
|
||||
self.assertIn("docker", die.call_args.args[0])
|
||||
|
||||
def test_firecracker_backend_refuses_the_flag(self) -> None:
|
||||
backend = FirecrackerBottleBackend()
|
||||
spec = _spec(Path(self.tmp.name), identity="demo-fc", nested_containers=True)
|
||||
with (
|
||||
patch(
|
||||
"bot_bottle.backend.resolve_common.die", side_effect=RuntimeError,
|
||||
) as die,
|
||||
self.assertRaises(RuntimeError),
|
||||
):
|
||||
backend.prepare(spec, Path(self.tmp.name) / "stage")
|
||||
self.assertIn("firecracker", die.call_args.args[0])
|
||||
|
||||
|
||||
class TestMintSlug(unittest.TestCase):
|
||||
def _spec(self, *, label: str = "", identity: str = "") -> BottleSpec:
|
||||
manifest = _manifest()
|
||||
|
||||
Reference in New Issue
Block a user