Skip to content

feat(gateway): multiplex_routing_only — single-credential multi-persona over the shared adapters - #61689

Open
CocaKova wants to merge 1 commit into
NousResearch:mainfrom
CocaKova:pr/matrix-room-profile-routing
Open

feat(gateway): multiplex_routing_only — single-credential multi-persona over the shared adapters#61689
CocaKova wants to merge 1 commit into
NousResearch:mainfrom
CocaKova:pr/matrix-room-profile-routing

Conversation

@CocaKova

@CocaKova CocaKova commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What

Two small, opt-in pieces that together let ONE Matrix gateway (one set of homeserver credentials) serve multiple agent personas:

  1. platforms.matrix.room_profile_map — a room_id -> profile mapping. Inbound messages from a mapped room get source.profile stamped, so the session runs under that profile's config/personality. Read from raw config.yaml (via get_hermes_dir()) because PlatformConfig currently drops unknown keys; happy to move it onto PlatformConfig if you'd prefer.

  2. HERMES_MULTIPLEX_ROUTING_ONLY (env, opt-in) — with multiplex_profiles on, 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

  • No behavior change unless room_profile_map is set / env is enabled.
  • Example config:
platforms:
  matrix:
    room_profile_map:
      '!abc:example.org': milo
      '!def:example.org': theo

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery platform/matrix Matrix adapter (E2EE) labels Jul 9, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:8473 returns before current main's secondary-profile startup populates _profile_adapters and per-profile pairing state (gateway/run.py:8489-8518). The mapped-room change then stamps a non-default source.profile; current gateway/authz_mixin.py:50-57 deliberately resolves an unregistered stamped profile to no adapter, and gateway/authz_mixin.py:249-262 falls back to the global pairing store. Routing-only therefore bypasses profile-specific adapter/authorization state.
  • gateway/run.py:8473 adds HERMES_MULTIPLEX_ROUTING_ONLY, a non-secret behavior flag. AGENTS.md:102-107 requires this configuration surface to live in config.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.

Comment thread gateway/run.py Outdated
@@ -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"):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@CocaKova
CocaKova force-pushed the pr/matrix-room-profile-routing branch from b1a2aa4 to 7a0521a Compare July 10, 2026 18:27
@CocaKova

CocaKova commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

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 config.yaml. HERMES_MULTIPLEX_ROUTING_ONLY is gone. The flag is now gateway.multiplex_routing_only on GatewayConfig (top-level and nested gateway.* forms, mirroring multiplex_profiles), and room_profile_map is bridged into the matrix PlatformConfig.extra by the shared-key loop, so the adapter no longer reads raw config.yaml.

Routing-only now registers, it doesn't skip. _start_secondary_profile_adapters no longer returns early: for each served secondary profile it sets _profile_adapters[profile] to the shared adapter map (no second connection — all profiles share the default profile's credentials, so a secondary adapter could only poll the same token twice) and the existing loop gives each profile its own PairingStore. A stamped source therefore resolves to a live adapter for authorization and egress, per-profile pairing whitelists stay isolated, and _authorization_adapter's fail-closed behavior is preserved for genuinely unregistered profiles. The adapter also refuses to stamp default or override an already-stamped source.

Latent bug found and fixed along the way: the per-profile PairingStore loop referenced PairingStore as an unbound name (its only import is method-local in __init__), so the blanket except swallowed the NameError and per-profile stores were silently never created — every profile's pairing checks collapsed onto the global store even in full multiplex mode. Store creation now imports locally and is separated from the runtime-status write.

Tests (tests/gateway/test_matrix_room_profile_routing.py, 17 cases): mapped/unmapped/default-mapped/pre-stamped inbound sources, session-key namespacing, config parsing incl. load_gateway_config and extra-bridging, shared-adapter registration, authorization + egress resolution for stamped sources, fail-closed for unserved profiles, per-profile pairing isolation, and the unchanged full-multiplex path. Related suites green: test_matrix, test_config, test_multiplex_profile_authz, test_multiplex_adapter_registry, test_pairing (419 passed). Docs section added to multi-profile-gateways.md.

