Skip to content

fix(msteams): rebase TeamsSDK patterns to simplify Teams Integration#76262

Merged
steipete merged 83 commits into
openclaw:mainfrom
heyitsaamir:feat/msteams-sdk-migration-v2
May 28, 2026
Merged

fix(msteams): rebase TeamsSDK patterns to simplify Teams Integration#76262
steipete merged 83 commits into
openclaw:mainfrom
heyitsaamir:feat/msteams-sdk-migration-v2

Conversation

@heyitsaamir

@heyitsaamir heyitsaamir commented May 2, 2026

Copy link
Copy Markdown
Contributor

Hey folks! 👋 Quick context before you dive in: I'm one of the maintainers of @microsoft/teams.ts — the TypeScript SDK this PR migrates onto. Happy to answer questions about the SDK side and help reconcile any incompatibilities.

The Microsoft Bot Framework SDK is deprecated and its patterns predate Teams as a first-class platform. The "boilerplate tax" of staying on it shows up clearly in this diff — most of the ~1300 deleted lines are things like a custom JWT validator, a no-op HTTP adapter, hand-rolled streaming-card protocol with streaminfo entities, manual REST calls to the Bot Connector for sends/edits/deletes, and a custom turn-context shim. The new @microsoft/teams.apps SDK absorbs all of that into typed, supported primitives (ExpressAdapter, tokenManager, ctx.stream, app.api.conversations.activities(), typed invoke routes for card.action / file.consent.*, and SDK-owned sign-in routes). That's why the diff is net negative even though it adds substantive new functionality (proper streaming with feedback loops, Action.Execute card handling, fixed edit/delete plumbing).


🤖 AI-assisted: built and validated end-to-end with Claude (Sonnet 4.6 + Opus 4.7) over multiple live Teams app + devtunnel sessions. Author has reviewed the diff. Degree of testing: full regression-pass against the closed-issue history of the msteams plugin — see "Validated end-to-end" below.

Summary

Migrate the msteams plugin from the legacy Bot Framework SDK to @microsoft/teams.apps 2.0.x.

The migration replaces ~580 lines of custom plumbing (JWT validator, HTTP adapter shim, manual REST calls, token casting, custom streaming protocol) with built-in SDK primitives. Migration scope (extensions/msteams/ + lockfile/workspace metadata): +1518 / -3608 lines, net -2090. Drops the plugin-local jsonwebtoken / jwks-rsa validator stack; JWT validation is now delegated to the Teams SDK. The plugin keeps direct SDK deps on @microsoft/teams.api and @microsoft/teams.apps.

Why

The old Bot Framework SDK forced us to:

  • maintain our own multi-issuer JWT validator with JWKS rotation
  • run a no-op HTTP adapter just to register routes
  • cast tokenManager results through unknown because internals weren't typed
  • implement Teams streaming protocol (streaminfo entities + sequence numbers + stream IDs) ourselves
  • manually POST to serviceUrl/v3/conversations/... for sends, edits, deletes

@microsoft/teams.apps 2.0.x ships all of this. Migrating reduces surface area we own, removes deps with their own audit footprint, and gets us on the Microsoft-supported codepath for future protocol changes.

Bugs fixed (not refactor-only)

This PR finishes the SDK migration started in earlier commits. It is not a refactor-only PR — it fixes concrete user-visible bugs introduced when the SDK swap left adapter mismatches behind:

  1. Streaming card never closed. The new SDK's ctx.reply() runs typing activities through TypingActivity.from() which strips our custom streaminfo entities. Our sendActivity mapping was routed through reply(), so streaming chunks went out without correlated stream IDs and Teams never collapsed the streaming card to the final message. Fixed by routing sendActivity → ctx.send() and replacing the entire custom streaming pipeline with the SDK's built-in ctx.stream.
  2. Final streamed message had no AI-generated badge or thumbs up/down. The custom streaming impl wired feedback via feedbackLoopEnabled channelData on each chunk; the SDK's HttpStream.close() only carries entities/channelData accumulated from emitted MessageActivitys. Fixed by emitting an empty final MessageActivity with addAiGenerated() + addFeedback() channelData before close().
  3. Adaptive card button clicks silently failed. Our poll card used Action.Submit + msteams.type: "messageBack" — a legacy Bot Framework pattern. With the new SDK, button clicks never reached the bot at all (zero log entries). Fixed by migrating the poll card to Action.Execute (Universal Action Model) and registering app.on('card.action', ...) that returns a real typed InvokeResponse. Without the typed response, Teams shows "Unable to reach app" after the 5s invoke timeout.
  4. Edit and delete didn't work. ctx.updateActivity / ctx.deleteActivity don't exist on the new SDK context — only on the old Bot Framework TurnContext. Calls were silently failing. Fixed by wiring them through app.api.conversations.activities(id).update/delete() in adaptSdkContext.
  5. ctx.sendActivity({ type: "invokeResponse" }) quietly POSTed garbage. The new SDK has no special-case for invokeResponse activities; our adapter routed them to ctx.send() which POSTed them to Bot Framework as regular outbound activities. Bot Framework rejected them silently. File-consent, card, and feedback invokes now use SDK route/return semantics; SSO intentionally stays on the SDK's built-in sign-in routes so Teams gets the right 200/412 fallback behavior.
  6. Test fixtures referenced an adapter field that no longer existed. The original migration commit renamed MSTeamsMessageHandlerDeps.adapterapp but three test fixtures (monitor-handler.adaptive-card.test.ts, monitor-handler/message-handler.test-support.ts, monitor-handler/reaction-handler.test.ts) and one test (monitor.lifecycle.test.ts) didn't get updated. Caught by the changed-gate now.
  7. Contract test stale. package-manifest.contract.test.ts still expected jsonwebtoken and jwks-rsa in msteams's plugin-local deps — both were removed in the original migration commit (the SDK does JWT validation internally now).
  8. Lint debt left over from earlier auth refactor. Unused appId destructures in five places in send.ts, an unused MSTEAMS_WEBHOOK_MAX_BODY_BYTES const + import in monitor.ts, an orphaned requireConversationId test helper, and String(token) triggering no-base-to-string in sdk.ts. All caught by pnpm check:changed.

Items 1–5 are user-visible Teams bugs caused by the SDK shape mismatch; 6–8 are gate-blocking debt left in the prior commit.

Follow-up fixes

These six fixes landed on top during a regression-test pass. Each is its own commit so they can be reviewed independently.

  1. Thread routing for channel and group-chat replies. (regression fix) adaptSdkContext now uses ctx.reply() for channels/groupChats (so the SDK threads outbound activities to the inbound's replyToId/serviceUrl) and ctx.send() only for personal DMs (where reply()'s blockquote-prepend is ugly). Companion fix in messenger.ts:sendProactively passes resolvedThreadId on the non-thread fallback so channel @mentions that route through outbound.ts → send.ts still land in the original thread. Live-validated: thread reply → bot reply in same thread, no top-level leak.

  2. OpenClaw User-Agent on outbound SDK calls. (observability) New buildOpenClawUserAgentFragment() returns just OpenClaw/<version>. Teams SDK 2.0.11 merges that with its own teams.ts[apps]/<sdk-version> identifier even when OpenClaw passes plain client headers, so the final UA looks like teams.ts[apps]/<sdk-version> OpenClaw/<openclaw-version>. Lets the Teams backend identify OpenClaw traffic for usage telemetry.

  3. StreamCancelledError when user presses Stop mid-stream. (regression fix) Real, gateway-crashing regression found during the regression-test pass. SDK throws StreamCancelledError synchronously from stream.emit/update when Teams replies 403 to the next chunk update (Stop button or 2-min timeout). Old custom TeamsHttpStream either swallowed cancel or didn't expose this exception type, so the migration inherited an SDK behavior the original code didn't have to handle. Result: Node 24 unhandled rejection → process crash → Docker restart, ~2 minutes after each Stop click. Fixed with try/catch around all stream.emit/update/close calls + wasCanceled latch that drops the would-be block fallback (so no duplicate message lands) and suppresses typing-keepalive (so no zombie typing pulses for the rest of the agent run). 6 new regression tests cover the crash and dedupe scenarios.

  4. SDK streaming delta tracking + app.reply for proactive thread sends. (one regression fix and one refactor, in the same commit)

    • Streaming delta tracking (regression fix) — the SDK's HttpStream.emit appends each chunk to its internal buffer (this.text += activity.text), but the channel reply pipeline emits cumulative text on each chunk. Forwarding cumulative into an appending sink produced "chunk1 + chunk1chunk2 + chunk1chunk2chunk3..." duplication for streamed (DM) replies. Track the emitted prefix length in the stream controller and only forward the new tail.
    • app.reply() in messenger.ts:sendProactively (refactor, not a regression fix — mechanically equivalent to the manual URL build it replaces) — replaced the manual ${convId};messageid=${msgId} URL construction with app.reply(), which builds the threaded conversation id via the SDK's own toThreadedConversationId helper. Removes coupling to Teams' URL format and tracks any future SDK changes; the threaded-proactive-reply behavior was already correct via follow-up fix: add @lid format support and allowFrom wildcard handling #1's resolvedThreadId plumbing. Adds the reply method to the structural MSTeamsApp type so the refactor typechecks without casts.
  5. Bump @microsoft/teams.api and teams.apps to 2.0.11. (dependency bump that resolves Codex review items) 2.0.10 includes the AAD v1 token issuer support from microsoft/teams.ts#556, and 2.0.11 preserves caller-provided User-Agent fragments when plain client headers are used. No release-age bypass remains for @microsoft/teams.*; 2.0.11 is now past the workspace freshness window.

  6. SSO sign-in invokes now use the SDK defaults, with OpenClaw token persistence. (regression fix found during live SSO testing) The first SSO pass registered explicit app.on("signin.token-exchange") / app.on("signin.verify-state") handlers. That replaced the SDK's built-in system routes, which was wrong: when Bot Framework returned Consent Required, our handler effectively turned the failed silent exchange into a successful HTTP 200, preventing Teams from continuing the consent fallback correctly. The fix leaves the SDK's built-in sign-in routes in control so Teams gets the SDK's expected 200/412 InvokeResponse semantics, passes channels.msteams.sso.connectionName into the SDK as the default OAuth connection, and persists successful delegated tokens from the SDK signin event into OpenClaw's msteams SSO token store. Live-validated against a real Teams app through devtunnel: msteams sso token persisted and a Graph delegated token was written for connection graph.

What changed

Auth + bootstrap

  • ExpressAdapter from @microsoft/teams.apps handles route registration and JWT validation internally — replaces our custom multi-issuer validator
  • app.tokenManager.getBotToken() / .getGraphToken() (public API) replaces our manual token acquisition + unknown casts. A small tokenToString() helper calls IToken.toString() explicitly to keep the lint clean
  • Bearer-presence middleware retained for pre-parse DoS protection (rejects requests with no Authorization header before the SDK does JSON body parsing)

SSO

  • SDK built-in signin.token-exchange / signin.verify-state system routes stay in control. OpenClaw intentionally does not replace them with user routes because the SDK defaults preserve Teams' required sign-in InvokeResponse semantics, including 412 when silent token exchange needs interactive consent fallback.
  • channels.msteams.sso.connectionName is passed into the SDK app as the default OAuth connection name, so the SDK verify-state handler uses the same connection OpenClaw is configured for.
  • OpenClaw subscribes to the SDK signin event and persists successful delegated tokens into the msteams SSO token store. This gives us token persistence without reimplementing the sign-in invoke protocol.

