fix(mcp): served profiles keep their own MCP connections without the multiplex flag
A dashboard/desktop backend (and the per-profile cron ticker) serves sessions of several profiles through the HERMES_HOME contextvar override while gateway.multiplex_profiles stays off. _mcp_registry_scope() keyed every MCP connection by the bare server name in that mode, so the first profile to discover `zernio` owned the only connection and every later served profile — including one whose config carries a different Authorization header — called the server through it and got the other account's data back (#111151). The registry scope now follows the served home: a routed profile (an override naming a home other than the process home) gets the same per-profile overlay the multiplexer uses, so a same-named server with other credentials is a separate connection, discovery for profile B is a connect candidate instead of "already connected", and status/tool views stay per profile. Single-profile processes (no override) keep bare keys, byte-identical to before. Fixes #111151 Credit: #111158 by @KoNit-K located the inert flag on hermes_cli surfaces; its fix (activating fail-closed multiplex secret scoping from config.yaml on the dashboard) is not taken — the connection-key seam, not the secret-scope mode, is what leaks the connection, and flipping the process-wide mode from the dashboard would change credential resolution for every code path in it.
This commit is contained in:
@@ -241,3 +241,34 @@ def test_parallel_safe_opt_in_is_per_profile(two_profiles):
|
||||
|
||||
two_profiles("a")
|
||||
assert disc.is_mcp_tool_parallel_safe("mcp__x__t") is False
|
||||
|
||||
|
||||
def test_served_profile_without_multiplex_flag_gets_its_own_connection(two_profiles, monkeypatch):
|
||||
"""A dashboard/desktop backend serves profiles through the HERMES_HOME override with
|
||||
``gateway.multiplex_profiles`` off; a same-named server with other credentials must still be a
|
||||
separate connection there, or profile B calls the server as profile A (#111151). The launch
|
||||
profile itself (no override) keeps the bare, unscoped key."""
|
||||
import tools.mcp_tool as core
|
||||
from tools import mcp_tool_discovery as disc
|
||||
from tools import mcp_tool_registration as reg
|
||||
from tools.mcp_tool_scope import _resolve_server_key, _server_key
|
||||
from tools.registry import registry
|
||||
|
||||
monkeypatch.setattr("agent.secret_scope.is_multiplex_active", lambda: False)
|
||||
cfg_a = {"url": "https://mcp.example/x", "headers": {"Authorization": "Bearer A"}}
|
||||
cfg_b = {"url": "https://mcp.example/x", "headers": {"Authorization": "Bearer B"}}
|
||||
|
||||
scope_a = two_profiles("a")
|
||||
srv_a = _server("x", cfg_a)
|
||||
disc._adopt_server("x", srv_a)
|
||||
srv_a._registered_tool_names = reg._register_server_tools("x", srv_a, cfg_a)
|
||||
assert (scope_a, "x") in core._servers
|
||||
|
||||
two_profiles("b")
|
||||
assert _resolve_server_key("x") != (scope_a, "x")
|
||||
assert registry.get_tool_names_for_toolset("mcp-x") == []
|
||||
assert "x" in disc._select_new_servers({"x": cfg_b})
|
||||
|
||||
with patch("hermes_constants.get_hermes_home_override", return_value=None):
|
||||
assert core._mcp_registry_scope() is None
|
||||
assert _server_key("x") == "x"
|
||||
|
||||
+10
-2
@@ -637,10 +637,18 @@ def _update_death_supervisor(verb: str, pgids) -> None:
|
||||
|
||||
|
||||
def _mcp_registry_scope() -> Optional[str]:
|
||||
"""Registry scope for MCP registrations: a profile overlay under a multiplexer, else None."""
|
||||
"""Registry scope for MCP registrations: a profile overlay when this process serves profiles,
|
||||
else None. Under ``gateway.multiplex_profiles`` every turn runs scoped; a process that serves a
|
||||
routed profile through the HERMES_HOME override (dashboard/desktop backend, per-profile cron
|
||||
ticker) is a multiplexer too, even with the flag off — keying its connections by the bare name
|
||||
would hand one profile's credentialed connection to every other served profile (#111151).
|
||||
Single-profile processes (no override, or an override naming their own home) keep bare names."""
|
||||
from agent.secret_scope import is_multiplex_active
|
||||
from hermes_constants import get_hermes_home_override, get_process_hermes_home, hermes_home_key
|
||||
if not is_multiplex_active():
|
||||
return None
|
||||
override = get_hermes_home_override()
|
||||
if override is None or hermes_home_key(override) == hermes_home_key(get_process_hermes_home()):
|
||||
return None
|
||||
from tools.registry import registry
|
||||
return registry.current_scope_key()
|
||||
|
||||
|
||||
@@ -436,6 +436,7 @@ profile and never shares with the default or any sibling:
|
||||
| Session-search knobs (`sessions.cjk_fts`, `sessions.search_slow_ms`) | The profile's `config.yaml` | Documented default — never the default profile's bridged value |
|
||||
| Platform proxies (`TELEGRAM_PROXY`, `DISCORD_PROXY`, `HTTPS_PROXY`, …) | The profile's own `.env` | Direct connection — never the default profile's proxy |
|
||||
| MCP discovery in the Desktop/dashboard backend | Once per served profile home | A profile selected after another has already built an agent still discovers its own `mcp_servers` |
|
||||
| MCP connections in the Desktop/dashboard backend and the per-profile cron ticker | Keyed per served profile even with `gateway.multiplex_profiles` off — same rule as the multiplexer | A same-named `mcp_servers` entry with other credentials is its own connection; a served profile never calls a server as another profile |
|
||||
| Dashboard actions (`hermes -p <name> …` spawned by the Desktop/dashboard) | A scrubbed child env pinned to that profile's `HERMES_HOME` | The child loads its own `.env`; the dashboard profile's tokens and ports are not inherited |
|
||||
| Cron `.env` tuning (`HERMES_CRON_TIMEOUT`, `HERMES_MODEL` fallback, `HERMES_CRON_MAX_PARALLEL`, prefill file), worker / Bot Chat child env | The profile's own `.env`; children never inherit the default profile's `.env` settings or bridged `TERMINAL_*` policy | Cron defaults / model refusal, exactly as a standalone `hermes -p <name> gateway run` |
|
||||
| Kanban workers and notifications for a profile's tasks | The assignee's `.env` + `config.yaml` (toolset pin, terminal backend, media policy, display language) | — |
|
||||
|
||||
Reference in New Issue
Block a user