From 032b91d9b97a300ceebee28fa73a075f146f6421 Mon Sep 17 00:00:00 2001 From: Italo Fernandes Date: Tue, 15 Sep 2026 10:49:19 -0300 Subject: [PATCH] fix(gateway): reject inbound events when route matching fails A matcher exception fell through to the default profile, so a transient failure while resolving a sender route silently served the message from the default profile's runtime. Raise the existing rejection instead, which the ingress gate already drops on. Only the failure path changes: a source that simply matches no route keeps falling through to the default/active profile as before. --- gateway/run.py | 6 ++--- tests/gateway/test_profile_resolution.py | 32 ++++++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 4f82c6895a..3bf9d1605b 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -4311,11 +4311,11 @@ class GatewayRunner( chat_id=source.chat_id, thread_id=getattr(source, "thread_id", None), parent_chat_id=getattr(source, "parent_chat_id", None), adapter_profile=adapter_profile, user_id=getattr(source, "user_id", None)) - except Exception: + except Exception as exc: logger.warning( - "Profile route matching failed for %s/%s, falling back to default", + "Rejecting %s/%s: profile route matching failed", source.platform, source.chat_id, exc_info=True) - return None + raise ProfileRouteRejected("matcher") from exc if matched: try: served = {name for name, _home in _multiplex_profile_homes(config)} diff --git a/tests/gateway/test_profile_resolution.py b/tests/gateway/test_profile_resolution.py index 1a57387126..d3dbc2031c 100644 --- a/tests/gateway/test_profile_resolution.py +++ b/tests/gateway/test_profile_resolution.py @@ -89,10 +89,10 @@ class TestMissingProfileWarning: with patch("hermes_constants.get_hermes_home", return_value=Path("/hermes")): with caplog.at_level(logging.WARNING): result = mock_runner._resolve_profile_home_for_source(discord_source) - + # Should fall back to global HERMES_HOME assert result == Path("/hermes") - + # Should have logged a warning assert len(caplog.records) == 1 assert caplog.records[0].levelname == "WARNING" @@ -141,9 +141,10 @@ class TestRoutingConsultation: mock_get_dir.return_value = Path("/hermes/profiles/routed") mock_runner._profile_name_for_source = MagicMock(return_value="routed") - - mock_runner._resolve_profile_home_for_source(discord_source) - + + with patch("hermes_cli.profiles.profile_exists", return_value=True): + mock_runner._resolve_profile_home_for_source(discord_source) + # Should have called routing mock_runner._profile_name_for_source.assert_called_once_with(discord_source) @@ -412,6 +413,27 @@ class TestAdapterToSessionKeyIntegration: ) assert result is None + def test_matcher_failure_rejects_instead_of_serving_the_default_profile(self, mock_runner): + mock_runner.config.multiplex_profiles = True + mock_runner.config.profile_routes = [ + ProfileRoute(name="r", platform="discord", profile="routed", chat_id="c") + ] + with patch("gateway.profile_routing.match_profile_route", side_effect=RuntimeError("boom")): + with pytest.raises(ProfileRouteRejected): + mock_runner._profile_name_for_source( + SessionSource(platform=Platform.DISCORD, chat_id="c") + ) + + def test_plain_no_match_still_serves_the_active_profile(self, mock_runner): + # Only failures fail closed; an ordinary unrouted sender keeps the historical behaviour. + mock_runner.config.multiplex_profiles = True + mock_runner.config.profile_routes = [ + ProfileRoute(name="r", platform="discord", profile="routed", chat_id="other") + ] + source = SessionSource(platform=Platform.DISCORD, chat_id="c", user_id="nobody") + assert mock_runner._profile_name_for_source(source) is None + assert mock_runner._resolve_profile_home_for_source(source) is not None + @pytest.mark.asyncio async def test_direct_source_is_rejected_at_shared_ingress(self, mock_runner): mock_runner.config.multiplex_profiles = True