Outbound messaging

  • app.send(conversationId, activity) replaces adapter.continueConversation() for proactive sends
  • app.reply(conversationId, messageId, activity) for proactive sends that need to land in an existing channel thread (uses the SDK's own toThreadedConversationId helper, no manual ;messageid= URL building)
  • app.api.conversations.activities(id).update/delete() replaces manual REST calls for edit/delete

Inbound dispatch + context adapter (the bridge)

The new SDK delivers an IActivityContext (with send / reply methods) but our handler chain expected the old MSTeamsTurnContext interface (with sendActivity / sendActivities / updateActivity / deleteActivity). adaptSdkContext in monitor.ts bridges them:

  • sendActivityctx.reply() for channel/groupChat (threaded), ctx.send() for personal DMs (no blockquote)
  • updateActivityapp.api.conversations.activities(id).update(activityId, activity)
  • deleteActivityapp.api.conversations.activities(id).delete(activityId)
  • stream → passthrough of ctx.stream

Streaming via ctx.stream

Deleted streaming-message.ts (~300 lines of custom streaminfo entity management). The SDK's HttpStream (exposed via ctx.stream) handles the full Teams streaming protocol — update(text) for informative status, emit(chunk) for streaming chunks, close() for the final message.

createTeamsReplyStreamController is now a small bridge that wires openclaw's reply-pipeline callbacks to ctx.stream. Two protocol mismatches surfaced and got fixed inline:

  • Cumulative-vs-delta: the channel reply pipeline emits cumulative text on each chunk, but the SDK's HttpStream.emit(activity) appends each chunk to its internal buffer (this.text += activity.text). The controller tracks the emitted prefix length and forwards only the new tail (follow-up commit 5).
  • Cancel handling: try/catch around all emit/update/close plus a wasCanceled latch (follow-up commit 3).

Before close, the controller emits a final empty MessageActivity carrying the AIGeneratedContent entity and feedbackLoopEnabled: true channelData — the SDK's HttpStream accumulates that into the closing activity, so streamed messages still get the AI-generated label and thumbs up/down.

Adaptive card actions

The poll card was using Action.Submit + msteams.type: "messageBack" (legacy Bot Framework pattern). With the new SDK, vote clicks weren't reaching the bot at all. Migrated to Action.Execute (Universal Action Model) which sends an adaptiveCard/action invoke.

Registered app.on('card.action', ...) that returns a real InvokeResponse. Poll vote detection is inlined in the handler — extracts pollId and choices from the new value.action.data payload shape (vs the old value.openclawPollId), records the vote via pollStore, and returns { statusCode: 200, type: 'application/vnd.microsoft.activity.message', value: 'Vote recorded.' }. Without a typed InvokeResponse, Teams shows "Unable to reach app" after the 5s invoke timeout.

Removed (dead/replaced)

  • streaming-message.ts and its test — replaced by ctx.stream
  • MSTEAMS_WEBHOOK_MAX_BODY_BYTES const + import — unused
  • requireConversationId test helper in messenger.test.ts — orphaned
  • appId destructures in send.ts — leftover from the earlier auth pass-through refactor
  • jsonwebtoken, jwks-rsa, @types/jsonwebtoken, and the now-unneeded direct @microsoft/teams.common dependency from the msteams package surface — JWT validation and User-Agent merging are SDK-owned now. @microsoft/teams.api and @microsoft/teams.apps remain direct SDK deps.

Polish

  • attachments/download.ts: improved "attachment download failed" warn (host + error inline in the message text — the structured meta object was being dropped by the logger formatter)
  • Docker Matrix native-addon verification now parses both comma- and space-separated OPENCLAW_EXTENSIONS, matching the manifest-copy stage.
  • Test fixtures: renamed adapterapp on MSTeamsMessageHandlerDeps (was renamed in the original migration commit but several test fixtures didn't get updated, causing pre-existing tsgo failures that the changed-gate now catches)
  • Contract test: dropped jsonwebtoken / jwks-rsa from msteams expected plugin-local deps (the new SDK does JWT validation internally — those packages are gone)

Validated end-to-end

Tested live in a Teams app installation behind a devtunnel, with a deliberate regression-pass against the closed-issue history of the msteams plugin.

Behavior Status
Inbound JWT validation + dispatch
DM install/welcome card
DM messaging + long replies via ctx.stream
Streaming text in DM (delta tracking, no chunk duplication)
Channel @mention routes reply into the original thread (#58030)
Threaded reply preserved via proactive fallback (#55198)
DM ↔ channel leak guard (#54520) — never observed cross-leak
Adaptive card poll vote (Action.Execute / UAM)
Adaptive card send via CLI
Feedback (thumbs up/down via message/submitAction)
File-upload DM image (+button → BF v3 attachments path, #62219)
Large file consent card + accept/upload path (>4MB PDF)
Edit and delete via CLI (app.api.conversations.activities().update/delete)
Proactive top-level send to channel
Proactive thread reply to channel (app.reply path)
Long streaming reply + Stop mid-stream (Stop-crash fix)
Stop mid-stream — no duplicate, no zombie typing
SSO sign-in token exchange + delegated token persistence (graph)
Clipboard-paste DM image ⚠️ pre-existing limitation, separate follow-up

Side-by-side parity check. The migration build was validated against a parallel main-branch bot (OpenMAINClaw, separate Azure App ID + tunnel + config) running on the same host. Channel @-mention with default config produced identical behavior on both bots (message_tool_only is the documented default for group/channel chats — the agent must call the message(action=send) tool to reply, or operators set messages.groupChat.visibleReplies: "automatic" to opt in to auto-replies). After flipping both configs to automatic, channel @-mentions, threaded replies, proactive sends, edits, and adaptive cards all produced equivalent output on the migration build vs main. No regression introduced by the SDK swap.

pnpm check:changed is green: typecheck, lint, contract tests, and all msteams unit tests pass (865 tests). Two pre-existing failures on the branch (parallels-smoke-model.test.ts, tui.test.ts) are unrelated to msteams.

Not tested / still out of scope:

  • Clipboard-paste DM image handling — pre-existing limitation, separate follow-up.
  • Full productized SSO UX/tool integration. The SDK sign-in flow and token persistence are live-proven here, but exposing that token through a polished user-facing auth/tool surface belongs in the dedicated SSO follow-up.

Codex review findings

@clawsweeper's review surfaced four items I worked through as part of the regression-test pass.

  • [P1] JWT validator audience/issuer contract — split into two pieces:
  • [P1] Proactive send routing metadata — public-cloud behavior is live-validated for DM, group/channel sends, thread replies, edits, deletes, cards, polls, and file-consent sends through the SDK path. The support boundary is now documented: this migration is validated for public cloud; GCC/GCC High/DoD or other non-public Connector routing needs an explicit future msteams cloud/serviceUrl configuration before we call it supported.
  • [P2] Dual /api/messages route registration — fixed with a compatibility forwarder. The SDK registers the configured webhook.path; when that path is not /api/messages, OpenClaw also accepts legacy /api/messages POSTs and forwards them to the configured path with a one-time deprecation warning. This keeps existing Azure Bot registrations working through the transition while still nudging operators to update the endpoint.
  • [P2] Docker port mismatch — fixed. docker-compose.yml now exposes ${OPENCLAW_MSTEAMS_PORT:-3978}:3978 to match the plugin's default webhook.port (3978, also the Bot Framework default used in Microsoft samples). Operators wanting a custom port override both webhook.port in config and OPENCLAW_MSTEAMS_PORT env var.

Known followups (intentionally out of scope, separate PRs)

  1. Productize SSO usage beyond token persistence. The SDK sign-in routes are now the active path: OpenClaw leaves signin.token-exchange / signin.verify-state to the SDK and persists successful tokens from the SDK signin event. Live testing confirmed a Graph delegated token is written for the configured graph connection. Follow-up work is to expose a normal user-facing sign-in entrypoint and wire the persisted token into tool/runtime auth surfaces. The old explicit signin-invoke.ts helper and lower-level sso.ts exchange helpers can likely be simplified or removed once the dedicated SSO/tooling path lands.

  2. Replace catch-all activity dispatch with per-type SDK routes: today we register app.on("activity", ...) as a catch-all that pattern-matches activity.type / activity.name internally via a buildActivityHandler shim mimicking the old Bot Framework ActivityHandler.run() shape. The new SDK has typed routes for every activity type/invoke we care about — app.on("message"), app.on("conversationUpdate"), app.on("messageReaction"), app.on("file.consent.accept"|"decline"), app.on("message.submit.feedback"), etc. This is what slack does (extensions/slack/src/monitor/events/*.ts registers ~10 specific Bolt handlers, no catch-all). Splitting drops buildActivityHandler + the MSTeamsActivityHandler interface + the if (ctx.activity?.type === "invoke" && ctx.activity?.name === "...") chain in the wrapper, gives each handler a typed context, and lets the SDK auto-ack invoke responses for paths that don't need a typed response. Keep SDK sign-in routes as SDK-owned unless/until we have a concrete reason to replace their 200/412 fallback behavior.

  3. Clipboard-paste DM image handling — Teams puts inline DM images directly on *.asm.skype.com URLs without an HTML <attachment> wrapper, so they don't trigger the existing BF-v3-attachments fallback. The +button "Upload from this device" path works because Teams generates the HTML wrapper. Fixing clipboard-paste means extracting the object ID from the asm.skype.com URL and rewriting to <serviceUrl>/v3/attachments/<id>/views/imgo.

  4. Re-implement upstream's preview / progress-draft / live-finalization features on top of ctx.stream. While this PR was open, upstream landed #77674 / #78081 and several other commits that extended the OLD TeamsHttpStream class with: preview-mode streaming, progress-draft labels (e.g. "Looking up the schema..." that updates as tools run), and live-finalization that edits the preview-stream activity in place when the final reply lands. This PR deletes TeamsHttpStream in favor of the SDK's ctx.stream, so those features need to be re-built on the new substrate. Scope is moderate (~200-300 lines): track preview-stream-id from the SDK's HttpStream, route progress-draft text through ctx.stream.update(...) instead of a custom sendInformativeUpdate, and use app.api.conversations.activities(id).update(activityId, ...) for the in-place final edit. The plugin-sdk helpers (createLiveMessageState, defineFinalizableLivePreviewAdapter, createChannelProgressDraftGate, etc.) already work shape-agnostically — they just need a different editFinal adapter that uses the SDK API instead of the custom TeamsHttpStream methods. Out of scope for this PR to keep the migration scope contained.

Rebase / merge notes

This branch was force-rebased onto upstream/main on 2026-05-06 (1454 upstream commits since the original migration commit). The rebase took ours wholesale for the 6 conflicting files in commit 8e90628cb6:

  • monitor.ts, reply-dispatcher.ts, reply-stream-controller.ts, reply-stream-controller.test.ts, sdk.ts, sdk.test.ts (took migration's version)
  • streaming-message.ts and its test (deleted by the migration; upstream had modified them)

Follow-up commit e38f323173 fix(msteams): post-rebase reconciliation with main reconciled the rest:

  • createChannelReplyPipelinecreateChannelMessageReplyPipeline (renamed in plugin-sdk during the rebase window)
  • Tightened onStartError typing for upstream's stricter type checks
  • Removed the unconditional thread suffix on the proactive bottom-fallback (upstream test asserts replyStyle: 'top-level' should not thread; the original fix(msteams): preserve channel reply threading in proactive fallback #55198 fix is preserved on the replyStyle === 'thread' onRevoked branch)
  • Updated attachments.test.ts warn assertion to match the migration's inline message format

On 2026-05-07 the branch was merged with upstream/main again (1820 commits since the prior rebase point). Two conflicts resolved in commit 10629d08df:

  • extensions/msteams/src/send.ts and send-context.ts: kept the SDK's app over main's legacy adapter, adopted main's new replyStyle propagation through MSTeamsProactiveContext (so CLI proactive sends to channel threads pick the policy-resolved replyStyle and route through messenger.ts:sendProactively with the thread root id, leveraging follow-up commit 4's app.reply path).

Same merge commit also rewrote two tests in extensions/msteams/src/messenger.test.ts (not conflict-marked but materially changed): the tests referenced the deleted MSTeamsAdapter / noopUpdateActivity / noopDeleteActivity and were ported to the migration's MSTeamsApp mock pattern.

All msteams unit tests (865) pass after the merge.

Real behavior proof

Behavior or issue addressed: SDK-migration end-to-end behavior on the live Teams runtime — DM streaming (with the cumulative-vs-delta tracking fix from follow-up #4), DM streaming with tool-progress informational updates above the streaming card, channel proactive + threaded-reply messaging (follow-up #1), and SSO token persistence via the SDK signin event. Together these exercise ctx.stream, app.on("card.action"), ctx.reply for channels, the proactive app.send / app.reply paths, and the SDK default sign-in invoke routes.

Real environment tested: Microsoft Teams app installed in an M365 tenant via devtunnel, against a docker-deployed openclaw gateway running this branch. A separate main-build control bot was installed in the same tenant for side-by-side parity comparison; behaviors below were observed equivalent on both bots after messages.groupChat.visibleReplies: "automatic" was set, so no migration-only regression.

Exact steps or command run after this patch:

  1. DM the bot a streaming-eligible prompt ("write a short sonnet about a llama") and watch the streaming card fill in.
  2. DM the bot a tool-triggering prompt and watch the tool-progress informational lines render above the streaming card as tools fire, then transition to the final reply.
  3. From the gateway, send a proactive top-level channel message via openclaw message send --channel msteams --target <channelId> --message …, then reply to that message in a thread inside Teams.
  4. Trigger a live SSO sign-in in DM with a temporary local /test-signin hook that called ctx.signin({ connectionName: "graph" }), complete consent, and verify token persistence.

Evidence after fix:

Basic streaming in DMs:

streaming.and.feedback.mp4

Streaming with tool-call informational updates:

streaming.tool.progress.mp4

Messaging in channels (proactive top-level + threaded reply):

Channel.threads.mp4

SSO flow working:

Screenshot 2026-05-09 at 4 50 59 PM

Observed result after fix:

  • DM streaming card fills in token-by-token without duplication (pre-fix, each chunk re-emitted the cumulative prefix on top of all earlier chunks).
  • Tool-progress informational status renders above the streaming card and the live preview transitions in place to the final reply on close.
  • Channel proactive top-level send lands at the channel root; the bot's reply to a user's threaded follow-up lands inside the same thread (no top-level leak).
  • app.reply() proactive thread path verified mechanically equivalent to the previous manual ;messageid= URL build.
  • SSO sign-in completed through the SDK default sign-in routes; OpenClaw logged msteams sso token persisted, and a Graph delegated token was written to ~/.openclaw/msteams-sso-tokens.json for connection graph.
  • The temporary /test-signin hook and JWT diagnostic logging used for live proof were removed before handoff.
  • Targeted local proof covered SDK construction/User-Agent, plugin package guardrails, extension typecheck, Docker build, and the live Teams smoke matrix above.

What was not tested: Clipboard-paste DM image handling remains a pre-existing limitation and is tracked as a separate follow-up. Full productized SSO/tool integration remains follow-up work; the SDK sign-in route and token persistence path itself is live-proven here.

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams docker Docker and sandbox tooling size: XL triage: blank-template Candidate: PR template appears mostly untouched. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context. labels May 2, 2026
@clawsweeper

clawsweeper Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Codex review: found issues before merge. Reviewed May 28, 2026, 5:45 PM ET / 21:45 UTC.

Summary
The PR migrates the Microsoft Teams plugin to @microsoft/teams.apps, rewires Teams send/stream/invoke/SSO paths, adds Teams cloud/serviceUrl configuration, and updates related docs, tests, Docker, and dependency metadata.

Reproducibility: yes. The blocking freshness issue is source-reproducible by comparing the added minimumReleaseAgeExclude entries with npm publish metadata and the workspace minimumReleaseAge: 2880 policy.

Review metrics: 3 noteworthy metrics.

  • MSTeams config surfaces: 2 added. channels.msteams.cloud and channels.msteams.serviceUrl affect upgrade behavior for existing Teams deployments.
  • Freshness exclusions: 5 added. The PR explicitly bypasses the workspace release-age guard for the new Teams SDK package set.
  • Teams SDK package change: 2 direct deps bumped, 2 local validator deps removed. Auth, JWT validation, and outbound delivery move from local code to the Microsoft Teams SDK dependency surface.

Merge readiness
Overall: 🦪 silver shellfish
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🦪 silver shellfish
Result: blocked by patch quality or review findings.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Remove or wait out the exact @microsoft/teams.*@2.0.12 release-age exclusions.
  • [P2] Record explicit maintainer acceptance if the PR must merge before the freshness window ends.
  • Verify exact-head CI and dependency review after the freshness decision.

Risk before merge

  • [P1] The PR bypasses the workspace freshness guard for five @microsoft/teams.*@2.0.12 packages published on 2026-05-27T23:34Z, so merging before the 48-hour window ends needs explicit supply-chain acceptance.
  • [P1] Existing non-public or custom Teams deployments may fail closed until operators configure channels.msteams.cloud/serviceUrl and refresh stored conversation references; that is a deliberate upgrade-sensitive behavior change.
  • [P1] The PR is broad and dependency-sensitive, so maintainers should verify exact-head CI and dependency review before merge even though the supplied Teams proof is useful.

Maintainer options:

  1. Wait Out Freshness And Remove Exclusions (recommended)
    After the 48-hour window ends around 2026-05-29T23:34Z, remove the exact @microsoft/teams.*@2.0.12 freshness exclusions and rerun the dependency and Teams gates.
  2. Accept The Fresh SDK Risk
    Maintainers can explicitly accept the fresh Teams SDK packages because this migration depends on them, but that should be a conscious supply-chain decision.
  3. Pause If The Upgrade Contract Is Too Broad
    If the cloud/serviceUrl upgrade behavior is not acceptable for this release, pause this branch and split a narrower public-cloud SDK migration from non-public-cloud hardening.

Next step before merge

  • [P2] Maintainer review is needed for the fresh SDK dependency bypass and upgrade-sensitive Teams cloud/serviceUrl contract; this is not a safe autonomous repair until maintainers choose whether to wait, remove exclusions, or accept risk.

Security
Needs attention: The diff has one concrete supply-chain concern: fresh Teams SDK packages are allowed around the workspace release-age guard.

Review findings

  • [P1] Remove fresh Teams SDK release-age bypasses — pnpm-workspace.yaml:40-44
Review details

Best possible solution:

Land the SDK migration only after the Teams SDK freshness exception is removed or explicitly accepted, with maintainers owning the documented cloud/serviceUrl upgrade behavior.

Do we have a high-confidence way to reproduce the issue?

Yes. The blocking freshness issue is source-reproducible by comparing the added minimumReleaseAgeExclude entries with npm publish metadata and the workspace minimumReleaseAge: 2880 policy.

Is this the best way to solve the issue?

No, not yet. The SDK migration direction looks reasonable, but the best merge shape removes or waits out the fresh dependency exclusions and gets maintainer acceptance for the fail-closed cloud/serviceUrl upgrade contract.

Full review comments:

  • [P1] Remove fresh Teams SDK release-age bypasses — pnpm-workspace.yaml:40-44
    This PR adds exact minimumReleaseAgeExclude entries for five @microsoft/teams.*@2.0.12 packages even though the workspace requires a 2880-minute release age. npm metadata shows 2.0.12 was published on 2026-05-27T23:34Z, so this bypasses the freshness guard for auth and message-delivery dependencies; remove these exclusions and wait for the window to pass, or get explicit maintainer risk acceptance before merge.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.86

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 0dbdaf98ea74.

Label changes

Label changes:

  • add rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦞 diamond lobster and patch quality is 🦪 silver shellfish.
  • remove rating: 🧂 unranked krab: Current PR rating is rating: 🦪 silver shellfish, so this older rating label is no longer current.

Label justifications:

  • P1: This is a major Teams channel migration touching auth and message delivery with an active supply-chain freshness blocker.
  • merge-risk: 🚨 compatibility: The new cloud/serviceUrl config and fail-closed proactive validation can change upgrade behavior for existing non-public or custom Teams deployments.
  • merge-risk: 🚨 message-delivery: The PR rewires Teams streaming, proactive sends/edits/deletes, card actions, file consent, feedback, and SSO invoke delivery.
  • merge-risk: 🚨 security-boundary: The migration changes JWT/auth/SSO-token handling and bypasses the workspace freshness guard for newly published SDK packages.
  • rating: 🦪 silver shellfish: Overall readiness is 🦪 silver shellfish; proof is 🦞 diamond lobster and patch quality is 🦪 silver shellfish.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (recording): The PR discussion includes live Teams Docker/devtunnel proof and an inspectable MOV attachment showing after-fix Teams behavior; non-public cloud paths remain covered by tests and documented boundaries rather than live proof.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR discussion includes live Teams Docker/devtunnel proof and an inspectable MOV attachment showing after-fix Teams behavior; non-public cloud paths remain covered by tests and documented boundaries rather than live proof.
  • proof: 🎥 video: Contributor real behavior proof includes video or recording evidence. The PR discussion includes live Teams Docker/devtunnel proof and an inspectable MOV attachment showing after-fix Teams behavior; non-public cloud paths remain covered by tests and documented boundaries rather than live proof.
Evidence reviewed

Security concerns:

  • [medium] Fresh auth/delivery SDKs bypass freshness guard — pnpm-workspace.yaml:40
    The added minimumReleaseAgeExclude entries let newly published Microsoft Teams SDK packages bypass the repository's 48-hour freshness policy while those packages own JWT validation, token routing, and Teams delivery behavior.
    Confidence: 0.9

What I checked:

  • Repository policy read: Root AGENTS.md and scoped guides for extensions, docs, scripts, src/plugins, and src/agents were read; the review applied the compatibility-sensitive config/dependency guidance. (AGENTS.md:16, 0dbdaf98ea74)
  • Fresh dependency guard bypass: The PR adds five exact @microsoft/teams.*@2.0.12 entries to minimumReleaseAgeExclude while the workspace keeps minimumReleaseAge: 2880. (pnpm-workspace.yaml:40, 8efdc2a66d36)
  • Package publish time: npm metadata reports @microsoft/[email protected] at 2026-05-27T23:34:05Z and @microsoft/[email protected] at 2026-05-27T23:34:02Z, inside the 48-hour workspace freshness window during this review.
  • SDK dependency bump: The msteams plugin now depends directly on @microsoft/teams.api and @microsoft/teams.apps 2.0.12 and drops the local validator stack. (extensions/msteams/package.json:11, 8efdc2a66d36)
  • Config surface added: The PR adds channels.msteams.cloud and channels.msteams.serviceUrl to the public config type and schema, including non-public-cloud validation. (src/config/zod-schema.providers-core.ts:1557, 8efdc2a66d36)
  • Fail-closed proactive boundary: Proactive Teams sends validate stored conversation service URLs against the configured cloud/serviceUrl boundary and fail closed on missing or mismatched references. (extensions/msteams/src/cloud.ts:66, 8efdc2a66d36)

Likely related people:

  • steipete: Authored the latest msteams SDK migration hardening commits for proactive references, serviceUrl contract, China cloud boundaries, invoke acks, and current CI sync. (role: recent area contributor; confidence: high; commits: 3f862bbac0dd, 63ac2a9b69da, 9b54f5ab56de; files: extensions/msteams/src/cloud.ts, extensions/msteams/src/sdk-proactive.ts, extensions/msteams/src/sdk.ts)
  • BradGroux: Tracked the main pre-merge blockers in the PR discussion, including invoke authz, Teams cloud/serviceUrl upgrade behavior, dependency freshness, Docker parsing, and User-Agent behavior. (role: reviewer; confidence: medium; files: extensions/msteams/src/monitor.ts, extensions/msteams/src/sdk.ts, extensions/msteams/src/cloud.ts)
  • heyitsaamir: Authored the original Teams SDK migration branch and identified themselves as a maintainer of the upstream @microsoft/teams.ts SDK used by the migration. (role: upstream SDK domain contributor; confidence: medium; commits: 8e90628cb684, 9b147bace899, 0a9a42a7ff72; files: extensions/msteams/src/sdk.ts, extensions/msteams/src/monitor.ts, extensions/msteams/src/reply-stream-controller.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@heyitsaamir
heyitsaamir marked this pull request as draft May 2, 2026 21:47
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 6, 2026
heyitsaamir and others added 7 commits May 6, 2026 04:30
Reapply the msteams SDK migration (originally on feat/msteams-sdk-migration)
on top of upstream/main, resolving conflicts with parallel msteams work that
landed upstream during our session.

What got applied vs decisions made:

CLEANLY APPLIED (3-way patch):
- monitor.ts, monitor-handler.ts, polls.ts, reply-stream-controller.ts/.test.ts,
  reply-dispatcher.ts, attachments/download.ts, monitor.lifecycle.test.ts,
  monitor-handler/message-handler.ts, monitor-handler.types.ts, etc.
- streaming-message.ts + .test.ts deletions

WHOLESALE TAKE FROM ORIGINAL BRANCH (partial 3-way left broken cross-refs):
- sdk.ts, sdk.test.ts, messenger.ts, feedback-reflection.ts,
  send-context.ts, send.test.ts

KEPT UPSTREAM (deferred for separate cleanup):
- extensions/msteams/package.json (still has jsonwebtoken/jwks-rsa per
  Peter's b3bc60a incremental approach)
- src/plugins/contracts/package-manifest.contract.test.ts (consistent with
  package.json)
- pnpm-lock.yaml (avoids lockfile churn; pnpm install --frozen-lockfile clean)

ADAPTED:
- Dockerfile matrix-sdk-crypto check now wraps upstream's new retry-loop in
  the if-matrix-bundled gate

KNOWN TEST FAILURES (need eyes — see PR comment):
- attachments.test.ts: 1 fail (pre-existing — warn meta arg shape changed in
  our migration but test wasn't updated)
- reply-dispatcher.test.ts: 6 fails (pre-existing — tests mock old
  TeamsHttpStream, not updated for our ctx.stream rewrite)
- send.test.ts: 4 fails (NEW from merge — upstream's send.ts changed media
  loading; our mocks need updating or take upstream's send.test.ts wholesale)

UPSTREAM COMMITS POTENTIALLY MISSED (in wholesale-take files):
- 08c4af0 fix(msteams): accept conversation id allowlists
- e1840b8 fix(msteams): bind global audience tokens to app id
- Channels turn-kernel refactor (ffe67e9 / 1ead1b2 / 9a9cd0c) —
  may be partially preserved in cleanly-patched files

Static checks pass: pnpm check:changed is green (typecheck, lint, contract
tests, import cycles, etc.). Manual testing required before merge.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- monitor.ts: adaptSdkContext now uses ctx.reply() for channel and groupChat
  conversations (so the SDK threads outbound activities to the inbound's
  replyToId/serviceUrl) and ctx.send() only for personal DMs (where
  reply()'s blockquote-prepend is ugly).
- messenger.ts: sendProactively passes resolvedThreadId on the non-thread
  fallback path so channel @mentions that fall through outbound.ts -> send.ts
  still land in the original thread instead of top-level.

Live-validated: channel @mention -> bot replies in thread, threaded reply
-> bot replies in same thread, no top-level leakage.
- user-agent.ts: add buildOpenClawUserAgentFragment() that returns just
  'OpenClaw/<version>'. The SDK's Client.clone merges this with its own
  'teams.ts[apps]/<sdk-version>' identifier — passing the full buildUserAgent()
  here would double-print the SDK token.
- sdk.ts: pass the fragment via AppOptions.client.headers['User-Agent'] so
  the Teams backend can identify OpenClaw traffic for usage telemetry.

Final UA looks like 'OpenClaw/<openclaw-version> teams.ts[apps]/<sdk-version>'.
…stream

The new SDK throws StreamCancelledError synchronously from stream.emit/update
when the user pressed Stop in Teams: Teams replies 403 to the next chunk
update, the SDK flips _canceled, and any subsequent emit() throws. The old
custom TeamsHttpStream either swallowed cancel or didn't expose this exception
type, so the migration inherited an SDK behavior the original code didn't have
to handle.

Symptom on 2026-05-05: pressing Stop during a streaming reply caused an
unhandled promise rejection that crashed the Node 24 process. Docker restarted
the gateway about two minutes after each Stop click. Two related bugs surfaced
once the crash was caught: the would-be block fallback re-delivered the full
text as a second message (duplicate after Stop), and the typing-keepalive kept
pulsing in Teams for the rest of the agent run because nothing told it to
stop.

reply-stream-controller.ts:
- Wrap stream.update / stream.emit / stream.close in try/catch that swallows
  StreamCancelledError (matched by .name to dodge tsgo's SDK re-export
  resolution quirk). Latch a wasCanceled flag so subsequent calls
  short-circuit even if stream.canceled is stale.
- preparePayload() returns undefined when the stream was canceled — the
  streamed prefix is already visible to the user, so dropping the payload
  prevents a duplicate block message from overriding the cancel intent.

reply-dispatcher.ts:
- Typing-keepalive gate now also checks streamController.wasCanceled() so
  typing pulses stop firing once Stop is observed. Otherwise the bot keeps
  pulsing for the rest of the (uncancellable) agent run.

reply-stream-controller.test.ts:
- 6 new regression tests cover: cancel-during-emit (the crash scenario),
  cancel-during-update, cancel-during-finalize, non-cancel error propagation,
  post-cancel inactivity, and dropped-payload-on-cancel.

Live-validated: long streaming reply + Stop mid-stream -> stream freezes,
no duplicate message, no zombie typing, container stays healthy.
Teams puts inline DM images and clipboard-pasted images on
*.asm.skype.com URLs (e.g. us-api.asm.skype.com/v1/objects/<id>/views/imgo).
The download path in attachments/download.ts already does a plain GET first
and falls back to a Bearer-token retry on 401/403 — but the retry was gated
on the URL being in DEFAULT_MEDIA_AUTH_HOST_ALLOWLIST. asm.skype.com hosts
were in DEFAULT_MEDIA_HOST_ALLOWLIST (download permitted) but not in the
auth-host list, so a 401 plain-GET response skipped the retry and surfaced
as a missing image to the agent.

Add asm.skype.com and ams.skype.com to the auth allowlist so openclaw
attempts the Bearer-token retry consistently, matching how it treats the
other CDN/Bot-Framework hosts already in the list.

Note: this does not unblock all clipboard-pasted DM images — for at least
some tenants asm.skype.com rejects the Bot Framework token (returns 401
even with auth). Routing those URLs through <serviceUrl>/v3/attachments/...
the way openclaw#62219 already handles HTML-wrapped attachments is a separate
follow-up. The +button 'Upload from this device' path works today because
Teams generates an attachment with an HTML wrapper that triggers the
existing BF v3 attachments fallback in monitor-handler/inbound-media.ts.
…efault

The plugin defaults webhook.port to 3978 (the Bot Framework standard used in
Microsoft samples) and listens on whatever the operator sets there. The
docker-compose.yml port mapping was exposing ${OPENCLAW_MSTEAMS_PORT:-3000}:3000
which only works for operators who explicitly set webhook.port to 3000.
Default-config users would have the plugin listening on 3978 inside the
container while compose forwarded 3000, causing connection refused.

Realign to ${OPENCLAW_MSTEAMS_PORT:-3978}:3978 so a default-config docker
compose up Just Works with Teams. Operators wanting a custom port override
both webhook.port in openclaw.json and OPENCLAW_MSTEAMS_PORT env var.
Three follow-ups after rebasing the SDK migration onto current main:

- reply-dispatcher.ts: rename createChannelReplyPipeline to its post-rebase
  identifier createChannelMessageReplyPipeline (the plugin-sdk barrel renamed
  it during the 1454-commit rebase window).
- reply-dispatcher.ts: tighten the typing-keepalive onStartError signature to
  (err: unknown) to satisfy upstream's stricter type checks.
- messenger.ts: drop the unconditional thread suffix on the bottom proactive
  fallback. The previous behavior threaded all top-level proactive sends when
  the stored ref had a threadId, which contradicts replyStyle='top-level'
  semantics (and breaks the new upstream test). Threading on the proactive
  path is preserved where it matters — the onRevoked branch within
  replyStyle==='thread' still passes resolvedThreadId, which is the original
  openclaw#55198 fix path.
- attachments.test.ts: update the warn-call assertion to match the migration's
  inline message format (host=... error=...) — the structured meta object was
  being dropped by the logger formatter pre-migration.
@heyitsaamir
heyitsaamir force-pushed the feat/msteams-sdk-migration-v2 branch from 8e2009e to e38f323 Compare May 6, 2026 05:35
heyitsaamir and others added 4 commits May 6, 2026 06:10
While the SDK migration was open, upstream landed preview/progress/draft
streaming features built on the OLD custom TeamsHttpStream class (which the
migration deletes). This commit ports the user-visible parts of those
features onto the new ctx.stream substrate so the migration doesn't lose
ground:

- pickInformativeStatusText: reads custom labels from
  msteams.streaming.progressDraft config via resolveChannelProgressDraftLabel.
  Falls back to the plugin-sdk default rotation. Pre-rebase used a hardcoded
  4-string array.
- streamMode resolution: "partial" (default, per-token streaming),
  "progress" (no tokens; preview card carries informative label that updates
  as tools run), or "block" (no native streaming). Mode is read from
  cfg.channels.msteams.streaming.preview.
- progress-draft gate: createChannelProgressDraftGate gates informative
  updates so the rotating label only starts firing once meaningful work has
  begun (avoids flicker before the first tool call).
- noteProgressWork() / pushProgressLine(): public methods on the controller
  for callers (typing keepalive ticks, tool-event callbacks) to signal work.
  pushProgressLine appends tool names as bullets above the rotating label
  when streaming.previewToolProgress is enabled. Wiring these into actual
  tool events is a separate follow-up.
- preparePayload progress-mode path: when stream is active but no tokens
  streamed (progress mode) and a final text payload arrives, emit the text
  into the stream so the preview card transitions in place to the final
  reply on close().

reply-dispatcher: pass log + msteamsConfig + a stable progressSeed
(${accountId}:${conversation.id}) to createTeamsReplyStreamController so the
informative-label rotation is consistent across reconnects.

What's NOT ported and why:
- Live-edit-via-replaceInformativeWithFinal: the SDK's HttpStream natively
  accumulates emitted text + entities + channelData and flushes ONE final
  activity at close() using the same activity id as the preview. So the
  separate "replace informative with final" call from upstream is
  unnecessary — we get live-finalization for free via the SDK's design.
- pushProgressLine triggers from tool events: needs reply-pipeline-side
  callbacks the new SDK migration didn't surface yet. Follow-up.

Tests: existing 22 reply-stream-controller tests still pass (the new
behaviors are additive).
…test debt

Two follow-ups from yesterday's stopping point:

1. Wire pipeline events into the stream controller's progress-draft surface.
   reply-dispatcher's replyOptions now exposes onReasoningStream, onToolStart,
   onItemEvent, onPlanUpdate, onApprovalEvent, onCommandOutput callbacks that
   format each event via the channel-streaming helpers and route through
   streamController.pushProgressLine(). Mirrors the discord adapter's wiring.
   Also:
   - resolveChannelStreamingPreviewToolProgress + ...SuppressDefaultTool... so
     the dispatcher exposes suppressDefaultToolProgressMessages on its
     replyOptions when progress mode is on.
   - Switch disableBlockStreaming resolution to the channel-streaming helpers
     (resolveChannelPreviewStreamMode + resolveChannelStreamingBlockEnabled)
     so streaming.mode='block' and streaming.block.enabled=true are honored
     alongside the legacy blockStreaming boolean.

2. Fix the test debt that the rebase exposed:
   - reply-dispatcher.test.ts: drop the streamInstances + TeamsHttpStream
     mock pattern (file deleted by migration); replace with a streamMock
     provided via context.stream that mirrors the SDK's IStreamer shape
     (update/emit/close/canceled). Update assertions on sendInformativeUpdate
     -> stream.update, stream.update -> stream.emit. Drop the
     resumes-typing-between-segments test (no equivalent in the new
     ctx.stream model — the SDK's HttpStream doesn't have a 'between
     segments' notion; close ends the stream).
   - send.test.ts: fix two stale mock targets — loadOutboundMediaFromUrl
     comes from openclaw/plugin-sdk/outbound-media (not /msteams), and
     resolveMarkdownTableMode comes from openclaw/plugin-sdk/markdown-table-runtime
     (not /config-runtime). The previous mock paths were no-ops post-migration.

All 854 msteams tests now pass (was 17 failing in 4 files yesterday).
…d sends

Two narrow regressions exposed by the @microsoft/teams.apps migration:

- The SDK's HttpStream.emit appends each chunk to its internal buffer
  (`this.text += activity.text`), but the channel reply pipeline emits
  cumulative text on each chunk. Forwarding cumulative text into an
  appending sink produced "chunk1 + chunk1chunk2 + chunk1chunk2chunk3..."
  duplication for streamed (DM) replies. Track the emitted prefix length
  in the stream controller and only forward the new tail.
- Replace the manual `${convId};messageid=${msgId}` URL construction in
  the proactive thread fallback with `app.reply()`, which builds the
  threaded conversation id via the SDK's own toThreadedConversationId
  helper. Mechanically equivalent today; removes coupling to Teams' URL
  format and tracks any future SDK changes.

Also adds the `reply` method to the structural MSTeamsApp type so the
refactor typechecks without casts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2.0.10 adds support for the AAD v1 token issuer that the Bot Framework
JWT validator needs. The minor version bump pulls teams.cards / common /
graph along to 2.0.10 too.

Add `@microsoft/teams.*` to `minimumReleaseAgeExclude` in
pnpm-workspace.yaml because 2.0.10 was published <48h ago and the default
`minimumReleaseAge: 2880` (~2 days) would otherwise reject it.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@heyitsaamir heyitsaamir changed the title fix(msteams): rebase SDK migration onto current main fix(msteams): rebase TeamsSDK patterns to simplify Teams Integration May 7, 2026
…ation-v2

# Conflicts:
#	extensions/msteams/src/send-context.ts
#	extensions/msteams/src/send.ts
@heyitsaamir
heyitsaamir marked this pull request as ready for review May 7, 2026 06:30
heyitsaamir and others added 5 commits May 7, 2026 06:39
These hosts were added in dfc169d for inline DM image auth-retry, but
the commit's own footnote acknowledges it doesn't actually unblock
clipboard-pasted images (asm.skype.com rejects Bot Framework tokens in
at least some tenants). The change is unrelated to the SDK migration and
the user-visible bug it claimed to fix isn't fixed; lifting it out keeps
this PR focused on the migration. Will land as a separate PR if the
auth-allowlist consistency improvement is wanted on its own.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…amid

The monitor's SDK bootstrap had an awkward chain:

  httpServerAdapter: new (
    (await import("@microsoft/teams.apps")) as unknown as {
      ExpressAdapter: new (app: unknown) => unknown;
    }
  ).ExpressAdapter(expressApp) as never,

Three casts (`unknown`, structural shape literal, `never`) were a
defensive workaround from when the SDK's hashed d.ts files tripped up
tsgo. With the SDK's exports now resolving cleanly, the same import can
be done with full types.

- Extend the lazy `loadSdkModules()` cache to include `ExpressAdapter`
  alongside `App` so the dynamic import is shared.
- Add `createMSTeamsExpressAdapter(serverOrApp)` helper in `sdk.ts` that
  encapsulates the lazy import and returns a properly-typed adapter
  instance.
- Replace `httpServerAdapter`'s structural shape on `CreateMSTeamsAppOptions`
  with the SDK's own `IHttpServerAdapter` interface (re-exported from
  `@microsoft/teams.apps`).

The call site in `monitor.ts` becomes a single typed call with no `any`,
no `unknown`, no `as never`. The lazy-load behavior is preserved: nothing
imports `@microsoft/teams.apps` at module load time.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
CI's check-prod-types failed because the previous commit's typed helper
used `typeof import("@microsoft/teams.apps").ExpressAdapter`, which
tsc/tsgo's NodeNext resolution can't follow through the SDK's chained
`export *` barrel:

    @microsoft/teams.apps/dist/index.d.ts:
        export * from "./http";          // folder with index.d.ts
        export * from "./app";           // single .d.ts file

The folder re-export drops `ExpressAdapter` and `IHttpServerAdapter` from
the namespace shape under `tsconfig.extensions.json` (passes under the
per-extension `tsconfig.json` because of inherited `paths`). Same root
cause as why we already model `MSTeamsApp` structurally (line 47 comment).

Switch the ExpressAdapter side to the same structural-shape pattern:
- Define `MSTeamsHttpServerAdapter` and `MSTeamsExpressAdapterCtor` locally.
- Cast `m.ExpressAdapter` once inside `loadSdkModules` (the runtime export
  is fine; only the type surface is hidden).
- `httpServerAdapter` on `CreateMSTeamsAppOptions` and the return type of
  `createMSTeamsExpressAdapter` use the local structural type.

Net result: the call site in `monitor.ts` stays the cast-free single line
the previous commit landed; the one remaining cast is confined to the
SDK-loading helper with an explanatory comment.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
The SDK migration removed all `import "jsonwebtoken"` / `import "jwks-rsa"`
from source code (the SDK does JWT validation internally now), but the
package.json entries and the matching `package-manifest.contract.test.ts`
expectation were left orphaned. Drop both:

- `extensions/msteams/package.json`: remove `jsonwebtoken` (^9), `jwks-rsa`
  (^4) from `dependencies` and `@types/jsonwebtoken` from `devDependencies`.
- `src/plugins/contracts/package-manifest.contract.test.ts`: remove the
  two entries from msteams's `pluginLocalRuntimeDeps` expectation.
- `monitor.lifecycle.test.ts`: extend the `./sdk.js` mock with the
  `createMSTeamsExpressAdapter` export added in the typed-helper cleanup,
  so the lifecycle suite still mounts after the deps drop.

Lockfile regenerates accordingly. All msteams tests (865) pass.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
CI's deadcode:dependencies (knip) flagged @microsoft/teams.api as
unused in extensions/msteams. The plugin source uses structural type
aliases (MSTeamsActivityParams, MSTeamsActivityLike, etc.) to dodge
tsgo resolution bugs with teams.api's hashed d.ts files, so it never
imports teams.api directly. The package is brought in transitively
via @microsoft/teams.apps; the only other reference is
probe.test.ts's vi.mock("@microsoft/teams.api"), which works on the
import-path string and doesn't require a direct dep declaration.

Lockfile regenerates accordingly. tsgo:extensions, knip, and all
865 msteams tests pass.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 7, 2026
@steipete

Copy link
Copy Markdown
Contributor

Verification for 89e0d7f:

Behavior addressed: Microsoft Teams SDK migration plus China cloud boundary support. China now uses the SDK sovereign cloud preset, permits China proactive service URLs on *.botframework.azure.cn, rejects China/public cloud service URL mismatches, adds Azure China Bot Framework attachment allowlists, and fails closed for Graph-backed Teams helpers until China Graph endpoint routing is implemented.

Real environment tested: Local Node/Vitest in this checkout and GitHub CI on PR SHA 89e0d7f.

Exact steps or command run after this patch:

  • pnpm config:channels:check
  • node scripts/run-tsgo.mjs -b tsconfig.projects.json
  • pnpm check:test-types
  • pnpm lint --threads=8
  • pnpm test extensions/msteams/src/attachments/shared.test.ts extensions/msteams/src/cloud.test.ts extensions/msteams/src/channel.test.ts extensions/msteams/src/sdk.test.ts extensions/msteams/src/graph.test.ts extensions/msteams/src/send-context.test.ts extensions/msteams/src/sdk-proactive.test.ts src/agents/skills.test.ts src/commands/channel-setup/plugin-install.test.ts -- --reporter=verbose
  • OPENCLAW_LOCAL_CHECK=0 OPENCLAW_VITEST_MAX_WORKERS=2 NODE_OPTIONS=--max-old-space-size=8192 OPENCLAW_VITEST_SHARD_NAME=agentic-commands-agent-channel OPENCLAW_TEST_PROJECTS_PARALLEL=2 OPENCLAW_VITEST_INCLUDE_FILE=/tmp/openclaw-agent-channel-include.json pnpm exec node scripts/test-projects.mjs test/vitest/vitest.commands.config.ts
  • autoreview --mode local, final run clean: no accepted/actionable findings reported

Evidence after fix: Local targeted Teams tests passed 7 files / 126 tests; channel setup shard passed 31 files / 293 tests; GitHub PR checks are green on 89e0d7f.

Observed result after fix: China cloud config and proactive URL boundaries are covered by tests; public/GCC/GCC High/DoD behavior remains covered by existing Teams tests.

What was not tested: Live Microsoft 365 operated by 21Vianet tenant/Bot Connector roundtrip; no live China tenant credentials were available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: msteams Channel integration: msteams channel: slack Channel integration: slack channel: zalouser Channel integration: zalouser commands Command implementations dependencies-changed PR changes dependency-related files docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: elevenlabs extensions: qa-lab gateway Gateway runtime merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. P1 High-priority user-facing bug, regression, or broken workflow. proof: supplied External PR includes structured after-fix real behavior proof. proof: 🎥 video Contributor real behavior proof includes video or recording evidence. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. scripts Repository scripts size: XL status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. triage: blank-template Candidate: PR template appears mostly untouched. triage: refactor-only Candidate: refactor/cleanup-only PR without maintainer context.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants