From f1fde49e453e508b9dc49ec5b70694fbbf0279c0 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:25:39 -0700 Subject: [PATCH] fix(gateway): avoid cross-profile session recovery (#59325) Co-authored-by: yoma --- gateway/session.py | 51 ++++++++++++++++++++ tests/gateway/test_multiplex_phase0.py | 64 ++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/gateway/session.py b/gateway/session.py index 9d20c204a83..c0f631ffd2b 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -1259,6 +1259,45 @@ class SessionStore: except Exception: return None + @staticmethod + def _profile_from_session_key(session_key: Optional[str]) -> Optional[str]: + """Extract the profile namespace encoded in a gateway session key.""" + if not session_key: + return None + parts = str(session_key).split(":") + if len(parts) < 2 or parts[0] != "agent": + return None + namespace = parts[1] or "main" + return "default" if namespace == "main" else namespace + + @staticmethod + def _active_profile_name() -> str: + try: + from hermes_cli.profiles import get_active_profile_name + return get_active_profile_name() or "default" + except Exception: + return "default" + + def _recovered_row_allowed_for_active_profile( + self, + *, + requested_session_key: str, + recovered: Dict[str, Any], + ) -> bool: + """Prevent non-multiplexed gateways from reviving another profile's row.""" + if getattr(self.config, "multiplex_profiles", False): + return True + + recovered_key = str(recovered.get("session_key") or "") + if not recovered_key or recovered_key == requested_session_key: + return True + + recovered_profile = self._profile_from_session_key(recovered_key) + if recovered_profile is None: + return True + + return recovered_profile == self._active_profile_name() + def _generate_session_key(self, source: SessionSource) -> str: """Generate a session key from a source.""" return build_session_key( @@ -1319,6 +1358,18 @@ class SessionStore: return None if not recovered: return None + if not self._recovered_row_allowed_for_active_profile( + requested_session_key=session_key, + recovered=recovered, + ): + logger.warning( + "Gateway session DB recovery ignored %s for %s because " + "multiplex_profiles is disabled and the row belongs to a " + "different profile", + recovered.get("session_key"), + session_key, + ) + return None try: self._db.reopen_session(str(recovered["id"])) except Exception as exc: diff --git a/tests/gateway/test_multiplex_phase0.py b/tests/gateway/test_multiplex_phase0.py index 798bca3a7d2..f7f5dbdc62d 100644 --- a/tests/gateway/test_multiplex_phase0.py +++ b/tests/gateway/test_multiplex_phase0.py @@ -9,6 +9,7 @@ Covers the three Phase 0 deliverables: on. """ import pytest +from datetime import datetime from unittest.mock import patch import yaml @@ -201,3 +202,66 @@ class TestSessionStoreProfileResolution: with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"): assert store._generate_session_key(s) == "agent:main:telegram:dm:99" + +class _RecoveringDB: + def __init__(self, row): + self.row = row + self.reopened = [] + + def find_latest_gateway_session_for_peer(self, **_kwargs): + return self.row + + def reopen_session(self, session_id): + self.reopened.append(session_id) + + +class TestSessionStoreUnmultiplexedRecovery: + """Turning multiplexing off must not recover another profile's session.""" + + def _store_with_row(self, tmp_path, row, **cfg_kw): + config = GatewayConfig(**cfg_kw) + with patch("gateway.session.SessionStore._ensure_loaded"): + store = SessionStore(sessions_dir=tmp_path, config=config) + store._db = _RecoveringDB(row) + store._loaded = True + return store + + def test_flag_off_rejects_other_profile_peer_fallback(self, tmp_path): + row = { + "id": "sess-coder", + "started_at": 1700000000, + "session_key": "agent:coder:telegram:dm:99", + } + store = self._store_with_row(tmp_path, row) + source = _src(chat_id="99", chat_type="dm") + + with patch("hermes_cli.profiles.get_active_profile_name", return_value="default"): + recovered = store._recover_session_from_db( + session_key="agent:main:telegram:dm:99", + source=source, + now=datetime.fromtimestamp(1700000001), + ) + + assert recovered is None + assert store._db.reopened == [] + + def test_flag_off_allows_active_profile_peer_fallback(self, tmp_path): + row = { + "id": "sess-coder", + "started_at": 1700000000, + "session_key": "agent:coder:telegram:dm:99", + } + store = self._store_with_row(tmp_path, row) + source = _src(chat_id="99", chat_type="dm") + + with patch("hermes_cli.profiles.get_active_profile_name", return_value="coder"): + recovered = store._recover_session_from_db( + session_key="agent:main:telegram:dm:99", + source=source, + now=datetime.fromtimestamp(1700000001), + ) + + assert recovered is not None + assert recovered.session_id == "sess-coder" + assert recovered.session_key == "agent:main:telegram:dm:99" + assert store._db.reopened == ["sess-coder"]