fix(gateway): the free-tier bootstrap runs inside the launch profile's scope under multiplex

Same bug class as the warm-up: `_start_free_tier_bootstrap` ran on a bare executor thread, and
`anon_auth.ensure_portal_identity` resolves the Portal through `_nous_portal_env_override`, so
under multiplex a non-production HERMES_PORTAL_BASE_URL read as absent and the identity would be
minted against the production Portal. Route the hop through `_run_boot_probe_in_launch_scope`.
Gated behind guest onboarding, so latent on today's fleet; one test pins the binding.
This commit is contained in:
kshitijk4poor
2026-09-16 16:07:18 +05:30
committed by kshitij
parent c71dbf7078
commit c622dd53c4
2 changed files with 18 additions and 2 deletions

View File

@@ -110,7 +110,8 @@ class GatewayStartupMixin:
async def _run_boot_probe_in_launch_scope(self, fn):
"""Run a boot-time probe on the default executor under the LAUNCH profile's scope when
multiplexing (the ``_discover_gateway_mcp_tools`` shape). Boot probes (``check_fn``s) read profile-scoped secrets; an unscoped read under multiplex fails
multiplexing (the ``_discover_gateway_mcp_tools`` shape). Boot probes (``check_fn``s, the
free-tier bootstrap) read profile-scoped secrets; an unscoped read under multiplex fails
closed, so routing overrides resolve as absent and default routing applies. ``copy_context``
carries the contextvars across the executor hop; single-profile keeps environ semantics."""
loop = asyncio.get_running_loop()
@@ -127,6 +128,11 @@ class GatewayStartupMixin:
getattr(fn, "__name__", fn), exc_info=True)
return await loop.run_in_executor(None, copy_context().run, fn)
async def _run_free_tier_bootstrap(self) -> None:
"""The free-tier bootstrap mints the Portal identity through the same profile-scoped routing
overrides the warm-up resolves, so it takes the same launch-profile binding."""
await self._run_boot_probe_in_launch_scope(self._start_free_tier_bootstrap)
async def _warm_turn_prerequisites(self) -> None:
"""Initialize turn machinery on an executor thread before the gate opens. Never raises: a
failed warm-up degrades to lazy init and must not block startup."""
@@ -1394,7 +1400,7 @@ class GatewayStartupMixin:
# identity to already exist. Blocking here, before any adapter connects, is what keeps a fast
# first DM from arriving with nothing to resolve. With the launch gate unset this is a local
# inventory and no network.
await asyncio.get_running_loop().run_in_executor(None, self._start_free_tier_bootstrap)
await self._run_free_tier_bootstrap()
# Serialize startup restore against inbound: adapters receive as soon as they connect, so inbound
# queues until every synthetic resume turn has finished.
self._startup_restore_in_progress = True

View File

@@ -88,3 +88,13 @@ def test_single_profile_warmup_keeps_environ_semantics(tmp_path, monkeypatch):
assert seen["scope_installed"] is False
assert seen["portal_override"] == PORTAL
def test_multiplex_free_tier_bootstrap_runs_inside_the_launch_profile_scope(multiplex_home, monkeypatch):
"""The free-tier bootstrap mints the Portal identity at boot through the same override; it is the
sibling unscoped executor hop and gets the same binding."""
seen: dict = {}
runner = _Runner(multiplex=True)
runner._start_free_tier_bootstrap = _probe(seen)
asyncio.run(runner._run_free_tier_bootstrap())
assert seen["scope_installed"] is True
assert seen["portal_override"] == PORTAL