feat(supervise): host TUI drives approvals over HTTP, not the DB (Step 2b/2c)
The `bot-bottle supervise` operator TUI read and wrote the queue DB directly and tried a backend-specific live "apply" (which was unwired — it raised). It now talks only to the orchestrator control plane: - OrchestratorClient gains supervise_pending() + supervise_respond(). - discover_orchestrator_url() finds the one running per-host control plane by health-probing the backends' well-known :8099 addresses (docker publishes on loopback; the firecracker infra VM serves it on the orchestrator TAP) — no backend branching in the TUI. - discover_pending/approve/reject call the client; the server does the apply + response + audit atomically. The dead direct-DB apply/audit helpers and the docker/macos applicator imports are gone. - A missing control plane is now a clean one-line error up front, not a mid-curses crash. CLI tests move to mocking the client (the DB-write behaviour they used to assert is now server-side, covered by test_orchestrator_service). Docker's orchestrator-container DB wiring lands next so its /supervise endpoints hit the same shared DB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WBMWTEtQdJ4W5UrWuLHCck
This commit is contained in:
@@ -135,10 +135,71 @@ class OrchestratorClient:
|
||||
bottles = payload.get("bottles")
|
||||
return bottles if isinstance(bottles, list) else []
|
||||
|
||||
# --- supervise queue (operator TUI) ------------------------------------
|
||||
|
||||
def supervise_pending(self) -> list[dict[str, object]]:
|
||||
"""Pending supervise proposals across all bottles
|
||||
(`GET /supervise/proposals`)."""
|
||||
payload = self._ok("GET", "/supervise/proposals")
|
||||
proposals = payload.get("proposals")
|
||||
return proposals if isinstance(proposals, list) else []
|
||||
|
||||
def supervise_respond(
|
||||
self,
|
||||
proposal_id: str,
|
||||
*,
|
||||
bottle_slug: str,
|
||||
decision: str,
|
||||
notes: str = "",
|
||||
final_file: str | None = None,
|
||||
) -> None:
|
||||
"""Record an operator decision (`POST /supervise/respond`). `decision`
|
||||
is approve/modify/reject. Raises `OrchestratorClientError` if the
|
||||
proposal is gone or the bottle can no longer be applied to (409)."""
|
||||
body: dict[str, object] = {
|
||||
"proposal_id": proposal_id,
|
||||
"bottle_slug": bottle_slug,
|
||||
"decision": decision,
|
||||
"notes": notes,
|
||||
}
|
||||
if final_file is not None:
|
||||
body["final_file"] = final_file
|
||||
self._ok("POST", "/supervise/respond", body)
|
||||
|
||||
|
||||
def discover_orchestrator_url(*, timeout: float = 2.0) -> str:
|
||||
"""The URL of the one running per-host orchestrator control plane, probing
|
||||
the backends' well-known control-plane addresses (both on port 8099):
|
||||
docker publishes it on loopback; the firecracker infra VM serves it on the
|
||||
orchestrator TAP. Returns the first that answers `/health`; raises if none
|
||||
do (no orchestrator up — launch a bottle first)."""
|
||||
candidates: list[str] = []
|
||||
try: # docker: loopback-published control plane
|
||||
from .lifecycle import DEFAULT_PORT as _DOCKER_PORT
|
||||
candidates.append(f"http://127.0.0.1:{_DOCKER_PORT}")
|
||||
except Exception: # noqa: BLE001 — backend optional
|
||||
candidates.append("http://127.0.0.1:8099")
|
||||
try: # firecracker: infra VM control plane on the orchestrator TAP
|
||||
from ..backend.firecracker import netpool
|
||||
from ..backend.firecracker.infra_vm import CONTROL_PLANE_PORT
|
||||
candidates.append(
|
||||
f"http://{netpool.orch_slot().guest_ip}:{CONTROL_PLANE_PORT}")
|
||||
except Exception: # noqa: BLE001 — backend optional / not firecracker
|
||||
pass
|
||||
for url in candidates:
|
||||
if OrchestratorClient(url, timeout=timeout).health():
|
||||
return url
|
||||
raise OrchestratorClientError(
|
||||
"no running orchestrator control plane found (tried "
|
||||
+ ", ".join(candidates)
|
||||
+ "); launch a bottle first"
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"OrchestratorClient",
|
||||
"OrchestratorClientError",
|
||||
"RegisteredBottle",
|
||||
"DEFAULT_TIMEOUT_SECONDS",
|
||||
"discover_orchestrator_url",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user