feat(macos): replace rootless Docker spike with rootless podman
test / stage-firecracker-inputs (pull_request) Successful in 2s
test / integration-docker (pull_request) Successful in 32s
test / unit (pull_request) Successful in 36s
lint / lint (push) Failing after 52s
test / build-infra (pull_request) Successful in 4m5s
tracker-policy-pr / check-pr (pull_request) Successful in 8s
test / integration-firecracker (pull_request) Successful in 2m2s
test / coverage (pull_request) Successful in 2m7s
test / publish-infra (pull_request) Has been skipped

Podman avoids the CAP_SYS_ADMIN requirement that makes rootless Docker
impossible here: with no subordinate UID range configured it falls back
to a single-UID self-mapping, which an unprivileged process may write
itself, so newuidmap is never invoked. The image build therefore strips
/etc/subuid and /etc/subgid entries rather than adding them.

The agent-facing surface is unchanged — docker and docker compose talk
to podman's Docker-compatible API socket.

Two device nodes need relaxing as root inside the bottle (0666 on
/dev/fuse and /dev/net/tun); both already exist and neither needs a
capability the bottle lacks, unlike CAP_SYS_ADMIN.

Verified live on macOS 26 / Apple Container 1.0.0: service starts with
zero added capabilities, compose pulls and serves from the workspace on
a published port, and a nested container cannot reach the network
outside the egress path.

Known limitation, not yet addressed: the agent base image is Debian
bookworm, whose podman 4.3.1 swallows container exit codes through the
compat API (docker run returns 0 regardless). podman 5.4.2 on trixie
fixes it; the base bump is a separate PR. The acceptance test asserts
on an in-band marker rather than an exit code so it cannot false-pass
in the meantime.

Nested containers give no isolation from the agent itself — root inside
one is the agent user outside it. They are a build/test convenience;
the bottle remains the security boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GnMp8SUGH57hZX7192Rv2F
This commit is contained in:
2026-07-21 12:59:10 -04:00
parent 6a22b9f654
commit 55def3732e
10 changed files with 454 additions and 294 deletions
+7 -4
View File
@@ -64,7 +64,7 @@ from .gateway_hosts import (
refresh_gateway_host,
set_gateway_host,
)
from . import rootless_docker
from . import rootless_podman
from .bottle_plan import MacosContainerBottlePlan
from ...orchestrator.config_store import resolve_teardown_timeout
from .consolidated_launch import (
@@ -174,7 +174,7 @@ def launch(
exec_env = {
**_identity_proxy_env(endpoint, ctx.identity_token),
**rootless_docker.guest_env(plan.docker_access),
**rootless_podman.guest_env(plan.docker_access),
}
bottle = MacosContainerBottle(
plan.container_name,
@@ -194,7 +194,10 @@ def launch(
bottle.prompt_path = provision(plan, bottle)
if plan.docker_access:
rootless_docker.start(bottle)
rootless_podman.prepare_guest_devices(
plan.container_name, container_mod.exec_container_as_root,
)
rootless_podman.start(bottle)
yield bottle
finally:
@@ -218,7 +221,7 @@ def _build_images(plan: MacosContainerBottlePlan) -> MacosContainerBottlePlan:
plan.image, _REPO_DIR, dockerfile=plan.dockerfile_path,
)
if plan.docker_access:
image = rootless_docker.build_image(plan.image, container_mod.build_image)
image = rootless_podman.build_image(plan.image, container_mod.build_image)
plan = dataclasses.replace(
plan,
agent_provision=dataclasses.replace(plan.agent_provision, image=image),