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.
This commit is contained in:
Italo Fernandes
2026-09-15 10:49:19 -03:00
committed by Teknium
parent e864bedf95
commit 032b91d9b9
2 changed files with 30 additions and 8 deletions

View File

@@ -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)}

View File

@@ -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