fix(multiplex): wire profile secret scope + complete adapter routing - #58587
fix(multiplex): wire profile secret scope + complete adapter routing#58587Tattooed-Geek wants to merge 6 commits into
Conversation
|
suggesting changes The patch still leaves secondary-profile slash commands routed through the default adapter. Security evidence: the affected boundary is multiplex profile isolation for per-profile Telegram sessions and approvals; the PR-head helper Please route the remaining slash-command adapter lookups through Signed: GPT-5.5-xhigh in Codex |
|
Thank you for the thorough review. Pushed a fix: all 10 Added All 14 tests pass. Verified live with two Telegram bots (default + secondary profile) both responding correctly to /status. |
|
suggesting changes The latest head fixes the direct Security evidence: the affected trust boundary is multiplex profile isolation for per-profile Telegram approval flows; the secondary-profile Please route the plain-text approval confirmation send in Signed: GPT-5.5-xhigh in Codex |
|
Thank you for catching this. Fixed: Added All 17 tests pass. I also audited the remaining
These remaining sites are out of scope for this PR but noted for future work. |
New commit: skip port-binding platform on secondary profiles instead of crashingProblem: When Root cause: The multiplex guard in Fix: Replace the fatal
This means a misconfigured secondary profile can no longer crash the entire gateway. The warning message is clear and actionable: Test updated: Validated in real conditions: Added |
|
suggesting changes
Security evidence:
Please route the remaining platform credential env overrides through the profile secret-scope helper and add focused coverage for at least one non-Telegram credential path so a secondary profile cannot inherit the default profile's credentials. Signed: GPT-5.5-xhigh in Codex |
multiplex_profiles: true does not actually work for per-profile Telegram bots. _apply_env_overrides() in config.py reads TELEGRAM_BOT_TOKEN (and all platform tokens) via os.getenv(), which sees the default profile's environment even when load_gateway_config() is called inside _profile_runtime_scope() for a secondary profile. Both profiles load the same token -> polling conflict. The infrastructure already exists (agent/secret_scope.py with get_secret(), _profile_runtime_scope(), build_profile_secret_scope()) but _apply_env_overrides() was never migrated to use it. Additionally, _adapter_for_source() is defined in run.py but only used in 5 of ~52 call sites. The remaining 47 still use self.adapters.get(source.platform), routing secondary-profile messages through the default adapter. Finally, BasePlatformAdapter.build_source() does not accept a profile parameter, so adapters cannot stamp source.profile on incoming messages. Without source.profile, _adapter_for_source() always sees profile=None and falls back to the default map. Changes: - config.py: add _profile_secret() / _profile_secret_str() wrappers around get_secret(), replace all os.getenv / env_var_enabled calls for platform tokens and settings with the profile-scoped versions - run.py: replace 47 self.adapters.get(source.platform) calls with self._adapter_for_source(source); add profile to inbound message log - base.py: add profile parameter to build_source(), propagate to SessionSource - adapter.py (telegram): pass profile to build_source() / build_session_key() in _build_message_event, _text_batch_key, _photo_batch_key - tests: add TestAdapterForSource with 3 tests covering default, secondary, and active-profile routing
…for_source Slash commands in gateway/slash_commands.py used self.adapters.get(source.platform) which ignores source.profile. In multiplex mode, a secondary-profile Telegram event could resume typing or inspect the default profile's adapter state. Fixed 10 sites: /status queue-depth, /status Matrix scope, /model picker, /goal pause, /goal clear, /goal start, /voice on, /voice status, /approve, /deny — all now route through self._adapter_for_source(source). Added TestSlashCommandProfileRouting with /approve and /deny coverage verifying the secondary adapter is used and the default adapter is untouched. Addresses PR review feedback from egilewski.
…sage BasePlatformAdapter.handle_message() computed the session key via build_session_key() before _make_profile_message_handler had a chance to stamp source.profile. This caused a key mismatch: the adapter guard map used agent:main:… while the session store (which re-reads source.profile inside _generate_session_key) used agent:<profile>:…. In multiplex mode this meant the active-session guard for a secondary-profile Telegram DM could miss a concurrently running agent on that profile, because the guard key and the session key lived in different namespaces. Fix: stamp source.profile from adapter.profile_name at the top of handle_message(), before build_session_key() is called. Existing profile values are preserved (not overwritten). Added TestProfileStampingBeforeSessionKey covering both the stamp and no-overwrite paths.
…r_source _handle_active_session_busy_message() used self.adapters.get(event.source.platform) to send the plain-text approval confirmation (bare 'yes'/'approve' replies). In multiplex mode, a secondary-profile Telegram user's approval confirmation was sent through the default adapter instead of the profile-owned one. Fix: use _adapter_for_source(event.source) for the confirmation send. Added TestBareTextApprovalRouting covering a secondary-profile bare 'yes' approval response — verifies the secondary adapter sends the confirmation and the default adapter is untouched. Addresses second round of PR review feedback from egilewski.
…ead of crashing When multiplex_profiles is enabled and a secondary profile has api_server in its platforms config, the gateway raised a fatal MultiplexConfigError that caused a crash-restart loop. Since the default profile owns the single shared HTTP listener and serves secondary profiles via /p/<profile>/, the api_server entry on a secondary profile is simply redundant — not a reason to crash. Replace the fatal raise with a WARNING log + skip: the platform is ignored for that profile, the gateway continues starting all other platforms normally. Test updated: test_secondary_webhook_raises → test_secondary_webhook_skipped asserts the platform is skipped (not started) and a warning is logged, instead of asserting a crash.
…rofile secret scope Migrate 73 os.getenv() calls in _apply_env_overrides() to _profile_secret()/ _profile_secret_str() for DingTalk, Feishu, WeCom, WeCom Callback, Weixin, BlueBubbles, QQBot, and Yuanbao — matching the pattern already applied to Telegram, Discord, WhatsApp, Slack, Signal, and Mattermost in commit 1. Add a guard in the plugin-enable pass: when a profile secret scope is active (multiplex mode), skip the is_connected() probe and auto-enablement for platforms not already explicitly configured in YAML. Plugin is_connected() implementations read os.getenv() directly and would see the default profile's credentials in os.environ, breaking isolation. Add test_dingtalk_not_inherited_from_default_env: verifies a secondary profile with no DINGTALK_* entries in its secret scope does not enable DingTalk even when the default profile's credentials are in os.environ.
197de48 to
dfe577f
Compare
New commit: route non-Telegram credential env overrides through profile secret scopeAddresses the remaining credential-isolation gap for non-Telegram platforms. Changes1. Migrated 73
This matches the pattern already applied to Telegram, Discord, WhatsApp, Slack, Signal, and Mattermost in commit 1. Non-credential gateway settings ( 2. Guarded the plugin-enable pass against credential leakage: During the review I discovered that even after migrating Fix: when a profile secret scope is active (
A secondary profile now only gets platforms it explicitly configured in YAML or whose credentials are in its own 3. Added Sets Validation
|
What does this PR do?
Fixes three integration gaps that prevent
multiplex_profiles: truefrom working with per-profile Telegram bots (and other polling platforms).Problem 1: Platform tokens read via
os.getenvinstead of profile secret scope_apply_env_overrides()ingateway/config.pyreadsTELEGRAM_BOT_TOKEN(and all platform tokens) viaos.getenv(). Even thoughload_gateway_config()is called inside_profile_runtime_scope(profile_home)for secondary profiles,os.getenvbypasses the secret scope and reads fromos.environ— which holds the default profile's token. Both profiles end up polling the same bot token.The infrastructure already exists (
agent/secret_scope.pywithget_secret(),set_secret_scope(),build_profile_secret_scope(),_profile_runtime_scope()) but_apply_env_overrides()was never migrated to use it.Problem 2:
_adapter_for_source()defined but unused at 47 call sites_adapter_for_source()is defined ingateway/run.pybut only called in 5 of ~52 call sites. The remaining 47 still useself.adapters.get(source.platform), routing secondary-profile messages through the default adapter.Problem 3:
build_source()does not stampsource.profileBasePlatformAdapter.build_source()does not accept aprofileparameter, sosource.profileis alwaysNone._adapter_for_source()falls back to the default adapter map. Batch keys (_text_batch_key,_photo_batch_key) are also not namespaced by profile.Related Issue
No existing issue found.
Type of Change
Changes Made
gateway/config.py: Add_profile_secret()/_profile_secret_str()wrappers aroundget_secret(). Replace allos.getenv/env_var_enabledcalls for platform tokens with profile-scoped versions.gateway/run.py: Replace 47self.adapters.get(source.platform)calls withself._adapter_for_source(source). Add profile to inbound message log.gateway/platforms/base.py: Addprofileparameter tobuild_source(), propagate toSessionSource.plugins/platforms/telegram/adapter.py: Passprofile=getattr(self, 'profile_name', None)tobuild_source()/build_session_key()in 3 call sites.tests/gateway/test_multiplex_adapter_registry.py: AddTestAdapterForSourcewith 3 tests.How to Test
.envfilesmultiplex_profiles: trueinconfig.yamlvenv/bin/pytest tests/gateway/test_multiplex_adapter_registry.py tests/gateway/test_multiplex_credential_isolation.py tests/gateway/test_multiplex_profile_authz.py -v(expected: 28 passed)Checklist
Notes
multiplex_profilesis off,get_secret()readsos.environidentically toos.getenv()._profile_secret()wrappers fall back toos.getenvwhenagent.secret_scopeis not importable.build_source()'s newprofileparameter defaults toNone, so existing callers are unaffected.