refactor(orchestrator): rename in-guest core Orchestrator -> OrchestratorCore

Free the `Orchestrator` name for the incoming host-side control-plane service
ABC (parallel to `Gateway`). The in-guest control-plane core (registry + broker,
in orchestrator/service.py) becomes `OrchestratorCore`; server.py, __main__.py,
the lazy facade, and the two service/server tests updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:29:32 -04:00
parent 07c975636d
commit 57d32d2c5c
9 changed files with 29 additions and 29 deletions
+4 -4
View File
@@ -14,7 +14,7 @@ backend-neutral "consolidation core" that needs no VM packaging:
lifecycle contract (idempotent singleton). One lifecycle contract (idempotent singleton). One
gateway shared by all bottles instead of one per gateway shared by all bottles instead of one per
bottle; backend impls live under `backend/*/gateway`. bottle; backend impls live under `backend/*/gateway`.
* `service` — the `Orchestrator`: owns the registry, brokers the * `service` — the `OrchestratorCore`: owns the registry, brokers the
launch lifecycle (launch/teardown), manages the launch lifecycle (launch/teardown), manages the
shared gateway, attributes. shared gateway, attributes.
* `server` — the HTTP control-plane RPC (launch / teardown / * `server` — the HTTP control-plane RPC (launch / teardown /
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
) )
from .docker_broker import DockerBroker, DockerBrokerError from .docker_broker import DockerBroker, DockerBrokerError
from ..gateway import Gateway, GatewayError from ..gateway import Gateway, GatewayError
from .service import Orchestrator from .service import OrchestratorCore
from .server import OrchestratorServer, dispatch, make_server from .server import OrchestratorServer, dispatch, make_server
@@ -64,7 +64,7 @@ _LAZY: dict[str, str] = {
"DockerBrokerError": ".docker_broker", "DockerBrokerError": ".docker_broker",
"Gateway": "..gateway", "Gateway": "..gateway",
"GatewayError": "..gateway", "GatewayError": "..gateway",
"Orchestrator": ".service", "OrchestratorCore": ".service",
"OrchestratorServer": ".server", "OrchestratorServer": ".server",
"dispatch": ".server", "dispatch": ".server",
"make_server": ".server", "make_server": ".server",
@@ -96,7 +96,7 @@ __all__ = [
"DockerBrokerError", "DockerBrokerError",
"Gateway", "Gateway",
"GatewayError", "GatewayError",
"Orchestrator", "OrchestratorCore",
"OrchestratorServer", "OrchestratorServer",
"dispatch", "dispatch",
"make_server", "make_server",
+2 -2
View File
@@ -21,7 +21,7 @@ from .broker import LaunchBroker, StubBroker
from .server import make_server from .server import make_server
from .docker_broker import DockerBroker from .docker_broker import DockerBroker
from .store.registry_store import RegistryStore, default_db_path from .store.registry_store import RegistryStore, default_db_path
from .service import Orchestrator from .service import OrchestratorCore
def main(argv: list[str] | None = None) -> int: def main(argv: list[str] | None = None) -> int:
@@ -52,7 +52,7 @@ def main(argv: list[str] | None = None) -> int:
# anything; 'docker' runs real containers (firecracker drops in later). # anything; 'docker' runs real containers (firecracker drops in later).
secret = secrets.token_bytes(32) secret = secrets.token_bytes(32)
broker: LaunchBroker = DockerBroker(secret) if args.broker == "docker" else StubBroker(secret) broker: LaunchBroker = DockerBroker(secret) if args.broker == "docker" else StubBroker(secret)
orchestrator = Orchestrator(registry, broker, secret) orchestrator = OrchestratorCore(registry, broker, secret)
server = make_server(orchestrator, host=args.host, port=args.port) server = make_server(orchestrator, host=args.host, port=args.port)
bound_host, bound_port = server.server_address[0], server.server_address[1] bound_host, bound_port = server.server_address[0], server.server_address[1]
+2 -2
View File
@@ -2,7 +2,7 @@
Bridges the existing per-bottle `prepare` output to the consolidated Bridges the existing per-bottle `prepare` output to the consolidated
registry: turns a prepared bottle's egress plan into the backend-neutral registry: turns a prepared bottle's egress plan into the backend-neutral
inputs `Orchestrator.launch_bottle` takes — the egress **policy** blob and inputs `OrchestratorCore.launch_bottle` takes — the egress **policy** blob and
launch **metadata**. launch **metadata**.
The policy blob is the exact routes YAML the per-bottle egress daemon used The policy blob is the exact routes YAML the per-bottle egress daemon used
@@ -26,7 +26,7 @@ from ..egress import EgressPlan, egress_render_routes
@dataclass(frozen=True) @dataclass(frozen=True)
class RegistrationInputs: class RegistrationInputs:
"""What `Orchestrator.launch_bottle` needs to register a bottle, derived """What `OrchestratorCore.launch_bottle` needs to register a bottle, derived
from its prepared plan. `policy` is served verbatim by the gateway's from its prepared plan. `policy` is served verbatim by the gateway's
`/resolve`; `metadata` is opaque forward-compat state — it carries the `/resolve`; `metadata` is opaque forward-compat state — it carries the
human slug so the console / supervise can show a name, not just the human slug so the console / supervise can show a name, not just the
+5 -5
View File
@@ -45,7 +45,7 @@ Both attribute the caller by `(source_ip, identity_token)` exactly like
`POST /bottles` / `DELETE` drive the full launch lifecycle: they mint (or `POST /bottles` / `DELETE` drive the full launch lifecycle: they mint (or
tear down) the bottle in the registry AND broker the backend-native launch tear down) the bottle in the registry AND broker the backend-native launch
via the orchestrator. Register/deregister without a launch are internal to via the orchestrator. Register/deregister without a launch are internal to
`Orchestrator`, not exposed here. `OrchestratorCore`, not exposed here.
Routing/handling is the pure function `dispatch()` so it is unit-testable Routing/handling is the pure function `dispatch()` so it is unit-testable
without a socket; `Handler` / `OrchestratorServer` / `make_server` are a without a socket; `Handler` / `OrchestratorServer` / `make_server` are a
@@ -66,7 +66,7 @@ from urllib.parse import urlsplit
from ..orchestrator_auth import ROLE_CLI, ROLES, verify from ..orchestrator_auth import ROLE_CLI, ROLES, verify
from ..paths import ORCHESTRATOR_TOKEN_ENV from ..paths import ORCHESTRATOR_TOKEN_ENV
from ..supervisor.types import TOOLS from ..supervisor.types import TOOLS
from .service import Orchestrator from .service import OrchestratorCore
# JSON body payload type (parsed request / rendered response). # JSON body payload type (parsed request / rendered response).
Json = dict[str, object] Json = dict[str, object]
@@ -109,7 +109,7 @@ def _parse_json_object(body: bytes) -> Json:
def dispatch( # pylint: disable=too-many-return-statements,too-many-branches def dispatch( # pylint: disable=too-many-return-statements,too-many-branches
orch: Orchestrator, method: str, path: str, body: bytes, *, role: str | None = ROLE_CLI, orch: OrchestratorCore, method: str, path: str, body: bytes, *, role: str | None = ROLE_CLI,
) -> tuple[int, Json]: ) -> tuple[int, Json]:
"""Route one control-plane request to a (status, payload) pair. Pure — """Route one control-plane request to a (status, payload) pair. Pure —
no I/O beyond the orchestrator — so it is fully testable without a socket. no I/O beyond the orchestrator — so it is fully testable without a socket.
@@ -411,7 +411,7 @@ class OrchestratorServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
daemon_threads = True daemon_threads = True
allow_reuse_address = True allow_reuse_address = True
def __init__(self, address: tuple[str, int], orchestrator: Orchestrator) -> None: def __init__(self, address: tuple[str, int], orchestrator: OrchestratorCore) -> None:
self.orchestrator = orchestrator self.orchestrator = orchestrator
self._signing_key = os.environ.get(ORCHESTRATOR_TOKEN_ENV, "").strip() self._signing_key = os.environ.get(ORCHESTRATOR_TOKEN_ENV, "").strip()
if not self._signing_key: if not self._signing_key:
@@ -437,7 +437,7 @@ class OrchestratorServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
def make_server( def make_server(
orchestrator: Orchestrator, host: str = "127.0.0.1", port: int = 0 orchestrator: OrchestratorCore, host: str = "127.0.0.1", port: int = 0
) -> OrchestratorServer: ) -> OrchestratorServer:
"""Build (but do not start) a control-plane server. `port=0` binds an """Build (but do not start) a control-plane server. `port=0` binds an
ephemeral port — read `server.server_address` for the actual one.""" ephemeral port — read `server.server_address` for the actual one."""
+3 -3
View File
@@ -1,6 +1,6 @@
"""The per-host orchestrator core (PRD 0070). """The per-host orchestrator core (PRD 0070).
`Orchestrator` is the single backend-neutral object the control plane talks `OrchestratorCore` is the single backend-neutral object the control plane talks
to: it owns the registry (runtime state) and brokers agent launches. It to: it owns the registry (runtime state) and brokers agent launches. It
never branches on backend — the `LaunchBroker` abstracts the backend-native never branches on backend — the `LaunchBroker` abstracts the backend-native
launch, so this same object drives docker / firecracker / apple once a real launch, so this same object drives docker / firecracker / apple once a real
@@ -54,7 +54,7 @@ _RESPOND_STATUS = {
_APPLY_TOOLS = (TOOL_EGRESS_ALLOW, TOOL_EGRESS_BLOCK) _APPLY_TOOLS = (TOOL_EGRESS_ALLOW, TOOL_EGRESS_BLOCK)
class Orchestrator: class OrchestratorCore:
"""Owns the registry + brokers launches, and manages the single """Owns the registry + brokers launches, and manages the single
consolidated per-host gateway. Backend-neutral (broker and gateway consolidated per-host gateway. Backend-neutral (broker and gateway
abstract the backend-native pieces).""" abstract the backend-native pieces)."""
@@ -390,4 +390,4 @@ class Orchestrator:
return {"configured": False} return {"configured": False}
__all__ = ["Orchestrator"] __all__ = ["OrchestratorCore"]
+1 -1
View File
@@ -1,6 +1,6 @@
"""The orchestrator's supervise service. """The orchestrator's supervise service.
`Supervisor` is the service object the `Orchestrator` owns and calls: it wraps `Supervisor` is the service object the `OrchestratorCore` owns and calls: it wraps
the supervise queue + audit stores (queue proposals, record operator responses, the supervise queue + audit stores (queue proposals, record operator responses,
write audit entries, stage the DB at launch). Binding those operations to one write audit entries, stage the DB at launch). Binding those operations to one
object optionally scoped to a `db_path` makes the orchestrator's supervise object optionally scoped to a `db_path` makes the orchestrator's supervise
+1 -1
View File
@@ -43,7 +43,7 @@ STATUSES: tuple[str, ...] = (STATUS_APPROVED, STATUS_MODIFIED, STATUS_REJECTED)
# proposal has no operator decision yet (`PENDING`) or no queued proposal with # proposal has no operator decision yet (`PENDING`) or no queued proposal with
# that id exists for the calling bottle (`UNKNOWN`). They are never a # that id exists for the calling bottle (`UNKNOWN`). They are never a
# `Response.status` — only the poll wire contract (see # `Response.status` — only the poll wire contract (see
# `Orchestrator.supervise_poll_response`) — so they are deliberately outside # `OrchestratorCore.supervise_poll_response`) — so they are deliberately outside
# `STATUSES`. # `STATUSES`.
POLL_STATUS_PENDING = "pending" POLL_STATUS_PENDING = "pending"
POLL_STATUS_UNKNOWN = "unknown" POLL_STATUS_UNKNOWN = "unknown"
+5 -5
View File
@@ -23,7 +23,7 @@ from bot_bottle.orchestrator_auth import ROLE_CLI, ROLE_GATEWAY, mint
from bot_bottle.orchestrator.broker import StubBroker from bot_bottle.orchestrator.broker import StubBroker
from bot_bottle.orchestrator.server import dispatch, make_server from bot_bottle.orchestrator.server import dispatch, make_server
from bot_bottle.orchestrator.store.registry_store import BottleRecord, RegistryStore from bot_bottle.orchestrator.store.registry_store import BottleRecord, RegistryStore
from bot_bottle.orchestrator.service import Orchestrator from bot_bottle.orchestrator.service import OrchestratorCore
from bot_bottle.orchestrator.store.store_manager import StoreManager from bot_bottle.orchestrator.store.store_manager import StoreManager
from bot_bottle.orchestrator.supervisor import ( from bot_bottle.orchestrator.supervisor import (
Proposal, Proposal,
@@ -36,11 +36,11 @@ def _body(obj: object) -> bytes:
return json.dumps(obj).encode() return json.dumps(obj).encode()
def _orchestrator(db_path: Path) -> Orchestrator: def _orchestrator(db_path: Path) -> OrchestratorCore:
store = RegistryStore(db_path) store = RegistryStore(db_path)
store.migrate() store.migrate()
secret = secrets.token_bytes(16) secret = secrets.token_bytes(16)
return Orchestrator(store, StubBroker(secret), secret) return OrchestratorCore(store, StubBroker(secret), secret)
class TestDispatch(unittest.TestCase): class TestDispatch(unittest.TestCase):
@@ -408,7 +408,7 @@ class TestDispatchSupervise(unittest.TestCase):
self.store.migrate() self.store.migrate()
StoreManager(db).migrate() StoreManager(db).migrate()
secret = secrets.token_bytes(16) secret = secrets.token_bytes(16)
self.orch = Orchestrator(self.store, StubBroker(secret), secret) self.orch = OrchestratorCore(self.store, StubBroker(secret), secret)
def tearDown(self) -> None: def tearDown(self) -> None:
self._env.stop() self._env.stop()
@@ -475,7 +475,7 @@ class TestDispatchSuperviseAgentRpc(unittest.TestCase):
self.store.migrate() self.store.migrate()
StoreManager(db).migrate() StoreManager(db).migrate()
secret = secrets.token_bytes(16) secret = secrets.token_bytes(16)
self.orch = Orchestrator(self.store, StubBroker(secret), secret) self.orch = OrchestratorCore(self.store, StubBroker(secret), secret)
def tearDown(self) -> None: def tearDown(self) -> None:
self._env.stop() self._env.stop()
+6 -6
View File
@@ -1,4 +1,4 @@
"""Unit tests for the Orchestrator launch lifecycle (PRD 0070).""" """Unit tests for the OrchestratorCore launch lifecycle (PRD 0070)."""
from __future__ import annotations from __future__ import annotations
@@ -13,7 +13,7 @@ from unittest.mock import patch
from bot_bottle.orchestrator.broker import LaunchBroker, LaunchRequest, StubBroker from bot_bottle.orchestrator.broker import LaunchBroker, LaunchRequest, StubBroker
from bot_bottle.orchestrator.store.registry_store import RegistryStore from bot_bottle.orchestrator.store.registry_store import RegistryStore
from bot_bottle.orchestrator.service import Orchestrator from bot_bottle.orchestrator.service import OrchestratorCore
from bot_bottle.orchestrator.store.secret_store import new_env_var_secret from bot_bottle.orchestrator.store.secret_store import new_env_var_secret
from bot_bottle.orchestrator.store.store_manager import StoreManager from bot_bottle.orchestrator.store.store_manager import StoreManager
from bot_bottle.orchestrator.supervisor import ( from bot_bottle.orchestrator.supervisor import (
@@ -42,7 +42,7 @@ class TestOrchestrator(unittest.TestCase):
self.store = RegistryStore(Path(self._tmp.name) / "r.db") self.store = RegistryStore(Path(self._tmp.name) / "r.db")
self.store.migrate() self.store.migrate()
self.broker = StubBroker(self.secret) self.broker = StubBroker(self.secret)
self.orch = Orchestrator(self.store, self.broker, self.secret) self.orch = OrchestratorCore(self.store, self.broker, self.secret)
def tearDown(self) -> None: def tearDown(self) -> None:
self._tmp.cleanup() self._tmp.cleanup()
@@ -137,7 +137,7 @@ class TestOrchestrator(unittest.TestCase):
self.assertIsNone(self.orch.resolve("10.243.0.3", "wrong-token")) self.assertIsNone(self.orch.resolve("10.243.0.3", "wrong-token"))
def test_launch_rolls_back_registry_on_broker_failure(self) -> None: def test_launch_rolls_back_registry_on_broker_failure(self) -> None:
orch = Orchestrator(self.store, _FailingBroker(self.secret), self.secret) orch = OrchestratorCore(self.store, _FailingBroker(self.secret), self.secret)
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
orch.launch_bottle("10.243.0.9") orch.launch_bottle("10.243.0.9")
self.assertEqual([], self.store.all()) # no orphan self.assertEqual([], self.store.all()) # no orphan
@@ -168,7 +168,7 @@ class TestOrchestratorSupervise(unittest.TestCase):
self.store.migrate() self.store.migrate()
StoreManager(db).migrate() StoreManager(db).migrate()
secret = secrets.token_bytes(16) secret = secrets.token_bytes(16)
self.orch = Orchestrator(self.store, StubBroker(secret), secret) self.orch = OrchestratorCore(self.store, StubBroker(secret), secret)
def tearDown(self) -> None: def tearDown(self) -> None:
self._env.stop() self._env.stop()
@@ -360,7 +360,7 @@ class TestOrchestratorReconcile(unittest.TestCase):
self.store = RegistryStore(self.db) self.store = RegistryStore(self.db)
self.store.migrate() self.store.migrate()
self.broker = StubBroker(self.secret) self.broker = StubBroker(self.secret)
self.orch = Orchestrator(self.store, self.broker, self.secret) self.orch = OrchestratorCore(self.store, self.broker, self.secret)
def tearDown(self) -> None: def tearDown(self) -> None:
self._tmp.cleanup() self._tmp.cleanup()