"""Run the orchestrator control plane as a plain process (PRD 0070 dev-harness). python -m bot_bottle.orchestrator [--host H] [--port P] [--db PATH] The PRD sequences the orchestrator as a plain-process dev-harness first, so the consolidation core (registry + attribution + HTTP control plane + live reload) can be exercised with fast iteration, decoupled from any VM / container packaging. Wrapping this exact service in a backend-native unit (docker container, then Firecracker VM) comes later. """ from __future__ import annotations import argparse import secrets from pathlib import Path from .. import log from .store.store_manager import StoreManager from ..paths import LAUNCH_BROKER_KEY_ENV from .broker import StubBroker, SubmitBroker from .broker_client import BrokerClient from .host_server import DEFAULT_PORT, broker_secret from .server import make_server from .docker_broker import DockerBroker from .store.registry_store import RegistryStore, default_db_path from .service import OrchestratorCore def main(argv: list[str] | None = None) -> int: """Parse args, migrate the registry, and serve the control plane.""" parser = argparse.ArgumentParser(prog="bot_bottle.orchestrator") parser.add_argument("--host", default="127.0.0.1", help="bind address") parser.add_argument("--port", type=int, default=8080, help="bind port (0 = ephemeral)") parser.add_argument( "--db", type=Path, default=None, help=f"registry DB path (default: {default_db_path()})", ) parser.add_argument( "--broker", choices=("stub", "docker", "http"), default="stub", help="launch broker: 'stub' records requests; 'docker' runs containers " "in-process; 'http' relays signed requests to a host control server", ) parser.add_argument( "--host-controller-url", default=f"http://127.0.0.1:{DEFAULT_PORT}", help="host control server URL (used only with --broker http)", ) args = parser.parse_args(argv) registry = RegistryStore(args.db) registry.migrate() # One DB per host: the supervise queue + audit tables live in the SAME # SQLite file the registry owns, so the control plane is the single # source of truth. The in-VM supervise daemon writes here; the host # operator reaches it over HTTP (never a second, disconnected DB). StoreManager(registry.db_path).migrate() # A signing secret ties the orchestrator (signer) to its broker (verifier). # 'stub' records launches instead of starting anything; 'docker' runs real # containers in-process; 'http' relays signed requests to a separate host # control server, which verifies and launches. For 'stub'/'docker' the secret # is ephemeral (signer and verifier share this process). For 'http' it must be # the SAME key the host controller holds — and this process is the *guest* # (signer), so it must be given that key by injection, NOT mint its own # process-local one (which would diverge from the host's and 401 every launch). broker: SubmitBroker if args.broker == "http": secret = broker_secret() # env-injected only; no host-file fallback here if secret is None: parser.error( f"--broker http requires the launch-broker key injected as " f"${LAUNCH_BROKER_KEY_ENV} (the host controller owns/mints it); the " "orchestrator must not mint its own or it would diverge from the host's" ) broker = BrokerClient(args.host_controller_url) else: secret = secrets.token_bytes(32) broker = DockerBroker(secret) if args.broker == "docker" else StubBroker(secret) orchestrator = OrchestratorCore(registry, broker, secret) server = make_server(orchestrator, host=args.host, port=args.port) bound_host, bound_port = server.server_address[0], server.server_address[1] log.info( "orchestrator control plane listening", context={"host": bound_host, "port": bound_port, "db": str(registry.db_path)}, ) try: server.serve_forever() except KeyboardInterrupt: log.info("orchestrator shutting down") finally: server.server_close() return 0 if __name__ == "__main__": raise SystemExit(main())