From ca1d341d4ffded224c5579268b1d19b51e8b60ea Mon Sep 17 00:00:00 2001 From: didericis Date: Fri, 24 Jul 2026 18:17:48 -0400 Subject: [PATCH] =?UTF-8?q?refactor:=20unify=20component=20naming=20?= =?UTF-8?q?=E2=80=94=20control=5Fplane/control=5Fauth=20->=20orchestrator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The codebase used "control plane" both as an architectural role term AND as an identifier alias for the orchestrator component, producing duplicate names for one thing (control_plane_url vs orchestrator_url, CONTROL_PLANE_PORT, host_control_plane_token, …). Going forward the concrete component is always named for what it is — Gateway or Orchestrator — and the plane vocabulary is reserved for prose (module descriptions, the security argument). Renamed (identifiers + the in-repo env/wire/file string values, all setters/getters are in this repo so the change is atomic): ControlPlaneServer -> OrchestratorServer control_plane_url -> orchestrator_url probe_control_plane_url -> probe_orchestrator_url host_control_plane_token -> host_orchestrator_token CONTROL_PLANE_PORT -> ORCHESTRATOR_PORT CONTROL_PLANE_TOKEN_ENV/FILE -> ORCHESTRATOR_TOKEN_ENV/FILENAME BOT_BOTTLE_CONTROL_PLANE_TOKEN-> BOT_BOTTLE_ORCHESTRATOR_TOKEN control-plane-token (file) -> orchestrator-token control_auth (module) -> orchestrator_auth (stays top-level; the gateway imports it and must not import the orchestrator/ package) CONTROL_AUTH_HEADER -> ORCHESTRATOR_AUTH_HEADER x-bot-bottle-control-auth -> x-bot-bottle-orchestrator-auth CONTROL_AUTH_JWT_ENV -> ORCHESTRATOR_AUTH_JWT_ENV BOT_BOTTLE_CONTROL_AUTH_JWT -> BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT _control_auth_headers -> _orchestrator_auth_headers Prose plane-terms ("control plane", "data plane") are preserved, including the test name test_data_plane_daemons_get_jwt_not_key (it names the security invariant). Gateway and orchestrator verified to agree on the renamed wire header; full unit suite green (2243). Co-Authored-By: Claude Opus 4.8 --- bot_bottle/backend/docker/gateway.py | 10 +++--- bot_bottle/backend/docker/infra.py | 18 +++++------ bot_bottle/backend/firecracker/backend.py | 2 +- .../firecracker/consolidated_launch.py | 2 +- bot_bottle/backend/firecracker/infra_vm.py | 32 +++++++++---------- bot_bottle/backend/macos_container/backend.py | 2 +- .../macos_container/consolidated_launch.py | 2 +- bot_bottle/backend/macos_container/infra.py | 26 +++++++-------- bot_bottle/gateway/bootstrap.py | 8 ++--- bot_bottle/gateway/policy_resolver.py | 12 +++---- bot_bottle/orchestrator/__init__.py | 6 ++-- bot_bottle/orchestrator/client.py | 18 +++++------ bot_bottle/orchestrator/server.py | 32 +++++++++---------- .../{control_auth.py => orchestrator_auth.py} | 0 bot_bottle/paths.py | 23 +++++++------ ...th.py => test_orchestrator_docker_auth.py} | 12 +++---- tests/unit/test_backend_selection.py | 4 +-- tests/unit/test_firecracker_infra_vm.py | 16 +++++----- tests/unit/test_gateway_init.py | 14 ++++---- tests/unit/test_macos_consolidated_launch.py | 4 +-- tests/unit/test_macos_infra.py | 10 +++--- ...trol_auth.py => test_orchestrator_auth.py} | 8 ++--- tests/unit/test_orchestrator_client.py | 6 ++-- tests/unit/test_orchestrator_server.py | 10 +++--- tests/unit/test_policy_resolver.py | 16 +++++----- 25 files changed, 146 insertions(+), 147 deletions(-) rename bot_bottle/{control_auth.py => orchestrator_auth.py} (100%) rename tests/integration/{test_orchestrator_docker_control_plane_auth.py => test_orchestrator_docker_auth.py} (95%) rename tests/unit/{test_control_auth.py => test_orchestrator_auth.py} (91%) diff --git a/bot_bottle/backend/docker/gateway.py b/bot_bottle/backend/docker/gateway.py index c45ff8e..221de21 100644 --- a/bot_bottle/backend/docker/gateway.py +++ b/bot_bottle/backend/docker/gateway.py @@ -4,11 +4,11 @@ import os import time from pathlib import Path -from ...control_auth import ROLE_GATEWAY, mint +from ...orchestrator_auth import ROLE_GATEWAY, mint from .util import run_docker from ...paths import ( - CONTROL_AUTH_JWT_ENV, - host_control_plane_token, + ORCHESTRATOR_AUTH_JWT_ENV, + host_orchestrator_token, host_gateway_ca_dir, ) from ...gateway import ( @@ -158,8 +158,8 @@ class DockerGateway(Gateway): # a `cli` token, so a compromised data-plane process can't drive the # operator routes (issue #469 review). Bare `--env NAME` keeps the value # off argv / `docker inspect`; only the gateway (not the agent) is given it. - argv += ["--env", CONTROL_AUTH_JWT_ENV] - run_env[CONTROL_AUTH_JWT_ENV] = mint(ROLE_GATEWAY, host_control_plane_token()) + argv += ["--env", ORCHESTRATOR_AUTH_JWT_ENV] + run_env[ORCHESTRATOR_AUTH_JWT_ENV] = mint(ROLE_GATEWAY, host_orchestrator_token()) argv.append(self.image_ref) proc = run_docker(argv, env=run_env) if proc.returncode != 0: diff --git a/bot_bottle/backend/docker/infra.py b/bot_bottle/backend/docker/infra.py index 5a1709e..d9d119d 100644 --- a/bot_bottle/backend/docker/infra.py +++ b/bot_bottle/backend/docker/infra.py @@ -25,13 +25,13 @@ import urllib.request from pathlib import Path from ... import log -from ...control_auth import ROLE_GATEWAY, mint +from ...orchestrator_auth import ROLE_GATEWAY, mint from .util import run_docker from ...paths import ( - CONTROL_AUTH_JWT_ENV, - CONTROL_PLANE_TOKEN_ENV, + ORCHESTRATOR_AUTH_JWT_ENV, + ORCHESTRATOR_TOKEN_ENV, bot_bottle_root, - host_control_plane_token, + host_orchestrator_token, host_gateway_ca_dir, ) from ...gateway import ( @@ -176,7 +176,7 @@ class DockerInfraService: so a later `ensure_running` can detect a real code change.""" self._ensure_network() run_docker(["docker", "rm", "--force", self._infra_name]) - _signing_key = host_control_plane_token() + _signing_key = host_orchestrator_token() proc = run_docker([ "docker", "run", "--detach", "--name", self._infra_name, @@ -204,8 +204,8 @@ class DockerInfraService: # pre-minted `gateway` JWT (data-plane daemons: present it). gateway_init # scopes each to its process, so a compromised data-plane daemon never # sees the key and can't mint a `cli` token (issue #469 review). - "--env", CONTROL_PLANE_TOKEN_ENV, - "--env", CONTROL_AUTH_JWT_ENV, + "--env", ORCHESTRATOR_TOKEN_ENV, + "--env", ORCHESTRATOR_AUTH_JWT_ENV, # Gateway daemons reach the orchestrator over loopback at its # fixed internal port (DEFAULT_PORT), independent of self.port. "--env", f"BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{DEFAULT_PORT}", @@ -214,8 +214,8 @@ class DockerInfraService: self.image, ], env={ **os.environ, - CONTROL_PLANE_TOKEN_ENV: _signing_key, - CONTROL_AUTH_JWT_ENV: mint(ROLE_GATEWAY, _signing_key), + ORCHESTRATOR_TOKEN_ENV: _signing_key, + ORCHESTRATOR_AUTH_JWT_ENV: mint(ROLE_GATEWAY, _signing_key), }) if proc.returncode != 0: raise OrchestratorStartError( diff --git a/bot_bottle/backend/firecracker/backend.py b/bot_bottle/backend/firecracker/backend.py index a528182..4119383 100644 --- a/bot_bottle/backend/firecracker/backend.py +++ b/bot_bottle/backend/firecracker/backend.py @@ -120,4 +120,4 @@ class FirecrackerBottleBackend( def ensure_orchestrator(self) -> str: from . import infra_vm - return infra_vm.ensure_running().control_plane_url + return infra_vm.ensure_running().orchestrator_url diff --git a/bot_bottle/backend/firecracker/consolidated_launch.py b/bot_bottle/backend/firecracker/consolidated_launch.py index 503d0af..24e8232 100644 --- a/bot_bottle/backend/firecracker/consolidated_launch.py +++ b/bot_bottle/backend/firecracker/consolidated_launch.py @@ -124,7 +124,7 @@ def launch_consolidated( provision its git-gate state into the gateway VM. Returns the context the agent-VM launch needs. Raises on failure — the caller tears down.""" infra = infra_vm.ensure_running() - url = infra.control_plane_url + url = infra.orchestrator_url client = OrchestratorClient(url) _reprovision_running_bottles(client) diff --git a/bot_bottle/backend/firecracker/infra_vm.py b/bot_bottle/backend/firecracker/infra_vm.py index 1542a60..cf644ab 100644 --- a/bot_bottle/backend/firecracker/infra_vm.py +++ b/bot_bottle/backend/firecracker/infra_vm.py @@ -33,7 +33,7 @@ from pathlib import Path from typing import Generator from ...log import die, info -from ...paths import CONTROL_PLANE_TOKEN_FILENAME, bot_bottle_root +from ...paths import ORCHESTRATOR_TOKEN_FILENAME, bot_bottle_root from .. import util as backend_util from ..docker import util as docker_mod from ..docker.gateway_provision import GatewayProvisionError @@ -42,7 +42,7 @@ from . import firecracker_vm, infra_artifact, netpool, util # Where the infra VM keeps its control-plane signing key (generated on the # persistent /dev/vdb volume mounted at BOT_BOTTLE_ROOT). The host mirrors it # back so the CLI signs `cli` tokens the VM verifies (issue #469 review). -_GUEST_SIGNING_KEY_PATH = "/var/lib/bot-bottle/control-plane-token" +_GUEST_SIGNING_KEY_PATH = "/var/lib/bot-bottle/orchestrator-token" # The single infra-VM image: gateway data plane + baked control-plane source # (Dockerfile.infra FROM the gateway image). Built from source by default; @@ -52,7 +52,7 @@ _GATEWAY_IMAGE = "bot-bottle-gateway:latest" _ORCHESTRATOR_IMAGE = "bot-bottle-orchestrator:latest" _REPO_ROOT = Path(__file__).resolve().parents[3] -CONTROL_PLANE_PORT = 8099 +ORCHESTRATOR_PORT = 8099 # Gateway data-plane ports (agent-facing): egress proxy, supervise MCP, # git-http. Reached by agent VMs over VM-to-VM routing (added next). EGRESS_PORT = 9099 @@ -84,8 +84,8 @@ class InfraVm: vm: firecracker_vm.VmHandle | None = None @property - def control_plane_url(self) -> str: - return f"http://{self.guest_ip}:{CONTROL_PLANE_PORT}" + def orchestrator_url(self) -> str: + return f"http://{self.guest_ip}:{ORCHESTRATOR_PORT}" def terminate(self) -> None: """Stop the infra VM — via the live handle if we booted it, else the @@ -168,7 +168,7 @@ def ensure_running() -> InfraVm: flock, so two simultaneous first launches don't both boot on the same rootfs/PID. The healthy fast-path takes no lock.""" slot = netpool.orch_slot() - url = f"http://{slot.guest_ip}:{CONTROL_PLANE_PORT}" + url = f"http://{slot.guest_ip}:{ORCHESTRATOR_PORT}" key = _infra_dir() / "id_ed25519" want = _expected_version() if _adoptable(key, url, want): @@ -192,7 +192,7 @@ def ensure_running() -> InfraVm: def _with_signing_key(infra: InfraVm) -> InfraVm: """Mirror the infra VM's control-plane signing key (generated on its - persistent volume) into the host's control-plane-token file, so the host CLI + persistent volume) into the host's orchestrator-token file, so the host CLI signs `cli` tokens the VM verifies (issue #469 review). Best-effort: an unreadable key is logged, not fatal — the VM still enforces auth, but the CLI may then be rejected until the key is readable. Returns `infra` for chaining.""" @@ -205,7 +205,7 @@ def _with_signing_key(infra: InfraVm) -> InfraVm: if proc.returncode != 0 or not signing_key: info("infra signing key not yet readable; control-plane auth may fail") return infra - path = bot_bottle_root() / CONTROL_PLANE_TOKEN_FILENAME + path = bot_bottle_root() / ORCHESTRATOR_TOKEN_FILENAME path.parent.mkdir(parents=True, exist_ok=True) fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w") as f: @@ -458,7 +458,7 @@ def wait_for_health( ) -> None: """Poll the control plane's /health until it answers 200 or the deadline passes. Dies (with the console tail) if the VMM exits early.""" - url = f"{infra.control_plane_url}/health" + url = f"{infra.orchestrator_url}/health" deadline = time.monotonic() + timeout while time.monotonic() < deadline: if infra.vm is not None and not infra.vm.is_alive(): @@ -467,7 +467,7 @@ def wait_for_health( try: with urllib.request.urlopen(url, timeout=1.0) as resp: if resp.status == 200: - info(f"infra control plane healthy at {infra.control_plane_url}") + info(f"infra control plane healthy at {infra.orchestrator_url}") return except (urllib.error.URLError, TimeoutError, OSError): pass @@ -525,11 +525,11 @@ cd /app # same VM — reaching the orchestrator over 127.0.0.1, past the nft boundary that # only fences off the separate agent VM — could drive the operator routes # (approve its own supervise proposals, rewrite policy, read injected tokens). -CP_KEY=$(BOT_BOTTLE_ROOT=/var/lib/bot-bottle python3 -c 'from bot_bottle.paths import host_control_plane_token as t; print(t())') -GW_JWT=$(BB_SIGNING_KEY="$CP_KEY" python3 -c 'import os; from bot_bottle.control_auth import mint, ROLE_GATEWAY; print(mint(ROLE_GATEWAY, os.environ["BB_SIGNING_KEY"]))') +CP_KEY=$(BOT_BOTTLE_ROOT=/var/lib/bot-bottle python3 -c 'from bot_bottle.paths import host_orchestrator_token as t; print(t())') +GW_JWT=$(BB_SIGNING_KEY="$CP_KEY" python3 -c 'import os; from bot_bottle.orchestrator_auth import mint, ROLE_GATEWAY; print(mint(ROLE_GATEWAY, os.environ["BB_SIGNING_KEY"]))') -BOT_BOTTLE_ROOT=/var/lib/bot-bottle BOT_BOTTLE_CONTROL_PLANE_TOKEN="$CP_KEY" python3 -m bot_bottle.orchestrator \\ - --host 0.0.0.0 --port {CONTROL_PLANE_PORT} --broker stub & +BOT_BOTTLE_ROOT=/var/lib/bot-bottle BOT_BOTTLE_ORCHESTRATOR_TOKEN="$CP_KEY" python3 -m bot_bottle.orchestrator \\ + --host 0.0.0.0 --port {ORCHESTRATOR_PORT} --broker stub & # Gateway data plane, multi-tenant: each request resolves source-IP -> # policy against the local control plane. The VM backend reaches git over @@ -540,8 +540,8 @@ BOT_BOTTLE_ROOT=/var/lib/bot-bottle BOT_BOTTLE_CONTROL_PLANE_TOKEN="$CP_KEY" pyt # the pre-minted `gateway` JWT; gateway_init keeps the signing key out of the # data-plane daemons' env. BOT_BOTTLE_GATEWAY_DAEMONS=egress,git-http,supervise \\ -BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{CONTROL_PLANE_PORT} \\ -BOT_BOTTLE_CONTROL_AUTH_JWT="$GW_JWT" \\ +BOT_BOTTLE_ORCHESTRATOR_URL=http://127.0.0.1:{ORCHESTRATOR_PORT} \\ +BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT="$GW_JWT" \\ python3 -m bot_bottle.gateway.bootstrap & # Reap as PID 1; children are backgrounded, so `wait` blocks. diff --git a/bot_bottle/backend/macos_container/backend.py b/bot_bottle/backend/macos_container/backend.py index 71b4d5e..e9225ed 100644 --- a/bot_bottle/backend/macos_container/backend.py +++ b/bot_bottle/backend/macos_container/backend.py @@ -102,7 +102,7 @@ class MacosContainerBottleBackend( (`supervise`) call when no control plane is running yet. Mirrors firecracker's infra-VM bring-up.""" from .infra import MacosInfraService - return MacosInfraService().ensure_running().control_plane_url + return MacosInfraService().ensure_running().orchestrator_url def prepare_cleanup(self) -> MacosContainerBottleCleanupPlan: return _cleanup.prepare_cleanup() diff --git a/bot_bottle/backend/macos_container/consolidated_launch.py b/bot_bottle/backend/macos_container/consolidated_launch.py index 94976fb..a641a9b 100644 --- a/bot_bottle/backend/macos_container/consolidated_launch.py +++ b/bot_bottle/backend/macos_container/consolidated_launch.py @@ -90,7 +90,7 @@ def ensure_gateway( service = service or MacosInfraService() infra = service.ensure_running() endpoint = GatewayEndpoint( - orchestrator_url=infra.control_plane_url, + orchestrator_url=infra.orchestrator_url, gateway_ip=infra.gateway_ip, gateway_ca_pem=service.ca_cert_pem(), network=service.network, diff --git a/bot_bottle/backend/macos_container/infra.py b/bot_bottle/backend/macos_container/infra.py index 5bb7f75..c2f14cc 100644 --- a/bot_bottle/backend/macos_container/infra.py +++ b/bot_bottle/backend/macos_container/infra.py @@ -48,12 +48,12 @@ from ...orchestrator.lifecycle import ( OrchestratorStartError, source_hash, ) -from ...control_auth import ROLE_GATEWAY, mint +from ...orchestrator_auth import ROLE_GATEWAY, mint from ...paths import ( - CONTROL_AUTH_JWT_ENV, - CONTROL_PLANE_TOKEN_ENV, + ORCHESTRATOR_AUTH_JWT_ENV, + ORCHESTRATOR_TOKEN_ENV, HOST_DB_FILENAME, - host_control_plane_token, + host_orchestrator_token, host_gateway_ca_dir, ) from .. import util as backend_util @@ -117,7 +117,7 @@ class InfraEndpoint: """How to reach the running infra container. The control plane and the gateway are the same container, so one address serves both.""" - control_plane_url: str # http://:8099 — host CLI + registration + orchestrator_url: str # http://:8099 — host CLI + registration gateway_ip: str # same container; agents' proxy / git-http / MCP target @@ -181,7 +181,7 @@ class MacosInfraService: return None url = self._resolve_url() if url and self.is_healthy(url): - return InfraEndpoint(control_plane_url=url, gateway_ip=_ip_of(url)) + return InfraEndpoint(orchestrator_url=url, gateway_ip=_ip_of(url)) return None def ensure_built(self) -> None: @@ -247,17 +247,17 @@ class MacosInfraService: # run process below, so neither lands on argv or in `container # inspect`'s command line. The agent runs in a SEPARATE container that # is never given these vars, which is the whole point. - "--env", CONTROL_PLANE_TOKEN_ENV, - "--env", CONTROL_AUTH_JWT_ENV, + "--env", ORCHESTRATOR_TOKEN_ENV, + "--env", ORCHESTRATOR_AUTH_JWT_ENV, "--entrypoint", "sh", self.image, "-c", _init_script(self.port), ] - _signing_key = host_control_plane_token() + _signing_key = host_orchestrator_token() run_env = { **os.environ, - CONTROL_PLANE_TOKEN_ENV: _signing_key, - CONTROL_AUTH_JWT_ENV: mint(ROLE_GATEWAY, _signing_key), + ORCHESTRATOR_TOKEN_ENV: _signing_key, + ORCHESTRATOR_AUTH_JWT_ENV: mint(ROLE_GATEWAY, _signing_key), } result = container_mod.run_container_argv(argv, env=run_env) if result.returncode != 0: @@ -272,7 +272,7 @@ class MacosInfraService: url = self._resolve_url() if url and self.is_healthy(url): log.info("infra container healthy", context={"url": url}) - return InfraEndpoint(control_plane_url=url, gateway_ip=_ip_of(url)) + return InfraEndpoint(orchestrator_url=url, gateway_ip=_ip_of(url)) if time.monotonic() >= deadline: raise OrchestratorStartError( f"infra container did not become healthy within " @@ -306,7 +306,7 @@ def _ip_of(url: str) -> str: return url.split("://", 1)[-1].rsplit(":", 1)[0] -def probe_control_plane_url(port: int = DEFAULT_PORT) -> str: +def probe_orchestrator_url(port: int = DEFAULT_PORT) -> str: """The running infra container's control-plane URL, or "" if it isn't up. Used by host-side control-plane discovery (`discover_orchestrator_url`); safe to call on any host — returns "" when the container or the `container` diff --git a/bot_bottle/gateway/bootstrap.py b/bot_bottle/gateway/bootstrap.py index 060a980..4e1c6d1 100644 --- a/bot_bottle/gateway/bootstrap.py +++ b/bot_bottle/gateway/bootstrap.py @@ -58,10 +58,10 @@ _READY_GATED_DAEMONS: tuple[str, ...] = ("git-gate", "git-http") # The data-plane daemons instead hold the pre-minted `gateway` JWT they present. # Scoping each to its process (even in the combined infra container) keeps a # compromised data-plane daemon from reading the key and minting a `cli` token -# (issue #469 review). Values match paths.CONTROL_PLANE_TOKEN_ENV / -# CONTROL_AUTH_JWT_ENV; hardcoded here so this supervisor stays import-light. -_SIGNING_KEY_ENV = "BOT_BOTTLE_CONTROL_PLANE_TOKEN" -_GATEWAY_JWT_ENV = "BOT_BOTTLE_CONTROL_AUTH_JWT" +# (issue #469 review). Values match paths.ORCHESTRATOR_TOKEN_ENV / +# ORCHESTRATOR_AUTH_JWT_ENV; hardcoded here so this supervisor stays import-light. +_SIGNING_KEY_ENV = "BOT_BOTTLE_ORCHESTRATOR_TOKEN" +_GATEWAY_JWT_ENV = "BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT" # Daemons that must be requested explicitly via BOT_BOTTLE_GATEWAY_DAEMONS # and are NOT started in the default (env-var-unset) case. The orchestrator diff --git a/bot_bottle/gateway/policy_resolver.py b/bot_bottle/gateway/policy_resolver.py index 9298a12..a61f3da 100644 --- a/bot_bottle/gateway/policy_resolver.py +++ b/bot_bottle/gateway/policy_resolver.py @@ -41,16 +41,16 @@ DEFAULT_TIMEOUT_SECONDS = 2.0 # rather than imported because this module is COPYed flat into the gateway image, # free of bot-bottle imports — same rationale as IDENTITY_HEADER in egress_addon # / git_http_backend. -CONTROL_AUTH_HEADER = "x-bot-bottle-control-auth" -CONTROL_AUTH_JWT_ENV = "BOT_BOTTLE_CONTROL_AUTH_JWT" +ORCHESTRATOR_AUTH_HEADER = "x-bot-bottle-orchestrator-auth" +ORCHESTRATOR_AUTH_JWT_ENV = "BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT" -def _control_auth_headers() -> dict[str, str]: +def _orchestrator_auth_headers() -> dict[str, str]: """The auth header to send, or {} when no token is configured (an open control plane, e.g. Firecracker behind its nft boundary — sending nothing is correct there and harmlessly ignored).""" - token = os.environ.get(CONTROL_AUTH_JWT_ENV, "").strip() - return {CONTROL_AUTH_HEADER: token} if token else {} + token = os.environ.get(ORCHESTRATOR_AUTH_JWT_ENV, "").strip() + return {ORCHESTRATOR_AUTH_HEADER: token} if token else {} class PolicyResolveError(RuntimeError): @@ -74,7 +74,7 @@ class PolicyResolver: body = json.dumps(payload).encode() req = urllib.request.Request( f"{self._base}{path}", data=body, method="POST", - headers={"Content-Type": "application/json", **_control_auth_headers()}, + headers={"Content-Type": "application/json", **_orchestrator_auth_headers()}, ) try: with urllib.request.urlopen(req, timeout=self._timeout) as resp: diff --git a/bot_bottle/orchestrator/__init__.py b/bot_bottle/orchestrator/__init__.py index 8a0a9da..06e3d18 100644 --- a/bot_bottle/orchestrator/__init__.py +++ b/bot_bottle/orchestrator/__init__.py @@ -17,7 +17,7 @@ backend-neutral "consolidation core" that needs no VM packaging: * `service` — the `Orchestrator`: owns the registry, brokers the launch lifecycle (launch/teardown), manages the shared gateway, attributes. - * `control_plane` — the HTTP control-plane RPC (launch / teardown / + * `server` — the HTTP control-plane RPC (launch / teardown / list / attribute / gateway / health). The actual backend-native launch (a real docker/firecracker broker) and @@ -40,7 +40,7 @@ from .broker import ( from .docker_broker import DockerBroker, DockerBrokerError from ..gateway import Gateway, GatewayError from .service import Orchestrator -from .server import ControlPlaneServer, dispatch, make_server +from .server import OrchestratorServer, dispatch, make_server __all__ = [ "BottleRecord", @@ -57,7 +57,7 @@ __all__ = [ "sign_request", "verify_request", "Orchestrator", - "ControlPlaneServer", + "OrchestratorServer", "dispatch", "make_server", ] diff --git a/bot_bottle/orchestrator/client.py b/bot_bottle/orchestrator/client.py index fb80c5e..b6c9549 100644 --- a/bot_bottle/orchestrator/client.py +++ b/bot_bottle/orchestrator/client.py @@ -18,9 +18,9 @@ import urllib.request from collections.abc import Iterable from dataclasses import dataclass -from ..control_auth import ROLE_CLI, mint -from ..paths import host_control_plane_token -from .server import CONTROL_AUTH_HEADER +from ..orchestrator_auth import ROLE_CLI, mint +from ..paths import host_orchestrator_token +from .server import ORCHESTRATOR_AUTH_HEADER DEFAULT_TIMEOUT_SECONDS = 5.0 @@ -32,7 +32,7 @@ def _host_auth_token() -> str: "" means 'send no auth header' — correct against an open (unconfigured) control plane, and harmlessly rejected by a secured one.""" try: - return mint(ROLE_CLI, host_control_plane_token()) + return mint(ROLE_CLI, host_orchestrator_token()) except (OSError, ValueError): return "" @@ -83,7 +83,7 @@ class OrchestratorClient: data = json.dumps(body).encode() if body is not None else None headers = {"Content-Type": "application/json"} if data is not None else {} if self._auth_token: - headers[CONTROL_AUTH_HEADER] = self._auth_token + headers[ORCHESTRATOR_AUTH_HEADER] = self._auth_token req = urllib.request.Request( f"{self._base}{path}", data=data, method=method, headers=headers, ) @@ -252,14 +252,14 @@ def discover_orchestrator_url(*, timeout: float = 2.0) -> str: 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 + from ..backend.firecracker.infra_vm import ORCHESTRATOR_PORT candidates.append( - f"http://{netpool.orch_slot().guest_ip}:{CONTROL_PLANE_PORT}") + f"http://{netpool.orch_slot().guest_ip}:{ORCHESTRATOR_PORT}") except Exception: # noqa: BLE001 — backend optional / not firecracker pass try: # macOS: infra container control plane on its host-only address - from ..backend.macos_container.infra import probe_control_plane_url - url = probe_control_plane_url() + from ..backend.macos_container.infra import probe_orchestrator_url + url = probe_orchestrator_url() if url: candidates.append(url) except Exception: # noqa: BLE001 — backend optional / not macOS diff --git a/bot_bottle/orchestrator/server.py b/bot_bottle/orchestrator/server.py index 4daed70..a7ec7a4 100644 --- a/bot_bottle/orchestrator/server.py +++ b/bot_bottle/orchestrator/server.py @@ -48,7 +48,7 @@ via the orchestrator. Register/deregister without a launch are internal to `Orchestrator`, not exposed here. Routing/handling is the pure function `dispatch()` so it is unit-testable -without a socket; `Handler` / `ControlPlaneServer` / `make_server` are a +without a socket; `Handler` / `OrchestratorServer` / `make_server` are a thin stdlib adapter around it. Listing redacts identity tokens — they are returned only once, to the caller that launches the bottle. """ @@ -63,8 +63,8 @@ import sys import typing from urllib.parse import urlsplit -from ..control_auth import ROLE_CLI, ROLES, verify -from ..paths import CONTROL_PLANE_TOKEN_ENV +from ..orchestrator_auth import ROLE_CLI, ROLES, verify +from ..paths import ORCHESTRATOR_TOKEN_ENV from ..supervisor.types import TOOLS from .service import Orchestrator @@ -72,13 +72,13 @@ from .service import Orchestrator Json = dict[str, object] # The request header carrying the caller's role-scoped control-plane token (a -# signed JWT naming the caller's role — see control_auth). The role gates which +# signed JWT naming the caller's role — see orchestrator_auth). The role gates which # routes the caller may reach: the data plane holds a `gateway` token good only # for the agent-facing lookups; the host CLI holds a `cli` token for the # operator/mutating routes. An agent that can merely *reach* the port holds no # token at all, and a compromised gateway holds only `gateway` — neither can # drive the operator routes (approve proposals, rewrite policy, read tokens). -CONTROL_AUTH_HEADER = "x-bot-bottle-control-auth" +ORCHESTRATOR_AUTH_HEADER = "x-bot-bottle-orchestrator-auth" # The routes the data plane (role `gateway`) is allowed to reach — exactly the # per-request lookups PolicyResolver makes. Every other authenticated route is @@ -116,7 +116,7 @@ def dispatch( # pylint: disable=too-many-return-statements,too-many-branches `role` is the caller's verified control-plane role (`gateway` or `cli`), or None for an unauthenticated request; an open-mode server (no signing key - configured — see `ControlPlaneServer`) passes `cli`. Every route except + configured — see `OrchestratorServer`) passes `cli`. Every route except `GET /health` requires a role: a missing role is 401, and a role that doesn't cover the route is 403 — so a `gateway` data-plane token can reach `/resolve` + `/supervise/{propose,poll}` but not the operator routes @@ -365,10 +365,10 @@ class Handler(http.server.BaseHTTPRequestHandler): crashing the connection, so one bad request can't take the control plane down for the caller.""" server = self.server - assert isinstance(server, ControlPlaneServer) + assert isinstance(server, OrchestratorServer) length = int(self.headers.get("Content-Length") or 0) body = self.rfile.read(length) if length > 0 else b"" - role = server.role_for(self.headers.get(CONTROL_AUTH_HEADER, "")) + role = server.role_for(self.headers.get(ORCHESTRATOR_AUTH_HEADER, "")) try: status, payload = dispatch( server.orchestrator, method, self.path, body, role=role) @@ -396,11 +396,11 @@ class Handler(http.server.BaseHTTPRequestHandler): self._serve("DELETE") -class ControlPlaneServer(socketserver.ThreadingMixIn, http.server.HTTPServer): +class OrchestratorServer(socketserver.ThreadingMixIn, http.server.HTTPServer): """Threading HTTP server that carries the orchestrator for its handlers. Holds the per-host control-plane *signing key* (from - `$BOT_BOTTLE_CONTROL_PLANE_TOKEN`, injected by the launcher into the + `$BOT_BOTTLE_ORCHESTRATOR_TOKEN`, injected by the launcher into the orchestrator process only) and verifies each request's role-scoped token against it. When a key is set, every route but `/health` requires a valid token whose role covers the route; when it is unset the server runs **open** @@ -413,11 +413,11 @@ class ControlPlaneServer(socketserver.ThreadingMixIn, http.server.HTTPServer): def __init__(self, address: tuple[str, int], orchestrator: Orchestrator) -> None: self.orchestrator = orchestrator - self._signing_key = os.environ.get(CONTROL_PLANE_TOKEN_ENV, "").strip() + self._signing_key = os.environ.get(ORCHESTRATOR_TOKEN_ENV, "").strip() if not self._signing_key: sys.stderr.write( "orchestrator: WARNING — no control-plane signing key " - f"(${CONTROL_PLANE_TOKEN_ENV}); running WITHOUT caller " + f"(${ORCHESTRATOR_TOKEN_ENV}); running WITHOUT caller " "authentication. Any client that can reach this port can drive " "it. Backends that put the control plane on an agent-reachable " "network MUST set this.\n" @@ -438,13 +438,13 @@ class ControlPlaneServer(socketserver.ThreadingMixIn, http.server.HTTPServer): def make_server( orchestrator: Orchestrator, host: str = "127.0.0.1", port: int = 0 -) -> ControlPlaneServer: +) -> OrchestratorServer: """Build (but do not start) a control-plane server. `port=0` binds an ephemeral port — read `server.server_address` for the actual one.""" - return ControlPlaneServer((host, port), orchestrator) + return OrchestratorServer((host, port), orchestrator) __all__ = [ - "dispatch", "Handler", "ControlPlaneServer", "make_server", "Json", - "CONTROL_AUTH_HEADER", + "dispatch", "Handler", "OrchestratorServer", "make_server", "Json", + "ORCHESTRATOR_AUTH_HEADER", ] diff --git a/bot_bottle/control_auth.py b/bot_bottle/orchestrator_auth.py similarity index 100% rename from bot_bottle/control_auth.py rename to bot_bottle/orchestrator_auth.py diff --git a/bot_bottle/paths.py b/bot_bottle/paths.py index e1723c4..7b991cb 100644 --- a/bot_bottle/paths.py +++ b/bot_bottle/paths.py @@ -36,17 +36,16 @@ HOST_DB_FILENAME = "bot-bottle.db" # reading route (see orchestrator/server.py); it is held only by the # trusted callers (control plane, gateway, host CLI) and never handed to an # agent, so an agent that can reach the control-plane port still can't drive it. -CONTROL_PLANE_TOKEN_FILENAME = "control-plane-token" -# The env var carrying the control-plane *signing key* — held only by the +ORCHESTRATOR_TOKEN_FILENAME = "orchestrator-token" +# The env var carrying the orchestrator's *signing key* — held only by the # orchestrator (to verify tokens) and the host CLI (to mint its own), never by -# the data plane. Same value as the host token file; the name is unchanged for -# backward compatibility with existing launchers. -CONTROL_PLANE_TOKEN_ENV = "BOT_BOTTLE_CONTROL_PLANE_TOKEN" +# the data plane. Same value as the host token file. +ORCHESTRATOR_TOKEN_ENV = "BOT_BOTTLE_ORCHESTRATOR_TOKEN" # The env var carrying the data plane's pre-minted `gateway`-role token (a # signed JWT the launcher mints from the signing key). The gateway presents this # on /resolve + /supervise/{propose,poll}; it never holds the signing key, so it # cannot forge a higher-privilege `cli` token (issue #469 review). -CONTROL_AUTH_JWT_ENV = "BOT_BOTTLE_CONTROL_AUTH_JWT" +ORCHESTRATOR_AUTH_JWT_ENV = "BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT" # The host directory holding the gateway's persistent mitmproxy CA. Bind-mounted # into the infra/gateway container at mitmproxy's confdir so the self-generated @@ -98,7 +97,7 @@ def host_gateway_ca_dir() -> Path: return ca_dir -def host_control_plane_token() -> str: +def host_orchestrator_token() -> str: """The per-host control-plane secret, minted (256-bit, url-safe) and persisted 0600 on first use, then reused. @@ -107,7 +106,7 @@ def host_control_plane_token() -> str: *host* artifact — the file lives under the root the agent never mounts, and the env var is set only on the trusted containers — so reading it here is safe on the host launch path but the value never reaches a bottle.""" - path = bot_bottle_root() / CONTROL_PLANE_TOKEN_FILENAME + path = bot_bottle_root() / ORCHESTRATOR_TOKEN_FILENAME try: existing = path.read_text().strip() if existing: @@ -131,13 +130,13 @@ def host_control_plane_token() -> str: __all__ = [ "HOST_DB_FILENAME", - "CONTROL_PLANE_TOKEN_FILENAME", - "CONTROL_PLANE_TOKEN_ENV", - "CONTROL_AUTH_JWT_ENV", + "ORCHESTRATOR_TOKEN_FILENAME", + "ORCHESTRATOR_TOKEN_ENV", + "ORCHESTRATOR_AUTH_JWT_ENV", "GATEWAY_CA_DIRNAME", "bot_bottle_root", "host_db_path", "host_db_dir", "host_gateway_ca_dir", - "host_control_plane_token", + "host_orchestrator_token", ] diff --git a/tests/integration/test_orchestrator_docker_control_plane_auth.py b/tests/integration/test_orchestrator_docker_auth.py similarity index 95% rename from tests/integration/test_orchestrator_docker_control_plane_auth.py rename to tests/integration/test_orchestrator_docker_auth.py index 08a6c62..50ecaa5 100644 --- a/tests/integration/test_orchestrator_docker_control_plane_auth.py +++ b/tests/integration/test_orchestrator_docker_auth.py @@ -25,10 +25,10 @@ import tempfile import unittest from pathlib import Path -from bot_bottle.control_auth import ROLE_CLI, ROLE_GATEWAY, mint +from bot_bottle.orchestrator_auth import ROLE_CLI, ROLE_GATEWAY, mint from bot_bottle.orchestrator.client import OrchestratorClient from bot_bottle.backend.docker.infra import DockerInfraService -from bot_bottle.paths import host_control_plane_token +from bot_bottle.paths import host_orchestrator_token from tests._docker import skip_unless_docker # Fixed (not per-run-suffixed) so repeated runs reuse the same layer-cached @@ -46,20 +46,20 @@ _TEST_INFRA_IMAGE = "bot-bottle-infra:itest" "runner's /workspace — same host-bind-mount constraint as the other " "bottle-bringup integration tests", ) -class TestDockerControlPlaneAuthIntegration(unittest.TestCase): +class TestDockerOrchestratorAuthIntegration(unittest.TestCase): @classmethod def setUpClass(cls) -> None: suffix = secrets.token_hex(4) cls._tmp = tempfile.TemporaryDirectory() # pylint: disable=consider-using-with cls.addClassCleanup(cls._tmp.cleanup) - # host_control_plane_token() — both the token read below and the one + # host_orchestrator_token() — both the token read below and the one # DockerInfraService injects into the container's env — resolves its # path via the *ambient* BOT_BOTTLE_ROOT env var, not the host_root # kwarg passed to the constructor (that kwarg only controls the DB # bind-mount destination). Without pointing the env var at the same # throwaway dir, this "isolated" test would read/write the developer's - # real ~/.bot-bottle/control-plane-token. + # real ~/.bot-bottle/orchestrator-token. previous_root = os.environ.get("BOT_BOTTLE_ROOT") def _restore_root() -> None: @@ -88,7 +88,7 @@ class TestDockerControlPlaneAuthIntegration(unittest.TestCase): cls.svc.ensure_running() # The control plane now verifies role-scoped signed tokens, not the raw # key. Mint one of each role from the host signing key (issue #469 review). - signing_key = host_control_plane_token() + signing_key = host_orchestrator_token() cls.cli_token = mint(ROLE_CLI, signing_key) cls.gateway_token = mint(ROLE_GATEWAY, signing_key) diff --git a/tests/unit/test_backend_selection.py b/tests/unit/test_backend_selection.py index 1a2304d..25b221c 100644 --- a/tests/unit/test_backend_selection.py +++ b/tests/unit/test_backend_selection.py @@ -408,7 +408,7 @@ class TestEnsureOrchestrator(unittest.TestCase): with patch( "bot_bottle.backend.firecracker.infra_vm.ensure_running" ) as ensure_running: - ensure_running.return_value.control_plane_url = ( + ensure_running.return_value.orchestrator_url = ( "http://10.243.255.1:8099" ) url = b.ensure_orchestrator() @@ -419,7 +419,7 @@ class TestEnsureOrchestrator(unittest.TestCase): with patch( "bot_bottle.backend.macos_container.infra.MacosInfraService" ) as service_cls: - service_cls.return_value.ensure_running.return_value.control_plane_url = ( + service_cls.return_value.ensure_running.return_value.orchestrator_url = ( "http://192.168.128.2:8099" ) url = b.ensure_orchestrator() diff --git a/tests/unit/test_firecracker_infra_vm.py b/tests/unit/test_firecracker_infra_vm.py index 7dcf4ea..157a5c5 100644 --- a/tests/unit/test_firecracker_infra_vm.py +++ b/tests/unit/test_firecracker_infra_vm.py @@ -31,7 +31,7 @@ class TestSigningKeySync(unittest.TestCase): patch.object(infra_vm, "bot_bottle_root", return_value=root): out = infra_vm._with_signing_key(self._infra()) self.assertIsInstance(out, infra_vm.InfraVm) # returned for chaining - token = root / infra_vm.CONTROL_PLANE_TOKEN_FILENAME + token = root / infra_vm.ORCHESTRATOR_TOKEN_FILENAME self.assertEqual("the-signing-key", token.read_text()) self.assertEqual(0o600, token.stat().st_mode & 0o777) # It cat'd the guest volume path over SSH. @@ -44,16 +44,16 @@ class TestSigningKeySync(unittest.TestCase): with patch.object(infra_vm.subprocess, "run", return_value=proc), \ patch.object(infra_vm, "bot_bottle_root", return_value=root): infra_vm._with_signing_key(self._infra()) # no raise - self.assertFalse((root / infra_vm.CONTROL_PLANE_TOKEN_FILENAME).exists()) + self.assertFalse((root / infra_vm.ORCHESTRATOR_TOKEN_FILENAME).exists()) -class TestControlPlaneUrl(unittest.TestCase): +class TestOrchestratorUrl(unittest.TestCase): def test_url_uses_guest_ip_and_port(self): infra = infra_vm.InfraVm( vm=MagicMock(), guest_ip="10.243.255.1", private_key=Path("/k")) self.assertEqual( - f"http://10.243.255.1:{infra_vm.CONTROL_PLANE_PORT}", - infra.control_plane_url, + f"http://10.243.255.1:{infra_vm.ORCHESTRATOR_PORT}", + infra.orchestrator_url, ) @@ -81,11 +81,11 @@ class TestBuildInfraRootfs(unittest.TestCase): # Role-scoped control-plane auth (issue #469 review): the orchestrator # gets the signing key, the gateway daemons get a pre-minted `gateway` # JWT — never open mode in the infra VM. - self.assertIn("host_control_plane_token", init) # key generated on the volume + self.assertIn("host_orchestrator_token", init) # key generated on the volume self.assertIn("mint, ROLE_GATEWAY", init) # gateway JWT minted from it - self.assertIn('BOT_BOTTLE_CONTROL_PLANE_TOKEN="$CP_KEY" python3 -m bot_bottle.orchestrator', + self.assertIn('BOT_BOTTLE_ORCHESTRATOR_TOKEN="$CP_KEY" python3 -m bot_bottle.orchestrator', init) # key -> orchestrator only - self.assertIn('BOT_BOTTLE_CONTROL_AUTH_JWT="$GW_JWT"', init) # JWT -> gateway daemons + self.assertIn('BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT="$GW_JWT"', init) # JWT -> gateway daemons class TestSshGatewayTransport(unittest.TestCase): diff --git a/tests/unit/test_gateway_init.py b/tests/unit/test_gateway_init.py index 7f5272c..1fb471f 100644 --- a/tests/unit/test_gateway_init.py +++ b/tests/unit/test_gateway_init.py @@ -64,7 +64,7 @@ class TestEnvForDaemon(unittest.TestCase): self.assertNotIn("X", self._BASE) -class TestControlPlaneEnvScoping(unittest.TestCase): +class TestOrchestratorEnvScoping(unittest.TestCase): """The control-plane signing key stays with the orchestrator; the pre-minted `gateway` JWT goes to the data-plane daemons (issue #469 review). Scoping them per-process keeps a compromised data-plane daemon from reading the key @@ -72,20 +72,20 @@ class TestControlPlaneEnvScoping(unittest.TestCase): _BASE = { "PATH": "/usr/bin", - "BOT_BOTTLE_CONTROL_PLANE_TOKEN": "sk-x", - "BOT_BOTTLE_CONTROL_AUTH_JWT": "gw-jwt", + "BOT_BOTTLE_ORCHESTRATOR_TOKEN": "sk-x", + "BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT": "gw-jwt", } def test_orchestrator_gets_key_not_jwt(self): env = _env_for_daemon("orchestrator", self._BASE) - self.assertEqual("sk-x", env["BOT_BOTTLE_CONTROL_PLANE_TOKEN"]) - self.assertNotIn("BOT_BOTTLE_CONTROL_AUTH_JWT", env) + self.assertEqual("sk-x", env["BOT_BOTTLE_ORCHESTRATOR_TOKEN"]) + self.assertNotIn("BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT", env) def test_data_plane_daemons_get_jwt_not_key(self): for name in ("egress", "git-gate", "git-http", "supervise"): env = _env_for_daemon(name, self._BASE) - self.assertNotIn("BOT_BOTTLE_CONTROL_PLANE_TOKEN", env, name) - self.assertEqual("gw-jwt", env["BOT_BOTTLE_CONTROL_AUTH_JWT"], name) + self.assertNotIn("BOT_BOTTLE_ORCHESTRATOR_TOKEN", env, name) + self.assertEqual("gw-jwt", env["BOT_BOTTLE_ORCHESTRATOR_AUTH_JWT"], name) class TestSelectedDaemons(unittest.TestCase): diff --git a/tests/unit/test_macos_consolidated_launch.py b/tests/unit/test_macos_consolidated_launch.py index b0f0b84..812e14e 100644 --- a/tests/unit/test_macos_consolidated_launch.py +++ b/tests/unit/test_macos_consolidated_launch.py @@ -58,7 +58,7 @@ class TestEnsureGateway(unittest.TestCase): from bot_bottle.backend.macos_container.infra import InfraEndpoint service = MagicMock() service.ensure_running.return_value = InfraEndpoint( - control_plane_url="http://192.168.128.2:8099", + orchestrator_url="http://192.168.128.2:8099", gateway_ip="192.168.128.2", ) service.network = "bot-bottle-mac-gateway" @@ -72,7 +72,7 @@ class TestEnsureGateway(unittest.TestCase): self.assertEqual("PEM", endpoint.gateway_ca_pem) self.assertEqual("bot-bottle-mac-gateway", endpoint.network) - def test_control_plane_and_gateway_share_one_address(self) -> None: + def test_orchestrator_and_gateway_share_one_address(self) -> None: """One infra container hosts both, so the gateway IP and the control-plane host are the same.""" endpoint = self._run(self._service()) diff --git a/tests/unit/test_macos_infra.py b/tests/unit/test_macos_infra.py index 914ed1c..f1341ec 100644 --- a/tests/unit/test_macos_infra.py +++ b/tests/unit/test_macos_infra.py @@ -10,7 +10,7 @@ from bot_bottle.backend.macos_container.infra import ( INFRA_DB_VOLUME, MacosInfraService, OrchestratorStartError, - probe_control_plane_url, + probe_orchestrator_url, ) _INFRA = "bot_bottle.backend.macos_container.infra" @@ -110,7 +110,7 @@ class TestInfraEnsureRunning(unittest.TestCase): mod.try_container_ipv4_on_network.return_value = "192.168.128.2" endpoint = svc.ensure_running() run.assert_not_called() - self.assertEqual("http://192.168.128.2:8099", endpoint.control_plane_url) + self.assertEqual("http://192.168.128.2:8099", endpoint.orchestrator_url) self.assertEqual("192.168.128.2", endpoint.gateway_ip) def test_changed_source_recreates(self) -> None: @@ -176,16 +176,16 @@ class TestCaCertPem(unittest.TestCase): svc.ca_cert_pem(timeout=0) -class TestProbeControlPlane(unittest.TestCase): +class TestProbeOrchestrator(unittest.TestCase): def test_returns_url_when_running(self) -> None: with patch(f"{_INFRA}.container_mod") as mod: mod.try_container_ipv4_on_network.return_value = "192.168.128.2" - self.assertEqual("http://192.168.128.2:8099", probe_control_plane_url()) + self.assertEqual("http://192.168.128.2:8099", probe_orchestrator_url()) def test_empty_when_absent(self) -> None: with patch(f"{_INFRA}.container_mod") as mod: mod.try_container_ipv4_on_network.return_value = "" - self.assertEqual("", probe_control_plane_url()) + self.assertEqual("", probe_orchestrator_url()) if __name__ == "__main__": diff --git a/tests/unit/test_control_auth.py b/tests/unit/test_orchestrator_auth.py similarity index 91% rename from tests/unit/test_control_auth.py rename to tests/unit/test_orchestrator_auth.py index bfcb7cc..78e2559 100644 --- a/tests/unit/test_control_auth.py +++ b/tests/unit/test_orchestrator_auth.py @@ -6,7 +6,7 @@ import base64 import json import unittest -from bot_bottle.control_auth import ROLE_CLI, ROLE_GATEWAY, ROLES, mint, verify +from bot_bottle.orchestrator_auth import ROLE_CLI, ROLE_GATEWAY, ROLES, mint, verify _KEY = "test-key" @@ -45,14 +45,14 @@ class TestMintVerify(unittest.TestCase): def test_validly_signed_but_wrong_alg_rejected(self) -> None: # Alg-confusion: even a *correctly signed* token whose header claims a # non-HS256 alg must be rejected. - from bot_bottle.control_auth import _sign # noqa: PLC0415 + from bot_bottle.orchestrator_auth import _sign # noqa: PLC0415 signing_input = f"{_b64({'alg': 'none', 'typ': 'JWT'})}.{_b64({'role': 'cli'})}" self.assertIsNone(verify(f"{signing_input}.{_sign(_KEY, signing_input)}", _KEY)) def test_validly_signed_but_undecodable_rejected(self) -> None: # A correct signature over a header that isn't valid base64/JSON still # fails closed rather than raising. - from bot_bottle.control_auth import _sign # noqa: PLC0415 + from bot_bottle.orchestrator_auth import _sign # noqa: PLC0415 signing_input = f"!!!not-base64!!!.{_b64({'role': 'cli'})}" self.assertIsNone(verify(f"{signing_input}.{_sign(_KEY, signing_input)}", _KEY)) @@ -67,7 +67,7 @@ class TestMintVerify(unittest.TestCase): header, _p, _s = mint(ROLE_CLI, _KEY).split(".") # Re-sign a token carrying an unknown role — a valid signature but a # role the control plane doesn't recognise must still be rejected. - from bot_bottle.control_auth import _sign # noqa: PLC0415 + from bot_bottle.orchestrator_auth import _sign # noqa: PLC0415 payload = _b64({"role": "root"}) signing_input = f"{header}.{payload}" forged = f"{signing_input}.{_sign(_KEY, signing_input)}" diff --git a/tests/unit/test_orchestrator_client.py b/tests/unit/test_orchestrator_client.py index 4086ade..a9f77da 100644 --- a/tests/unit/test_orchestrator_client.py +++ b/tests/unit/test_orchestrator_client.py @@ -7,7 +7,7 @@ import unittest import urllib.error from unittest.mock import MagicMock, patch -from bot_bottle.control_auth import ROLE_CLI, verify +from bot_bottle.orchestrator_auth import ROLE_CLI, verify from bot_bottle.orchestrator.client import ( OrchestratorClient, OrchestratorClientError, @@ -20,13 +20,13 @@ _URLOPEN = "bot_bottle.orchestrator.client.urllib.request.urlopen" class TestHostAuthToken(unittest.TestCase): def test_mints_a_cli_token_from_the_host_key(self) -> None: - with patch("bot_bottle.orchestrator.client.host_control_plane_token", + with patch("bot_bottle.orchestrator.client.host_orchestrator_token", return_value="signing-key"): tok = _host_auth_token() self.assertEqual(ROLE_CLI, verify(tok, "signing-key")) def test_returns_empty_when_key_unreadable(self) -> None: - with patch("bot_bottle.orchestrator.client.host_control_plane_token", + with patch("bot_bottle.orchestrator.client.host_orchestrator_token", side_effect=OSError("no host root")): self.assertEqual("", _host_auth_token()) diff --git a/tests/unit/test_orchestrator_server.py b/tests/unit/test_orchestrator_server.py index 2d5c736..d55908b 100644 --- a/tests/unit/test_orchestrator_server.py +++ b/tests/unit/test_orchestrator_server.py @@ -19,7 +19,7 @@ from contextlib import closing from pathlib import Path from unittest.mock import patch -from bot_bottle.control_auth import ROLE_CLI, ROLE_GATEWAY, mint +from bot_bottle.orchestrator_auth import ROLE_CLI, ROLE_GATEWAY, mint from bot_bottle.orchestrator.broker import StubBroker from bot_bottle.orchestrator.server import dispatch, make_server from bot_bottle.orchestrator.registry import BottleRecord, RegistryStore @@ -284,7 +284,7 @@ class TestServerRoundTrip(unittest.TestCase): self.assertEqual(reg["bottle_id"], attr["bottle_id"]) -class TestControlPlaneAuth(unittest.TestCase): +class TestOrchestratorAuth(unittest.TestCase): """Role-scoped control-plane tokens (issue #400 / #469 review): every route but /health needs a valid token, and the token's role gates which routes it reaches — a `gateway` data-plane token can't drive the operator routes.""" @@ -349,7 +349,7 @@ class TestControlPlaneAuth(unittest.TestCase): self.assertIsNotNone(self.orch.registry.get(rec.bottle_id)) def _server_with_key(self, signing_key: str): - with patch.dict("os.environ", {"BOT_BOTTLE_CONTROL_PLANE_TOKEN": signing_key}): + with patch.dict("os.environ", {"BOT_BOTTLE_ORCHESTRATOR_TOKEN": signing_key}): server = make_server(self.orch, "127.0.0.1", 0) self.addCleanup(server.server_close) threading.Thread(target=server.serve_forever, daemon=True).start() @@ -360,7 +360,7 @@ class TestControlPlaneAuth(unittest.TestCase): def _status(self, url: str, *, header: str | None = None) -> int: req = urllib.request.Request(url) if header is not None: - req.add_header("x-bot-bottle-control-auth", header) + req.add_header("x-bot-bottle-orchestrator-auth", header) try: return urllib.request.urlopen(req, timeout=5).status except urllib.error.HTTPError as e: @@ -384,7 +384,7 @@ class TestControlPlaneAuth(unittest.TestCase): grants full cli access, so existing round-trip behavior is unchanged.""" with patch.dict("os.environ", {}, clear=False): import os - os.environ.pop("BOT_BOTTLE_CONTROL_PLANE_TOKEN", None) + os.environ.pop("BOT_BOTTLE_ORCHESTRATOR_TOKEN", None) server = make_server(self.orch, "127.0.0.1", 0) self.addCleanup(server.server_close) self.assertEqual(ROLE_CLI, server.role_for("")) diff --git a/tests/unit/test_policy_resolver.py b/tests/unit/test_policy_resolver.py index 44178a3..e034d23 100644 --- a/tests/unit/test_policy_resolver.py +++ b/tests/unit/test_policy_resolver.py @@ -8,11 +8,11 @@ import urllib.error from unittest.mock import MagicMock, patch from bot_bottle.gateway.policy_resolver import ( - CONTROL_AUTH_HEADER, - CONTROL_AUTH_JWT_ENV, + ORCHESTRATOR_AUTH_HEADER, + ORCHESTRATOR_AUTH_JWT_ENV, PolicyResolveError, PolicyResolver, - _control_auth_headers, + _orchestrator_auth_headers, ) _URLOPEN = "bot_bottle.gateway.policy_resolver.urllib.request.urlopen" @@ -29,16 +29,16 @@ def _http_error(code: int) -> urllib.error.HTTPError: return urllib.error.HTTPError("http://x/resolve", code, "err", {}, None) # type: ignore[arg-type] -class TestControlAuthHeaders(unittest.TestCase): +class TestOrchestratorAuthHeaders(unittest.TestCase): def test_sends_the_gateway_jwt_when_configured(self) -> None: - with patch.dict("os.environ", {CONTROL_AUTH_JWT_ENV: "gateway.jwt.tok"}): - self.assertEqual({CONTROL_AUTH_HEADER: "gateway.jwt.tok"}, _control_auth_headers()) + with patch.dict("os.environ", {ORCHESTRATOR_AUTH_JWT_ENV: "gateway.jwt.tok"}): + self.assertEqual({ORCHESTRATOR_AUTH_HEADER: "gateway.jwt.tok"}, _orchestrator_auth_headers()) def test_sends_nothing_when_unset(self) -> None: import os with patch.dict("os.environ", {}, clear=False): - os.environ.pop(CONTROL_AUTH_JWT_ENV, None) - self.assertEqual({}, _control_auth_headers()) + os.environ.pop(ORCHESTRATOR_AUTH_JWT_ENV, None) + self.assertEqual({}, _orchestrator_auth_headers()) class TestPolicyResolver(unittest.TestCase):