Skip to content

fix(secrets): secret-scope migration wave — /compress, Matrix, model tools, cold hydration, WhatsApp (cluster salvage) - #76573

Merged
teknium1 merged 9 commits into
mainfrom
fix/secret-scope-wave
Aug 2, 2026
Merged

fix(secrets): secret-scope migration wave — /compress, Matrix, model tools, cold hydration, WhatsApp (cluster salvage)#76573
teknium1 merged 9 commits into
mainfrom
fix/secret-scope-wave

Conversation

@teknium1

@teknium1 teknium1 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Consolidated salvage of the secret-scope migration wave: five contributor fixes land the remaining per-subsystem scope migrations, all verified against the profile-boundary rule (isolation between profiles; a profile's own children — subagents, cron, review forks, bridge subprocesses — inherit its secrets).

Every premise was re-verified on current main by parallel review before cherry-picking; contributor authorship preserved per-commit.

Changes (in composition order)

Validation

Path Verified
Targeted suites (compress, matrix, registry, discord, HASS, env_loader, secret sources, multiplex isolation, whatsapp×6, camofox) 250+ tests green
E2E: whatsapp _wenv default-profile unscoped → own environ (no crash); secondary scoped → own value
E2E: matrix _scoped_recovery_key scoped → profile key; scoped miss → empty (no borrow)
E2E: cold hydration .op.env token seeded into profile-local env; os.environ untouched
Cross-PR compose stacked merge-tree checks clean; branch based on today's main

Credits: @CocaKova (#74964), @sergioperezcheco (#69110), @tachyon-r (#74023), @kingrubic (#74549), @x7peeps (#75382).
Closes #69090, #75349, #74317.

Cluster context: #74722 and #76199 close separately as superseded (#74340 / #76213); #70616 closes as wrong-premise (roots-into-every-profile inheritance violates the isolation contract).

Infographic

scope wave complete

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on e1e5b58

all good!

@teknium1
teknium1 force-pushed the fix/secret-scope-wave branch from 366e1ae to fe869f5 Compare August 2, 2026 04:41
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets area/auth Authentication, OAuth, credential pools area/profiles Multi-profile isolation, HERMES_HOME scoping 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-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 2, 2026
CocaKova and others added 9 commits August 1, 2026 23:50
Multiplexed gateways resolve credentials through the fail-closed
per-profile secret scope (agent/secret_scope.py, Workstream A): any
get_secret() read outside a set_secret_scope(...) block raises
UnscopedSecretError. The agent turn installs the scope via _run_agent's
profile-scoping wrapper, but slash-command dispatch does not — so manual
/compress reached provider resolution unscoped and every invocation on a
gateway.multiplex_profiles: true deployment failed with:

  Manual compress failed: get_secret('OPENROUTER_BASE_URL') called with
  no profile secret scope active while multiplexing is on.

Same bug class as the cron scheduler (#57692) and the /v1/runs agent
path — an un-migrated call site the fail-closed design is meant to catch.

Two changes, both required:

- _handle_compress_command becomes a profile-scoping wrapper around the
  existing handler (renamed _handle_compress_command_inner), mirroring
  _run_agent: gated on multiplex_profiles, resolves the source profile's
  home and runs the whole handler inside _profile_runtime_scope. Covers
  the coroutine-side read (_resolve_session_agent_runtime).
- The compressor call switches from a bare loop.run_in_executor(None, …)
  to the existing _run_in_executor_with_context helper, so the scope
  contextvar survives the thread hop into _compress_context, where the
  aux-client provider resolution reads credentials.

Single-profile gateways take the pass-through branch — zero behavior
change (pinned by test).

Tests: 2 added (scoped read inside the executor under fail-closed
multiplexing reproduces the field failure pre-fix; single-profile
pass-through). 162 gateway compress/multiplex-scope tests green.
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 (#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 (#59739). Applied to both the startup verification
site and the status diagnostic.

Fixes #69090
…ltiplex profiles

Fix #75349

Root cause:
Under multiplex_profiles, secondary profiles run inside
_profile_runtime_scope which installs a per-profile secret scope via
set_secret_scope.  The WhatsApp adapter (and the shared
WhatsAppBehaviorMixin + Cloud API adapter) read WHATSAPP_MODE,
WHATSAPP_DM_POLICY, etc. via raw os.getenv(), bypassing the secret
scope.  Since os.environ doesn't contain secondary profile .env values,
the bridge silently falls back to 'self-chat' and rejects all inbound
messages with self_chat_mode_rejects_non_self.

Fix:
- Add _wenv() helper in adapter.py that reads WHATSAPP_* vars through
  get_secret() (agent.secret_scope), which honors the active scope.
- Replace all os.getenv('WHATSAPP_*') calls in adapter.py,
  whatsapp_common.py, and whatsapp_cloud.py with get_secret()-based
  equivalents.
- Inject resolved WHATSAPP_* values into the bridge subprocess
  environment so the Node.js bridge (which reads process.env) sees the
  profile's own configuration.

Changes:
- plugins/platforms/whatsapp/adapter.py: 37 lines (+ helper, bridge_env
  injection, 2 os.getenv→_wenv)
- gateway/platforms/whatsapp_common.py: 13 lines (6 os.getenv→_get_wsecret)
- gateway/platforms/whatsapp_cloud.py: 21 lines (9 os.getenv→_get_wsecret)
- New regression test: 6 test cases covering scope isolation, fallback,
  and cross-profile non-leakage.
…idge env set

Follow-ups on the #75382 salvage (review findings):
- _wenv/_get_wsecret now catch UnscopedSecretError and fall back to
  os.getenv for the DEFAULT profile's adapter, which constructs and sends
  outside any _profile_runtime_scope under multiplexing — a bare
  get_secret would crash its WhatsApp path (fixing one profile by
  breaking another). Same pattern as Slack SLACK_APP_TOKEN (#59739) and
  the Matrix recovery key. Scoped misses still return the default — no
  cross-profile borrow.
- bridge_env overlay extended to the full WHATSAPP_* set bridge.js
  consumes (DEBUG, FORWARD_OWNER_MESSAGES, REPLY_PREFIX,
  MAX_MESSAGE_LENGTH, CHUNK_DELAY_MS, SEND_TIMEOUT_MS).
- Removed the always-true conditional on WHATSAPP_MODE injection.
The salvaged hydrate_profile_secret_sources (#74549) seeded its
profile-local env from <home>/.env only, but the documented 1Password
bootstrap flow puts OP_SERVICE_ACCOUNT_TOKEN in the gitignored
<home>/.op.env (mirrored from load_hermes_dotenv). A cold profile using
that flow still failed 1Password hydration — the one unaddressed item
from the sweeper review on #74549. Seed .op.env via setdefault so .env
values win; never touches os.environ. Two regression tests.
@teknium1
teknium1 force-pushed the fix/secret-scope-wave branch from fe869f5 to e1e5b58 Compare August 2, 2026 06:52
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/compression Context compression and continuation sessions area/profiles Multi-profile isolation, HERMES_HOME scoping comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists 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.

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

7 participants