190 lines
7.9 KiB
Markdown
190 lines
7.9 KiB
Markdown
# PRD prd-new: macOS Container backend
|
|
|
|
- **Status:** Draft
|
|
- **Author:** Codex
|
|
- **Created:** 2026-06-10
|
|
- **Issue:** #220
|
|
|
|
## Summary
|
|
|
|
Add an experimental `macos-container` backend that integrates Apple's
|
|
`container` CLI as a host runtime on macOS. The first shipped slice
|
|
registers the backend and implements reusable host primitives
|
|
(`build`, `exec`, `cp`, image inspection, cleanup, active
|
|
enumeration). Follow-up slices 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.
|
|
- 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 change the default backend from `smolmachines`.
|
|
- 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.
|