From 5910de20bc9839fdd36e791a9d72ba2c2e722f66 Mon Sep 17 00:00:00 2001 From: KoNit-K <124019182+KoNit-K@users.noreply.github.com> Date: Tue, 15 Sep 2026 11:57:59 -0700 Subject: [PATCH] fix(gateway): setup.status / setup.runtime_check scope the launch profile under multiplex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_readiness_check` bound a secret scope only for a named non-launch profile and used nullcontext for the launch profile. Once the process multiplexes (`set_multiplex_active(True)`) `get_secret` fails closed, so the launch profile's `setup.runtime_check` died on the first profile-scoped read inside `resolve_runtime_provider` (`HERMES_CODEX_BASE_URL` in `_pool_entry_mode_and_url`, added by b62bb2a3d5) and the Desktop showed onboarding while sessions — which resolve under `_session_profile_runtime_scope` — worked fine. Route the launch profile through the same helper: `profile_home=None` binds the launch profile's frozen `.env` scope only when multiplexing is active (`_profile_runtime_scope_tokens` returns None otherwise, keeping the single-profile `os.environ` fallthrough for systemd / `op run` injection). The unknown-profile `ok:False` answer is untouched — no `@_profile_scoped`, whose `_profile_home` raise would turn it into an error. Slimmer shape than the PR's `scope_launch_profile` flag: the flag guarded nothing the helper does not already decide, and setup.status reads the same `.env`-derived state. Fixes #112061 --- tests/tui_gateway/test_tui_gateway_server.py | 39 ++++++++++++++++++++ tui_gateway/methods_config.py | 11 +++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/tui_gateway/test_tui_gateway_server.py b/tests/tui_gateway/test_tui_gateway_server.py index eba5eec7bd..aedced354f 100644 --- a/tests/tui_gateway/test_tui_gateway_server.py +++ b/tests/tui_gateway/test_tui_gateway_server.py @@ -9289,6 +9289,45 @@ def test_setup_runtime_check_reports_target_model_on_credential_failure(monkeypa assert resp["result"]["ok"] is False assert resp["result"]["model"] == "z-ai/glm-5.2" +def test_setup_runtime_check_scopes_launch_profile_in_multiplex_backend(monkeypatch, tmp_path): + """The launch profile needs a scope too when its Codex route reads an override.""" + from agent import secret_scope + from tui_gateway import launch_profile_policy + + launch_home = tmp_path / ".hermes" + launch_home.mkdir() + monkeypatch.setenv("HERMES_CODEX_BASE_URL", "https://codex.launch.test/v1") + monkeypatch.setattr(server, "_hermes_home", launch_home) + monkeypatch.setattr(launch_profile_policy, "_snapshot", None) + monkeypatch.setattr("hermes_cli.main._has_any_provider_configured", lambda **_kw: True) + monkeypatch.setattr(server, "_resolve_startup_runtime", lambda: ("gpt-5.3-codex", None)) + + def resolve_codex(requested=None, **_kwargs): + assert requested == "openai-codex" + return { + "provider": "openai-codex", + "api_key": "codex-oauth-token", + "base_url": secret_scope.get_secret("HERMES_CODEX_BASE_URL"), + "source": "credential-pool", + } + + monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", resolve_codex) + secret_scope.set_multiplex_active(True) + try: + response = server.handle_request( + {"id": "1", "method": "setup.runtime_check", "params": {"provider": "openai-codex"}} + ) + finally: + secret_scope.set_multiplex_active(False) + + assert response["result"] == { + "ok": True, + "provider": "openai-codex", + "model": "gpt-5.3-codex", + "source": "credential-pool", + "free_tier": False, + } + def test_setup_readiness_scopes_to_requested_profile(monkeypatch, tmp_path): """#94071: the Desktop preflights a freshly created bot on its target diff --git a/tui_gateway/methods_config.py b/tui_gateway/methods_config.py index 87daa9480d..c0ed508659 100644 --- a/tui_gateway/methods_config.py +++ b/tui_gateway/methods_config.py @@ -248,18 +248,19 @@ def _readiness_check(rid, params, probe): stay isolated); ``scoped`` is the ``{"profile": ...}`` payload stamp (``{}`` for the launch profile). An unknown profile answers ``ok=False`` (never a JSON-RPC error, never a quiet answer for the launch profile instead).""" - import contextlib profile = str(params.get("profile") or "").strip() if isinstance(params, dict) else "" - scope = contextlib.nullcontext() + home = None if profile: from hermes_cli import profiles as profiles_mod if not profiles_mod.profile_exists(profile): return _ok(rid, {"ok": False, "profile": params.get("profile"), "error": f"Profile '{profile}' does not exist on this backend."}) home = _profile_home(profile) - if home is not None: - scope = _session_profile_runtime_scope({"profile_home": str(home)}) - with scope: + # ``profile_home=None`` is the launch profile: once this process multiplexes its probe must + # run under its own frozen secret scope too (``_profile_runtime_scope_tokens`` binds nothing in + # a single-profile process), or the first profile-scoped read inside the resolver + # (``HERMES_CODEX_BASE_URL`` for openai-codex) fails closed and the UI shows onboarding. + with _session_profile_runtime_scope({"profile_home": str(home) if home is not None else None}): payload = probe(profile, {"profile": profile} if profile else {}) return _ok(rid, payload)