Files
bot-bottle/docs/research/apple-container-backend.md
T
didericis e1efc64862
test / run tests/run_tests.py (push) Successful in 14s
docs: add research note on Apple container as an alternative backend
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>
2026-05-10 02:36:11 -04:00

6.5 KiB
Raw Blame History

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 45 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 — for claude itself, 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=^...$, info for 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:

  1. Backend abstraction (12 days). claude_bottle/docker.py is already a partial seam, but claude_bottle/network.py, claude_bottle/pipelock.py, claude_bottle/ssh.py, claude_bottle/skills.py, and claude_bottle/cli/start.py all call subprocess.run(["docker", ...]) directly. Define a Backend protocol — run, exec, cp, build, network_create, network_connect, inspect, rm — route every call through it, keep Docker as the default impl. Mostly mechanical.

  2. container backend impl (23 days). The easy 80%: run, exec, build, image inspect, cp. Plus a require_container() analogous to require_docker(). Verify container cp recursion and --env-file support against actual binary behavior, not docs.

  3. Networking and pipelock (35 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.

  4. Manifest spec (½ day). Collapse runtime: "runsc" and "use container backend" into a single sandbox: "shared-kernel" | "vm" field. Backend selection follows from the value. Documenting why the runsc knob disappears on the container path matters more than the code change.

  5. Tests and docs (23 days). The existing test suite mocks docker; needs equivalents for container. Document which features are macOS-only and what the container backend trades away (currently: pipelock semantics, possibly some network introspection).

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 container as a first-class peer to Docker indefinitely.

  • Simplified port (~45 days). The container backend 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 the container path anyway, and the goal is to get container working 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.