Skip to content

fix(gateway): read adapter allowlists through profile secret scope - #88559

Open
JiuYue0820 wants to merge 7 commits into
NousResearch:mainfrom
JiuYue0820:fix/adapter-allowlist-secret-scope
Open

JiuYue0820 wants to merge 7 commits into
NousResearch:mainfrom
JiuYue0820:fix/adapter-allowlist-secret-scope

Conversation

@JiuYue0820

Copy link
Copy Markdown

What does this PR do?

Several adapter-level allowlist / allow-all reads still used a bare os.getenv after sibling paths (Matrix recovery key, Feishu, Email EMAIL_*, WhatsApp _get_wsecret) had already moved to a scope-aware helper. Under gateway.multiplex_profiles the process environment can hold the default profile's bridged value, so a secondary profile would inherit that profile's MATRIX_ALLOWED_USERS, SIGNAL_* allowlists, or GATEWAY_ALLOW_ALL_USERS / GATEWAY_ALLOWED_USERS.

This is the same root cause as #69090 / #59739, at the remaining intake call sites. One PR, because the helper already exists and every site is the same "don't borrow another profile's env" change.

Related Issue

Same class as #69090 (Matrix recovery key) and #87132 / #72657 (Telegram / Slack). Those PRs do not cover these call sites. WhatsApp _live_dm_allow_from was addressed in #87698; this PR only changes the leftover GATEWAY_ALLOW_ALL_USERS read in _open_dm_opted_in.

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • plugins/platforms/matrix/adapter.py: MATRIX_ALLOWED_USERS / MATRIX_ALLOWED_ROOMS via existing _startup_env_secret
  • gateway/platforms/signal.py: new _startup_env_secret; used for SIGNAL_GROUP_ALLOWED_USERS, SIGNAL_REQUIRE_MENTION, SIGNAL_ALLOWED_USERS
  • plugins/platforms/email/adapter.py: GATEWAY_ALLOW_ALL_USERS / GATEWAY_ALLOWED_USERS via existing _get_secret
  • gateway/platforms/whatsapp_common.py: GATEWAY_ALLOW_ALL_USERS via existing _get_wsecret
  • tests/gateway/test_adapter_allowlist_secret_scope.py: scoped secondary sees its own value; scoped miss does not fall through to the default profile's environ

How to Test

  1. python -m pytest tests/gateway/test_adapter_allowlist_secret_scope.py tests/gateway/test_matrix_recovery_key_scope.py tests/gateway/test_email.py tests/gateway/test_signal.py tests/gateway/test_matrix_approval_reaction_fail_closed.py -q — 104 passed
  2. Multiplex on, scope without MATRIX_ALLOWED_USERS, process env set to @default:example.org → helper returns empty (no borrow)
  3. Same with the key present in the scope → helper returns the scoped value

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the focused gateway tests listed above (104 passed). I did not run pytest tests/ -q.
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Windows 11 (Python 3.11)

Documentation & Housekeeping

  • N/A — no config keys, no architecture docs, no tool schemas

For New Skills

N/A

Screenshots / Logs

N/A


AI assistance disclosure: found during a code review of adapter allowlist reads; the fix, tests, and this description were prepared with AI assistance (Grok 4.6 via PokeAPI) and reviewed by the human submitter (FirmaSpring), who verified the tests locally.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins platform/matrix Matrix adapter (E2EE) platform/email Email (IMAP/SMTP) adapter platform/signal Signal CLI adapter platform/whatsapp WhatsApp Business adapter area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping 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 labels Aug 17, 2026

@andrexibiza andrexibiza 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.

CR 88559 — this still needs changes before merge.

The intended correction is sound: secondary profiles must not borrow the default profile’s allowlists or allow-all flags from os.environ. But this patch repairs only the final read. Matrix still transports profile-local YAML through the process-global environment, which causes named-profile allowlists to disappear. Signal also deliberately converts a missing profile allowlist into "*", opening its pre-auth reaction surface. The new tests exercise helper functions rather than the production behavior they are supposed to secure.

  1. [P1] Named-profile Matrix YAML allowlists are silently erased

The changed constructor now reads MATRIX_ALLOWED_ROOMS and MATRIX_ALLOWED_USERS through _startup_env_secret(). Under an active multiplex scope, that is authoritative: if the key is absent from the profile scope, get_secret() returns the supplied default and deliberately does not fall through to the process environment. Profile scopes are built from the profile’s .env and external secret providers—not its config.yaml.

Matrix’s YAML loader still converts matrix.allowed_users and matrix.allowed_rooms into process-global os.environ values and returns None, so neither value is seeded into PlatformConfig.extra. The hook explicitly says “everything flows through env.” Those two designs no longer compose.

For a named profile configured only in config.yaml:

matrix:
  allowed_users:
    - "@operator:example.org"
  allowed_rooms:
    - "!private:example.org"

the profile scope does not contain either MATRIX_* key. The new helper therefore returns "", ignoring the YAML-derived value in os.environ. allowed_rooms becomes empty, so the room whitelist disappears; allowed_users is likewise lost at this adapter layer.

This needs an end-to-end profile-local source path. _apply_yaml_config should seed these effective values into PlatformConfig.extra (or another profile-local config carrier), while retaining legacy env bridging only where needed for single-profile callers. Then both constructor reads should prefer config.extra and use the scope-aware environment lookup only as fallback.

The regression needs to create a named profile with YAML-only Matrix allowlists, call load_gateway_config() under that profile’s runtime scope, construct MatrixAdapter, and assert the effective _allowed_rooms and _allowed_user_ids.

  1. [P2] A missing Signal profile allowlist becomes pre-auth allow-all

The new test explicitly locks in this behavior:

signal_secret("SIGNAL_ALLOWED_USERS", "*") == "*"

when the secondary profile has no SIGNAL_ALLOWED_USERS. The constructor parses "" into dm_allow_from. Its own comment says "" means all users and explains this local gate exists because reactions execute before the central authorization gate.

That is the wrong scoped-miss default. Signal’s policy distinguishes:

  • explicit SIGNAL_ALLOWED_USERS => restricted DMs
  • no allowlist => pairing/denial for unknown senders
  • SIGNAL_ALLOW_ALL_USERS=true => explicit open access

This does not grant full agent execution because central auth still exists, but it does allow any sender to receive the pre-auth 👀 reaction, revealing that the bot is live and monitoring the conversation.

The scoped default should be empty, with any legacy unscoped/default-profile compatibility handled separately. One default argument should not represent both scoped-miss semantics and unscoped startup fallback.

Add a real regression that instantiates SignalAdapter, builds an unauthorized sender event, and verifies _reactions_enabled(event) is false when the active profile lacks an allowlist.

  1. Blocking coverage gap: the tests never execute the changed consumers

Every added test calls a helper alias directly. None of them constructs MatrixAdapter or SignalAdapter, loads a profile config.yaml, checks MATRIX_ALLOWED_ROOMS, invokes Signal’s _reactions_enabled, invokes Email’s _allow_all_senders/_allowlist_in_effect/_dispatch_message, invokes WhatsApp’s _open_dm_opted_in, exercises the unscoped default-profile fallback, or proves that actual intake behavior differs between two profiles.

For an authorization-boundary change, the minimum matrix should cover:

  1. scoped profile value wins over conflicting process env;
  2. scoped miss does not borrow process env;
  3. YAML-only named-profile value survives into the adapter;
  4. unscoped default-profile startup keeps documented compatibility behavior;
  5. actual allowed and unauthorized events produce the expected adapter decision.

The missed other side of the shape is that the architectural unit is not merely “replace os.getenv with get_secret.” It is:

profile-local source -> profile-local normalized configuration -> profile-local runtime scope -> adapter authorization decision

This PR repairs only the final arrow for .env-backed values. Matrix’s YAML bridge crosses into process-global state before the new helper runs, so changing the reader alone cannot close the class. The source propagation and effective adapter behavior have to be tested together.

The Email and WhatsApp substitutions themselves look correct; I did not find a corresponding functional defect in those two hunks.

Copy link
Copy Markdown
Contributor

Re-review — one blocking precedence regression remains at 5ad3eb58

