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>
Codex review on #496:
- **High — ambiguous delivery no longer orphans a launched bottle.** A
timeout / dropped response from the host controller is now the ambiguous
BrokerUnavailableError (distinct from the definite BrokerAuthError /
BrokerClientError). OrchestratorCore.launch_bottle keeps the registry
row on the ambiguous case instead of deregistering — deregistering would
orphan a running container with no record (reconcile reaps rows, never
containers). The row is left for reconcile to reap iff the bottle is not
actually live. Definite failures still roll back, so a real failure
leaves no orphan row.
- **Medium — the privileged endpoint bounds request bodies.** The host
server rejects an oversized Content-Length with 413 before reading it,
and sets a per-request socket timeout, so a caller that can merely reach
the socket (no signed token) can't exhaust memory or a handler thread.
Tests: ambiguous-keep vs definite-rollback in the launch path; the
BrokerUnavailableError/BrokerClientError split in BrokerClient; the 413
body cap + handler error paths (driven in-thread, since daemon request
threads lose coverage); and the __main__ entrypoint broker selection.
Diff-coverage 98%; pyright clean; pylint 9.88.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>