fix(relay): carry routed profile through the passthrough-plane forward

The relay text lane stamps `SessionSource.profile` from the wire frame
(#60586), but `PassthroughForward` had no profile field, so a relayed
Discord slash-command/button/modal always landed in the default profile's
agent:main namespace even when the connector resolved a specific profile.

Add an optional `profile` to `PassthroughForward` (read off the wire in
`_passthrough_from_wire`) and stamp it on the interaction's SessionSource.
Absent on the wire → None → legacy routing, byte-identical for
single-profile gateways. Contract doc updated.

Salvaged from #61012 (pierrenode); two context conflicts resolved
(delivered_via_upstream_relay / _platform_by_chat landed on main).
This commit is contained in:
pierrenode
2026-09-02 03:42:56 -07:00
committed by Teknium
parent dca9a54427
commit 3245264668
4 changed files with 82 additions and 3 deletions

View File

@@ -119,8 +119,13 @@ Both absent ⇒ byte-identical to today. A connector that never sends them, or a
`PassthroughForward` is the wire form of a forwarded passthrough-plane request
(Class-2/3 webhooks — Discord interactions, Twilio): `{platform, botId, method,
path, headers: [[k,v],…], bodyB64}`. The body is base64-encoded so arbitrary
bytes survive the newline-delimited-JSON transport; the gateway base64-decodes
path, headers: [[k,v],…], bodyB64, profile?}`. `profile` is optional — the
connector stamps it when NAS resolves the target profile for a Team-Gateway
interaction; omitting it (single-profile gateways) preserves legacy routing to
the default `agent:main` session namespace, mirroring the `profile` field the
`inbound` frame's `SessionSource` already carries (#60586). The body is
base64-encoded so arbitrary bytes survive the newline-delimited-JSON transport;
the gateway base64-decodes
back to the exact bytes the connector forwarded (the connector already verified
the provider signature and stripped any shared-identity credential at the edge —
§6 — so the gateway re-processes a sanitized, token-free body and acts on it via

View File

@@ -1620,6 +1620,13 @@ class RelayAdapter(BasePlatformAdapter):
# how platform=RELAY home channels slipped through in the first
# place. Set locally, never read off the wire.
delivered_via_upstream_relay=True,
# The HERMES profile this interaction is routed to (multiplex
# mode) — mirrors _event_from_wire's profile stamping for plain
# relayed messages (#60586). Without this, a Team-Gateway's
# Discord slash-command/button/modal always fell back to the
# legacy agent:main namespace even when the connector resolved
# a specific profile for it.
profile=getattr(forward, "profile", None),
)
event = MessageEvent(text=text, message_type=message_type, source=source)
if itype == 3:

View File

@@ -402,6 +402,16 @@ class PassthroughForward:
path: str
headers: list[tuple[str, str]]
body: bytes
# The HERMES profile this interaction is routed to (multiplex mode).
# Mirrors the ``profile`` field _event_from_wire already carries on the
# ``inbound`` frame's SessionSource (#60586) — the connector stamps it
# when NAS resolves the target profile for a Team-Gateway interaction;
# absent for a single-profile gateway, where it stays None and session
# keys keep the legacy ``agent:main`` namespace. Without this, a Discord
# slash-command/button/modal relayed through the passthrough plane always
# fell back to agent:main even when the equivalent plain message would
# have been routed to the correct profile.
profile: Optional[str] = None
def _passthrough_from_wire(raw: Dict[str, Any]) -> PassthroughForward:
@@ -431,6 +441,7 @@ def _passthrough_from_wire(raw: Dict[str, Any]) -> PassthroughForward:
path=str(raw.get("path", "")),
headers=headers,
body=body,
profile=raw.get("profile"),
)

View File

@@ -44,7 +44,7 @@ def adapter():
return RelayAdapter(PlatformConfig(), _desc(), transport=StubConnector(_desc()))
def _interaction_forward(payload: dict) -> PassthroughForward:
def _interaction_forward(payload: dict, *, profile: str | None = None) -> PassthroughForward:
body = json.dumps(payload).encode("utf-8")
return PassthroughForward(
platform="discord",
@@ -53,6 +53,7 @@ def _interaction_forward(payload: dict) -> PassthroughForward:
path="/interactions/discord/appShared",
headers=[("content-type", "application/json")],
body=body,
profile=profile,
)
@@ -75,6 +76,27 @@ def test_passthrough_from_wire_byte_preserves_body():
assert fwd.headers == [("content-type", "application/json")]
def test_passthrough_from_wire_stamps_routed_profile():
"""A connector-routed profile on the wire frame lands on PassthroughForward.
Mirrors _event_from_wire's profile stamping for the ``inbound`` frame
(#60586) — the passthrough plane needs the same carry-through so a
Team-Gateway's Discord interactions route to the same profile a plain
message would.
"""
wire = {
"platform": "discord",
"botId": "appShared",
"method": "POST",
"path": "/interactions/discord/appShared",
"headers": [],
"bodyB64": "",
"profile": "reviewer",
}
fwd = _passthrough_from_wire(wire)
assert fwd.profile == "reviewer"
@pytest.mark.asyncio
async def test_connect_wires_passthrough_handler_over_ws(adapter):
"""connect() registers the passthrough handler on the transport so a
@@ -137,6 +159,40 @@ async def test_discord_interaction_routes_through_handle_message(adapter, monkey
assert adapter._platform_by_chat.get("chan-9") == "discord"
@pytest.mark.asyncio
async def test_discord_interaction_stamps_routed_profile(adapter, monkeypatch):
"""A connector-routed profile on the passthrough forward lands on the
resulting event's SessionSource, the same way it does for a plain relayed
message (#60586) — so a Team-Gateway's Discord slash-command/button/modal
routes to the same profile a plain message would, instead of always
falling back to agent:main."""
await adapter.connect()
stub = adapter._transport
seen = []
async def fake_handle(event):
seen.append(event)
monkeypatch.setattr(adapter, "handle_message", fake_handle)
fwd = _interaction_forward(
{
"id": "interaction-2",
"type": 2, # APPLICATION_COMMAND
"channel_id": "chan-9",
"guild_id": "guild-7",
"data": {"name": "summarize"},
"member": {"user": {"id": "user-3", "username": "ben"}},
},
profile="reviewer",
)
await stub.push_passthrough(fwd, buffer_id=None)
assert len(seen) == 1
assert seen[0].source.profile == "reviewer"
@pytest.mark.asyncio
async def test_application_command_subcommand_nesting_renders_names_then_values(
adapter, monkeypatch