Chunk 2 of the host-control-server stack: close the PRD's **durable secret** gap and replace chunk 1's BOT_BOTTLE_BROKER_SECRET stopgap. - trust_domain.py: two new domains. LAUNCH_BROKER holds the durable HS256 key both the orchestrator (signer) and the host control server (verifier) share for the broker's launch JWT — a host-canonical key file minted 0600 on first use, so a restarted orchestrator re-verifies against the same key. HOST_CONTROLLER is the separate domain for the controller's own lifecycle endpoints, keyed by a key the orchestrator never holds (its role is `host`, deliberately outside control-plane ROLES). LaunchBrokerProvisioning is the fail-closed seam. - orchestrator_auth.py: ROLE_HOST, outside ROLES. - paths.py: key-file + env-var constants for both domains. Key resolution is split by owner (addresses codex review on #497): broker_secret(allow_host_file=...) — the host controller / dev-harness (True) may mint/read the durable host key file it owns; the GUEST orchestrator (--broker http, default False) must be *injected* the key and fails closed if it isn't. A guest that fell back to the host file would mint a process-local key unrelated to the host controller's, so startup would succeed but every launch would 401 — this prevents that silent divergence. Tested: domain boundary + separation, provisioning fail-closed, and broker_secret env-only (guest) vs host-file (host) resolution. pyright clean; pylint 9.86. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tests
Plain-Python test suite using stdlib unittest. No external
dependencies. Unit tests run anywhere Python 3 is present; integration
tests run through the backend named by BOT_BOTTLE_BACKEND (default
docker) and skip cleanly when that backend isn't available on the host.
Layout
tests/
fixtures.py # JSON manifest builders (shared)
_backend.py # backend selection + skip guards (shared)
unit/
test_egress.py
test_egress_addon_core.py
test_manifest_egress.py
test_dlp_detectors.py
test_manifest_runtime.py
... # many others; see unit/ directory
integration/
test_gateway_image.py
test_dry_run_plan.py
test_orphan_cleanup.py
...
canaries/ # opt-in; see below (currently empty)
Classification falls out of the directory — no hand-maintained list to keep in sync.
Running
python -m unittest discover -t . -s tests/unit -v # unit only
python -m unittest discover -t . -s tests/integration -v # integration only
python -m unittest discover -t . -s tests -v # both (recursive)
python -m unittest tests.unit.test_manifest_egress # one file
Discovery is invoked with -t . (top-level dir = repo root) so the
bot_bottle package on sys.path resolves correctly.
What the integration tests cover
test_dry_run_plan.py—cli.py start --dry-run --format=jsonemits a structured plan that contains the resolved egress allowlist and the bottle's runtime, and creates zero Docker resources.test_orphan_cleanup.py—network_removeis idempotent against missing resources, so the EXIT trap can call it unconditionally.test_gateway_image.py— builds Dockerfile.gateway and probes that gitleaks / mitmdump / supervise are all reachable inside the gateway image.
Canaries
tests/canaries/ holds upstream-regression checks gated on
BOT_BOTTLE_RUN_CANARIES=1 and not part of the per-push suite.
They're invoked by the scheduled canaries workflow. Currently
no canaries are defined.
BOT_BOTTLE_RUN_CANARIES=1 python -m unittest discover -t . -s tests/canaries -v
What's NOT covered
bot_bottle/ssh.pyend-to-end (would need a fake SSH host inside the container).- A live SSH-through-git-gate tunnel against a real Tailscale-style IP.
- DLP false-positive measurements.
- TLS handling / cert pinning behavior.
Adding a test
-
Pick the directory:
tests/unit/for a pure unit test,tests/integration/for one that needs a backend. -
Filename:
test_<topic>.py. -
Boilerplate:
import unittest from bot_bottle.<module> import <symbol> class TestThing(unittest.TestCase): def test_x(self): ... if __name__ == "__main__": unittest.main() -
Skip guards live in
tests._backendand gate on the backend's own readiness check,bot_bottle.backend.has_backend— the same probe behind./cli.py backend status:- Backend-agnostic tests (go through
get_bottle_backend()) decorate the class with@skip_unless_selected_backend_available()— the test runs against whichever backendBOT_BOTTLE_BACKENDselects and skips unless that backend is available (checking, e.g., Linux +/dev/kvmfor Firecracker rather than unrelated Docker availability). - Backend-specific tests (exercise
DockerBroker,DockerGateway,backend.docker.*, …) decorate with@skip_unless_backend("docker")so they no-op under a run targeting a different backend.
Each CI integration job runs
./cli.py backend status --backend=<name>as a preflight, which prints a clear per-check summary and exits non-zero when the backend is missing — so absent infrastructure fails the job instead of hiding among per-testunittest.skiplines. - Backend-agnostic tests (go through