Captures the surface area of the current Docker integration, how it maps to Apple's `container` framework, the dominant networking risk (pipelock multi-network attach), and the cost difference between a faithful port and a simplified VM-firewall variant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.5 KiB
Supporting Apple's container as an alternative backend
Research into the cost and shape of adding Apple's container framework
(per-container Linux microVMs on Apple Silicon, announced WWDC 2025) as
an alternative backend alongside Docker. Motivated by the observation
that Apple's tool gives VM-grade isolation "for free" on macOS — no
Firecracker or Kata orchestration to deploy — and that the project's
threat model already cares about the kernel boundary.
Summary
Realistic effort: roughly two weeks of focused work for one person. The
mechanical 80% (build / run / exec / cp) is a long-but-straightforward
weekend. The remaining 20% is networking: the pipelock egress sidecar
relies on Linux bridge-network multi-attach semantics that Apple's tool
does not model the same way, and either has to be redesigned or
simplified for the container path.
The honest framing: a clean port of the easy parts plus a different
networking story for the container backend (no sidecar, just VM-level
firewall rules), end-to-end in 4–5 days. A faithful port that preserves
pipelock semantics across both backends is closer to two weeks. Pick
which version you want before starting.
Current Docker surface area
The places claude-bottle shells out to docker today:
build— base image plus a per-cwd derived image (claude_bottle/docker.py:67-103).run— with--runtime,--env-file,-e,--name,--network, and volume mounts (claude_bottle/cli/start.py:217-261).exec -it/exec -u 0— forclaudeitself, file-ownership fixups, and SSH provisioning (claude_bottle/ssh.py,claude_bottle/skills.py,claude_bottle/cli/start.py).cp— skills, SSH keys, the prompt file, the workspace.git, and the pipelock config (claude_bottle/skills.py:73,claude_bottle/ssh.py:106,claude_bottle/cli/start.py:279,claude_bottle/pipelock.py:218).network create/connect/inspect/rm— bottle network plus multi-network attach for the pipelock sidecar (claude_bottle/network.py,claude_bottle/pipelock.py:227).create/start/rm -f— pipelock sidecar lifecycle (claude_bottle/pipelock.py:207-258).- Misc preflight:
image inspect,ps -a -f name=^...$,infofor registered runtimes (claude_bottle/docker.py).
Mapping to Apple's container
| Capability | container story |
|---|---|
| build / run / exec / images | Direct equivalents, OCI-compatible |
cp |
container cp exists, but recursion semantics (trailing ./) need verifying against the Docker behavior the codebase relies on |
--env-file |
Needs verification; may have to translate to repeated -e flags |
--runtime=runsc |
Becomes a no-op. Every container is already in its own VM, so gVisor is redundant. This is a win — require_runsc collapses or the manifest unifies the concept (see "Manifest" below). |
| User-defined networks | Limited — fewer knobs than Docker bridge networks |
Multi-attach: network connect to a running container |
The hard one. The pipelock sidecar pattern attaches to two networks. Apple's tool does not model that the same way. |
Effort breakdown
Roughly two weeks for one person, split as:
-
Backend abstraction (1–2 days).
claude_bottle/docker.pyis already a partial seam, butclaude_bottle/network.py,claude_bottle/pipelock.py,claude_bottle/ssh.py,claude_bottle/skills.py, andclaude_bottle/cli/start.pyall callsubprocess.run(["docker", ...])directly. Define aBackendprotocol —run,exec,cp,build,network_create,network_connect,inspect,rm— route every call through it, keep Docker as the default impl. Mostly mechanical. -
containerbackend impl (2–3 days). The easy 80%: run, exec, build, image inspect, cp. Plus arequire_container()analogous torequire_docker(). Verifycontainer cprecursion and--env-filesupport against actual binary behavior, not docs. -
Networking and pipelock (3–5 days, dominant risk). The egress sidecar design assumes Linux bridge-network semantics with multi- network attach. On Apple's tool the likely redesign is one of:
- Run pipelock as a host-side process and have the bottle dial it directly via the host loopback. Simpler, but loses the "egress proxy is itself isolated" property.
- Keep pipelock in its own VM and wire the bottle's egress through it via a different mechanism (port forwarding, shared network if the tool grows that capability). Closer to current semantics, more work.
Either way this is real design work, not a port. Worth a separate PRD before code lands.
-
Manifest spec (½ day). Collapse
runtime: "runsc"and "usecontainerbackend" into a singlesandbox: "shared-kernel" | "vm"field. Backend selection follows from the value. Documenting why therunscknob disappears on thecontainerpath matters more than the code change. -
Tests and docs (2–3 days). The existing test suite mocks
docker; needs equivalents forcontainer. Document which features are macOS-only and what thecontainerbackend trades away (currently: pipelock semantics, possibly some network introspection).
The recommended split
Two distinct paths, each with a clear cost/benefit:
-
Faithful port (~2 weeks). Both backends offer the same egress guarantees. Worth it if pipelock is load-bearing for the threat model and the project intends to support
containeras a first-class peer to Docker indefinitely. -
Simplified port (~4–5 days). The
containerbackend uses VM-level firewall rules instead of pipelock; documentation calls out the difference. Worth it if the VM kernel boundary is judged to make pipelock less critical on thecontainerpath anyway, and the goal is to getcontainerworking as an experimental backend without blocking on a redesign.
The simplified path is probably the right starting point. The kernel
boundary that container provides was the original motivation for
exploring this in the first place; pipelock's value-add on top of a
real VM is smaller than it is on top of shared-kernel Docker.
Recommendation
Don't start the implementation before deciding which split is intended,
and don't start any of it before the Backend abstraction lands. The
abstraction makes the language choice reversible (per the
bash-vs-python-vs-go note) and makes adding a second backend
mechanical. Skipping it means rewriting the same call sites twice.