fix(mcp): retain profile secret scope during discovery
(cherry picked from commit f344095fcf16c5d154ef2d207cce9b13339bf2e4)
This commit is contained in:
@@ -4,10 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from contextlib import nullcontext
|
||||
from contextvars import copy_context
|
||||
from typing import Optional
|
||||
|
||||
from hermes_constants import get_hermes_home_override, reset_hermes_home_override, set_hermes_home_override
|
||||
|
||||
_mcp_discovery_lock = threading.Lock()
|
||||
_mcp_discovery_started = False
|
||||
_mcp_discovery_thread: Optional[threading.Thread] = None
|
||||
@@ -95,15 +94,13 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
if not _has_configured_mcp_servers():
|
||||
return
|
||||
|
||||
# Re-install the caller's context-local HERMES_HOME override (multi-profile dashboard/desktop
|
||||
# backends) inside the thread: ContextVars don't propagate into bare threads, so a session
|
||||
# switched to profile X would otherwise discover the LAUNCH profile's mcp_servers.
|
||||
# The config gate above already runs on the caller's thread, so it sees the same override. See
|
||||
# #67605.
|
||||
home_override = get_hermes_home_override()
|
||||
|
||||
# Bare threads start from an empty context: run discovery under a copy of the caller's, so
|
||||
# the context-local HERMES_HOME override (multi-profile dashboard/desktop backends, #67605)
|
||||
# AND the profile's secret scope reach it. Without the scope a session switched to profile
|
||||
# X would discover the LAUNCH profile's mcp_servers, and ``${TOKEN}`` interpolation / the
|
||||
# stdio child env would fail closed (multiplex) or resolve the launch profile's value.
|
||||
# The config gate above already runs on the caller's thread, so it sees the same context.
|
||||
def _discover() -> None:
|
||||
token = set_hermes_home_override(home_override)
|
||||
try:
|
||||
_discover_mcp_tools_without_interactive_oauth()
|
||||
try:
|
||||
@@ -114,12 +111,11 @@ def start_background_mcp_discovery(*, logger, thread_name: str) -> None:
|
||||
except Exception:
|
||||
logger.debug("Background MCP tool discovery failed", exc_info=True)
|
||||
finally:
|
||||
reset_hermes_home_override(token)
|
||||
with _mcp_discovery_lock:
|
||||
global _mcp_discovery_thread
|
||||
_mcp_discovery_thread = None
|
||||
|
||||
thread = threading.Thread(target=_discover, name=thread_name, daemon=True)
|
||||
thread = threading.Thread(target=copy_context().run, args=(_discover,), name=thread_name, daemon=True)
|
||||
_mcp_discovery_thread = thread
|
||||
thread.start()
|
||||
|
||||
|
||||
@@ -236,6 +236,46 @@ def test_background_mcp_discovery_suppresses_interactive_oauth(monkeypatch):
|
||||
assert state["active"] is False
|
||||
|
||||
|
||||
def test_background_mcp_discovery_propagates_profile_secret_scope(monkeypatch):
|
||||
"""A dashboard-profile discovery thread must retain that profile's secrets."""
|
||||
from agent.secret_scope import current_secret_scope, reset_secret_scope, set_secret_scope
|
||||
|
||||
seen = []
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"hermes_cli.config",
|
||||
types.SimpleNamespace(
|
||||
read_raw_config=lambda: {"mcp_servers": {"demo": {"url": "https://mcp.example.test/mcp"}}},
|
||||
),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"tools.mcp_oauth",
|
||||
types.SimpleNamespace(suppress_interactive_oauth=lambda: nullcontext()),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"tools.mcp_tool_discovery",
|
||||
types.SimpleNamespace(
|
||||
discover_mcp_tools=lambda: seen.append(dict(current_secret_scope() or {})),
|
||||
),
|
||||
)
|
||||
|
||||
expected_scope = {"MCP_DEMO_TOKEN": "profile-only-secret"}
|
||||
token = set_secret_scope(expected_scope)
|
||||
try:
|
||||
mcp_startup.start_background_mcp_discovery(
|
||||
logger=types.SimpleNamespace(debug=lambda *_a, **_k: None),
|
||||
thread_name="test-mcp-discovery",
|
||||
)
|
||||
assert mcp_startup._mcp_discovery_thread is not None
|
||||
mcp_startup._mcp_discovery_thread.join(timeout=1.0)
|
||||
finally:
|
||||
reset_secret_scope(token)
|
||||
|
||||
assert seen == [expected_scope]
|
||||
|
||||
|
||||
def test_portable_only_mcp_configuration_opens_startup_gate(monkeypatch):
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
|
||||
Reference in New Issue
Block a user