6.9 KiB
PRD 0077: macOS (Apple Container) CI runner
- Status: Active
- Author: Claude
- Created: 2026-07-25
- Issue: #426
Summary
CI has no runner for the macos-container (Apple Container) backend.
.gitea/workflows/test.yml exercises Docker (ubuntu-latest) and
Firecracker (self-hosted kvm) but never the macOS backend. This PRD adds a
self-hosted macOS runner (label macos) and an advisory integration-macos
job that runs the integration suite against BOT_BOTTLE_BACKEND=macos-container,
so the backend that is the default on macOS stops shipping unexercised.
Problem
The gap is not theoretical. 5ad3449 moved bot_bottle from flat files under
/app into a pip-installed package but left init scripts spawning the
supervisor as python3 /app/gateway_init.py, which no longer exists. Both the
Firecracker and macOS backends carried the identical bug:
- firecracker — caught and fixed in
127ba49because the KVM runner (added inc193b04, PR #349) runs that backend's integration suite. - macos-container — survived on
mainand only surfaced when a human ranbot-bottle startby hand.
The failure mode is expensive to debug: the supervisor never starts, so
mitmdump never generates its CA, and launch dies downstream with
GatewayError: gateway CA not available, which points at TLS rather than at
the supervisor. Unit tests did not help — test_macos_infra asserted the
substring "gateway_init.py", which the broken path satisfies. (That
specific assertion has since been tightened to the module form
bot_bottle.gateway_init, matching its Firecracker twin, so the exact
regression is now covered on ubuntu-latest. What remains missing is the
end-to-end runner that would catch the next macOS-only launch regression.)
PR #470 (#414) already made the integration suite backend-agnostic:
skip_unless_selected_backend_available() gates on the selected backend's
own is_backend_ready() rather than docker_available(), and each
integration job runs ./cli.py backend status --backend=<name> as a preflight
that fails loudly when the backend is missing. That is the machinery this job
plugs into; this PRD supplies the runner and the job.
Goals / Success criteria
- A macOS runner is registered and picks up jobs by the
macoslabel. - An
integration-macosjob runs the integration suite againstBOT_BOTTLE_BACKEND=macos-container. - The job fails, not skips, when the backend is unavailable on the runner
(via the
backend statuspreflight). - Reverting the
macos_container/infra.pysupervisor fix makes the job fail: the broken supervisor path throwsGatewayErrorat bottle launch, which isTestSandboxEscape.setUpClass, failing the whole class before any individual attack runs.
Non-goals
- Making
integration-macosa required PR check. It runs onworkflow_dispatch(manual dispatch) only — never on push or PRs. A single non-redundant laptop that sleeps and roams must never be able to block a PR merge or churn unattended on every push to main, and it is deliberately kept out of thecoveragejob'sneedsso the diff-coverage gate never depends on it. - Multi-machine or hosted macOS runners. Apple Container needs the host virtualization framework, so the runner must be a physical/VM macOS host on Apple Silicon — it cannot reuse the KVM runner or run in a Linux container.
- Coverage aggregation from the macOS job into the combined gate (would couple the gate to the laptop).
Design
Runner (operational, provisioned once)
- Apple Silicon macOS host with Apple's
containerCLI installed andcontainer system statusreportingrunning. - Install the runner:
brew install gitea-runner(theact_runnerrename), registered in host mode with labelmacos— not docker mode, because Apple Container needs the hostcontainerCLI and virtualization framework, not a nested container. - A Python ≥ 3.11 with
coverageimportable on the runner'sPATH. Because a launchd service does not inherit an interactive shell'sPATH, pinnode(for the JSactions/*) and the Python env explicitly in the service environment rather than relying onnvm/shell profile. - Concurrency 1. The infra container is a singleton (
bot-bottle-mac-infra), so two simultaneous runs on one host collide (#425). The job also declares aconcurrencygroup as belt-and-suspenders and tears the singleton down after each run.
integration-macos job
Modeled on integration-firecracker:
runs-on: [self-hosted, macos].if:workflow_dispatchonly (advisory, manual dispatch; never push or PRs, so no fork-PR exposure, no merge-blocking, and no unattended runs on push).concurrency: { group: integration-macos-infra, cancel-in-progress: false }to serialize runs against the singleton.- Preflight —
command -v container,container system status, then./cli.py backend status --backend=macos-container; any failure exits non-zero so a misprovisioned runner fails loudly instead of silently skipping. - Run the integration suite under coverage with
BOT_BOTTLE_BACKEND=macos-containerand print acoverage report -mfor visibility (no upload, not in the gate). - Teardown (
if: always()) —MacosInfraService().stop()removes the singleton so a crashed run cannot wedge the next one.
The test_sandbox_escape CI guard (the trap #470 left)
TestSandboxEscape is the only backend-agnostic integration test that boots a
real bottle, so it is the one that would catch a macOS launch regression. It
still carries a second guard that skips under GITEA_ACTIONS for every backend
except firecracker:
@unittest.skipIf(
os.environ.get("GITEA_ACTIONS") == "true"
and os.environ.get("BOT_BOTTLE_BACKEND") != "firecracker",
...,
)
The skip exists because the containerized act_runner (docker on
ubuntu-latest) can't see a host bind mount and hides sibling-gateway network
topology. Those constraints do not apply to a host-mode runner — neither
the KVM host runner nor a macOS host runner is containerized. This PRD relaxes
the guard to allow both host-mode backends (firecracker, macos-container)
through while still skipping on the containerized Docker job. Without this
change the macOS job would run green while skipping the exact test that proves
the backend launches — the very false-green this issue is about.
Open questions
- None known that block the job. git-gate is fully implemented on the macOS
backend (the gateway's consolidated
git-httpdaemon plus dynamic key provisioning/revocation), soTestSandboxEscapeattack 5 — secret exfil pushed through git-gate, rejected by the gitleaks hook before the upstream push — runs the same as on the other backends. Any genuinely macOS-specific test adjustment would surface at first runner bring-up, but none is anticipated from the current backend implementation.