From c622dd53c445a04e2ec97cd5724e81c38e41122f Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 16 Sep 2026 16:07:18 +0530 Subject: [PATCH] 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. --- gateway/run_startup.py | 10 ++++++++-- tests/gateway/test_startup_warmup_profile_scope.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/gateway/run_startup.py b/gateway/run_startup.py index ea2e321239..5445134a42 100644 --- a/gateway/run_startup.py +++ b/gateway/run_startup.py @@ -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 diff --git a/tests/gateway/test_startup_warmup_profile_scope.py b/tests/gateway/test_startup_warmup_profile_scope.py index a336ce2784..0cb491c0df 100644 --- a/tests/gateway/test_startup_warmup_profile_scope.py +++ b/tests/gateway/test_startup_warmup_profile_scope.py @@ -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