feat: add Microsoft Teams gateway adapter with OpenClaw parity - #10037
feat: add Microsoft Teams gateway adapter with OpenClaw parity#10037shuwenjunn wants to merge 2 commits into
Conversation
275e542 to
38ff606
Compare
|
tested this branch in my environment and it works for text chat, but not support attachment like picture or file. |
Bot Framework / A365 channel infra retries deliveries on 5xx and slow ACK; without a dedupe the operator webhook is hit multiple times for the same activity. Add a TTL-keyed in-memory cache on `(conversationId, activityId)` so duplicate POSTs short-circuit before touching the webhook. `_IdempotencyCache` is a small TTL-pruned dict (matching the `_JwksCache` pattern), driven by `BridgeConfig.idempotency_ttl_seconds` (default 1h). Activities without either id (channel-control flows sometimes lack `id`) bypass dedupe — better to over-deliver than to risk dropping legitimate traffic on missing-id alone. Pattern adapted from NousResearch/hermes-agent#10037 (`gateway/platforms/msteams.py::_is_duplicate_delivery`). Their adapter is classic-BF shaped (issue #7) so we re-implement in our idiom rather than vendor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bridge's outbound reply path POSTs the inbound activity's `serviceUrl` plus a freshly minted user-FIC bearer for the Messaging Bot API SP. JWT validation (slice 19f) ties that bearer to our tenant + audience, but once we POST to the URL the destination has full control over the bearer's traffic. A forged or malformed activity could redirect us anywhere. Gate the messages handler on `_is_trusted_service_url(activity.serviceUrl, cfg.trusted_service_url_suffixes)` — must be `https`, must have a hostname, and the hostname must end with one of the configured DNS suffixes. Untrusted: 403 with `untrusted serviceUrl: <repr>`. Default suffixes adapted from NousResearch/hermes-agent#10037's `TRUSTED_SERVICE_URL_HOST_SUFFIXES`. The round-3 walkthrough pinned real Teams traffic at `*.trafficmanager.net` so that's the load-bearing entry; `.botframework.com`, `.botframework.us`, `.cloud.microsoft`, `.azure.com` cover cross-channel and Bot Service-hosted endpoints. The DNS-boundary detail matters: each suffix carries a leading `.` so `evil-trafficmanager.net` doesn't slip through a naive endswith. Tested. Empty `cfg.trusted_service_url_suffixes` is treated as a config bug and 403s every request, matching the empty-allowlist behaviour from slice 19f. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the 2026-05-12 walkthrough surfaced Azure Bot Service as a hard prerequisite for Custom Engine Agent / Copilot Chat surfacing, the project's value props sharpen materially: - Path A (AI Teammate): agent-as-M365-tenant-directory-identity. Teams 1:1 chat with M365-native identity. No Azure subscription. - Path B (Custom Engine Agent + Azure Bot Service): Copilot Chat agents picker + side-panels in Word/Excel/PowerPoint/Outlook. Requires Azure subscription. Both are structurally distinct from Hermes' sibling Teams adapter at plugins/platforms/teams/adapter.py (classic Bot Framework, shipped v2026.4.30; in-flight in NousResearch/hermes-agent#10037 and #13767). The sibling has none of: - M365 tenant directory identity (Path A's value prop) - Copilot Chat fabric surfacing (Path B's value prop) And Hermes-A365 has none of the sibling's: - Generic Teams chat reach (group, channel, threading, attachments without M365 directory presence) So no overlap. The reframe makes that explicit. Changes: - references/m365-surface-coverage.md -- new "Positioning" section with two-path table, sibling-plugin signposts, when- to-pick-what guidance. Coverage matrix rewritten to attribute each surface to its right home (Hermes-A365 Path A / Path B / sibling / separate skill). Validation status updated for round-8 + slice 19u-a. Highest-value walkthroughs ranked against unique Hermes-A365 lane. - README.md -- new "Hermes-A365 vs sibling Teams plugins" section near the top. Surface table reworked for path attribution. Known limitations refreshed (streaming shipped, wizard shipped, Path B Azure prerequisite documented, #25 + #26 follow-ups listed). - SKILL.md -- frontmatter description and Overview rewritten with two-path framing + sibling-plugin distinction. When/ Don't-Use sections clarified. Surfaces-that-work-today refreshed for round-8 + slice 19u-a + Path B Azure prerequisite. Issue cleanup: - #17 (Teams group + channel walkthrough) closed -- explicitly sibling-plugin lane under reframe. - #18 (invoke activities umbrella) scope-narrowed via comment -- composeExtension invokes move to sibling lane; Path B-relevant invokes (Outlook task/* via Copilot, search, signin/verifyState) stay. - #16 (Copilot Chat walkthrough) classified as Path B's primary validation in comment. - Upstream comment posted on NousResearch/hermes-agent#20133 to clarify the non-overlap to maintainers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the substantial Teams work. Current main already ships Microsoft Teams as the bundled teams platform plugin (commit b3137d758), including native media sends (5f55f0ff8) and inbound document/media caching (0fd34e8c5). The remaining Graph-action work may still be useful, but this implementation needs substantial re-scoping.
Problems
gateway/platforms/msteams.py:329definesconnect()withoutis_reconnect; currentgateway/run.py:3445-3462always passes that keyword, so this adapter raisesTypeErroron current main.gateway/config.pyadds a separate coremsteamsplatform, while current main’s Teams channel is the lazily registeredteamsplugin (plugins/platforms/teams/adapter.py:1412-1451). Keeping both would create duplicate Teams configuration and runtime paths.gateway/platforms/msteams.py:350createsweb.Application()withoutclient_max_size, despite exposing_max_body_bytes; current Teams code applies this limit at application construction (plugins/platforms/teams/adapter.py:744-750).
Suggested changes
- Port only unimplemented Graph actions into
plugins/platforms/teams/adapter.pyand its tests; drop the parallel core adapter/config/toolset wiring. - Match the base adapter
connect(*, is_reconnect=False)contract and enforce the aiohttp body limit at construction.
Automated hermes-sweeper review.
| self._conversations = ConversationRegistry.load_from_path(self._state_path) | ||
|
|
||
| async def connect(self) -> bool: | ||
| if not check_msteams_requirements(): |
There was a problem hiding this comment.
Current GatewayRunner._connect_adapter_with_timeout() always invokes adapter.connect(is_reconnect=...) (gateway/run.py:3445-3462). This signature will raise TypeError on startup and reconnect; accept *, is_reconnect: bool = False even if this adapter does not use the flag.
| self._graph = MSTeamsGraphClient(self._app_id, self._app_password, self._tenant_id, self._http) | ||
| self._auth_validator = BotFrameworkJWTValidator(self._app_id, self._http, self._auth_cache_ttl_seconds) | ||
| self._app = web.Application() | ||
| self._app.router.add_get("/health", self._handle_health) |
There was a problem hiding this comment.
_max_body_bytes is checked only after aiohttp has parsed/read the request. Pass it as web.Application(client_max_size=self._max_body_bytes) so the configured cap is enforced by the server parser as well.
GottZ
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
Summary
Four PRs address Microsoft Teams integration: #10037, #13753, and #13767 add parallel first-class msteams adapters with Bot Framework/Graph support, while #17683 implements the SDK-based teams platform that aligns with the plugin architecture now shipped on main. The attachment gap motivating the later variants is also covered on main by native outbound media and inbound document/media caching.
Related pull requests
- #10037
related— (+6496/-54) — close as superseded, salvage Graph actions only: the diff adds a separate coremsteamsstack, but current main already provides the bundledteamsplugin, and this adapter is incompatible with currentconnect(is_reconnect=...)calls and omitsclient_max_size. Despite the keep_open review on #10037, the diff shows that retaining the PR would duplicate configuration/runtime paths; any still-useful Graph actions should be re-scoped onto the existing plugin. - #13753 [closed]
related— (+4939/-7) — superseded by #13767: this closed attempt adds a broad standalonemsteamsadapter with Bot Framework ingress, Graph, multiple auth flows, files, setup, and docs, but #13767 is its expanded successor and the same base capability is now implemented on main. - #13767 [closed]
related— (+6542/-30) — close as implemented on main: this expands the standalonemsteamsapproach with inbound image/document caching, FileConsent and SharePoint uploads, edits, and standalone delivery, but current main's dynamically registeredteamsplugin already supplies gateway integration plus the attachment paths that motivated the PR. - #17683 [closed]
related— (+1524/-11) — superseded by #17764 and relevant as the architectural precursor: unlike themsteamsvariants, it uses the Microsoft Teams SDK and theteamsplatform path, adding webhook ingress, conversation references, proactive sends, and interactive approval cards. It was closed in favor of #17764, and current main now ships Teams through this plugin-oriented architecture.
Duplicates
#10037, #13753, and #13767 are substantially overlapping standalone msteams implementations, with #13753 superseding #10037 and #13767 superseding both; #17683 covers the same platform capability through the distinct SDK/plugin-oriented teams architecture and was superseded by #17764.
Suggested consolidation
Merge none — retain the Microsoft Teams implementation already shipped on main through the bundled teams plugin. Close #10037 as superseded despite its keep_open review because its diff duplicates the active plugin path and is incompatible with the current adapter lifecycle; #13753 and #13767 are duplicate standalone implementations already superseded by main, and #17683 remains closed as superseded by #17764. If any Graph-only actions from #10037 are still missing, extract them into a narrowly scoped follow-up against the existing teams plugin rather than reopening or merging these PRs.
Cross-PR triage: Reviewed 4 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 932 kB of PR diffs, 21 kB of issue/PR text, 5 kB of discussion (12 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.
Summary
What’s included
gateway/platforms/msteams.pygateway/platforms/msteams_graph.pygateway/platforms/msteams_mentions.pygateway/platforms/msteams_state.pymsteamssend_messageand cron delivery support for Teams proactive sendsValidation
Result:
168 passedNotes