# PRD 0059: macOS Container backend

- **Status:** Active
- **Author:** Codex
- **Created:** 2026-06-10
- **Issue:** #220

## Summary

Add a `macos-container` backend that integrates Apple's `container`
CLI as a host runtime on macOS. The shipped slices register the
backend, implement reusable host primitives (`build`, `exec`, `cp`,
image inspection, cleanup, active enumeration), make launch runnable
with the proven two-network sidecar topology, and add real-runtime
coverage without weakening bot-bottle's sidecar egress model.

## Problem

bot-bottle currently has two local execution paths:

- `docker`, which runs the whole bottle topology through Docker
  Compose.
- `smolmachines`, which runs the agent in smolvm but still depends on
  Docker for the sidecar bundle and image-building pipeline.

Issue #220 explored removing Docker as a host dependency. A follow-up
review comment verified that smolvm can publish guest ports back to
host loopback and that another smolvm guest can reach that service
through the existing per-bottle loopback alias plus `--allow-cidr`
path. That keeps the VM-contained sidecar direction viable and rejects
the host-process sidecar fallback.

Apple's `container` CLI is another macOS-native way to run OCI images
as lightweight Linux VMs. Its current command surface includes
Docker-like `build`, `run`, `exec`, `cp`, port publishing, image
inspection, and user-defined networks. That makes it a plausible local
backend, but it does not remove the need to preserve bot-bottle's
sidecar enforcement property: the agent must not have a direct egress
path around the egress sidecar.

## Goals / Success Criteria

- `--backend=macos-container` and
  `BOT_BOTTLE_BACKEND=macos-container` are accepted by the existing
  backend selector.
- Compatible macOS hosts default to `macos-container` when
  `BOT_BOTTLE_BACKEND` and `--backend` are both unset.
- Backend availability is true only on macOS hosts with `container` on
  `PATH`.
- The backend has tested wrappers for Apple Container image build,
  image inspection, container `exec`, container `cp`, cleanup, and
  active-agent enumeration.
- Full launch uses a host-only internal network for the agent and a
  separate NAT egress network for the sidecar bundle.
- The agent container does not attach to the egress network. It reaches
  allowed outbound hosts through HTTP(S)_PROXY pointing at the
  sidecar's internal-network IP.
- `bottle.git` / git-gate bottles fail loudly on this backend until a
  safe Apple Container key-delivery path exists.
- Real-runtime integration coverage is present and guarded by macOS and
  Apple Container availability.

## Non-goals

- Do not remove or deprecate the Docker backend.
- Do not remove or deprecate the smolmachines backend.
- Do not run sidecar daemons as host processes.
- Do not launch a degraded backend where the agent can bypass the
  egress sidecar through direct network access.
- Do not require Docker Desktop as part of the macOS Container backend.

## Design

### Backend name

The selectable backend name is `macos-container`. The Python package
uses `bot_bottle.backend.macos_container` because module names cannot
contain hyphens.

### Availability and preflight

`MacosContainerBottleBackend.is_available()` returns true only when:

- `platform.system() == "Darwin"`
- `container` is discoverable on `PATH`

`prepare()` calls `require_container()`, which produces a concrete
install pointer and rejects non-macOS hosts.

### Implemented primitives

The backend owns an Apple Container wrapper module instead of reusing
Docker wrappers. The wrapper maps bot-bottle's backend needs to
Apple's CLI:

| bot-bottle need | Apple Container command |
|---|---|
| Build provider image | `container build -t <ref> [-f Dockerfile] <context>` |
| Run agent commands | `container exec [--interactive --tty] <id> ...` |
| Copy files into guest | `container cp <host> <id>:<path>` |
| Inspect image identity | `container image inspect <ref>` |
| Cleanup stale containers | `container delete --force <id>` |
| Cleanup stale networks | `container network delete <name>` |
| Active enumeration | `container list --quiet` |

The bottle handle mirrors `DockerBottle`: it builds a host argv for
foreground agent execution, pipes shell snippets through stdin for
`Bottle.exec`, and exposes `cp_in` for provisioning.

### Launch topology

`launch()` uses Apple Container's two-network topology:

- create a host-only internal network for the bottle;
- create a normal NAT egress network for the sidecar bundle;
- start the sidecar bundle attached to the egress network first and the
  internal network second;
- discover the sidecar's internal-network IPv4 address from
  `container inspect`;
- start the agent attached only to the internal network, with
  HTTP_PROXY / HTTPS_PROXY / lowercase proxy vars pointing at the
  sidecar IP and egress port.

This keeps the agent off the outbound network while preserving the
proxy-env contract that existing agent tooling already honors. The
integration smoke also removes the proxy env in-guest and confirms
direct egress fails.

### Deferred git-gate support

Apple Container currently rejects single-file bind mounts, and
`container cp` into a stopped container is not available. Starting the
container earlier would allow `container cp` into a running container,
but it would also mean delivering SSH private key material into a live
sidecar before the git-gate daemon is ready to own it. Mounting broad
host SSH directories is not acceptable.

For this PRD, `bottle.git` / git-gate support is explicitly deferred on
the `macos-container` backend. Bottles with git-gate upstreams fail
loudly and should use `docker` or `smolmachines` until a narrower key
delivery design lands.

## Implementation chunks

1. Register `macos-container`, add availability/preflight, bottle
   handle, utility wrappers, cleanup, active enumeration, unit tests,
   and this PRD.
2. Spike Apple Container networking against real macOS 26 hosts:
   repeated `--network`, internal network egress behavior, published
   loopback reachability from another container, DNS behavior, and
   labels/JSON output stability.
3. Implement launch once the enforcement shape is proven. Reuse the
   existing sidecar bundle image and daemon subset env contract where
   possible.
4. Add real-runtime integration tests guarded by `container` presence
   and macOS version.
5. Consider moving smolmachines sidecar/image-building work to
   VM-contained or Apple Container-backed execution only after the
   `macos-container` launch path is trustworthy.

## Testing Strategy

- Unit tests cover backend registration through `known_backend_names`.
- Unit tests cover availability/preflight behavior without requiring
  macOS.
- Unit tests cover `MacosContainerBottle` command construction and
  stdin-based shell execution.
- Unit tests cover cleanup and active enumeration parsing.
- Unit tests cover launch argv/env construction, sidecar mount
  staging, sidecar IP parsing, and git-gate rejection.
- Integration tests run on macOS hosts with Apple Container installed
  and verify that egress cannot bypass the sidecar. They also preflight
  Apple Container BuildKit DNS because image builds must resolve
  package mirrors before a launch smoke can be meaningful. The backend
  probes the running builder before image builds and leaves it alone
  when its current resolver works. If the probe fails, or if the
  operator explicitly sets `BOT_BOTTLE_MACOS_CONTAINER_DNS`, the backend
  restarts the Apple Container builder with the configured DNS server.
  Without an explicit override, that server is discovered from the
  host's directly reachable IPv4 resolver before falling back to a
  public resolver.

## References

- [Issue #220 review comment](https://gitea.dideric.is/didericis/bot-bottle/issues/220#issuecomment-1980):
  smolvm `--port/-p` can expose a guest service to host loopback, and
  another smolvm guest can reach it through the existing per-bottle
  loopback alias path.
- Apple Container command reference: `container run`, `build`, `exec`,
  port publishing, and network commands.
