fix(secrets): secret-scope migration wave — /compress, Matrix, model tools, cold hydration, WhatsApp (cluster salvage) - #76573
Merged
Merged
Conversation
Contributor
૮ >ﻌ< ა ci reviewran on e1e5b58 all good! |
teknium1
force-pushed
the
fix/secret-scope-wave
branch
from
August 2, 2026 04:41
366e1ae to
fe869f5
Compare
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
force-pushed
the
fix/secret-scope-wave
branch
from
August 2, 2026 06:52
fe869f5 to
e1e5b58
Compare
This was referenced Aug 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
_profile_runtime_scopeand itsrun_in_executor(None, …)hop dropped the ContextVar scope →UnscopedSecretErrorunder multiplexing. Now wraps the handler in the profile scope and uses_run_in_executor_with_context— scopes the spawn, no fallback, no swallowing.MATRIX_RECOVERY_KEYread viaget_secret; scoped miss returns empty (no cross-profile borrow); default-profile startup uses the established Slack-patternUnscopedSecretError→os.getenvfallback. 6 tests.get_secret;check_fn/tool-defs caches gain a(fn, profile_home)key (unresolved scope ⇒ cache bypass, fail-closed); fixes the default profile's own unscoped ingress via_primary_message_handlerscope wrap. Child-inheritance verified: cache scope rides the same ContextVarcopy_contextpropagates._profile_runtime_scopebefore scope build — profile-local env, never mutatesos.environ. Our follow-up seeds the documented.op.envbootstrap (OP_SERVICE_ACCOUNT_TOKEN) the original missed (sweeper-flagged); 2 regression tests.WHATSAPP_*reads via scope-aware helpers; bridge subprocess overlay injects the routed profile's values (child inherits — correct direction). Our follow-ups:UnscopedSecretErrorfallback so the default profile's adapter doesn't crash (the PR's one real defect), full bridge.js env set (DEBUG/FORWARD/PREFIX/lengths/delays), dead-conditional removal. Junk commits (CI-retrigger, stray changelog) dropped from the original.Validation
_wenv_scoped_recovery_key.op.envtoken seeded into profile-local env;os.environuntouchedCredits: @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