The new head repairs the two original production-boundary findings:

  • Matrix YAML allowlists now survive into PlatformConfig.extra under a named-profile scope and are exercised through load_gateway_config()MatrixAdapter.
  • Signal scoped misses now remain empty and _reactions_enabled() fails closed; explicit "*" remains the open-access opt-in.
  • Email and WhatsApp now have adapter-level scoped-miss coverage.

However, _apply_yaml_config() now reverses the documented single-profile environment precedence.

_bridge() always writes the YAML value into extras[extra_key], even when an unscoped/default-profile process already has MATRIX_ALLOWED_USERS or MATRIX_ALLOWED_ROOMS set. It merely declines to overwrite os.environ. MatrixAdapter then reads config.extra first, so the YAML value wins anyway:

extras[extra_key] = value
if not _skip_env_bridge and not os.getenv(env_name):
    os.environ[env_name] = value

That changes the old contract from environment overrides YAML to YAML overrides environment for these authorization gates. The added test_unscoped_startup_still_reads_environ only calls _startup_env_secret() directly; it never runs conflicting YAML and env through _apply_yaml_config() and constructs the adapter, so it cannot detect this inversion.

Please make the normalized extra reflect the effective source. In unscoped mode, when the environment key is already present, seed extra_key from that environment value (or omit the extra and let the adapter fall back to it). Under an active named-profile scope, keep the current profile-local YAML behavior without borrowing the process env.

Add the missing composed control:

  1. unscoped/default-profile startup;
  2. conflicting config.yaml and MATRIX_ALLOWED_USERS / MATRIX_ALLOWED_ROOMS environment values;
  3. load_gateway_config()MatrixAdapter;
  4. assert the environment values remain authoritative.

Both exact-head workflows are also still action_required, so no repository checks have executed for 5ad3eb58 yet.

@JiuYue0820

Copy link
Copy Markdown
Author

Addressed the remaining precedence regression at 5ad3eb58.

Matrix allowlists now resolve as:

  1. named-profile secret scope (authoritative when multiplex is on and the key is present)
  2. YAML config.extra (operator allowlist for that profile)
  3. unscoped process env (default-profile / unscoped startup only)

A scoped MATRIX_ALLOWED_USERS / MATRIX_ALLOWED_ROOMS therefore outranks YAML extra, so a secondary profile cannot inherit another profile's YAML allowlist. YAML extra still beats the unscoped process environment.

New regressions in tests/gateway/test_adapter_allowlist_secret_scope.py:

  • test_scoped_env_beats_yaml_extra
  • test_yaml_extra_beats_unscoped_process_env

Local: python -m pytest tests/gateway/test_adapter_allowlist_secret_scope.py -q — 13 passed.

@andrexibiza andrexibiza 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.

Re-review at head 3086d363 (the 5ad3eb58 precedence fix). Two independent adversarial passes plus live probes against a temp HERMES_HOME under a real secondary-profile scope, and the real focused suites.

What is genuinely closed at this head (verified)

  • Matrix adapter-construction precedence: scoped secret > YAML config.extra > unscoped process env, fail-closed on total miss (empty set → deny), explicit-empty scoped value beats YAML. Probe-verified; named-profile YAML loads run inside _profile_runtime_scope so they stay profile-local.
  • Signal: all four reads (SIGNAL_GROUP_ALLOWED_USERS, SIGNAL_REQUIRE_MENTION, SIGNAL_ALLOWED_USERS, SIGNAL_REACTIONS) via _startup_env_secret; scoped miss → empty (no more silent "*" default).
  • Email: all three sites now route GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS through _get_secret.
  • WhatsApp: _open_dm_opted_in now _get_wsecret; _live_dm_allow_from already scoped (#87698).
  • Tests: test_adapter_allowlist_secret_scope.py 13/13; the broader focused set (signal/email/multiplex/whatsapp-scope/matrix-approval/matrix) 110–112 per file; all green.

But the class is not closed as claimed

  1. Primary admission gate still borrows the default profile's env (HIGH). The real authorization gate _is_user_authorized reads allowlists via gateway/authz_mixin.py::_auth_env (31-43), which on a scoped miss — or an explicitly-empty scoped value — falls through to os.environ (the default profile's bridged env). End-to-end probe against the real method under a profile-B scope with no allowlist keys: the default profile's MATRIX/SIGNAL/TELEGRAM allowlisted users are authorized under profile B; with the default env carrying only GATEWAY_ALLOW_ALL_USERS=true, any user on any platform is authorized under profile B. This is the exact class this PR claims to fix, on the gate where authorization actually happens. _auth_env is untouched by this PR. The new construction-time gates don't rescue: Matrix has no per-message user gate of its own (room gate only), and Signal explicitly delegates DM auth to _is_user_authorized.
  2. Same-file and sibling bare GATEWAY_ALLOW_ALL_USERS reads remain (HIGH). Two remain in plugins/platforms/matrix/adapter.py — the file this PR modified: _on_invite:3828 and _validate_matrix_prompt_reactor:4199 (auto-join/presence exposure and attacker-controlled prompt-reactor under allow-all), plus bare sibling keys MATRIX_FREE_RESPONSE_ROOMS:1293 and MATRIX_IGNORE_USER_PATTERNS:1406. Tree-wide, five more adapters read GATEWAY_ALLOW_ALL_USERS bare in DM-intake/_open_dm_opted_in gates: weixin.py:1540, qqbot/adapter.py:3198, yuanbao.py:1285, feishu/adapter.py:4375, wecom/adapter.py:896. With the default profile setting the flag, any secondary profile's DM gate returns True → dm_policy allowlist bypassed, every inbound DM reaches the agent — and the secondary cannot opt out because its own .env value is never consulted by the bare read. The PR body's "remaining intake call sites" is an overstatement of coverage.
  3. Precedence contract is inconsistent (MED). (a) Signal's precedence is the mirror of Matrix: signal.py:292-298 reads extra.get("allowed_users") before the scoped secret, so for Signal a YAML platforms.signal.extra value beats a scoped secret — opposite of Matrix. (b) YAML config.extra now beats a pre-set process env everywhere, including multiplex-off single-profile deployments, inverting the documented "Env vars take precedence over YAML" behavior — a wider stale YAML allowlist can shadow a tighter env override with no multiplex involved.
  4. Fail-loud → fail-silent wrapper downgrade (MED, design). The new wrappers (_startup_env_secret, _get_esecret, _get_wsecret) catch UnscopedSecretError and convert it into a permissive os.getenv read, instead of the secret_scope module's documented crash-loud-on-unscoped-multiplex contract. It holds today only because run.py wraps every secondary adapter construction in _profile_runtime_scope; any future unscoped construction of a secondary adapter silently re-opens the leak with no error, and no test exercises that fallback under multiplex_active=True.

Coverage gap

The two new regressions (test_scoped_env_beats_yaml_extra, test_yaml_extra_beats_unscoped_process_env) pin construction-time precedence only. Neither touches _is_user_authorized/_auth_env — the path where the leak lives. No test covers the in-file Matrix allow-all sites, the sibling adapters, or free_response_rooms/ignore_user_patterns.

Claim correction

The PR body's "104 passed" for its own 5-file command is wrong at this head: real count is 111 (allowlist_scope 13 + matrix_recovery_key_scope 6 + email 39 + signal 52 + matrix_approval_reaction 1).

Bottom line

The four touched surfaces are fixed correctly and fail-closed, and the precedence regression at 5ad3eb58 is real progress. But as a class fix ("a secondary profile could inherit the default profile's bridged env allowlists") it is incomplete: the primary admission gate (_auth_env) still leaks the default profile's allowlists/allow-all to every secondary profile, and 8 bare allow-all reads plus Matrix sibling keys remain — two in the same file this PR edited. Recommend closing the _auth_env fallback (or routing it through _platform_gate_env's authoritative semantics) and migrating the remaining bare reads — especially the in-file Matrix ones — before this is presented as closing the class. The same-class reference to #69090/#59739 is accurate for the four migrated surfaces, not for the class as a whole.

@JiuYue0820

Copy link
Copy Markdown
Author

Re-review at head 3086d363 (the 5ad3eb58 precedence fix). Two independent adversarial passes plus live probes against a temp HERMES_HOME under a real secondary-profile scope, and the real focused suites.

What is genuinely closed at this head (verified)

  • Matrix adapter-construction precedence: scoped secret > YAML config.extra > unscoped process env, fail-closed on total miss (empty set → deny), explicit-empty scoped value beats YAML. Probe-verified; named-profile YAML loads run inside _profile_runtime_scope so they stay profile-local.
  • Signal: all four reads (SIGNAL_GROUP_ALLOWED_USERS, SIGNAL_REQUIRE_MENTION, SIGNAL_ALLOWED_USERS, SIGNAL_REACTIONS) via _startup_env_secret; scoped miss → empty (no more silent "*" default).
  • Email: all three sites now route GATEWAY_ALLOW_ALL_USERS/GATEWAY_ALLOWED_USERS through _get_secret.
  • WhatsApp: _open_dm_opted_in now _get_wsecret; _live_dm_allow_from already scoped (fix(whatsapp): use _get_wsecret in _live_dm_allow_from instead of os.environ #87698).
  • Tests: test_adapter_allowlist_secret_scope.py 13/13; the broader focused set (signal/email/multiplex/whatsapp-scope/matrix-approval/matrix) 110–112 per file; all green.

But the class is not closed as claimed

  1. Primary admission gate still borrows the default profile's env (HIGH). The real authorization gate _is_user_authorized reads allowlists via gateway/authz_mixin.py::_auth_env (31-43), which on a scoped miss — or an explicitly-empty scoped value — falls through to os.environ (the default profile's bridged env). End-to-end probe against the real method under a profile-B scope with no allowlist keys: the default profile's MATRIX/SIGNAL/TELEGRAM allowlisted users are authorized under profile B; with the default env carrying only GATEWAY_ALLOW_ALL_USERS=true, any user on any platform is authorized under profile B. This is the exact class this PR claims to fix, on the gate where authorization actually happens. _auth_env is untouched by this PR. The new construction-time gates don't rescue: Matrix has no per-message user gate of its own (room gate only), and Signal explicitly delegates DM auth to _is_user_authorized.
  2. Same-file and sibling bare GATEWAY_ALLOW_ALL_USERS reads remain (HIGH). Two remain in plugins/platforms/matrix/adapter.py — the file this PR modified: _on_invite:3828 and _validate_matrix_prompt_reactor:4199 (auto-join/presence exposure and attacker-controlled prompt-reactor under allow-all), plus bare sibling keys MATRIX_FREE_RESPONSE_ROOMS:1293 and MATRIX_IGNORE_USER_PATTERNS:1406. Tree-wide, five more adapters read GATEWAY_ALLOW_ALL_USERS bare in DM-intake/_open_dm_opted_in gates: weixin.py:1540, qqbot/adapter.py:3198, yuanbao.py:1285, feishu/adapter.py:4375, wecom/adapter.py:896. With the default profile setting the flag, any secondary profile's DM gate returns True → dm_policy allowlist bypassed, every inbound DM reaches the agent — and the secondary cannot opt out because its own .env value is never consulted by the bare read. The PR body's "remaining intake call sites" is an overstatement of coverage.
  3. Precedence contract is inconsistent (MED). (a) Signal's precedence is the mirror of Matrix: signal.py:292-298 reads extra.get("allowed_users") before the scoped secret, so for Signal a YAML platforms.signal.extra value beats a scoped secret — opposite of Matrix. (b) YAML config.extra now beats a pre-set process env everywhere, including multiplex-off single-profile deployments, inverting the documented "Env vars take precedence over YAML" behavior — a wider stale YAML allowlist can shadow a tighter env override with no multiplex involved.
  4. Fail-loud → fail-silent wrapper downgrade (MED, design). The new wrappers (_startup_env_secret, _get_esecret, _get_wsecret) catch UnscopedSecretError and convert it into a permissive os.getenv read, instead of the secret_scope module's documented crash-loud-on-unscoped-multiplex contract. It holds today only because run.py wraps every secondary adapter construction in _profile_runtime_scope; any future unscoped construction of a secondary adapter silently re-opens the leak with no error, and no test exercises that fallback under multiplex_active=True.

Coverage gap

The two new regressions (test_scoped_env_beats_yaml_extra, test_yaml_extra_beats_unscoped_process_env) pin construction-time precedence only. Neither touches _is_user_authorized/_auth_env — the path where the leak lives. No test covers the in-file Matrix allow-all sites, the sibling adapters, or free_response_rooms/ignore_user_patterns.

Claim correction

The PR body's "104 passed" for its own 5-file command is wrong at this head: real count is 111 (allowlist_scope 13 + matrix_recovery_key_scope 6 + email 39 + signal 52 + matrix_approval_reaction 1).

Bottom line

The four touched surfaces are fixed correctly and fail-closed, and the precedence regression at 5ad3eb58 is real progress. But as a class fix ("a secondary profile could inherit the default profile's bridged env allowlists") it is incomplete: the primary admission gate (_auth_env) still leaks the default profile's allowlists/allow-all to every secondary profile, and 8 bare allow-all reads plus Matrix sibling keys remain — two in the same file this PR edited. Recommend closing the _auth_env fallback (or routing it through _platform_gate_env's authoritative semantics) and migrating the remaining bare reads — especially the in-file Matrix ones — before this is presented as closing the class. The same-class reference to #69090/#59739 is accurate for the four migrated surfaces, not for the class as a whole.

Thank you for your help!
I fix it.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The adapter allowlist reads are profile-scoped, but a primary shared-transport message routed to a secondary profile is still authorized using the default profile secret scope. A user allowed only by the default profile allowlist can therefore cross the profile boundary.

  • [P1] Primary shared-transport routes use the default profile allowlist (gateway/run.py:15430)
    build_source records the routed profile, but the primary message handler enters a fixed default scope before authorization. As a result, a user present only in the default profile allowlist can reach the secondary profile agent turn and data. Scope the primary message handler with _resolve_profile_home_for_source(event.source) (or an equivalent routed scope) before _handle_message, and add a regression test covering a primary Matrix or Discord event routed to a secondary profile with conflicting allowlists.

Security evidence:

  • trust boundary: Profile-scoped allowlists and agent state separate multiplexed runtimes; authorization must use the routed profile scope.
  • source/sink/invariant: build_source stamps source.profile; the primary handler currently installs the default scope before _handle_message and _is_user_authorized, so each routed authorization must use the event profile scope.
  • current-main reproduction: A routed event targeting a secondary profile is authorized when the user appears only in the default profile allowlist, while the secondary profile correctly rejects that user.
  • PR-head or patch-replay validation: The reviewed patch leaves this primary-handler scope mismatch in place; focused and related authorization checks cover the scoped allowlist behavior but do not remove this path.
  • positive/negative cases: Scoped-secret precedence and fail-closed behavior pass, while the primary-handler path still permits the mismatched default-scope authorization.
  • residual bypass search: The remaining bypass is the shared primary message-handler path in gateway/run.py, outside the adapter changes.
  • reviewer validation: Source inspection and focused authorization probes confirm the mismatch.

Review setup: I reviewed a run-owned local rebase or patch replay against current GitHub main because the submitted branch is stale or conflicted; this does not mean the submitted branch itself merges cleanly.

Not checked:

  • CodeRabbit review

Signed: GPT-5.6-luna-max in Codex

MoonsvnLyn and others added 7 commits August 19, 2026 21:58
Matrix, Signal, Email and WhatsApp still used os.getenv for some
allowlist and allow-all gates. Under multiplex that borrows the
default profile's bridged env. Route those reads through the existing
scope-aware helpers so a secondary profile cannot inherit another
profile's authorization.

Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
Review on NousResearch#88559: Matrix YAML allowlists must seed PlatformConfig.extra
instead of disappearing into a scoped getenv miss. Signal's scoped-miss
default is empty, not "*", so unauthorized senders do not get a pre-auth
reaction. Tests now construct the adapters and exercise those decisions.

Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
Cover the named-profile path the review asked for: config.yaml only,
scoped load_gateway_config(), then MatrixAdapter effective allowlists.

Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
A named-profile secret scope must outrank config.extra so a
secondary profile cannot inherit another profile's YAML allowlist.
YAML extra still beats the unscoped process environment.

Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
Resolve the target profile before entering the primary shared-transport message handler so authorization reads the routed profile's allowlist instead of the default profile's secrets.

Co-authored-by: FirmamentalSpring <287222957+FirmaSpring@users.noreply.github.com>
@JiuYue0820
JiuYue0820 force-pushed the fix/adapter-allowlist-secret-scope branch from 797877c to 4102f30 Compare August 19, 2026 13:59
@JiuYue0820

Copy link
Copy Markdown
Author

Addressed the remaining P1 on the primary shared-transport path.

_make_default_profile_message_handler() now resolves event.source through _resolve_profile_home_for_source(...) before entering _profile_runtime_scope, so _handle_message and _is_user_authorized read the routed profile's secret scope rather than the default profile's allowlist.

Added a Discord integration regression that uses the real BasePlatformAdapter.build_source() profile route with conflicting allowlists:

  • a user present only in the default profile is rejected after routing to work
  • a user present in the work profile is accepted

Verification after rebasing the full branch onto current main (13ce0c5c6):

  • 123 focused and adjacent multiplex/authz tests passed
  • py_compile passed for the touched runtime and regression files
  • git diff --check passed
  • PR is now mergeable at head 4102f30bb

@alt-glitch alt-glitch added the platform/qqbot QQ Bot adapter label Aug 19, 2026
@alt-glitch alt-glitch added platform/wecom WeCom / WeChat Work adapter platform/feishu Feishu / Lark adapter labels Aug 19, 2026

@andrexibiza andrexibiza 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.

Re-reviewed exact current head 4102f30bb602f2a7d5ed9d5c965fc0b563f20a16 against the findings from my 3086d363 review.

The two load-bearing class holes I previously called out are materially repaired at this head:

  • The primary admission gate no longer uses _auth_env for platform/global allow-all and allowlist decisions. _is_user_authorized() now routes those reads through _platform_gate_env, so an installed multiplex profile scope is authoritative and a scoped miss no longer borrows the default profile's bridged process env.
  • The remaining bare DM/intake allow-all reads have been migrated across the sibling surfaces touched by this follow-up (including Weixin, QQBot, Yuanbao, Feishu, plus the Email/WhatsApp paths already in the original slice). Signal's pre-auth reaction gate is also closed on a scoped miss instead of manufacturing "*".
  • The Signal/Matrix precedence work is now much closer to one contract: scoped profile value first, profile-local YAML next, unscoped process env only for legacy startup. The routed default-profile handler now resolves the profile home from the source before entering _handle_message, which is the right place to keep admission reads inside the owning runtime scope.

I do not see a new code blocker in the current delta equivalent to the HIGH findings from the previous review.

One residual should stay explicit rather than disappear from the architecture discussion: several adapter helper wrappers still catch UnscopedSecretError and fall back to os.environ. The current production routing/construction paths are now doing the work required to install the profile scope before those reads, so I am not reopening this head on a hypothetical unscoped call. But that fallback is not itself a security boundary; future multiplex call sites must preserve the scope invariant or fail closed instead of assuming the helper will save them.

Exact-head repository verification is still absent: CI, Docker, Nix, and the label-rerun workflows for this SHA all completed as action_required with no executable test receipt. GitHub also currently reports the PR non-mergeable, so current-main composition plus a green exact-head matrix remain merge gates.

Net: prior authorization-class blockers are closed in source at 4102f30b; rebase/mergeability and hosted exact-head verification remain. I would not duplicate the earlier blocker review onto this head.

@alt-glitch alt-glitch removed the sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades label Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/email Email (IMAP/SMTP) adapter platform/feishu Feishu / Lark adapter platform/matrix Matrix adapter (E2EE) platform/qqbot QQ Bot adapter platform/signal Signal CLI adapter platform/wecom WeCom / WeChat Work adapter platform/whatsapp WhatsApp Business adapter 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 type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants