85ff967c3e
A registry row only ever left the registry two ways: an explicit teardown_bottle (the launcher's cleanup callback) or the same-IP supersede sweep in register(). Neither runs when the launching CLI dies hard, so the row outlives its container. That orphan is not inert. Source IPs are recycled by the backend's DHCP and by_source_ip fail-closes on ambiguity, so a leftover row at a reused address resolves no policy at all for the next bottle that lands there — and a bottle with no policy denies every host, which surfaces to the agent as "host X is not in the allowlist" for hosts that were never the problem. Add reap_absent/reconcile and call it from the macOS launch path before registering, so each launch self-heals the registry. Restores the invariant the data plane needs: at most one active row per live address, and none for a dead one. The second half matters as much as the first — when several rows claim a *live* address the newest wins and the rest are swept, otherwise a recycled address stays ambiguous, which is exactly the bricked state. The host supplies the live set because the orchestrator runs inside the infra container and cannot see the backend. A grace window exempts rows younger than it, so reconciliation cannot race a bottle still coming up, and a reconcile failure is logged rather than blocking an otherwise-fine launch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
46 lines
1.6 KiB
Python
46 lines
1.6 KiB
Python
"""Active-agent enumeration for the macOS Apple Container backend."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
|
|
from ...bottle_state import read_metadata
|
|
from .. import ActiveAgent
|
|
from .infra import INFRA_NAME
|
|
|
|
# The name every agent container carries: `bot-bottle-<slug>`. Exported
|
|
# because reconciliation has to map an enumerated slug back to a container
|
|
# name to read its address.
|
|
CONTAINER_NAME_PREFIX = "bot-bottle-"
|
|
# The shared per-host infra container carries the same prefix as agent
|
|
# containers but is infrastructure, not a bottle — one control plane + gateway
|
|
# serves every agent, so listing it as an agent would invent one per host.
|
|
_INFRA_NAMES = frozenset({INFRA_NAME})
|
|
|
|
|
|
def enumerate_active() -> list[ActiveAgent]:
|
|
result = subprocess.run(
|
|
["container", "list", "--quiet"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return []
|
|
out: list[ActiveAgent] = []
|
|
for name in sorted(line.strip() for line in result.stdout.splitlines()):
|
|
if not name.startswith(CONTAINER_NAME_PREFIX) or name in _INFRA_NAMES:
|
|
continue
|
|
slug = name[len(CONTAINER_NAME_PREFIX):]
|
|
metadata = read_metadata(slug)
|
|
out.append(ActiveAgent(
|
|
backend_name="macos-container",
|
|
slug=slug,
|
|
agent_name=metadata.agent_name if metadata else "?",
|
|
started_at=metadata.started_at if metadata else "",
|
|
services=(),
|
|
label=metadata.label if metadata else "",
|
|
color=metadata.color if metadata else "",
|
|
))
|
|
return out
|