fix(teams): lazy-install SDK when gateway starts Teams - #79812
Conversation
Platform registry create_adapter() gates on check_fn before the adapter exists, so wiring the passive probe permanently blocked connect() and the existing check_teams_requirements() lazy-install never ran.
Step 5 only showed docker compose from a clone; native/systemd users hit missing compose files and PEP 668 system-pip failures.
…ive installer) PlatformEntry.check_fn served three contradictory roles: adapter-creation gate, config auto-enablement gate, and status display. Plugins had to pick one function for all three: - Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/ feishu): every status display could pip-install SDKs as a side effect (the desktop 94% boot-loop class). - Passive probe as check_fn (teams, wecom_callback): create_adapter() returned None before connect() could lazy-install, so the SDK never installed (NousResearch#79812 deadlock; wecom_callback's platform.wecom_callback LAZY_DEPS entry was dead code). The split makes both call sites correct by construction: - check_fn is now contractually PASSIVE (probe only, never installs). - New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer; create_adapter() runs it exactly when check_fn is False — the platform is enabled+configured and the gateway is about to connect it. - Config enablement keeps a configured platform whose deps are missing but installable; the install itself is deferred to create_adapter(). - Status surfaces (_platform_status, hermes status) read only the passive probe and can never trigger pip. Migrated all lazy-installable platform plugins to the split; platforms with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged — no ensure_deps_fn means a False check_fn stays a hard block. wecom_callback gains a working installer for the first time. Builds on @xxxigm's NousResearch#79812 (both commits cherry-picked with authorship preserved), reworking the check_fn swap into the two-field split so the Teams fix doesn't reintroduce install-on-status.
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps, ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior check_*_requirements combined credential env checks with the install, so a platform configured via PlatformConfig.extra (which is_connected accepts) would pass enablement, reach create_adapter(), and have the 'installer' veto on env-var grounds before installing anything — re-creating the NousResearch#79812 deadlock for extra-configured setups. The combined deps+credentials functions remain for setup/status callers. - matrix/feishu passive probes: use the existing lazy_deps.is_available() instead of hand-rolling 'not feature_missing(...)' (reuse finding). - teams: module docstring no longer recommends bare system pip (the PEP 668 trap purged everywhere else); docs troubleshooting row updated to match the new hint text. - wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE' (ensure_and_bind mutates the module dict directly; nothing assigns). - tests: parametrized wiring contract for all 8 lazy-installable platforms — ensure_deps_fn present and distinct from check_fn (behavior contract, not identity snapshot, so renames don't churn it).
…ive installer) PlatformEntry.check_fn served three contradictory roles: adapter-creation gate, config auto-enablement gate, and status display. Plugins had to pick one function for all three: - Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/ feishu): every status display could pip-install SDKs as a side effect (the desktop 94% boot-loop class). - Passive probe as check_fn (teams, wecom_callback): create_adapter() returned None before connect() could lazy-install, so the SDK never installed (#79812 deadlock; wecom_callback's platform.wecom_callback LAZY_DEPS entry was dead code). The split makes both call sites correct by construction: - check_fn is now contractually PASSIVE (probe only, never installs). - New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer; create_adapter() runs it exactly when check_fn is False — the platform is enabled+configured and the gateway is about to connect it. - Config enablement keeps a configured platform whose deps are missing but installable; the install itself is deferred to create_adapter(). - Status surfaces (_platform_status, hermes status) read only the passive probe and can never trigger pip. Migrated all lazy-installable platform plugins to the split; platforms with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged — no ensure_deps_fn means a False check_fn stays a hard block. wecom_callback gains a working installer for the first time. Builds on @xxxigm's #79812 (both commits cherry-picked with authorship preserved), reworking the check_fn swap into the two-field split so the Teams fix doesn't reintroduce install-on-status.
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps, ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior check_*_requirements combined credential env checks with the install, so a platform configured via PlatformConfig.extra (which is_connected accepts) would pass enablement, reach create_adapter(), and have the 'installer' veto on env-var grounds before installing anything — re-creating the #79812 deadlock for extra-configured setups. The combined deps+credentials functions remain for setup/status callers. - matrix/feishu passive probes: use the existing lazy_deps.is_available() instead of hand-rolling 'not feature_missing(...)' (reuse finding). - teams: module docstring no longer recommends bare system pip (the PEP 668 trap purged everywhere else); docs troubleshooting row updated to match the new hint text. - wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE' (ensure_and_bind mutates the module dict directly; nothing assigns). - tests: parametrized wiring contract for all 8 lazy-installable platforms — ensure_deps_fn present and distinct from check_fn (behavior contract, not identity snapshot, so renames don't churn it).
|
Thanks @xxxigm — your diagnosis was exactly right: the passive We salvaged both of your commits with authorship preserved into #80305, then widened the fix to the whole bug class: swapping to the active installer as This PR will close automatically when #80305 merges. Thanks again for the report and the fix! |
…ract Upstream NousResearch#79812 split the platform dep hooks into a PASSIVE probe (check_fn, safe for status displays) and an ACTIVE installer (ensure_deps_fn, run by create_adapter right before connect). Every upstream adapter was migrated; whatsapp was not, because our aiohttp fix (0fae0cc) predates the split and installed inline from connect(). Register the two roles properly: - whatsapp_deps_present() — passive: Node bridge + aiohttp via find_spec, so a status pass never imports or installs aiohttp. - ensure_whatsapp_requirements() — active: lazy-install platform.whatsapp, then re-probe. connect() keeps a defensive call for the reconnect and standalone-sender paths that bypass the registry, and now reports an explicit fatal error when aiohttp is genuinely uninstallable instead of dying on a later ImportError. Behavior is unchanged when deps are present; the install just moves to the hook upstream calls. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ive installer) PlatformEntry.check_fn served three contradictory roles: adapter-creation gate, config auto-enablement gate, and status display. Plugins had to pick one function for all three: - Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/ feishu): every status display could pip-install SDKs as a side effect (the desktop 94% boot-loop class). - Passive probe as check_fn (teams, wecom_callback): create_adapter() returned None before connect() could lazy-install, so the SDK never installed (NousResearch#79812 deadlock; wecom_callback's platform.wecom_callback LAZY_DEPS entry was dead code). The split makes both call sites correct by construction: - check_fn is now contractually PASSIVE (probe only, never installs). - New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer; create_adapter() runs it exactly when check_fn is False — the platform is enabled+configured and the gateway is about to connect it. - Config enablement keeps a configured platform whose deps are missing but installable; the install itself is deferred to create_adapter(). - Status surfaces (_platform_status, hermes status) read only the passive probe and can never trigger pip. Migrated all lazy-installable platform plugins to the split; platforms with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged — no ensure_deps_fn means a False check_fn stays a hard block. wecom_callback gains a working installer for the first time. Builds on @xxxigm's NousResearch#79812 (both commits cherry-picked with authorship preserved), reworking the check_fn swap into the two-field split so the Teams fix doesn't reintroduce install-on-status.
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps, ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior check_*_requirements combined credential env checks with the install, so a platform configured via PlatformConfig.extra (which is_connected accepts) would pass enablement, reach create_adapter(), and have the 'installer' veto on env-var grounds before installing anything — re-creating the NousResearch#79812 deadlock for extra-configured setups. The combined deps+credentials functions remain for setup/status callers. - matrix/feishu passive probes: use the existing lazy_deps.is_available() instead of hand-rolling 'not feature_missing(...)' (reuse finding). - teams: module docstring no longer recommends bare system pip (the PEP 668 trap purged everywhere else); docs troubleshooting row updated to match the new hint text. - wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE' (ensure_and_bind mutates the module dict directly; nothing assigns). - tests: parametrized wiring contract for all 8 lazy-installable platforms — ensure_deps_fn present and distinct from check_fn (behavior contract, not identity snapshot, so renames don't churn it).
…ive installer) PlatformEntry.check_fn served three contradictory roles: adapter-creation gate, config auto-enablement gate, and status display. Plugins had to pick one function for all three: - Active installer as check_fn (discord/slack/telegram/matrix/dingtalk/ feishu): every status display could pip-install SDKs as a side effect (the desktop 94% boot-loop class). - Passive probe as check_fn (teams, wecom_callback): create_adapter() returned None before connect() could lazy-install, so the SDK never installed (NousResearch#79812 deadlock; wecom_callback's platform.wecom_callback LAZY_DEPS entry was dead code). The split makes both call sites correct by construction: - check_fn is now contractually PASSIVE (probe only, never installs). - New optional PlatformEntry.ensure_deps_fn is the ACTIVE installer; create_adapter() runs it exactly when check_fn is False — the platform is enabled+configured and the gateway is about to connect it. - Config enablement keeps a configured platform whose deps are missing but installable; the install itself is deferred to create_adapter(). - Status surfaces (_platform_status, hermes status) read only the passive probe and can never trigger pip. Migrated all lazy-installable platform plugins to the split; platforms with no optional deps (irc/ntfy/buzz/simplex/line/a2a/...) are unchanged — no ensure_deps_fn means a False check_fn stays a hard block. wecom_callback gains a working installer for the first time. Builds on @xxxigm's NousResearch#79812 (both commits cherry-picked with authorship preserved), reworking the check_fn swap into the two-field split so the Teams fix doesn't reintroduce install-on-status.
- matrix/dingtalk: extract deps-only installers (ensure_matrix_deps, ensure_dingtalk_deps) and register THOSE as ensure_deps_fn — the prior check_*_requirements combined credential env checks with the install, so a platform configured via PlatformConfig.extra (which is_connected accepts) would pass enablement, reach create_adapter(), and have the 'installer' veto on env-var grounds before installing anything — re-creating the NousResearch#79812 deadlock for extra-configured setups. The combined deps+credentials functions remain for setup/status callers. - matrix/feishu passive probes: use the existing lazy_deps.is_available() instead of hand-rolling 'not feature_missing(...)' (reuse finding). - teams: module docstring no longer recommends bare system pip (the PEP 668 trap purged everywhere else); docs troubleshooting row updated to match the new hint text. - wecom_callback: drop dead 'global ET, DEFUSEDXML_AVAILABLE' (ensure_and_bind mutates the module dict directly; nothing assigns). - tests: parametrized wiring contract for all 8 lazy-installable platforms — ensure_deps_fn present and distinct from check_fn (behavior contract, not identity snapshot, so renames don't churn it).
Summary
check_fn, socreate_adapter()returnedNonebeforeconnect()and the existing lazy-install path never ran.check_fntocheck_teams_requirements(same contract as Discord/Slack/Telegram) so enabled Teams installs its SDK into the Hermes venv on gateway start.hermes gateway restart) vs Docker compose cwd, plus Hermes-venv install (avoids Ubuntu PEP 668 systempip).Test plan
scripts/run_tests.sh tests/gateway/test_teams.py -qhermes gateway restart,firm adapter startsload_gateway_config