Skip to content

fix(matrix): honor profile secret scope for recovery key under multiplex - #69110

Closed
sergioperezcheco wants to merge 1 commit into
NousResearch:mainfrom
sergioperezcheco:fix/matrix-recovery-key-profile-scope-69090
Closed

fix(matrix): honor profile secret scope for recovery key under multiplex#69110
sergioperezcheco wants to merge 1 commit into
NousResearch:mainfrom
sergioperezcheco:fix/matrix-recovery-key-profile-scope-69090

Conversation

@sergioperezcheco

Copy link
Copy Markdown
Contributor

Fixes #69090.

Under gateway.multiplex_profiles, the Matrix adapter read MATRIX_RECOVERY_KEY with os.getenv, so every profile resolved the default profile's key. That caused "recovery key verification failed: Key MAC does not match" and broke E2EE for secondary profiles — direct sends and hermes -p worked fine because only the multiplexed gateway path hit this os.getenv call.

The fix routes the recovery-key read through agent.secret_scope.get_secret, which is scope-aware: a secondary profile under multiplex resolves its own key from the installed secret scope instead of falling through to os.environ. An unscoped read under multiplex (the default-profile startup loop / background reconnect) raises UnscopedSecretError, and in that context os.environ is that profile's own value, so we fall back to it — the same pattern the Slack adapter already uses for SLACK_APP_TOKEN (#59739).

This is a credential-read bug, so the secret scope (not os.environ) must be authoritative: a secondary profile that hasn't set a recovery key returns empty rather than silently borrowing the default profile's key (which is what produced the confusing MAC mismatch).

Applied to both the startup verification site and the status diagnostic so they stay consistent. Added a regression test covering all four scopes: inactive/env, active+scoped (uses scope not env), active+unscoped (falls back to env), and active+scoped-without-key (returns empty, no cross-profile leak). The remaining os.getenv reads in the adapter are non-credential config knobs (E2EE mode, allowed rooms, batch delays) so they're intentionally untouched.

Verified the three resolution paths behave correctly and that get_secret's scope-isolation semantics hold. The 6 new tests pass; existing secret_scope tests (13) still pass.

The Matrix adapter read MATRIX_RECOVERY_KEY via os.getenv, so under
gateway.multiplex_profiles every profile resolved the default profile's
key. That produced "recovery key verification failed: Key MAC does not
match" and broke E2EE for secondary profiles (NousResearch#69090).

Route the read through agent.secret_scope.get_secret, which honors the
active profile's scope, with an os.getenv fallback for an unscoped read
under multiplex (default-profile startup loop) — mirroring the Slack
app-token pattern (NousResearch#59739). Applied to both the startup verification
site and the status diagnostic.

Fixes NousResearch#69090
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused profile-isolation fix. The premise remains valid on current main: Matrix still reads MATRIX_RECOVERY_KEY from process environment at plugins/platforms/matrix/adapter.py:1580 before E2EE verification, and separately at plugins/platforms/matrix/adapter.py:1878 for diagnostics.

The proposed helper matches the current secret-scope contract in agent/secret_scope.py:150-174: a scoped profile is authoritative, a missing scoped key stays absent, and only an unscoped multiplex read raises. The explicit fallback follows the established Slack startup/reconnect handling at plugins/platforms/slack/adapter.py:1762-1773. Secondary Matrix adapters are in fact connected under _profile_runtime_scope at gateway/run.py:10474-10477, where the per-profile secret mapping is installed.

No correctness or completeness defect was identified in the PR diff. Its two replacements cover every current runtime MATRIX_RECOVERY_KEY read in the Matrix adapter, and the focused tests exercise the helper's relevant resolution states. GitHub reports the PR as mergeable against current main.

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@teknium1

teknium1 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Salvaged and merged in #76573 — your commit is on main as 153442d with your authorship, closing #69090. Clean cherry-pick, zero changes needed: get_secret with no cross-profile borrow on scoped miss, and the Slack-pattern UnscopedSecretError fallback for the default profile's startup path was exactly right (we reused it as the reference pattern for the WhatsApp salvage in the same PR). All 6 of your tests run on main. Thanks @sergioperezcheco!

@teknium1 teknium1 closed this Aug 2, 2026
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/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/matrix Matrix adapter (E2EE) sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Matrix adapter uses only the default recovery key in multi-profile mode

3 participants