From 4e4a69cbf7c3c92f8965552bc26afaa50248ee78 Mon Sep 17 00:00:00 2001 From: Ben Barclay Date: Wed, 8 Jul 2026 10:20:23 +1000 Subject: [PATCH] feat(relay): carry routed profile from the connector wire source (#60586) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multiplex machinery already routes an inbound message to a profile via SessionSource.profile (build_session_key namespacing + the per-turn config/credential scope in SessionStore._resolve_profile_for_key). But the relay path never populated it: _event_from_wire rebuilt the SessionSource field-by-field and dropped any 'profile' the connector sent, so a Team-Gateway (connector + relay) message could not be routed to a specific profile the way the /p// HTTP prefix and per-credential polling adapters already can. Stamp source.profile from the wire payload in _event_from_wire. This is the last missing link for NAS-driven per-profile routing over the relay in multiplex mode; the connector populating the field ships separately (gateway-gateway contract adds the optional wire field). Back-compat: absent 'profile' → None → legacy agent:main namespace, byte-identical to today for every single-profile gateway. --- gateway/relay/ws_transport.py | 9 ++++ tests/gateway/test_relay_upstream_authz.py | 48 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/gateway/relay/ws_transport.py b/gateway/relay/ws_transport.py index 2f79222a446..f93c3c2cd92 100644 --- a/gateway/relay/ws_transport.py +++ b/gateway/relay/ws_transport.py @@ -120,6 +120,15 @@ def _event_from_wire(raw: Dict[str, Any]) -> MessageEvent: scope_id=src.get("scope_id"), parent_chat_id=src.get("parent_chat_id"), message_id=src.get("message_id"), + # The HERMES profile this event is routed to (multiplex mode). The + # connector stamps it on the wire source when NAS resolves the target + # profile for a Team-Gateway message; absent for a single-profile + # gateway, where it stays None and session keys keep the legacy + # ``agent:main`` namespace (SessionStore._resolve_profile_for_key). + # Consumed by build_session_key's profile namespacing + the per-turn + # config/credential scope — the same field the /p// HTTP + # prefix and per-credential polling adapters already set. + profile=src.get("profile"), # Authentic upstream-trust signal: this event arrived over the # per-instance-authenticated relay WS, so the connector already resolved # it to this instance's owner-bound author. ``platform`` is the diff --git a/tests/gateway/test_relay_upstream_authz.py b/tests/gateway/test_relay_upstream_authz.py index 1fca02b1745..c712331124e 100644 --- a/tests/gateway/test_relay_upstream_authz.py +++ b/tests/gateway/test_relay_upstream_authz.py @@ -243,3 +243,51 @@ def test_event_from_wire_sets_relay_delivery_marker(): ) assert event.source.platform is Platform.DISCORD assert event.source.delivered_via_upstream_relay is True + + +def test_event_from_wire_stamps_routed_profile(): + """A connector-routed profile on the wire source lands on SessionSource. + + In multiplex mode the connector resolves the target HERMES profile for a + Team-Gateway message and stamps ``profile`` on the wire source. The relay + transport must carry it through so build_session_key namespaces the session + and the agent turn resolves that profile's config/credentials. + """ + from gateway.relay.ws_transport import _event_from_wire + + event = _event_from_wire( + { + "text": "hello!", + "source": { + "platform": "discord", + "chat_id": "123", + "chat_type": "dm", + "user_id": "267171776755269633", + "user_name": "rewbs", + "profile": "reviewer", + }, + } + ) + assert event.source.profile == "reviewer" + + +def test_event_from_wire_profile_absent_is_none(): + """No ``profile`` on the wire (single-profile gateway) → None. + + Back-compat: a connector that never sets ``profile`` yields the legacy + behaviour, and session keys stay in the ``agent:main`` namespace. + """ + from gateway.relay.ws_transport import _event_from_wire + + event = _event_from_wire( + { + "text": "hi", + "source": { + "platform": "discord", + "chat_id": "123", + "chat_type": "dm", + "user_id": "1", + }, + } + ) + assert event.source.profile is None