feat(gateway): multiplex_routing_only — single-credential multi-persona over the shared adapters - #61689
feat(gateway): multiplex_routing_only — single-credential multi-persona over the shared adapters#61689CocaKova wants to merge 1 commit into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the concrete Matrix multiplexing use case. The room mapping is a useful direction, but the routing-only path needs revision before it can safely use the existing multiplex machinery.
Problems
gateway/run.py:8473returns before current main's secondary-profile startup populates_profile_adaptersand per-profile pairing state (gateway/run.py:8489-8518). The mapped-room change then stamps a non-defaultsource.profile; currentgateway/authz_mixin.py:50-57deliberately resolves an unregistered stamped profile to no adapter, andgateway/authz_mixin.py:249-262falls back to the global pairing store. Routing-only therefore bypasses profile-specific adapter/authorization state.gateway/run.py:8473addsHERMES_MULTIPLEX_ROUTING_ONLY, a non-secret behavior flag.AGENTS.md:102-107requires this configuration surface to live inconfig.yaml.- The two-file PR contains no tests for mapped-room routing or routing-only startup.
Suggested changes
- Model routing-only as explicit shared-Matrix-adapter support that still registers mapped profiles' authorization/pairing state and routes egress through the shared adapter.
- Put the opt-in in
GatewayConfig/config.yaml, and add end-to-end-focused tests for mapped inbound messages, profile scope/session namespace, authorization, and replies.
This is an automated hermes-sweeper review.
| @@ -8470,6 +8470,14 @@ async def _start_secondary_profile_adapters(self) -> int: | |||
| if not getattr(self.config, "multiplex_profiles", False): | |||
| return 0 | |||
|
|
|||
| if os.environ.get("HERMES_MULTIPLEX_ROUTING_ONLY", "").strip().lower() in ("1", "true", "yes", "on"): | |||
There was a problem hiding this comment.
Please do not add a new user-facing HERMES_* switch for this behavior. AGENTS.md:102-107 requires non-secret feature configuration to use config.yaml; wire this through GatewayConfig instead.
| @@ -2702,6 +2725,14 @@ async def _resolve_message_context( | |||
| if thread_id: | |||
| self._threads.mark(thread_id) | |||
|
|
|||
There was a problem hiding this comment.
In routing-only mode _start_secondary_profile_adapters() returns before registering this non-default profile. Current gateway/authz_mixin.py:50-57 then resolves this stamped source to no adapter, and pairing falls back to the global store. Please establish explicit shared-adapter and per-profile authorization/pairing state before stamping the mapped profile.
b1a2aa4 to
7a0521a
Compare
|
Thanks — the review was right that routing-only was bypassing per-profile authorization state. Reworked in 70318dcb2 (rebased onto current main): Opt-in moved to Routing-only now registers, it doesn't skip. Latent bug found and fixed along the way: the per-profile PairingStore loop referenced Tests ( |
7a0521a to
70318dc
Compare
|
Heads-up: the Two things in this PR remain valuable beyond main:
A rebase narrowing this PR to those two pieces (reusing the merged route matcher for the room mapping) would be very welcome. Leaving open. |
#65118) The served-profiles block in _start_secondary_profile_adapters references PairingStore, but the class's only import in gateway/run.py is method-local inside __init__ — so the reference raised NameError at runtime, silently swallowed by the enclosing try/except ('could not record served_profiles'). Result: multiplexing gateways never created per-profile pairing stores, and authz pairing checks for secondary profiles fell through to the global whitelist. Also masked the served_profiles runtime-status write. One-line fix (local import alongside write_runtime_status) + regression tests that drive the real method and assert the stores materialize, verified red without the import and green with it. Surfaced during the profile-routing sweep by @CocaKova's PR #61689, which included the same fix as part of a larger feature.
|
Update: the |
…e shared adapters Adds gateway.multiplex_routing_only (opt-in refinement of multiplex_profiles; top-level and nested gateway.* forms, mirroring multiplex_profiles). With it on, the gateway starts NO secondary-profile adapters — every served profile shares the default profile's connections and credentials, routed per-chat via the merged gateway.profile_routes matcher. Served profiles are still REGISTERED: _profile_adapters[profile] points at the shared adapter map and each profile gets its own PairingStore, so a route-stamped source.profile resolves to a live adapter for authorization and egress instead of failing closed, while pairing whitelists stay profile-isolated. This is the missing shape for single-credential multi-persona deployments (e.g. one Matrix account serving a different profile per room): under plain multiplexing each served profile tries to start its own adapters, the shared credential is refused as a duplicate poll, the profile stays unregistered, and _authorization_adapter fails closed on its routed messages. Narrowed from the original PR per review: room_profile_map is superseded by gateway.profile_routes (NousResearch#64835) and the PairingStore NameError fix landed via NousResearch#65118 — this PR now carries only the routing-only mode. Documented in website/docs/user-guide/multi-profile-gateways.md alongside profile_routes. tests/gateway/test_multiplex_routing_only.py: config parsing (defaults, top-level, nested, to_dict roundtrip, load_gateway_config), shared-adapter registration without starting secondaries, authorization/egress resolution for stamped sources, profile_routes matcher end-to-end, fail-closed for unserved profiles, per-profile pairing isolation, unchanged full-multiplex path, served-profiles runtime status. 12 passed; related routing, config, multiplex-authz, adapter-registry, and pairing suites (233) green on current main.
70318dc to
90eba91
Compare
|
Narrowed as suggested — 90eba91, recreated as a single commit on current main:
Docs: a "Routing-only: one connection, many personas" section in Tests: |
NousResearch#65118) The served-profiles block in _start_secondary_profile_adapters references PairingStore, but the class's only import in gateway/run.py is method-local inside __init__ — so the reference raised NameError at runtime, silently swallowed by the enclosing try/except ('could not record served_profiles'). Result: multiplexing gateways never created per-profile pairing stores, and authz pairing checks for secondary profiles fell through to the global whitelist. Also masked the served_profiles runtime-status write. One-line fix (local import alongside write_runtime_status) + regression tests that drive the real method and assert the stores materialize, verified red without the import and green with it. Surfaced during the profile-routing sweep by @CocaKova's PR NousResearch#61689, which included the same fix as part of a larger feature.
NousResearch#65118) The served-profiles block in _start_secondary_profile_adapters references PairingStore, but the class's only import in gateway/run.py is method-local inside __init__ — so the reference raised NameError at runtime, silently swallowed by the enclosing try/except ('could not record served_profiles'). Result: multiplexing gateways never created per-profile pairing stores, and authz pairing checks for secondary profiles fell through to the global whitelist. Also masked the served_profiles runtime-status write. One-line fix (local import alongside write_runtime_status) + regression tests that drive the real method and assert the stores materialize, verified red without the import and green with it. Surfaced during the profile-routing sweep by @CocaKova's PR NousResearch#61689, which included the same fix as part of a larger feature.
What
Two small, opt-in pieces that together let ONE Matrix gateway (one set of homeserver credentials) serve multiple agent personas:
platforms.matrix.room_profile_map— aroom_id -> profilemapping. Inbound messages from a mapped room getsource.profilestamped, so the session runs under that profile's config/personality. Read from raw config.yaml (viaget_hermes_dir()) becausePlatformConfigcurrently drops unknown keys; happy to move it ontoPlatformConfigif you'd prefer.HERMES_MULTIPLEX_ROUTING_ONLY(env, opt-in) — withmultiplex_profileson, skip starting secondary profile adapters and rely on routing alone. Without this, profiles that share the same Matrix credentials spin secondary adapters that fight over the same sync loop. A config key instead of env is fine too — kept minimal for review.Why
Multi-persona single-homeserver deployments are currently impossible without one full credential set per profile. This has been running in production on a DGX Spark deployment (3 personas, 7 rooms) since 2026-06-23.
Notes
room_profile_mapis set / env is enabled.🤖 Generated with Claude Code