Skip to content

fix(platforms): per-profile adapter settings resolve env → own YAML → default under multiplex (#108440 review, salvage #109036 #109477 #110109 #110111 #110131) - #110293

Merged
teknium1 merged 16 commits into
mainfrom
fix/mux-triage-adapter-config
Sep 13, 2026
Merged

teknium1 merged 16 commits into
mainfrom
fix/mux-triage-adapter-config

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Under gateway.multiplex_profiles, every adapter setting now resolves for the owning profile as explicit scoped env/.env → that profile's config.yaml → the adapter's default, through one shared reader — a secondary never inherits the launch process's env on a miss, and single-profile installs keep the documented env-over-YAML contract.

Resolves the andrexibiza / ehz0ah post-merge review of #108440 (findings 3–7 + both inline bugs) and salvages the community PRs in the same adapter-config cluster. Terminal-scope findings 1–2 ship separately in #110207.

The rule (applied uniformly)

gateway.platforms._shared.extra_or_secret(extra, key, ENV, default) is the ONE per-profile setting reader:

Rung Source Notes
1 explicit env ENV via get_scoped_secret a secondary sees only its own .env; blank = unset
2 the profile's YAML (PlatformConfig.extra[key]) explicit false/0 is a real value
3 default a scoped miss returns this — never the launch process's os.environ

Unscoped (single-profile / default profile) the env rung reads os.environ, so DISCORD_ALLOW_MENTION_EVERYONE=false beats allow_mentions.everyone: true and TELEGRAM_REACTIONS=true beats the stock reactions: false exactly as each platform page documents.

Changes

Validation

Review probes (contract_probes.py from the #108440 review, fresh process each, temp HOME, real load_gateway_config + real adapter constructors under _profile_runtime_scope):

Probe origin/main this branch
discord_precedence env=false vs YAML true parse: ["everyone","users"] parse: ["users"]
ambient_readers secondary omits keys inherited launch values (pre-fix run) all defaults
matrix_readers seeded lists allowed_user_ids: [], ignored sender dispatched, owner cannot approve ["@owner"], 0 dispatched, owner approves
telegram_proxy secondary YAML proxy [null, null] [proxy, proxy]
bots_auth YAML allow_bots: all adapter all, gateway false both true
Yuanbao secondary auto-sethome top-level key, reload → None live + reloaded dm:tenant-b2, env untouched
WhatsApp secondary dm_policy: pairing bridge env allowlist + default's allowlist bridge env pairing, no allowlist

Tests: tests/gateway/test_adapter_settings_scoped_precedence.py (6 invariants) + 2 in test_shared_platform_boilerplate.py — all 8 red on origin/main via source swap, green here. scripts/run_tests.sh tests/gateway tests/plugins tests/tools tests/hermes_cli tests/agent tests/cron: 41,989 passed; the 8 failures (modal/parallel-web lazy-install, live-guard, update-gate) fail identically on origin/main. ruff / windows-footguns / compat-pointers / git diff --check clean.

Refs #108440, #109032, #109475, #110111, #110109, #110131, #107442. Related: #110207 (terminal scope).

Infographic

mux-adapter-config-precedence

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 021100b — test(whatsapp): an explicit empty free_response_chats list i

⚠️ Warnings

OSV vulnerability scan · View job

76 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.


debug info

CI timings

CI timings · View report · View job

Wall time 5m44s vs 4m40s (+22.9%). 6 job(s) slower, 7 faster, 2 unchanged.

  • OS-specific tests / Windows-only tests: +64.0s
  • Docs Site / docs-site-checks: -59.0s
  • Python tests / Run tests: +48.0s
  • Check no case-colliding filenames / check-case-collisions: -18.0s
  • OSV scan / Scan lockfiles / osv-scan: -10.0s

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter platform/discord Discord bot adapter platform/slack Slack app adapter platform/matrix Matrix adapter (E2EE) platform/feishu Feishu / Lark adapter platform/whatsapp WhatsApp Business adapter platform/wecom WeCom / WeChat Work adapter area/config Config system, migrations, profiles area/profiles Multi-profile isolation, HERMES_HOME scoping 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 labels Sep 13, 2026
liuhao1024 and others added 13 commits September 13, 2026 15:10
…ed YAML default

545e74d made _reactions_enabled consult extra.reactions before the env
var, and _apply_yaml_config seeds extra["reactions"] whenever the YAML key
is present — including the stock reactions: false every install
materializes. The documented TELEGRAM_REACTIONS=true switch therefore
became a silent no-op after the 0.21.2 update (#109032), contradicting
yaml_env_setter's "explicit env wins over YAML" contract.

Read the scoped env first and fall back to the profile's own YAML: under
multiplex a scoped miss returns the default instead of another profile's
process-env value (#72348), so only a scoped/env hit counts as explicit
and per-profile isolation is unchanged.

Fixes #109032

(cherry picked from commit 2bd5a0a)
…ofiles

#69090 scoped MATRIX_RECOVERY_KEY itself (via _scoped_recovery_key())
so a secondary profile resolves its own recovery key under multiplex,
but left its sibling, MATRIX_RECOVERY_KEY_OUTPUT_FILE, on a bare
os.getenv(). _recovery_key_output_path() is called from inside
_verify_or_bootstrap_cross_signing(), which runs fully inside
_profile_runtime_scope for a secondary profile: when that profile
bootstraps a new recovery key, it either doesn't get written to a
file at all, or gets written to the default profile's configured
path, depending on which one has the env var set.

Route it through the same _get_scoped_secret() helper _scoped_recovery_key()
already uses.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit fb765ee)
Every other WEIXIN_* tunable in this __init__ block (dm_policy,
group_policy, rate_limit_circuit_*, send_chunk_*) already reads
extra-first with a scoped-secret fallback via _extra_or_secret(). This
one field was missed and still fell back to a bare os.getenv(), so a
secondary profile without its own split_multiline_messages setting
silently inherited the default profile's process-env value instead of
the coded default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 44e3c0d)
A2A_PORT and A2A_ADVERTISED_TOOLSETS are already captured at
construction time (inside _profile_runtime_scope) via
_get_scoped_secret(), but A2A_PUBLIC_URL was still read with a bare
os.getenv() inside A2ARequestHandler._request_public_url() - which
runs on ThreadingHTTPServer's per-connection OS thread, not the
constructing thread.

Raw threading.Thread never inherits contextvars, so even swapping the
reader to _get_scoped_secret() at that call site would not help: the
request thread has no scope, secret_scope falls back to os.environ
either way. The value must be captured once at construction time
(which does run in profile scope) and threaded through as instance
state instead - same fix shape as A2A_PORT above.

A secondary multiplex profile without its own A2A_PUBLIC_URL now
falls back to the X-Forwarded-Host/Host-derived URL (or the bind
host) instead of silently advertising the default profile's public
URL in its Agent Card / discovery response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 0c36aca)
…fault, per profile

One reader (gateway.platforms._shared.extra_or_secret) now implements the
precedence every per-profile setting follows for the OWNING profile:
explicit scoped env/.env → that profile's config.yaml (PlatformConfig.extra)
→ the adapter's default. A scoped miss returns the default, never the launch
process's os.environ; single-profile / default-profile installs keep the
documented env-over-YAML contract.

Why: 545e74d (#108705) stopped bridging a secondary's YAML into the
process env and moved readers to config.extra, but the shared reader and the
hand-rolled helpers in Discord/Slack/Matrix/Telegram consulted YAML FIRST and
then fell back to a scoped env read. Two bug classes followed (#108440
post-merge review by andrexibiza, #109032):
- an explicit env value could no longer beat YAML for the owning profile
  (DISCORD_ALLOW_MENTION_EVERYONE=false lost to allow_mentions.everyone: true;
  TELEGRAM_REACTIONS=true lost to the stock reactions: false);
- a secondary that OMITTED a key inherited the launch profile's bridged env
  through the fallback (Matrix process_notices/session_scope, Discord
  auto_thread/reactions/mentions, Slack reactions/ignored_channels).

Consumers migrated to the shared reader: Discord _build_allowed_mentions and
_extra_or_env_flag; Slack _slack_allow_bots, _reactions_enabled (the
_extra_or_env_* getters already used it); Matrix _extra_truthy, _extra_csv_set,
session_scope, reactions, require_mention parsers, and — new — the
allowed_users / ignore_user_patterns consumers that never read the seeded YAML
lists; Telegram _extra_bool, _extra_str_set, _reactions_enabled; Feishu
allow_bots; WhatsApp dm_policy/group_policy.

Refs #108440, #109032
…onstruction without an env bridge

545e74d correctly stopped writing telegram.proxy_url into TELEGRAM_PROXY
for a multiplexed secondary, but _build_ptb_requests still resolved the proxy
only from that env var, so the secondary silently connected direct (or via the
default's proxy). #100448 had deliberately left this bridge unscoped for that
reason; this finishes the consumer migration instead.

_apply_yaml_config seeds proxy_url into extra and resolve_proxy_url gains a
`configured` rung: scoped TELEGRAM_PROXY → the profile's YAML → HTTPS_PROXY/
HTTP_PROXY/ALL_PROXY (trust_env) → macOS system proxy, with NO_PROXY semantics
unchanged.

Refs #108440 (finding 6)
…e's YAML policy

GatewayAuthorizationMixin._chat_scoped_grant read only the scoped
{PLATFORM}_ALLOW_BOTS env var, so a secondary whose config.yaml said
`allow_bots: all` was admitted by its own adapter and then denied centrally
(Discord, Slack Workflow posts with user=None, Feishu, Telegram). The gate now
resolves the routed adapter's effective policy with the same reader as intake:
scoped env → adapter YAML → none. Mention requirement and loop guard are
unchanged.

Refs #108440 (finding 7)
…d updates the live config

For a multiplexed secondary the middleware wrote a top-level
YUANBAO_HOME_CHANNEL key that load_gateway_config never reads and skipped the
(correctly suppressed) process-env write, so cron and home-channel delivery had
no target in-process and none after a reload either. Persist through the
gateway's persist_home_channel (the profile-aware config path every /sethome
uses) and set the live PlatformConfig.home_channel; the process env is still
untouched under a secondary's scope.

Refs #108440 (ehz0ah inline, gateway/platforms/yuanbao.py)
…ow_from, not the launch env's

_bridge_env copied os.environ (the default profile's WHATSAPP_* values under
multiplex) and only overlaid scoped hits, so a secondary with YAML
`dm_policy: pairing` launched its Node bridge under the default profile's
`allowlist` policy and the bridge rejected valid pairing DMs before Python saw
them. The child env now carries the values the adapter resolved (scoped env →
own YAML → default); a scoped miss removes the key rather than inheriting it.

Refs #108440 (ehz0ah inline, plugins/platforms/whatsapp/adapter.py)
… its consumers

Real loader + real adapter constructors under _profile_runtime_scope: a
secondary reads its own YAML lists/flags and never the launch env on a miss;
explicit env beats YAML for the owning profile; the central allow_bots gate
agrees with the adapter; Matrix YAML lists gate intake and approval; Yuanbao
home channel is live and reloadable; the WhatsApp bridge env carries the
secondary's policy. All eight cases red on origin/main.
#109036 added six TELEGRAM_REACTIONS cases and #110111 five recovery-key-path
cases; keep the two contracts per fix (explicit env beats YAML; a scoped miss
returns the default) and drop the change-detector permutations.
… default)

Multi-profile guide gains the rule and the consumers it covers; the adapter
authoring guide and the Slack allow_bots page no longer claim YAML wins.
… own YAML like every sibling

Rebase reconciliation with main's JSON-allowlist decoding (#109423): the two
remaining readers that consulted config.extra before the env var now follow the
per-profile precedence rule (explicit scoped env → the profile's YAML → default),
and ignored_threads still decodes a JSON-string list after the read.

The Matrix blank-YAML test asserted YAML-over-env, the old precedence #108440's
review flagged; it now pins the contract: explicit env beats YAML, a blank env
value is unset (YAML applies), YAML beats the default, and an explicit empty
list is a real "no rooms" value.
…d without an explicit env value

Under env → YAML → default an explicit env CSV beats the YAML list; the test now
blanks the env (blank env = unset) before asserting that [] is a real 'no chats'
value.
@teknium1
teknium1 force-pushed the fix/mux-triage-adapter-config branch from 0da507d to 021100b Compare September 13, 2026 22:30
@teknium1
teknium1 merged commit b602ba2 into main Sep 13, 2026
37 checks passed
@teknium1
teknium1 deleted the fix/mux-triage-adapter-config branch September 13, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles area/profiles Multi-profile isolation, HERMES_HOME scoping comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/discord Discord bot adapter platform/feishu Feishu / Lark adapter platform/matrix Matrix adapter (E2EE) platform/slack Slack app adapter platform/telegram Telegram bot adapter platform/wecom WeCom / WeChat Work adapter platform/whatsapp WhatsApp Business adapter 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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants