feat(gateway): route shared bot chats to profiles - #57873
Conversation
Allow a multiplexed gateway to keep one inbound polling adapter/token on the default profile and route selected chats or topics into named profile homes via <platform>.profile_routes. This supports one public bot identity across multiple isolated profile memories, skills, configs, and session namespaces without duplicate polling.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for covering the shared-token routing use case. The capability is still absent from current main, but the current placement is unsafe.
Problems
gateway/run.py:8786resolves the route only after authorization.gateway/authz_mixin.py:249-262chooses a pairing store fromsource.profile; therefore routed traffic is checked against the default profile's authorization state, then executes with the target profile's configuration and credentials.gateway/platforms/base.py:4660-4664derives the adapter active-session key before it calls_handle_message. The PR only setssource.profileatgateway/run.py:8793, so that key remains unprofiled. The direct helper assertion intests/gateway/test_multiplex_shared_profile_routes.py:76-78does not exercise this path.
Suggested changes
- Stamp a validated route in
BasePlatformAdapter.build_sourcebefore authorization and before any adapter session-key/queue work, then route auth using that source. - Thread
source.profilethrough adapter-owned session/batch key construction and add an adapter-to-handler integration test for routing, authorization, and namespacing.
Automated hermes-sweeper review.
| and not getattr(source, "profile", None) | ||
| and getattr(getattr(self, "config", None), "multiplex_profiles", False) | ||
| ): | ||
| _route_profile = _resolve_shared_credential_profile_route( |
There was a problem hiding this comment.
This lookup is after _is_user_authorized(source). In multiplex mode the authorization mixin selects the pairing store from source.profile, so this admits users under the default profile's policy and only then switches them into the target profile. Resolve and stamp the route before authorization.
| if _route_profile: | ||
| event = dataclasses.replace( | ||
| event, | ||
| source=dataclasses.replace(source, profile=_route_profile), |
There was a problem hiding this comment.
This stamp is too late for adapter-owned state: BasePlatformAdapter.handle_message() builds its active-session key before it invokes this handler. That key is still agent:main:...; route the source at build_source() time and pass its profile through all adapter session/batch-key calls.
|
This capability has now landed on main via PR #64835 (a salvage of #20096 by @Burgunthy, the earliest submission): gateway:
multiplex_profiles: true
profile_routes:
- platform: telegram
chat_id: "-100111"
profile: research
- platform: telegram
chat_id: "-100111"
thread_id: "42"
profile: supportThe merged matcher additionally supports guild routing, parent-chain matching (threads inherit their channel's route), explicit specificity ordering, and profile-name validation, and routes at Docs: https://hermes-agent.nousresearch.com/docs/user-guide/multi-profile-gateways You independently arrived at the same design the merged feature uses — good instincts, and thanks for the contribution. Closing as superseded. |
Summary
Adds shared-credential chat/topic → profile routing for multiplexed gateways.
This lets one platform adapter/bot token own inbound messages while routing selected chats/topics into named Hermes profiles. It preserves the duplicate-polling/token-lock invariant because routed profiles do not start their own adapter; only the agent turn/session namespace is profile-stamped after authorization succeeds.
Why
The existing multiplex docs correctly require separate credentials for separate adapters. That is safe, but it leaves an important operator workflow uncovered:
The safe model is:
What changed
_resolve_shared_credential_profile_route(...)and a compact route matcher.SessionSource.profilewhen a route matches.Safety notes
gateway.multiplex_profiles: true.defaultroutes are no-ops.Test plan
python -m pytest tests/gateway/test_multiplex_shared_profile_routes.py tests/gateway/test_multiplex_phase0.py -q -o 'addopts=' python -m compileall -q gateway/run.py tests/gateway/test_multiplex_shared_profile_routes.py python -m ruff check gateway/run.py tests/gateway/test_multiplex_shared_profile_routes.py git diff --checkLocal result:
Closes #57871