@CocaKova
CocaKova force-pushed the pr/matrix-room-profile-routing branch from 7a0521a to 70318dc Compare July 10, 2026 18:54
@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Heads-up: the room_profile_map half of this PR is now covered on main via PR #64835 (salvage of #20096 by @Burgunthy) — gateway.profile_routes with platform: matrix, chat_id: <room_id> routes rooms to profiles platform-generically.

Two things in this PR remain valuable beyond main:

  1. multiplex_routing_only — on main, multiplexing always tries to start secondary adapters per profile and refuses shared credentials; a routed profile without its own adapter registration then fails closed in authz. Your routing-only mode (registering served profiles onto the shared adapter map) is exactly what single-credential multi-persona deployments need.
  2. The PairingStore NameError fix — confirmed still live on main (gateway/run.py references PairingStore in _start_secondary_profile_adapters but the only import is method-local in __init__; the NameError is swallowed and per-profile pairing stores are silently never created).

A rebase narrowing this PR to those two pieces (reusing the merged route matcher for the room mapping) would be very welcome. Leaving open.

teknium1 added a commit that referenced this pull request Jul 15, 2026
#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.
@teknium1

Copy link
Copy Markdown
Contributor

Update: the PairingStore NameError fix (item 2 above) has now landed on main via PR #65118 (with credit to this PR in its body). What remains unique to this PR is item 1 — the multiplex_routing_only mode for single-credential multi-persona deployments. A rebase narrowing to just that, reusing the merged gateway.profile_routes matcher for the room mapping, would be the cleanest path forward.

…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.
@CocaKova
CocaKova force-pushed the pr/matrix-room-profile-routing branch from 70318dc to 90eba91 Compare July 16, 2026 13:23
@CocaKova CocaKova changed the title feat(matrix): per-room profile routing (room_profile_map) + routing-only multiplex mode feat(gateway): multiplex_routing_only — single-credential multi-persona over the shared adapters Jul 16, 2026
@CocaKova

Copy link
Copy Markdown
Contributor Author

Narrowed as suggested — 90eba91, recreated as a single commit on current main:

  • Dropped room_profile_map entirely: room→profile mapping now comes from the merged gateway.profile_routes matcher (feat(gateway): profile-based routing for inbound messages (salvage #20096) #64835) — a platform: matrix, chat_id: "!room:server" route covers it, and this PR no longer touches the Matrix adapter or the config extra-bridging at all.
  • Dropped the PairingStore fix — thanks for landing it via fix(gateway): import PairingStore in _start_secondary_profile_adapters #65118.
  • What remains is only gateway.multiplex_routing_only (top-level and nested gateway.* forms, mirroring multiplex_profiles): with it on, _start_secondary_profile_adapters connects nothing — each served profile is registered onto the shared adapter map and gets its own PairingStore via the existing loop, 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. The full-multiplex path is untouched when the flag is off.

Docs: a "Routing-only: one connection, many personas" section in multi-profile-gateways.md, placed directly under the existing profile_routes section, including the all-or-nothing caveat (mixed deployments with per-profile tokens should stay on plain multiplexing).

Tests: tests/gateway/test_multiplex_routing_only.py (12) — config parsing incl. load_gateway_config, registration without starting secondaries, authorization/egress resolution for stamped sources, an end-to-end case through the merged route matcher (_profile_name_for_source on a Matrix room route → shared adapter), fail-closed for unserved profiles, per-profile pairing isolation, unchanged flag-off path, and served-profiles runtime status. Related routing/config/multiplex-authz/adapter-registry/pairing suites: 233 passed on current main.

@teknium1 teknium1 added the area/profiles Multi-profile isolation, HERMES_HOME scoping label Jul 19, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
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.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/matrix Matrix adapter (E2EE) sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants