Skip to content

fix(email): honor profile secret scope for email adapter env reads - #59076

Closed
phantom-instruction-set wants to merge 1 commit into
NousResearch:mainfrom
phantom-instruction-set:fix/email-adapter-secret-scope-multiplex
Closed

phantom-instruction-set wants to merge 1 commit into
NousResearch:mainfrom
phantom-instruction-set:fix/email-adapter-secret-scope-multiplex

Conversation

@phantom-instruction-set

@phantom-instruction-set phantom-instruction-set commented Jul 5, 2026

Copy link
Copy Markdown

What does this PR do?

The email adapter (plugins/platforms/email/adapter.py) read EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_IMAP_HOST, EMAIL_SMTP_HOST, EMAIL_ALLOWED_USERS, and EMAIL_ALLOW_ALL_USERS via os.getenv() directly. In a multiplexed gateway, os.environ holds the default profile's .env values, so every secondary profile inherited the default profile's email credentials instead of its own — the adapter polled the wrong inbox and used the wrong allowlist.

This is a sibling of the api_server env-leak bug (#52307/#50051): the same os.getenvget_secret migration that PR #50094 applies to gateway/config.py, but for the email adapter itself, which neither PR #50094 nor #51374 covers.

Related Issue

Sibling of #50051 and #52307. Covers the email adapter gap left by PR #50094 (which fixes gateway/config.py only).

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • 🔒 Security fix (prevents cross-profile credential leakage)

Changes Made

  • plugins/platforms/email/adapter.py: replace os.getenv with agent.secret_scope.get_secret for all EMAIL_* credential reads (adapter __init__, check_email_requirements, _allowlist_in_effect, _dispatch_message allowlist gate, _send_email SMTP helper, EMAIL_AUTHSERV_ID).
  • gateway/config.py: add _getenv/_getenv_str/_getenv_int helpers (from PR fix(gateway): isolate multiplex profile config env reads #50094) and replace os.getenv with _getenv for the email block in _apply_env_overrides.
  • tests/gateway/test_email_secret_scope.py: 5 new tests.

How to Test

  1. Run the new tests:
python -m pytest tests/gateway/test_email_secret_scope.py -v

Expected: 5 passed.

  1. Verify the RED→GREEN cycle: stash the adapter.py change, re-run — 2 tests FAIL. Restore — all 5 pass.

  2. Run existing suites (no regressions):

python -m pytest tests/gateway/test_email.py tests/gateway/test_email_robustness.py tests/gateway/test_config.py tests/gateway/test_multiplex_phase0.py tests/agent/test_secret_scope.py -q

Expected: 219 passed.

  1. Manual reproduction (multiplexed gateway with a secondary profile):
# Before this PR — secondary profile leaks primary's address:
[Email] Adapter initialized for alpha@test.invalid  ← leaked from os.environ

# After this PR — secondary profile uses its own scoped address:
[Email] Adapter initialized for beta@test.invalid  ← from profile-scoped .env

Checklist

Code

Documentation & Housekeeping

  • N/A — no config key or architecture changes

@phantom-instruction-set
phantom-instruction-set force-pushed the fix/email-adapter-secret-scope-multiplex branch from 8dbb12d to b159fe6 Compare July 5, 2026 19:27
The email adapter (plugins/platforms/email/adapter.py) read
EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_IMAP_HOST, EMAIL_SMTP_HOST,
EMAIL_ALLOWED_USERS, and EMAIL_ALLOW_ALL_USERS via os.getenv()
directly. In a multiplexed gateway, os.environ holds the default
profile's .env values, so every secondary profile inherited the
default profile's email credentials instead of its own.

This was a sibling of the api_server env-leak bug (NousResearch#52307/NousResearch#50051):
the same os.getenv→get_secret migration that PR NousResearch#50094 applies to
gateway/config.py, but for the email adapter itself, which neither
PR NousResearch#50094 nor NousResearch#51374 covers.

Changes:
- plugins/platforms/email/adapter.py: replace os.getenv with
  agent.secret_scope.get_secret for all EMAIL_* credential reads
  (adapter __init__, check_email_requirements, _allowlist_in_effect,
  _dispatch_message allowlist gate, _send_email SMTP helper).
- gateway/config.py: add _getenv/_getenv_str/_getenv_int helpers
  (from PR NousResearch#50094) and replace os.getenv with _getenv for the email
  block in _apply_env_overrides, so config.platforms[EMAIL].extra
  is populated from the scoped value.
- tests/gateway/test_email_secret_scope.py: 5 new tests covering
  scoped credential reads, environ fallback without scope, missing-
  key-no-leak, allowlist scoping, and check_email_requirements scoping.

Related: NousResearch#50051, NousResearch#52307, PR NousResearch#50094, PR NousResearch#51374
@phantom-instruction-set
phantom-instruction-set force-pushed the fix/email-adapter-secret-scope-multiplex branch from b159fe6 to 18b70b4 Compare July 5, 2026 19:29
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/plugins Plugin system and bundled plugins comp/gateway Gateway runner, session dispatch, delivery platform/email Email (IMAP/SMTP) adapter area/auth Authentication, OAuth, credential pools sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 5, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for isolating the adapter-side gap. The premise is still valid on current main: plugins/platforms/email/adapter.py:437-443 reads email credentials from os.getenv() even though multiplexed adapter creation runs under _profile_runtime_scope() (gateway/run.py:8657-8689), which installs a profile secret scope (gateway/run.py:1441-1472).

Problems

  • The gateway/config.py portion is already superseded by 0f154e780e71c74f8a1cdccb25c97a6abd8e5a57: current gateway/config.py:174-204 provides _getenv, and gateway/config.py:1717-1736 already routes the Email block through it.
  • The adapter retains direct reads for EMAIL_IMAP_PORT, EMAIL_SMTP_PORT, and EMAIL_POLL_INTERVAL at plugins/platforms/email/adapter.py:440-443, plus EMAIL_TRUST_FROM_HEADER at line 467.
  • The new allowlist test only checks a truthy result; its default-profile and scoped values are both nonempty, so it would pass before the migration.

Suggested changes

  • Salvage the adapter/test portion only, complete the remaining per-profile EMAIL_* reads, and make the allowlist regression test distinguish scoped from process-global input.

Automated hermes-sweeper review.

self._password = os.getenv("EMAIL_PASSWORD", "")
self._imap_host = (os.getenv("EMAIL_IMAP_HOST", "") or extra.get("imap_host", "")).strip()
self._address = (_get_secret("EMAIL_ADDRESS", "") or extra.get("address", "")).strip()
self._password = _get_secret("EMAIL_PASSWORD", "")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please complete the scope migration for the adjacent per-profile Email settings too: EMAIL_IMAP_PORT, EMAIL_SMTP_PORT, and EMAIL_POLL_INTERVAL still use env_int, and EMAIL_TRUST_FROM_HEADER still uses env_bool. In multiplex mode those helpers read process-global environment values rather than the profile scope.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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 15, 2026
@teknium1 teknium1 added the area/profiles Multi-profile isolation, HERMES_HOME scoping label Jul 19, 2026
teknium1 added a commit that referenced this pull request Aug 2, 2026
… + scope ports/trust flag

Follow-up to the salvaged #59076 commit:

- Replace the bare get_secret import with a module-level Slack-pattern
  helper (_get_esecret): try get_secret, on UnscopedSecretError fall back
  to os.getenv. The DEFAULT profile's email adapter constructs UNSCOPED
  under multiplexing, where a bare get_secret raises and would crash the
  email path on startup — the exact WhatsApp defect fixed in 5438e9c
  (whatsapp_common._get_wsecret).
- Extend scope coverage to the remaining scope-blind reads:
  EMAIL_IMAP_PORT / EMAIL_SMTP_PORT / EMAIL_POLL_INTERVAL (_esecret_int
  replacing utils.env_int) and EMAIL_TRUST_FROM_HEADER (_esecret_bool
  replacing utils.env_bool).
- Add tests: default-profile unscoped-under-multiplex construction, and
  scoped ports/trust-flag no-environ-inheritance.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
… + scope ports/trust flag

Follow-up to the salvaged NousResearch#59076 commit:

- Replace the bare get_secret import with a module-level Slack-pattern
  helper (_get_esecret): try get_secret, on UnscopedSecretError fall back
  to os.getenv. The DEFAULT profile's email adapter constructs UNSCOPED
  under multiplexing, where a bare get_secret raises and would crash the
  email path on startup — the exact WhatsApp defect fixed in 43e0fe7
  (whatsapp_common._get_wsecret).
- Extend scope coverage to the remaining scope-blind reads:
  EMAIL_IMAP_PORT / EMAIL_SMTP_PORT / EMAIL_POLL_INTERVAL (_esecret_int
  replacing utils.env_int) and EMAIL_TRUST_FROM_HEADER (_esecret_bool
  replacing utils.env_bool).
- Add tests: default-profile unscoped-under-multiplex construction, and
  scoped ports/trust-flag no-environ-inheritance.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
… + scope ports/trust flag

Follow-up to the salvaged NousResearch#59076 commit:

- Replace the bare get_secret import with a module-level Slack-pattern
  helper (_get_esecret): try get_secret, on UnscopedSecretError fall back
  to os.getenv. The DEFAULT profile's email adapter constructs UNSCOPED
  under multiplexing, where a bare get_secret raises and would crash the
  email path on startup — the exact WhatsApp defect fixed in 754cdd2
  (whatsapp_common._get_wsecret).
- Extend scope coverage to the remaining scope-blind reads:
  EMAIL_IMAP_PORT / EMAIL_SMTP_PORT / EMAIL_POLL_INTERVAL (_esecret_int
  replacing utils.env_int) and EMAIL_TRUST_FROM_HEADER (_esecret_bool
  replacing utils.env_bool).
- Add tests: default-profile unscoped-under-multiplex construction, and
  scoped ports/trust-flag no-environ-inheritance.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
… + scope ports/trust flag

Follow-up to the salvaged NousResearch#59076 commit:

- Replace the bare get_secret import with a module-level Slack-pattern
  helper (_get_esecret): try get_secret, on UnscopedSecretError fall back
  to os.getenv. The DEFAULT profile's email adapter constructs UNSCOPED
  under multiplexing, where a bare get_secret raises and would crash the
  email path on startup — the exact WhatsApp defect fixed in 5438e9c
  (whatsapp_common._get_wsecret).
- Extend scope coverage to the remaining scope-blind reads:
  EMAIL_IMAP_PORT / EMAIL_SMTP_PORT / EMAIL_POLL_INTERVAL (_esecret_int
  replacing utils.env_int) and EMAIL_TRUST_FROM_HEADER (_esecret_bool
  replacing utils.env_bool).
- Add tests: default-profile unscoped-under-multiplex construction, and
  scoped ports/trust-flag no-environ-inheritance.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 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 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-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/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants