fix(macos): review fixes — token on plan, self-heal, symmetric digest, DHCP poll
lint / lint (push) Successful in 2m18s
test / unit (pull_request) Successful in 1m6s
test / integration (pull_request) Successful in 23s
test / coverage (pull_request) Successful in 1m18s

Addresses findings from a high-effort review of the PRD 0070 macOS backend.

Correctness:
- Stamp identity_token onto MacosContainerBottlePlan after registration. git's
  gitconfig extraHeader and the supervise MCP --header read
  getattr(plan,"identity_token","") at provision time, and both reach the
  gateway on NO_PROXY (bypassing the egress proxy that carries the token). The
  plan never carried it, so /resolve fail-closed and every git fetch/push and
  supervise call from a macOS bottle would have been denied. Registration
  precedes provision(), so — unlike the run-time env — the plan can carry it.
- Self-heal the orchestrator: recreate when it is not (source-current AND
  answering /health), not on the source-hash label alone. A container running
  current code but with a wedged HTTP server was left alone and polled to
  death, failing every launch until manual deletion.
- image_digest and container_image_digest now read the same descriptor.digest
  field; dropped image_digest's id/tag fallback that could yield a value the
  container side can't produce — a permanent mismatch would have recreated the
  shared gateway on every launch (severing every live bottle's egress, since
  the replacement gets a new DHCP address).
- Poll for the agent's and gateway's DHCP address instead of a fatal read
  right after `container run` (there is no --ip; the address can lag start).

Cleanup:
- One _inspect_first + _descriptor_digest behind the four inspect readers.
- Shared bind_mount_spec (util) and host_db_dir (paths) replace per-module
  copies; _GIT_HTTP_PORT now imports git_http_backend.DEFAULT_PORT.
- Drop the dead _url cache / url property and the write-only agent_proxy_url.

Deferred (noted on the PR, not fixed here): the gateway image rebuilding on
every launch (needs source-hash-labeled build), SQLite shared across VM
guests, and the sh -lc profile-override edge — each is design-level or
behavior-risk beyond a review fix.

Verified: real Apple Container bring-up is green and idempotent; 1826 unit
tests pass with `container` absent (CI parity), pyright clean, pylint 9.86.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 03:26:11 -04:00
parent a5910696a5
commit e24b62b6b9
9 changed files with 242 additions and 112 deletions
@@ -49,6 +49,7 @@ from .gateway import (
AppleGateway,
ensure_networks,
)
from .util import bind_mount_spec as _mount
# Distinct from the docker backend's container name so both can run on one host.
ORCHESTRATOR_NAME = "bot-bottle-mac-orchestrator"
@@ -61,13 +62,6 @@ _HEALTH_POLL_SECONDS = 0.25
_HEALTH_REQUEST_TIMEOUT_SECONDS = 1.0
def _mount(source: str, target: str, *, readonly: bool = False) -> str:
spec = f"type=bind,source={source},target={target}"
if readonly:
spec += ",readonly"
return spec
class MacosOrchestratorService:
"""Manages the orchestrator control-plane container + the shared Apple
gateway. Callers only need `ensure_running()`, which returns the URL."""
@@ -92,30 +86,24 @@ class MacosOrchestratorService:
self._repo_root = repo_root
self._host_root = host_root or bot_bottle_root()
self._orchestrator_name = orchestrator_name
# Resolved once the container is up — there is no name to fall back on.
self._url = ""
@property
def url(self) -> str:
"""The control-plane URL, or "" before the container is up. One URL for
both the host CLI and the gateway (see the module docstring)."""
return self._url
def _resolve_url(self) -> str:
"""The control-plane URL, or "" while the container has no address."""
"""The control-plane URL, or "" while the container has no address.
Resolved fresh each time (there is no name to fall back on, and a
recreated orchestrator can come back on a different DHCP address), so
nothing caches a URL that could go stale."""
ip = container_mod.try_container_ipv4_on_network(
self._orchestrator_name, self.network,
)
return f"http://{ip}:{self.port}" if ip else ""
def is_healthy(
self, url: str = "", *, timeout: float = _HEALTH_REQUEST_TIMEOUT_SECONDS,
self, url: str, *, timeout: float = _HEALTH_REQUEST_TIMEOUT_SECONDS,
) -> bool:
target = url or self._url
if not target:
if not url:
return False
try:
with urllib.request.urlopen(f"{target}/health", timeout=timeout) as resp:
with urllib.request.urlopen(f"{url}/health", timeout=timeout) as resp:
return resp.status == 200
except (urllib.error.URLError, TimeoutError, OSError):
return False
@@ -192,16 +180,15 @@ class MacosOrchestratorService:
control-plane URL. Idempotent — a healthy control plane running current
code and a running gateway are left untouched."""
current_hash = source_hash(self._repo_root)
if not self._orchestrator_source_current(current_hash):
url = self._running_healthy_url(current_hash)
if not url:
self._ensure_orchestrator_image()
log.info(
"starting orchestrator container",
context={"name": self._orchestrator_name},
)
self._run_orchestrator_container(current_hash)
url = self._wait_healthy(startup_timeout)
self._url = url
url = self._wait_healthy(startup_timeout)
# Gateway second: it can only reach the control plane by IP, which does
# not exist until the orchestrator container is up (see the docstring).
@@ -210,6 +197,21 @@ class MacosOrchestratorService:
gateway.ensure_running()
return url
def _running_healthy_url(self, current_hash: str) -> str:
"""The control-plane URL if the running orchestrator is BOTH current
and answering /health, else "" (→ recreate).
Checking health, not just the source-hash label, is what lets the
service self-heal: a container that is running the current code but
whose HTTP server is wedged (bind failure, deadlock, OOM'd thread)
would otherwise be left alone and polled to death on every launch
forever. Mirrors the docker service's `is_healthy() and source_current`
gate."""
if not self._orchestrator_source_current(current_hash):
return ""
url = self._resolve_url()
return url if url and self.is_healthy(url) else ""
def _wait_healthy(self, startup_timeout: float) -> str:
"""Poll until the control plane answers /health, resolving its address
each time: the container is up before it has an IP, and it has an IP
@@ -231,7 +233,6 @@ class MacosOrchestratorService:
"""Remove the orchestrator + gateway containers (idempotent)."""
container_mod.force_remove_container(self._orchestrator_name)
self.gateway("").stop()
self._url = ""
__all__ = [