8185834bc9
Weixin, WeCom, QQBot, WhatsApp (common + cloud) and Yuanbao's AccessPolicy each carried their own `_open_dm_opted_in` / `_is_dm_allowed` / `_is_dm_intake_allowed` / `_is_group_allowed`, differing only in the platform prefix of the allow-all env var and in small drifts. The same "allow-all must be scoped / fail closed" fix has landed on this trio at least four times; a shared rule means it lands once. gateway/platforms/access_policy_mixin.py::OwnAccessPolicyMixin owns the predicates, reads every env name through the scoped `get_scoped_secret` and exposes two hooks: `_entry_matches` (platform allowlist matching) and `_live_dm_allow_from` (env-seeded lists re-read live). `ALLOW_ALL_ENV_PREFIX` is the only per-adapter datum. Retained overrides (behavior genuinely differs): - whatsapp_cloud `_allow_all_env_names` adds WHATSAPP_CLOUD_ALLOW_ALL_USERS; `_is_dm_allowed` keeps its bare-wa_id normalisation. - whatsapp_common `_entry_matches` -> phone/LID alias matching; `_live_dm_allow_from`. - wecom `_is_group_allowed(chat_id, sender_id)` adds the per-group sender allowlist on top of the shared chat-level rule; `_entry_matches` strips `wecom:user:` prefixes. - qqbot `_entry_matches` (case-insensitive, `*`). - yuanbao `AccessPolicy.is_group_allowed`: `open` groups still require the allow-all opt-in (no runner-side mention gate), so it wraps the shared rule. Behavior change: Weixin `_is_dm_intake_allowed` now denies a blank/whitespace principal (the other four already did; safe variant chosen). Weixin's inline group gate now goes through `_is_group_allowed` with identical verdicts.