feat(relay): carry routed profile from the connector wire source (#60586)

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/<profile>/ 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.
This commit is contained in:
Ben Barclay 2026-07-08 10:20:23 +10:00 committed by GitHub
parent 8d66e78844
commit 4e4a69cbf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 0 deletions

View file

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

View file

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