Add Teams delegated auth for plugin tools#77784
Conversation
|
Codex review: found issues before merge. Reviewed July 4, 2026, 12:40 PM ET / 16:40 UTC. Summary PR surface: Source +1677, Tests +3465, Docs +541, Config +51, Generated 0, Other +727. Total +6461 across 80 files. Reproducibility: not applicable. as a bug reproduction; this is a new feature, and the PR body supplies a Teams dev-app live run showing consent, six-digit fallback completion, and retry success. Review metrics: 3 noteworthy metrics.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof path suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Refresh the branch, remove the changelog entry, and land only after maintainer security/product approval of the delegated-auth SDK/config contract and example workspace surface. Do we have a high-confidence way to reproduce the issue? Not applicable as a bug reproduction; this is a new feature, and the PR body supplies a Teams dev-app live run showing consent, six-digit fallback completion, and retry success. Is this the best way to solve the issue? Unclear until maintainer approval: the owner-boundary split is plausible and default-deny, but the best fix depends on accepting a raw delegated-token SDK surface and root-workspace example shape. Full review comments:
Overall correctness: patch is correct AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against cf36cb915dd6. Label changesLabel justifications:
Evidence reviewedPR surface: Source +1677, Tests +3465, Docs +541, Config +51, Generated 0, Other +727. Total +6461 across 80 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6c6ecd769b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Addressed the Teams SSO stale challenge-key finding in 27e19a7d7bdb. The fix groups all user-id aliases recorded for one pending sign-in challenge, so clearing via either AAD object id or Teams channel user id removes the full challenge. Added handler-level regression coverage for the mixed-id flow: record consent challenge with both ids, complete verifyState with only one id, then confirm a later six-digit message with the other id reaches normal dispatch. Validation:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 27e19a7d7b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
3fac718 to
98a9469
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 71b55d98d6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
1f34650 to
b035ebd
Compare
b035ebd to
03e1a49
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 03e1a49019
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
03e1a49 to
b865817
Compare
There was a problem hiding this comment.
💡 Codex Review
https://github.com/openclaw/openclaw/blob/b865817d5cfe4f16003aa62c54be33b47501203a/extensions/msteams/src/sso.ts#L455-L456
Reject token-exchange invokes for wrong OAuth connection
handleSigninTokenExchangeInvoke trusts value.connectionName from the incoming invoke and falls back to deps.connectionName, but it never rejects a non-empty mismatch. If Teams sends a stale or different connection name (for example in multi-connection setups or misconfigured manifests), this path can report success and the caller clears the pending sign-in challenge, even though delegated tool auth later requests tokens only for the configured connection and continues failing. This creates a false-success sign-in flow and can strand users in missing_consent until a new challenge is triggered.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
b865817 to
aa13dcb
Compare
|
ClawSweeper PR egg ✨ Hatched: 💎 rare Brave Clawlet Hatch commandComment Hatchability rules:
Rarity: 💎 rare. What is this egg doing here?
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 297671e998
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return { | ||
| getDelegatedAccessToken: async (request) => { |
There was a problem hiding this comment.
Return undefined when channel delegated auth is absent
Fresh evidence for re-raising the cache-poisoning concern: this function now always returns an auth wrapper whenever delegatedAccess.enabled is true, even when params.auth is missing, so resolvePluginToolFactoryDelegatedAuth still marks factoryDelegatedAuth as true and descriptor cache keys stay identical across auth/no-auth runs. Any plugin factory that conditionally registers tools after calling ctx.auth.getDelegatedAccessToken(...) can therefore cache a no-auth descriptor set and reuse it in a later auth-enabled run, hiding auth-gated tools until cache invalidation.
Useful? React with 👍 / 👎.
297671e to
d15d8a3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d15d8a36d5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (typeof value === "string") { | ||
| return expectedAudiences.has(value); | ||
| } |
There was a problem hiding this comment.
Normalize GUID audience comparisons
The delegated-auth claim gate compares aud with exact string equality, so GUID-based audiences can be rejected when casing differs (for example plugin/request uses api://B379... while the JWT claim is lowercase b379...). Azure app IDs are case-insensitive, so this causes valid Teams delegated tokens to be returned as unavailable even though audience intent matches. Normalizing GUID/app-id audience forms before set membership checks avoids these false negatives.
Useful? React with 👍 / 👎.
d15d8a3 to
a0c10ab
Compare
…76262) * fix(msteams): rebase SDK migration onto current main 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]> * fix(msteams): preserve thread routing for channel and group-chat replies - 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. * fix(msteams): tag outbound SDK calls with OpenClaw User-Agent - 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>'. * fix(msteams): handle StreamCancelledError when user presses Stop mid-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. * fix(msteams): allow Bearer-token retry on Skype CDN attachment downloads 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 #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. * fix(msteams): align docker-compose msteams port default with plugin default 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. * fix(msteams): post-rebase reconciliation with main 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 #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. * feat(msteams): port streaming preview/progress features to ctx.stream 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). * feat(msteams): wire pipeline tool events to streaming progress + fix 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). * fix(msteams): SDK streaming delta + use app.reply for proactive thread 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]> * chore(msteams): bump @microsoft/teams.api and teams.apps to 2.0.10 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]> * revert(msteams): remove asm.skype.com auth-host allowlist additions 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]> * refactor(msteams): typed ExpressAdapter helper, drop unknown-cast pyramid 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]> * fix(msteams): unbreak tsgo:extensions on the ExpressAdapter helper 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]> * chore(msteams): drop unused jsonwebtoken/jwks-rsa deps 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]> * chore(msteams): drop unused @microsoft/teams.api direct dep 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]> * fix(msteams): clear three CI gate failures (lint, contract, deprecated config API) Three CI checks flagged on the latest run; all three are msteams-local and unrelated to one another: - **check-lint** / **check-additional-extension-bundled**: `oxlint` flagged a redundant `as string[]` assertion in `reply-dispatcher.ts:431`. The preceding `every((s: unknown) => typeof s === "string")` already narrows the array type, so the cast does nothing. Drop it. - **checks-fast-contracts-plugins-c**: the `package-manifest.contract.test.ts` `pluginLocalRuntimeDeps` for msteams still expected `@microsoft/teams.api`, but the deadcode cleanup commit (8f4050f) dropped it from `extensions/msteams/package.json`. Remove it from the contract test too — `teams.api` is only present transitively via `teams.apps`, which is the reason knip flagged it. - **check-additional-runtime-topology-architecture**: the deprecated internal config API guard caught `messenger.ts:223` calling `getMSTeamsRuntime().config.loadConfig()`. Switch to `config.current()` to match the pattern used by phone-control, synology-chat, and matrix. Pre-existing failures on this run that are NOT msteams-related and not caused by this PR: `check-test-types` (errors in `src/agents/openai-transport-stream.test.ts` and `pi-embedded-runner/openai-stream-wrappers.test.ts`) and `macos-swift` (`hoistAwait` in `MacNodeRuntime.swift`). Leaving those for upstream. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): cast config.current() return to OpenClawConfig The previous commit switched `messenger.ts:223` from the deprecated `config.loadConfig()` to `config.current()` to satisfy the architecture guard, but `config.current()` returns a deeply-readonly type that's not assignable to the `Partial<OpenClawConfig>` parameter `resolveMarkdownTableMode` expects (a mutable type from the SDK contract). Phone-control, synology-chat, and matrix all cast at this seam — adopt the same pattern. Verified locally: tsgo:core, tsgo:extensions, check:architecture, and test:extensions:package-boundary:compile all pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): address PR review — pre-auth body limit, allowlist log level, /api/messages forwarder, narrow release-age exclude Four narrow fixes from the PR review (BradGroux + clawsweeper bot + galiniliev's plan), each its own concern: - **pre-auth-body-limit** (monitor.ts) — install `express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES })` before the bearer-presence gate and SDK route. Express memoizes the parsed body on the request, so the SDK's later `json()` becomes a no-op and our limit applies before any handler parses bodies. Closes the gap where a `Bearer garbage`-shaped attacker could force unbounded JSON parsing before token validation. - **allowlist-error-logging** (monitor.ts) — restore main's `runtime.error` level for the `msteams resolve failed` catch (was downgraded to `runtime.log` mid-merge). Graph allowlist resolution failures are security-relevant; they need to surface to operators. - **legacy-messages-route** (monitor.ts) — when `webhook.path` is set to a custom value, also accept POSTs on the legacy `/api/messages` path with a one-time deprecation warning, then re-enter the Express middleware chain on the configured path. Keeps existing Azure Bot registrations working through the transition. Cast-free (`expressApp(req, res, next)` works because `Application extends IRouter extends RequestHandler`). - **release-age-scope** (pnpm-workspace.yaml) — narrow `@microsoft/teams.*` glob to the single direct dep `@microsoft/teams.apps`. Future scoped packages no longer get a freshness-guard pass. Tests + checks: msteams suite (867), tsgo:core, tsgo:extensions, tsgo:test, lint:extensions, check:architecture, knip --dependencies, package-manifest contract, all green. Still pending from the review (separate commits): - auth-coverage-tests (Brad #1 + comment) — tests proving the SDK accepts `aud=<bot app id>` and rejects `aud=api.botframework.com`. - invoke-response-handling (Brad #2, codex P2) — file-consent invoke ack must return through the SDK invoke handler, not `ctx.sendActivity`. - stream-failure-fallback (codex P2, galin F5) — `streamFailed` latch so partial streams fall back to block delivery on non-cancel errors. - serviceurl-routing (Brad #4, codex P2) — proposed rebuttal pending empirical confirmation that `smba.trafficmanager.net/teams` routes to non-default-region conversations. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(msteams): lock SDK auth contract — aud + v1/v2 issuer coverage Adds extensions/msteams/src/auth-coverage.test.ts driving ServiceTokenValidator and createEntraTokenValidator directly with jose-minted RS256 tokens against an in-memory JWKS (via JwksClient.prototype patch). Locks in the three contract cases @BradGroux flagged on #76262: aud=<bot app id> accepted, aud=api.botframework.com rejected even when appid/azp match, and v1/v2 issuers accepted for allowed tenant (disallowed tenant rejected). Drops a stale ambient module declaration in src/types/microsoft-teams-sdk.d.ts that was shadowing the SDK's real jwt-validator types with a long-renamed createServiceTokenValidator surface. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route file-consent invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR #76262 — `ctx.sendActivity({ type: "invokeResponse", ... })` no longer reaches Teams as an HTTP InvokeResponse on the new SDK; it becomes an outbound Bot Framework activity instead. Move file-consent accept/decline to typed `app.on("file.consent.accept|decline", ...)` handlers. The SDK's typed-route layer wraps a void return into `{ status: 200 }` (`app.process.js:130`), so the manual ack disappears. While in here, type `MSTeamsApp.on` properly. Borrowing the SDK's `App.on` directly fails because that function carries a `this: App<TPlugin>` constraint our structural alias can't satisfy, so we model an equivalent generic over `IRoutes` with route-specific overloads (`card.action`, `file.consent.*`, `activity`). The overloads work around a tsgo bug — the `@microsoft/teams.api` `Activity` discriminated union collapses to `any`, turning `ActivityRoutes` into a `[string]: RouteHandler<X, void>` index signature that swallows every typed `Out` not already void-compatible (card.action returns `AdaptiveCardActionResponse`; the others happen to include `void`). Real tsc resolves cleanly. Linked upstream: microsoft/typescript-go#1057. Other cleanups: - Cast-free call sites for `adaptSdkContext` (now returns `MSTeamsTurnContext` instead of `unknown`). - card.action error responses include `innerHttpError` per the SDK's `HttpError` shape requirement. - Activity catch-all also skips `fileConsent/invoke` now that it's typed-routed (parallel to the existing `adaptiveCard/action` skip). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route SSO sign-in invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR #76262, SSO half. Continue the typed-route migration: `signin/tokenExchange` and `signin/verifyState` now register via `app.on("signin.token-exchange" | "signin.verify-state", ...)`. Per the SDK's router, registering a user route with the same name as a system route removes the system default — so the SDK's built-in handlers (which would call `api.users.token.exchange` themselves and emit a `signin` event nobody currently subscribes to) are silenced, and only ours runs. The SDK wraps a void return into the HTTP 200 InvokeResponse, so the legacy `ctx.sendActivity({ type: "invokeResponse", ... })` ack — broken on the new SDK because it becomes an outbound BF activity instead of the HTTP response — is gone. The handler body is extracted from the activity-catch-all dispatch in `monitor-handler.ts` to a new `signin-invoke.ts`, parallel to `file-consent-invoke.ts`. `isSigninInvokeAuthorized` is now exported from `monitor-handler.ts` so the new handler can reuse it. The activity catch-all skips the SSO invoke names alongside the existing skips for `adaptiveCard/action` and `fileConsent/invoke`. `MSTeamsAppOn` overloads now cover the two SSO routes with their typed ctx (`ISignInTokenExchangeInvokeActivity` / `ISignInVerifyStateInvokeActivity`). Tests in `monitor-handler.sso.test.ts` were rewritten to call the extracted handler directly — the `registered.run(ctx)` shape no longer covers SSO, and the `expect(ctx.sendActivity).toHaveBeenCalledWith({ type: "invokeResponse" })` assertions were dropped to match the new contract (the SDK ack happens via the typed-route return value). Note on overlap with #77784 (Stefan Stüben, Microsoft): that PR is doing a much bigger SSO rework (sign-in card / sign-in-link / six-digit-code fallbacks plus a `ctx.auth` plumbed to plugin tools). This change is the small migration-correctness fix and is structured so #77784's SSO body changes drop into the typed-route registrations cleanly on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route message-submit (feedback) invokes through typed app.on Last invoke off the activity catch-all dispatch. `message/submitAction` (thumbs up/down on AI-generated messages) now registers via `app.on("message.submit", ...)`. Same shape as file-consent and SSO: handler body extracted to a new `feedback-invoke.ts`, the SDK wraps a void return into the HTTP 200 InvokeResponse, the broken `ctx.sendActivity({ type: "invokeResponse", ... })` line is gone, and the activity catch-all skips this invoke name alongside the others. `isFeedbackInvokeAuthorized` is exported from `monitor-handler.ts` so `feedback-invoke.ts` can reuse it. Tests in `monitor-handler.feedback-authz.test.ts` were rewritten to call the extracted handler directly — the old `handler.run(ctx)` shape no longer intercepts feedback, and `originalRun` was removed because the typed route is the dispatch point now. `MSTeamsAppOn` overload added with the typed `IMessageSubmitActionInvokeActivity` ctx, slotted between the SSO overloads and the `activity` catch-all so `activity` stays last. This leaves only `message`, `conversationUpdate`, and `messageReaction` flowing through `app.on("activity", ...)` → `handler.run`. Promoting those is the path to deleting the catch-all entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): fall back to block delivery when partial-mode stream fails mid-flight codex #5 / Galin F5 on PR #76262. `reply-stream-controller.ts` previously re-threw any non-cancel error from `stream.emit` during partial streaming and from `stream.emit`/`stream.close` during finalize. Combined with `preparePayload` suppressing block delivery once `tokensEmitted` was true, that meant a network blip or API error mid-stream produced a truncated reply with no recovery — the user saw the prefix that made it through and nothing else. Add a `streamFailed` latch parallel to `canceledLocally` / `tokensEmitted`: - `onPartialReply`: catch non-cancel errors, set `streamFailed = true`, log a warn, don't propagate (the pipeline must keep running so `preparePayload` can decide). - `preparePayload`: when `tokensEmitted && streamFailed`, fall through to block delivery instead of suppressing. The user may see a duplicate (streamed prefix + full block reply); intentional — matches the pre-migration `TeamsHttpStream.hasContent` recovery and is better than truncated-only. - `finalize`: same latch + warn on non-cancel close failure, swallow rather than throw. The streamed content already reached the user; the closing activity (AI-Generated marker, feedback channelData) is the only loss, not worth blowing up the dispatcher. - `isStreamActive` returns false once the stream has failed. New tests cover crash-mid-stream after tokens were emitted (assert block delivery payload is returned), happy-path no-duplicate behavior (assert `preparePayload` still suppresses when nothing failed), and finalize close-failure (assert no throw). The pre-existing "re-throws non-cancel" test was inverted to assert non-throwing latch behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): declare @microsoft/teams.api as a runtime dependency Type-only `import("@microsoft/teams.api/dist/...").TypeName` references in `sdk.ts` (added when typed `MSTeamsApp.on` overloads were introduced) are picked up by the `extension-runtime-dependencies` contract test as genuine runtime imports. Declaring `@microsoft/teams.api` as a direct dep makes the contract pass; the package was already coming in transitively via `@microsoft/teams.apps`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): keep SSO on SDK signin routes * test(msteams): avoid redundant signin handler assertion * docs(msteams): clarify Teams cloud support * fix(msteams): use current SDK string helper * fix(msteams): gate SDK invoke side effects * test(msteams): avoid implicit any in lifecycle tests * fix(msteams): preserve SDK user agent and matrix check * fix(msteams): expose SDK common dependency * fix(msteams): use SDK user agent merge * fix(msteams): fall back when stream close no-ops * chore(msteams): drop unrelated merge artifacts * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * test(msteams): type stream close mock result * fix(msteams): configure Teams cloud service URL * chore(msteams): refresh shrinkwrap * chore(deps): refresh shrinkwrap locks * chore(ci): rerun guards after main sync * chore(deps): refresh shrinkwrap for node 24 * chore(config): refresh docs baseline * fix(msteams): preserve Teams SDK proactive references * fix(msteams): harden SDK proactive sends * fix(msteams): align service url contract * test: fix bonjour beacon type narrowing * fix(msteams): ignore ambient service url * fix(msteams): fall through submit invokes * test: align shrinkwrap override policy with Teams SDK deps * fix(msteams): ack invoke routes promptly * fix(msteams): support china cloud boundaries * test: sync PR with current CI gates * test: isolate channel setup registry metadata --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#76262) * fix(msteams): rebase SDK migration onto current main 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 4b3286a 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): - 1b0c5f4 fix(msteams): accept conversation id allowlists - 8cf4ec7 fix(msteams): bind global audience tokens to app id - Channels turn-kernel refactor (bc8b243 / c2c5d6f / 4d1c409) — 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]> * fix(msteams): preserve thread routing for channel and group-chat replies - 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. * fix(msteams): tag outbound SDK calls with OpenClaw User-Agent - 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>'. * fix(msteams): handle StreamCancelledError when user presses Stop mid-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. * fix(msteams): allow Bearer-token retry on Skype CDN attachment downloads 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. * fix(msteams): align docker-compose msteams port default with plugin default 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. * fix(msteams): post-rebase reconciliation with main 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. * feat(msteams): port streaming preview/progress features to ctx.stream 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). * feat(msteams): wire pipeline tool events to streaming progress + fix 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). * fix(msteams): SDK streaming delta + use app.reply for proactive thread 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]> * chore(msteams): bump @microsoft/teams.api and teams.apps to 2.0.10 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]> * revert(msteams): remove asm.skype.com auth-host allowlist additions 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]> * refactor(msteams): typed ExpressAdapter helper, drop unknown-cast pyramid 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]> * fix(msteams): unbreak tsgo:extensions on the ExpressAdapter helper 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]> * chore(msteams): drop unused jsonwebtoken/jwks-rsa deps 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]> * chore(msteams): drop unused @microsoft/teams.api direct dep 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]> * fix(msteams): clear three CI gate failures (lint, contract, deprecated config API) Three CI checks flagged on the latest run; all three are msteams-local and unrelated to one another: - **check-lint** / **check-additional-extension-bundled**: `oxlint` flagged a redundant `as string[]` assertion in `reply-dispatcher.ts:431`. The preceding `every((s: unknown) => typeof s === "string")` already narrows the array type, so the cast does nothing. Drop it. - **checks-fast-contracts-plugins-c**: the `package-manifest.contract.test.ts` `pluginLocalRuntimeDeps` for msteams still expected `@microsoft/teams.api`, but the deadcode cleanup commit (8f4050f) dropped it from `extensions/msteams/package.json`. Remove it from the contract test too — `teams.api` is only present transitively via `teams.apps`, which is the reason knip flagged it. - **check-additional-runtime-topology-architecture**: the deprecated internal config API guard caught `messenger.ts:223` calling `getMSTeamsRuntime().config.loadConfig()`. Switch to `config.current()` to match the pattern used by phone-control, synology-chat, and matrix. Pre-existing failures on this run that are NOT msteams-related and not caused by this PR: `check-test-types` (errors in `src/agents/openai-transport-stream.test.ts` and `pi-embedded-runner/openai-stream-wrappers.test.ts`) and `macos-swift` (`hoistAwait` in `MacNodeRuntime.swift`). Leaving those for upstream. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): cast config.current() return to OpenClawConfig The previous commit switched `messenger.ts:223` from the deprecated `config.loadConfig()` to `config.current()` to satisfy the architecture guard, but `config.current()` returns a deeply-readonly type that's not assignable to the `Partial<OpenClawConfig>` parameter `resolveMarkdownTableMode` expects (a mutable type from the SDK contract). Phone-control, synology-chat, and matrix all cast at this seam — adopt the same pattern. Verified locally: tsgo:core, tsgo:extensions, check:architecture, and test:extensions:package-boundary:compile all pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): address PR review — pre-auth body limit, allowlist log level, /api/messages forwarder, narrow release-age exclude Four narrow fixes from the PR review (BradGroux + clawsweeper bot + galiniliev's plan), each its own concern: - **pre-auth-body-limit** (monitor.ts) — install `express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES })` before the bearer-presence gate and SDK route. Express memoizes the parsed body on the request, so the SDK's later `json()` becomes a no-op and our limit applies before any handler parses bodies. Closes the gap where a `Bearer garbage`-shaped attacker could force unbounded JSON parsing before token validation. - **allowlist-error-logging** (monitor.ts) — restore main's `runtime.error` level for the `msteams resolve failed` catch (was downgraded to `runtime.log` mid-merge). Graph allowlist resolution failures are security-relevant; they need to surface to operators. - **legacy-messages-route** (monitor.ts) — when `webhook.path` is set to a custom value, also accept POSTs on the legacy `/api/messages` path with a one-time deprecation warning, then re-enter the Express middleware chain on the configured path. Keeps existing Azure Bot registrations working through the transition. Cast-free (`expressApp(req, res, next)` works because `Application extends IRouter extends RequestHandler`). - **release-age-scope** (pnpm-workspace.yaml) — narrow `@microsoft/teams.*` glob to the single direct dep `@microsoft/teams.apps`. Future scoped packages no longer get a freshness-guard pass. Tests + checks: msteams suite (867), tsgo:core, tsgo:extensions, tsgo:test, lint:extensions, check:architecture, knip --dependencies, package-manifest contract, all green. Still pending from the review (separate commits): - auth-coverage-tests (Brad #1 + comment) — tests proving the SDK accepts `aud=<bot app id>` and rejects `aud=api.botframework.com`. - invoke-response-handling (Brad #2, codex P2) — file-consent invoke ack must return through the SDK invoke handler, not `ctx.sendActivity`. - stream-failure-fallback (codex P2, galin F5) — `streamFailed` latch so partial streams fall back to block delivery on non-cancel errors. - serviceurl-routing (Brad #4, codex P2) — proposed rebuttal pending empirical confirmation that `smba.trafficmanager.net/teams` routes to non-default-region conversations. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(msteams): lock SDK auth contract — aud + v1/v2 issuer coverage Adds extensions/msteams/src/auth-coverage.test.ts driving ServiceTokenValidator and createEntraTokenValidator directly with jose-minted RS256 tokens against an in-memory JWKS (via JwksClient.prototype patch). Locks in the three contract cases @BradGroux flagged on openclaw#76262: aud=<bot app id> accepted, aud=api.botframework.com rejected even when appid/azp match, and v1/v2 issuers accepted for allowed tenant (disallowed tenant rejected). Drops a stale ambient module declaration in src/types/microsoft-teams-sdk.d.ts that was shadowing the SDK's real jwt-validator types with a long-renamed createServiceTokenValidator surface. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route file-consent invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR openclaw#76262 — `ctx.sendActivity({ type: "invokeResponse", ... })` no longer reaches Teams as an HTTP InvokeResponse on the new SDK; it becomes an outbound Bot Framework activity instead. Move file-consent accept/decline to typed `app.on("file.consent.accept|decline", ...)` handlers. The SDK's typed-route layer wraps a void return into `{ status: 200 }` (`app.process.js:130`), so the manual ack disappears. While in here, type `MSTeamsApp.on` properly. Borrowing the SDK's `App.on` directly fails because that function carries a `this: App<TPlugin>` constraint our structural alias can't satisfy, so we model an equivalent generic over `IRoutes` with route-specific overloads (`card.action`, `file.consent.*`, `activity`). The overloads work around a tsgo bug — the `@microsoft/teams.api` `Activity` discriminated union collapses to `any`, turning `ActivityRoutes` into a `[string]: RouteHandler<X, void>` index signature that swallows every typed `Out` not already void-compatible (card.action returns `AdaptiveCardActionResponse`; the others happen to include `void`). Real tsc resolves cleanly. Linked upstream: microsoft/typescript-go#1057. Other cleanups: - Cast-free call sites for `adaptSdkContext` (now returns `MSTeamsTurnContext` instead of `unknown`). - card.action error responses include `innerHttpError` per the SDK's `HttpError` shape requirement. - Activity catch-all also skips `fileConsent/invoke` now that it's typed-routed (parallel to the existing `adaptiveCard/action` skip). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route SSO sign-in invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR openclaw#76262, SSO half. Continue the typed-route migration: `signin/tokenExchange` and `signin/verifyState` now register via `app.on("signin.token-exchange" | "signin.verify-state", ...)`. Per the SDK's router, registering a user route with the same name as a system route removes the system default — so the SDK's built-in handlers (which would call `api.users.token.exchange` themselves and emit a `signin` event nobody currently subscribes to) are silenced, and only ours runs. The SDK wraps a void return into the HTTP 200 InvokeResponse, so the legacy `ctx.sendActivity({ type: "invokeResponse", ... })` ack — broken on the new SDK because it becomes an outbound BF activity instead of the HTTP response — is gone. The handler body is extracted from the activity-catch-all dispatch in `monitor-handler.ts` to a new `signin-invoke.ts`, parallel to `file-consent-invoke.ts`. `isSigninInvokeAuthorized` is now exported from `monitor-handler.ts` so the new handler can reuse it. The activity catch-all skips the SSO invoke names alongside the existing skips for `adaptiveCard/action` and `fileConsent/invoke`. `MSTeamsAppOn` overloads now cover the two SSO routes with their typed ctx (`ISignInTokenExchangeInvokeActivity` / `ISignInVerifyStateInvokeActivity`). Tests in `monitor-handler.sso.test.ts` were rewritten to call the extracted handler directly — the `registered.run(ctx)` shape no longer covers SSO, and the `expect(ctx.sendActivity).toHaveBeenCalledWith({ type: "invokeResponse" })` assertions were dropped to match the new contract (the SDK ack happens via the typed-route return value). Note on overlap with openclaw#77784 (Stefan Stüben, Microsoft): that PR is doing a much bigger SSO rework (sign-in card / sign-in-link / six-digit-code fallbacks plus a `ctx.auth` plumbed to plugin tools). This change is the small migration-correctness fix and is structured so openclaw#77784's SSO body changes drop into the typed-route registrations cleanly on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route message-submit (feedback) invokes through typed app.on Last invoke off the activity catch-all dispatch. `message/submitAction` (thumbs up/down on AI-generated messages) now registers via `app.on("message.submit", ...)`. Same shape as file-consent and SSO: handler body extracted to a new `feedback-invoke.ts`, the SDK wraps a void return into the HTTP 200 InvokeResponse, the broken `ctx.sendActivity({ type: "invokeResponse", ... })` line is gone, and the activity catch-all skips this invoke name alongside the others. `isFeedbackInvokeAuthorized` is exported from `monitor-handler.ts` so `feedback-invoke.ts` can reuse it. Tests in `monitor-handler.feedback-authz.test.ts` were rewritten to call the extracted handler directly — the old `handler.run(ctx)` shape no longer intercepts feedback, and `originalRun` was removed because the typed route is the dispatch point now. `MSTeamsAppOn` overload added with the typed `IMessageSubmitActionInvokeActivity` ctx, slotted between the SSO overloads and the `activity` catch-all so `activity` stays last. This leaves only `message`, `conversationUpdate`, and `messageReaction` flowing through `app.on("activity", ...)` → `handler.run`. Promoting those is the path to deleting the catch-all entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): fall back to block delivery when partial-mode stream fails mid-flight codex #5 / Galin F5 on PR openclaw#76262. `reply-stream-controller.ts` previously re-threw any non-cancel error from `stream.emit` during partial streaming and from `stream.emit`/`stream.close` during finalize. Combined with `preparePayload` suppressing block delivery once `tokensEmitted` was true, that meant a network blip or API error mid-stream produced a truncated reply with no recovery — the user saw the prefix that made it through and nothing else. Add a `streamFailed` latch parallel to `canceledLocally` / `tokensEmitted`: - `onPartialReply`: catch non-cancel errors, set `streamFailed = true`, log a warn, don't propagate (the pipeline must keep running so `preparePayload` can decide). - `preparePayload`: when `tokensEmitted && streamFailed`, fall through to block delivery instead of suppressing. The user may see a duplicate (streamed prefix + full block reply); intentional — matches the pre-migration `TeamsHttpStream.hasContent` recovery and is better than truncated-only. - `finalize`: same latch + warn on non-cancel close failure, swallow rather than throw. The streamed content already reached the user; the closing activity (AI-Generated marker, feedback channelData) is the only loss, not worth blowing up the dispatcher. - `isStreamActive` returns false once the stream has failed. New tests cover crash-mid-stream after tokens were emitted (assert block delivery payload is returned), happy-path no-duplicate behavior (assert `preparePayload` still suppresses when nothing failed), and finalize close-failure (assert no throw). The pre-existing "re-throws non-cancel" test was inverted to assert non-throwing latch behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): declare @microsoft/teams.api as a runtime dependency Type-only `import("@microsoft/teams.api/dist/...").TypeName` references in `sdk.ts` (added when typed `MSTeamsApp.on` overloads were introduced) are picked up by the `extension-runtime-dependencies` contract test as genuine runtime imports. Declaring `@microsoft/teams.api` as a direct dep makes the contract pass; the package was already coming in transitively via `@microsoft/teams.apps`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): keep SSO on SDK signin routes * test(msteams): avoid redundant signin handler assertion * docs(msteams): clarify Teams cloud support * fix(msteams): use current SDK string helper * fix(msteams): gate SDK invoke side effects * test(msteams): avoid implicit any in lifecycle tests * fix(msteams): preserve SDK user agent and matrix check * fix(msteams): expose SDK common dependency * fix(msteams): use SDK user agent merge * fix(msteams): fall back when stream close no-ops * chore(msteams): drop unrelated merge artifacts * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * test(msteams): type stream close mock result * fix(msteams): configure Teams cloud service URL * chore(msteams): refresh shrinkwrap * chore(deps): refresh shrinkwrap locks * chore(ci): rerun guards after main sync * chore(deps): refresh shrinkwrap for node 24 * chore(config): refresh docs baseline * fix(msteams): preserve Teams SDK proactive references * fix(msteams): harden SDK proactive sends * fix(msteams): align service url contract * test: fix bonjour beacon type narrowing * fix(msteams): ignore ambient service url * fix(msteams): fall through submit invokes * test: align shrinkwrap override policy with Teams SDK deps * fix(msteams): ack invoke routes promptly * fix(msteams): support china cloud boundaries * test: sync PR with current CI gates * test: isolate channel setup registry metadata --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…76262) * fix(msteams): rebase SDK migration onto current main 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]> * fix(msteams): preserve thread routing for channel and group-chat replies - 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. * fix(msteams): tag outbound SDK calls with OpenClaw User-Agent - 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>'. * fix(msteams): handle StreamCancelledError when user presses Stop mid-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. * fix(msteams): allow Bearer-token retry on Skype CDN attachment downloads 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 #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. * fix(msteams): align docker-compose msteams port default with plugin default 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. * fix(msteams): post-rebase reconciliation with main 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 #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. * feat(msteams): port streaming preview/progress features to ctx.stream 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). * feat(msteams): wire pipeline tool events to streaming progress + fix 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). * fix(msteams): SDK streaming delta + use app.reply for proactive thread 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]> * chore(msteams): bump @microsoft/teams.api and teams.apps to 2.0.10 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]> * revert(msteams): remove asm.skype.com auth-host allowlist additions 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]> * refactor(msteams): typed ExpressAdapter helper, drop unknown-cast pyramid 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]> * fix(msteams): unbreak tsgo:extensions on the ExpressAdapter helper 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]> * chore(msteams): drop unused jsonwebtoken/jwks-rsa deps 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]> * chore(msteams): drop unused @microsoft/teams.api direct dep 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]> * fix(msteams): clear three CI gate failures (lint, contract, deprecated config API) Three CI checks flagged on the latest run; all three are msteams-local and unrelated to one another: - **check-lint** / **check-additional-extension-bundled**: `oxlint` flagged a redundant `as string[]` assertion in `reply-dispatcher.ts:431`. The preceding `every((s: unknown) => typeof s === "string")` already narrows the array type, so the cast does nothing. Drop it. - **checks-fast-contracts-plugins-c**: the `package-manifest.contract.test.ts` `pluginLocalRuntimeDeps` for msteams still expected `@microsoft/teams.api`, but the deadcode cleanup commit (8f4050f) dropped it from `extensions/msteams/package.json`. Remove it from the contract test too — `teams.api` is only present transitively via `teams.apps`, which is the reason knip flagged it. - **check-additional-runtime-topology-architecture**: the deprecated internal config API guard caught `messenger.ts:223` calling `getMSTeamsRuntime().config.loadConfig()`. Switch to `config.current()` to match the pattern used by phone-control, synology-chat, and matrix. Pre-existing failures on this run that are NOT msteams-related and not caused by this PR: `check-test-types` (errors in `src/agents/openai-transport-stream.test.ts` and `pi-embedded-runner/openai-stream-wrappers.test.ts`) and `macos-swift` (`hoistAwait` in `MacNodeRuntime.swift`). Leaving those for upstream. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): cast config.current() return to OpenClawConfig The previous commit switched `messenger.ts:223` from the deprecated `config.loadConfig()` to `config.current()` to satisfy the architecture guard, but `config.current()` returns a deeply-readonly type that's not assignable to the `Partial<OpenClawConfig>` parameter `resolveMarkdownTableMode` expects (a mutable type from the SDK contract). Phone-control, synology-chat, and matrix all cast at this seam — adopt the same pattern. Verified locally: tsgo:core, tsgo:extensions, check:architecture, and test:extensions:package-boundary:compile all pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): address PR review — pre-auth body limit, allowlist log level, /api/messages forwarder, narrow release-age exclude Four narrow fixes from the PR review (BradGroux + clawsweeper bot + galiniliev's plan), each its own concern: - **pre-auth-body-limit** (monitor.ts) — install `express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES })` before the bearer-presence gate and SDK route. Express memoizes the parsed body on the request, so the SDK's later `json()` becomes a no-op and our limit applies before any handler parses bodies. Closes the gap where a `Bearer garbage`-shaped attacker could force unbounded JSON parsing before token validation. - **allowlist-error-logging** (monitor.ts) — restore main's `runtime.error` level for the `msteams resolve failed` catch (was downgraded to `runtime.log` mid-merge). Graph allowlist resolution failures are security-relevant; they need to surface to operators. - **legacy-messages-route** (monitor.ts) — when `webhook.path` is set to a custom value, also accept POSTs on the legacy `/api/messages` path with a one-time deprecation warning, then re-enter the Express middleware chain on the configured path. Keeps existing Azure Bot registrations working through the transition. Cast-free (`expressApp(req, res, next)` works because `Application extends IRouter extends RequestHandler`). - **release-age-scope** (pnpm-workspace.yaml) — narrow `@microsoft/teams.*` glob to the single direct dep `@microsoft/teams.apps`. Future scoped packages no longer get a freshness-guard pass. Tests + checks: msteams suite (867), tsgo:core, tsgo:extensions, tsgo:test, lint:extensions, check:architecture, knip --dependencies, package-manifest contract, all green. Still pending from the review (separate commits): - auth-coverage-tests (Brad #1 + comment) — tests proving the SDK accepts `aud=<bot app id>` and rejects `aud=api.botframework.com`. - invoke-response-handling (Brad #2, codex P2) — file-consent invoke ack must return through the SDK invoke handler, not `ctx.sendActivity`. - stream-failure-fallback (codex P2, galin F5) — `streamFailed` latch so partial streams fall back to block delivery on non-cancel errors. - serviceurl-routing (Brad #4, codex P2) — proposed rebuttal pending empirical confirmation that `smba.trafficmanager.net/teams` routes to non-default-region conversations. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(msteams): lock SDK auth contract — aud + v1/v2 issuer coverage Adds extensions/msteams/src/auth-coverage.test.ts driving ServiceTokenValidator and createEntraTokenValidator directly with jose-minted RS256 tokens against an in-memory JWKS (via JwksClient.prototype patch). Locks in the three contract cases @BradGroux flagged on #76262: aud=<bot app id> accepted, aud=api.botframework.com rejected even when appid/azp match, and v1/v2 issuers accepted for allowed tenant (disallowed tenant rejected). Drops a stale ambient module declaration in src/types/microsoft-teams-sdk.d.ts that was shadowing the SDK's real jwt-validator types with a long-renamed createServiceTokenValidator surface. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route file-consent invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR #76262 — `ctx.sendActivity({ type: "invokeResponse", ... })` no longer reaches Teams as an HTTP InvokeResponse on the new SDK; it becomes an outbound Bot Framework activity instead. Move file-consent accept/decline to typed `app.on("file.consent.accept|decline", ...)` handlers. The SDK's typed-route layer wraps a void return into `{ status: 200 }` (`app.process.js:130`), so the manual ack disappears. While in here, type `MSTeamsApp.on` properly. Borrowing the SDK's `App.on` directly fails because that function carries a `this: App<TPlugin>` constraint our structural alias can't satisfy, so we model an equivalent generic over `IRoutes` with route-specific overloads (`card.action`, `file.consent.*`, `activity`). The overloads work around a tsgo bug — the `@microsoft/teams.api` `Activity` discriminated union collapses to `any`, turning `ActivityRoutes` into a `[string]: RouteHandler<X, void>` index signature that swallows every typed `Out` not already void-compatible (card.action returns `AdaptiveCardActionResponse`; the others happen to include `void`). Real tsc resolves cleanly. Linked upstream: microsoft/typescript-go#1057. Other cleanups: - Cast-free call sites for `adaptSdkContext` (now returns `MSTeamsTurnContext` instead of `unknown`). - card.action error responses include `innerHttpError` per the SDK's `HttpError` shape requirement. - Activity catch-all also skips `fileConsent/invoke` now that it's typed-routed (parallel to the existing `adaptiveCard/action` skip). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route SSO sign-in invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR #76262, SSO half. Continue the typed-route migration: `signin/tokenExchange` and `signin/verifyState` now register via `app.on("signin.token-exchange" | "signin.verify-state", ...)`. Per the SDK's router, registering a user route with the same name as a system route removes the system default — so the SDK's built-in handlers (which would call `api.users.token.exchange` themselves and emit a `signin` event nobody currently subscribes to) are silenced, and only ours runs. The SDK wraps a void return into the HTTP 200 InvokeResponse, so the legacy `ctx.sendActivity({ type: "invokeResponse", ... })` ack — broken on the new SDK because it becomes an outbound BF activity instead of the HTTP response — is gone. The handler body is extracted from the activity-catch-all dispatch in `monitor-handler.ts` to a new `signin-invoke.ts`, parallel to `file-consent-invoke.ts`. `isSigninInvokeAuthorized` is now exported from `monitor-handler.ts` so the new handler can reuse it. The activity catch-all skips the SSO invoke names alongside the existing skips for `adaptiveCard/action` and `fileConsent/invoke`. `MSTeamsAppOn` overloads now cover the two SSO routes with their typed ctx (`ISignInTokenExchangeInvokeActivity` / `ISignInVerifyStateInvokeActivity`). Tests in `monitor-handler.sso.test.ts` were rewritten to call the extracted handler directly — the `registered.run(ctx)` shape no longer covers SSO, and the `expect(ctx.sendActivity).toHaveBeenCalledWith({ type: "invokeResponse" })` assertions were dropped to match the new contract (the SDK ack happens via the typed-route return value). Note on overlap with #77784 (Stefan Stüben, Microsoft): that PR is doing a much bigger SSO rework (sign-in card / sign-in-link / six-digit-code fallbacks plus a `ctx.auth` plumbed to plugin tools). This change is the small migration-correctness fix and is structured so #77784's SSO body changes drop into the typed-route registrations cleanly on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route message-submit (feedback) invokes through typed app.on Last invoke off the activity catch-all dispatch. `message/submitAction` (thumbs up/down on AI-generated messages) now registers via `app.on("message.submit", ...)`. Same shape as file-consent and SSO: handler body extracted to a new `feedback-invoke.ts`, the SDK wraps a void return into the HTTP 200 InvokeResponse, the broken `ctx.sendActivity({ type: "invokeResponse", ... })` line is gone, and the activity catch-all skips this invoke name alongside the others. `isFeedbackInvokeAuthorized` is exported from `monitor-handler.ts` so `feedback-invoke.ts` can reuse it. Tests in `monitor-handler.feedback-authz.test.ts` were rewritten to call the extracted handler directly — the old `handler.run(ctx)` shape no longer intercepts feedback, and `originalRun` was removed because the typed route is the dispatch point now. `MSTeamsAppOn` overload added with the typed `IMessageSubmitActionInvokeActivity` ctx, slotted between the SSO overloads and the `activity` catch-all so `activity` stays last. This leaves only `message`, `conversationUpdate`, and `messageReaction` flowing through `app.on("activity", ...)` → `handler.run`. Promoting those is the path to deleting the catch-all entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): fall back to block delivery when partial-mode stream fails mid-flight codex #5 / Galin F5 on PR #76262. `reply-stream-controller.ts` previously re-threw any non-cancel error from `stream.emit` during partial streaming and from `stream.emit`/`stream.close` during finalize. Combined with `preparePayload` suppressing block delivery once `tokensEmitted` was true, that meant a network blip or API error mid-stream produced a truncated reply with no recovery — the user saw the prefix that made it through and nothing else. Add a `streamFailed` latch parallel to `canceledLocally` / `tokensEmitted`: - `onPartialReply`: catch non-cancel errors, set `streamFailed = true`, log a warn, don't propagate (the pipeline must keep running so `preparePayload` can decide). - `preparePayload`: when `tokensEmitted && streamFailed`, fall through to block delivery instead of suppressing. The user may see a duplicate (streamed prefix + full block reply); intentional — matches the pre-migration `TeamsHttpStream.hasContent` recovery and is better than truncated-only. - `finalize`: same latch + warn on non-cancel close failure, swallow rather than throw. The streamed content already reached the user; the closing activity (AI-Generated marker, feedback channelData) is the only loss, not worth blowing up the dispatcher. - `isStreamActive` returns false once the stream has failed. New tests cover crash-mid-stream after tokens were emitted (assert block delivery payload is returned), happy-path no-duplicate behavior (assert `preparePayload` still suppresses when nothing failed), and finalize close-failure (assert no throw). The pre-existing "re-throws non-cancel" test was inverted to assert non-throwing latch behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): declare @microsoft/teams.api as a runtime dependency Type-only `import("@microsoft/teams.api/dist/...").TypeName` references in `sdk.ts` (added when typed `MSTeamsApp.on` overloads were introduced) are picked up by the `extension-runtime-dependencies` contract test as genuine runtime imports. Declaring `@microsoft/teams.api` as a direct dep makes the contract pass; the package was already coming in transitively via `@microsoft/teams.apps`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): keep SSO on SDK signin routes * test(msteams): avoid redundant signin handler assertion * docs(msteams): clarify Teams cloud support * fix(msteams): use current SDK string helper * fix(msteams): gate SDK invoke side effects * test(msteams): avoid implicit any in lifecycle tests * fix(msteams): preserve SDK user agent and matrix check * fix(msteams): expose SDK common dependency * fix(msteams): use SDK user agent merge * fix(msteams): fall back when stream close no-ops * chore(msteams): drop unrelated merge artifacts * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * test(msteams): type stream close mock result * fix(msteams): configure Teams cloud service URL * chore(msteams): refresh shrinkwrap * chore(deps): refresh shrinkwrap locks * chore(ci): rerun guards after main sync * chore(deps): refresh shrinkwrap for node 24 * chore(config): refresh docs baseline * fix(msteams): preserve Teams SDK proactive references * fix(msteams): harden SDK proactive sends * fix(msteams): align service url contract * test: fix bonjour beacon type narrowing * fix(msteams): ignore ambient service url * fix(msteams): fall through submit invokes * test: align shrinkwrap override policy with Teams SDK deps * fix(msteams): ack invoke routes promptly * fix(msteams): support china cloud boundaries * test: sync PR with current CI gates * test: isolate channel setup registry metadata --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#76262) * fix(msteams): rebase SDK migration onto current main 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]> * fix(msteams): preserve thread routing for channel and group-chat replies - 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. * fix(msteams): tag outbound SDK calls with OpenClaw User-Agent - 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>'. * fix(msteams): handle StreamCancelledError when user presses Stop mid-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. * fix(msteams): allow Bearer-token retry on Skype CDN attachment downloads 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. * fix(msteams): align docker-compose msteams port default with plugin default 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. * fix(msteams): post-rebase reconciliation with main 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. * feat(msteams): port streaming preview/progress features to ctx.stream 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). * feat(msteams): wire pipeline tool events to streaming progress + fix 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). * fix(msteams): SDK streaming delta + use app.reply for proactive thread 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]> * chore(msteams): bump @microsoft/teams.api and teams.apps to 2.0.10 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]> * revert(msteams): remove asm.skype.com auth-host allowlist additions 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]> * refactor(msteams): typed ExpressAdapter helper, drop unknown-cast pyramid 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]> * fix(msteams): unbreak tsgo:extensions on the ExpressAdapter helper 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]> * chore(msteams): drop unused jsonwebtoken/jwks-rsa deps 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]> * chore(msteams): drop unused @microsoft/teams.api direct dep 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]> * fix(msteams): clear three CI gate failures (lint, contract, deprecated config API) Three CI checks flagged on the latest run; all three are msteams-local and unrelated to one another: - **check-lint** / **check-additional-extension-bundled**: `oxlint` flagged a redundant `as string[]` assertion in `reply-dispatcher.ts:431`. The preceding `every((s: unknown) => typeof s === "string")` already narrows the array type, so the cast does nothing. Drop it. - **checks-fast-contracts-plugins-c**: the `package-manifest.contract.test.ts` `pluginLocalRuntimeDeps` for msteams still expected `@microsoft/teams.api`, but the deadcode cleanup commit (8f4050f) dropped it from `extensions/msteams/package.json`. Remove it from the contract test too — `teams.api` is only present transitively via `teams.apps`, which is the reason knip flagged it. - **check-additional-runtime-topology-architecture**: the deprecated internal config API guard caught `messenger.ts:223` calling `getMSTeamsRuntime().config.loadConfig()`. Switch to `config.current()` to match the pattern used by phone-control, synology-chat, and matrix. Pre-existing failures on this run that are NOT msteams-related and not caused by this PR: `check-test-types` (errors in `src/agents/openai-transport-stream.test.ts` and `pi-embedded-runner/openai-stream-wrappers.test.ts`) and `macos-swift` (`hoistAwait` in `MacNodeRuntime.swift`). Leaving those for upstream. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): cast config.current() return to OpenClawConfig The previous commit switched `messenger.ts:223` from the deprecated `config.loadConfig()` to `config.current()` to satisfy the architecture guard, but `config.current()` returns a deeply-readonly type that's not assignable to the `Partial<OpenClawConfig>` parameter `resolveMarkdownTableMode` expects (a mutable type from the SDK contract). Phone-control, synology-chat, and matrix all cast at this seam — adopt the same pattern. Verified locally: tsgo:core, tsgo:extensions, check:architecture, and test:extensions:package-boundary:compile all pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): address PR review — pre-auth body limit, allowlist log level, /api/messages forwarder, narrow release-age exclude Four narrow fixes from the PR review (BradGroux + clawsweeper bot + galiniliev's plan), each its own concern: - **pre-auth-body-limit** (monitor.ts) — install `express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES })` before the bearer-presence gate and SDK route. Express memoizes the parsed body on the request, so the SDK's later `json()` becomes a no-op and our limit applies before any handler parses bodies. Closes the gap where a `Bearer garbage`-shaped attacker could force unbounded JSON parsing before token validation. - **allowlist-error-logging** (monitor.ts) — restore main's `runtime.error` level for the `msteams resolve failed` catch (was downgraded to `runtime.log` mid-merge). Graph allowlist resolution failures are security-relevant; they need to surface to operators. - **legacy-messages-route** (monitor.ts) — when `webhook.path` is set to a custom value, also accept POSTs on the legacy `/api/messages` path with a one-time deprecation warning, then re-enter the Express middleware chain on the configured path. Keeps existing Azure Bot registrations working through the transition. Cast-free (`expressApp(req, res, next)` works because `Application extends IRouter extends RequestHandler`). - **release-age-scope** (pnpm-workspace.yaml) — narrow `@microsoft/teams.*` glob to the single direct dep `@microsoft/teams.apps`. Future scoped packages no longer get a freshness-guard pass. Tests + checks: msteams suite (867), tsgo:core, tsgo:extensions, tsgo:test, lint:extensions, check:architecture, knip --dependencies, package-manifest contract, all green. Still pending from the review (separate commits): - auth-coverage-tests (Brad #1 + comment) — tests proving the SDK accepts `aud=<bot app id>` and rejects `aud=api.botframework.com`. - invoke-response-handling (Brad #2, codex P2) — file-consent invoke ack must return through the SDK invoke handler, not `ctx.sendActivity`. - stream-failure-fallback (codex P2, galin F5) — `streamFailed` latch so partial streams fall back to block delivery on non-cancel errors. - serviceurl-routing (Brad #4, codex P2) — proposed rebuttal pending empirical confirmation that `smba.trafficmanager.net/teams` routes to non-default-region conversations. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(msteams): lock SDK auth contract — aud + v1/v2 issuer coverage Adds extensions/msteams/src/auth-coverage.test.ts driving ServiceTokenValidator and createEntraTokenValidator directly with jose-minted RS256 tokens against an in-memory JWKS (via JwksClient.prototype patch). Locks in the three contract cases @BradGroux flagged on openclaw#76262: aud=<bot app id> accepted, aud=api.botframework.com rejected even when appid/azp match, and v1/v2 issuers accepted for allowed tenant (disallowed tenant rejected). Drops a stale ambient module declaration in src/types/microsoft-teams-sdk.d.ts that was shadowing the SDK's real jwt-validator types with a long-renamed createServiceTokenValidator surface. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route file-consent invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR openclaw#76262 — `ctx.sendActivity({ type: "invokeResponse", ... })` no longer reaches Teams as an HTTP InvokeResponse on the new SDK; it becomes an outbound Bot Framework activity instead. Move file-consent accept/decline to typed `app.on("file.consent.accept|decline", ...)` handlers. The SDK's typed-route layer wraps a void return into `{ status: 200 }` (`app.process.js:130`), so the manual ack disappears. While in here, type `MSTeamsApp.on` properly. Borrowing the SDK's `App.on` directly fails because that function carries a `this: App<TPlugin>` constraint our structural alias can't satisfy, so we model an equivalent generic over `IRoutes` with route-specific overloads (`card.action`, `file.consent.*`, `activity`). The overloads work around a tsgo bug — the `@microsoft/teams.api` `Activity` discriminated union collapses to `any`, turning `ActivityRoutes` into a `[string]: RouteHandler<X, void>` index signature that swallows every typed `Out` not already void-compatible (card.action returns `AdaptiveCardActionResponse`; the others happen to include `void`). Real tsc resolves cleanly. Linked upstream: microsoft/typescript-go#1057. Other cleanups: - Cast-free call sites for `adaptSdkContext` (now returns `MSTeamsTurnContext` instead of `unknown`). - card.action error responses include `innerHttpError` per the SDK's `HttpError` shape requirement. - Activity catch-all also skips `fileConsent/invoke` now that it's typed-routed (parallel to the existing `adaptiveCard/action` skip). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route SSO sign-in invokes through typed app.on, drop broken invokeResponse send Brad #2 / codex #4 on PR openclaw#76262, SSO half. Continue the typed-route migration: `signin/tokenExchange` and `signin/verifyState` now register via `app.on("signin.token-exchange" | "signin.verify-state", ...)`. Per the SDK's router, registering a user route with the same name as a system route removes the system default — so the SDK's built-in handlers (which would call `api.users.token.exchange` themselves and emit a `signin` event nobody currently subscribes to) are silenced, and only ours runs. The SDK wraps a void return into the HTTP 200 InvokeResponse, so the legacy `ctx.sendActivity({ type: "invokeResponse", ... })` ack — broken on the new SDK because it becomes an outbound BF activity instead of the HTTP response — is gone. The handler body is extracted from the activity-catch-all dispatch in `monitor-handler.ts` to a new `signin-invoke.ts`, parallel to `file-consent-invoke.ts`. `isSigninInvokeAuthorized` is now exported from `monitor-handler.ts` so the new handler can reuse it. The activity catch-all skips the SSO invoke names alongside the existing skips for `adaptiveCard/action` and `fileConsent/invoke`. `MSTeamsAppOn` overloads now cover the two SSO routes with their typed ctx (`ISignInTokenExchangeInvokeActivity` / `ISignInVerifyStateInvokeActivity`). Tests in `monitor-handler.sso.test.ts` were rewritten to call the extracted handler directly — the `registered.run(ctx)` shape no longer covers SSO, and the `expect(ctx.sendActivity).toHaveBeenCalledWith({ type: "invokeResponse" })` assertions were dropped to match the new contract (the SDK ack happens via the typed-route return value). Note on overlap with openclaw#77784 (Stefan Stüben, Microsoft): that PR is doing a much bigger SSO rework (sign-in card / sign-in-link / six-digit-code fallbacks plus a `ctx.auth` plumbed to plugin tools). This change is the small migration-correctness fix and is structured so openclaw#77784's SSO body changes drop into the typed-route registrations cleanly on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route message-submit (feedback) invokes through typed app.on Last invoke off the activity catch-all dispatch. `message/submitAction` (thumbs up/down on AI-generated messages) now registers via `app.on("message.submit", ...)`. Same shape as file-consent and SSO: handler body extracted to a new `feedback-invoke.ts`, the SDK wraps a void return into the HTTP 200 InvokeResponse, the broken `ctx.sendActivity({ type: "invokeResponse", ... })` line is gone, and the activity catch-all skips this invoke name alongside the others. `isFeedbackInvokeAuthorized` is exported from `monitor-handler.ts` so `feedback-invoke.ts` can reuse it. Tests in `monitor-handler.feedback-authz.test.ts` were rewritten to call the extracted handler directly — the old `handler.run(ctx)` shape no longer intercepts feedback, and `originalRun` was removed because the typed route is the dispatch point now. `MSTeamsAppOn` overload added with the typed `IMessageSubmitActionInvokeActivity` ctx, slotted between the SSO overloads and the `activity` catch-all so `activity` stays last. This leaves only `message`, `conversationUpdate`, and `messageReaction` flowing through `app.on("activity", ...)` → `handler.run`. Promoting those is the path to deleting the catch-all entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): fall back to block delivery when partial-mode stream fails mid-flight codex #5 / Galin F5 on PR openclaw#76262. `reply-stream-controller.ts` previously re-threw any non-cancel error from `stream.emit` during partial streaming and from `stream.emit`/`stream.close` during finalize. Combined with `preparePayload` suppressing block delivery once `tokensEmitted` was true, that meant a network blip or API error mid-stream produced a truncated reply with no recovery — the user saw the prefix that made it through and nothing else. Add a `streamFailed` latch parallel to `canceledLocally` / `tokensEmitted`: - `onPartialReply`: catch non-cancel errors, set `streamFailed = true`, log a warn, don't propagate (the pipeline must keep running so `preparePayload` can decide). - `preparePayload`: when `tokensEmitted && streamFailed`, fall through to block delivery instead of suppressing. The user may see a duplicate (streamed prefix + full block reply); intentional — matches the pre-migration `TeamsHttpStream.hasContent` recovery and is better than truncated-only. - `finalize`: same latch + warn on non-cancel close failure, swallow rather than throw. The streamed content already reached the user; the closing activity (AI-Generated marker, feedback channelData) is the only loss, not worth blowing up the dispatcher. - `isStreamActive` returns false once the stream has failed. New tests cover crash-mid-stream after tokens were emitted (assert block delivery payload is returned), happy-path no-duplicate behavior (assert `preparePayload` still suppresses when nothing failed), and finalize close-failure (assert no throw). The pre-existing "re-throws non-cancel" test was inverted to assert non-throwing latch behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): declare @microsoft/teams.api as a runtime dependency Type-only `import("@microsoft/teams.api/dist/...").TypeName` references in `sdk.ts` (added when typed `MSTeamsApp.on` overloads were introduced) are picked up by the `extension-runtime-dependencies` contract test as genuine runtime imports. Declaring `@microsoft/teams.api` as a direct dep makes the contract pass; the package was already coming in transitively via `@microsoft/teams.apps`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): keep SSO on SDK signin routes * test(msteams): avoid redundant signin handler assertion * docs(msteams): clarify Teams cloud support * fix(msteams): use current SDK string helper * fix(msteams): gate SDK invoke side effects * test(msteams): avoid implicit any in lifecycle tests * fix(msteams): preserve SDK user agent and matrix check * fix(msteams): expose SDK common dependency * fix(msteams): use SDK user agent merge * fix(msteams): fall back when stream close no-ops * chore(msteams): drop unrelated merge artifacts * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * test(msteams): type stream close mock result * fix(msteams): configure Teams cloud service URL * chore(msteams): refresh shrinkwrap * chore(deps): refresh shrinkwrap locks * chore(ci): rerun guards after main sync * chore(deps): refresh shrinkwrap for node 24 * chore(config): refresh docs baseline * fix(msteams): preserve Teams SDK proactive references * fix(msteams): harden SDK proactive sends * fix(msteams): align service url contract * test: fix bonjour beacon type narrowing * fix(msteams): ignore ambient service url * fix(msteams): fall through submit invokes * test: align shrinkwrap override policy with Teams SDK deps * fix(msteams): ack invoke routes promptly * fix(msteams): support china cloud boundaries * test: sync PR with current CI gates * test: isolate channel setup registry metadata --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
…penclaw#76262) * fix(msteams): rebase SDK migration onto current main 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 14c85a9 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): - c1b0656 fix(msteams): accept conversation id allowlists - 9f24870 fix(msteams): bind global audience tokens to app id - Channels turn-kernel refactor (e5f5209 / acea210 / bf56f73) — 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]> * fix(msteams): preserve thread routing for channel and group-chat replies - 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. * fix(msteams): tag outbound SDK calls with OpenClaw User-Agent - 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>'. * fix(msteams): handle StreamCancelledError when user presses Stop mid-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. * fix(msteams): allow Bearer-token retry on Skype CDN attachment downloads 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. * fix(msteams): align docker-compose msteams port default with plugin default 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. * fix(msteams): post-rebase reconciliation with main 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. * feat(msteams): port streaming preview/progress features to ctx.stream 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). * feat(msteams): wire pipeline tool events to streaming progress + fix 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). * fix(msteams): SDK streaming delta + use app.reply for proactive thread 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]> * chore(msteams): bump @microsoft/teams.api and teams.apps to 2.0.10 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]> * revert(msteams): remove asm.skype.com auth-host allowlist additions 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]> * refactor(msteams): typed ExpressAdapter helper, drop unknown-cast pyramid 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]> * fix(msteams): unbreak tsgo:extensions on the ExpressAdapter helper 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]> * chore(msteams): drop unused jsonwebtoken/jwks-rsa deps 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]> * chore(msteams): drop unused @microsoft/teams.api direct dep 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]> * fix(msteams): clear three CI gate failures (lint, contract, deprecated config API) Three CI checks flagged on the latest run; all three are msteams-local and unrelated to one another: - **check-lint** / **check-additional-extension-bundled**: `oxlint` flagged a redundant `as string[]` assertion in `reply-dispatcher.ts:431`. The preceding `every((s: unknown) => typeof s === "string")` already narrows the array type, so the cast does nothing. Drop it. - **checks-fast-contracts-plugins-c**: the `package-manifest.contract.test.ts` `pluginLocalRuntimeDeps` for msteams still expected `@microsoft/teams.api`, but the deadcode cleanup commit (8f4050f) dropped it from `extensions/msteams/package.json`. Remove it from the contract test too — `teams.api` is only present transitively via `teams.apps`, which is the reason knip flagged it. - **check-additional-runtime-topology-architecture**: the deprecated internal config API guard caught `messenger.ts:223` calling `getMSTeamsRuntime().config.loadConfig()`. Switch to `config.current()` to match the pattern used by phone-control, synology-chat, and matrix. Pre-existing failures on this run that are NOT msteams-related and not caused by this PR: `check-test-types` (errors in `src/agents/openai-transport-stream.test.ts` and `pi-embedded-runner/openai-stream-wrappers.test.ts`) and `macos-swift` (`hoistAwait` in `MacNodeRuntime.swift`). Leaving those for upstream. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): cast config.current() return to OpenClawConfig The previous commit switched `messenger.ts:223` from the deprecated `config.loadConfig()` to `config.current()` to satisfy the architecture guard, but `config.current()` returns a deeply-readonly type that's not assignable to the `Partial<OpenClawConfig>` parameter `resolveMarkdownTableMode` expects (a mutable type from the SDK contract). Phone-control, synology-chat, and matrix all cast at this seam — adopt the same pattern. Verified locally: tsgo:core, tsgo:extensions, check:architecture, and test:extensions:package-boundary:compile all pass. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * fix(msteams): address PR review — pre-auth body limit, allowlist log level, /api/messages forwarder, narrow release-age exclude Four narrow fixes from the PR review (BradGroux + clawsweeper bot + galiniliev's plan), each its own concern: - **pre-auth-body-limit** (monitor.ts) — install `express.json({ limit: DEFAULT_WEBHOOK_MAX_BODY_BYTES })` before the bearer-presence gate and SDK route. Express memoizes the parsed body on the request, so the SDK's later `json()` becomes a no-op and our limit applies before any handler parses bodies. Closes the gap where a `Bearer garbage`-shaped attacker could force unbounded JSON parsing before token validation. - **allowlist-error-logging** (monitor.ts) — restore main's `runtime.error` level for the `msteams resolve failed` catch (was downgraded to `runtime.log` mid-merge). Graph allowlist resolution failures are security-relevant; they need to surface to operators. - **legacy-messages-route** (monitor.ts) — when `webhook.path` is set to a custom value, also accept POSTs on the legacy `/api/messages` path with a one-time deprecation warning, then re-enter the Express middleware chain on the configured path. Keeps existing Azure Bot registrations working through the transition. Cast-free (`expressApp(req, res, next)` works because `Application extends IRouter extends RequestHandler`). - **release-age-scope** (pnpm-workspace.yaml) — narrow `@microsoft/teams.*` glob to the single direct dep `@microsoft/teams.apps`. Future scoped packages no longer get a freshness-guard pass. Tests + checks: msteams suite (867), tsgo:core, tsgo:extensions, tsgo:test, lint:extensions, check:architecture, knip --dependencies, package-manifest contract, all green. Still pending from the review (separate commits): - auth-coverage-tests (Brad openclaw#1 + comment) — tests proving the SDK accepts `aud=<bot app id>` and rejects `aud=api.botframework.com`. - invoke-response-handling (Brad openclaw#2, codex P2) — file-consent invoke ack must return through the SDK invoke handler, not `ctx.sendActivity`. - stream-failure-fallback (codex P2, galin F5) — `streamFailed` latch so partial streams fall back to block delivery on non-cancel errors. - serviceurl-routing (Brad openclaw#4, codex P2) — proposed rebuttal pending empirical confirmation that `smba.trafficmanager.net/teams` routes to non-default-region conversations. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> * test(msteams): lock SDK auth contract — aud + v1/v2 issuer coverage Adds extensions/msteams/src/auth-coverage.test.ts driving ServiceTokenValidator and createEntraTokenValidator directly with jose-minted RS256 tokens against an in-memory JWKS (via JwksClient.prototype patch). Locks in the three contract cases @BradGroux flagged on openclaw#76262: aud=<bot app id> accepted, aud=api.botframework.com rejected even when appid/azp match, and v1/v2 issuers accepted for allowed tenant (disallowed tenant rejected). Drops a stale ambient module declaration in src/types/microsoft-teams-sdk.d.ts that was shadowing the SDK's real jwt-validator types with a long-renamed createServiceTokenValidator surface. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route file-consent invokes through typed app.on, drop broken invokeResponse send Brad openclaw#2 / codex openclaw#4 on PR openclaw#76262 — `ctx.sendActivity({ type: "invokeResponse", ... })` no longer reaches Teams as an HTTP InvokeResponse on the new SDK; it becomes an outbound Bot Framework activity instead. Move file-consent accept/decline to typed `app.on("file.consent.accept|decline", ...)` handlers. The SDK's typed-route layer wraps a void return into `{ status: 200 }` (`app.process.js:130`), so the manual ack disappears. While in here, type `MSTeamsApp.on` properly. Borrowing the SDK's `App.on` directly fails because that function carries a `this: App<TPlugin>` constraint our structural alias can't satisfy, so we model an equivalent generic over `IRoutes` with route-specific overloads (`card.action`, `file.consent.*`, `activity`). The overloads work around a tsgo bug — the `@microsoft/teams.api` `Activity` discriminated union collapses to `any`, turning `ActivityRoutes` into a `[string]: RouteHandler<X, void>` index signature that swallows every typed `Out` not already void-compatible (card.action returns `AdaptiveCardActionResponse`; the others happen to include `void`). Real tsc resolves cleanly. Linked upstream: microsoft/typescript-go#1057. Other cleanups: - Cast-free call sites for `adaptSdkContext` (now returns `MSTeamsTurnContext` instead of `unknown`). - card.action error responses include `innerHttpError` per the SDK's `HttpError` shape requirement. - Activity catch-all also skips `fileConsent/invoke` now that it's typed-routed (parallel to the existing `adaptiveCard/action` skip). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route SSO sign-in invokes through typed app.on, drop broken invokeResponse send Brad openclaw#2 / codex openclaw#4 on PR openclaw#76262, SSO half. Continue the typed-route migration: `signin/tokenExchange` and `signin/verifyState` now register via `app.on("signin.token-exchange" | "signin.verify-state", ...)`. Per the SDK's router, registering a user route with the same name as a system route removes the system default — so the SDK's built-in handlers (which would call `api.users.token.exchange` themselves and emit a `signin` event nobody currently subscribes to) are silenced, and only ours runs. The SDK wraps a void return into the HTTP 200 InvokeResponse, so the legacy `ctx.sendActivity({ type: "invokeResponse", ... })` ack — broken on the new SDK because it becomes an outbound BF activity instead of the HTTP response — is gone. The handler body is extracted from the activity-catch-all dispatch in `monitor-handler.ts` to a new `signin-invoke.ts`, parallel to `file-consent-invoke.ts`. `isSigninInvokeAuthorized` is now exported from `monitor-handler.ts` so the new handler can reuse it. The activity catch-all skips the SSO invoke names alongside the existing skips for `adaptiveCard/action` and `fileConsent/invoke`. `MSTeamsAppOn` overloads now cover the two SSO routes with their typed ctx (`ISignInTokenExchangeInvokeActivity` / `ISignInVerifyStateInvokeActivity`). Tests in `monitor-handler.sso.test.ts` were rewritten to call the extracted handler directly — the `registered.run(ctx)` shape no longer covers SSO, and the `expect(ctx.sendActivity).toHaveBeenCalledWith({ type: "invokeResponse" })` assertions were dropped to match the new contract (the SDK ack happens via the typed-route return value). Note on overlap with openclaw#77784 (Stefan Stüben, Microsoft): that PR is doing a much bigger SSO rework (sign-in card / sign-in-link / six-digit-code fallbacks plus a `ctx.auth` plumbed to plugin tools). This change is the small migration-correctness fix and is structured so openclaw#77784's SSO body changes drop into the typed-route registrations cleanly on rebase. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): route message-submit (feedback) invokes through typed app.on Last invoke off the activity catch-all dispatch. `message/submitAction` (thumbs up/down on AI-generated messages) now registers via `app.on("message.submit", ...)`. Same shape as file-consent and SSO: handler body extracted to a new `feedback-invoke.ts`, the SDK wraps a void return into the HTTP 200 InvokeResponse, the broken `ctx.sendActivity({ type: "invokeResponse", ... })` line is gone, and the activity catch-all skips this invoke name alongside the others. `isFeedbackInvokeAuthorized` is exported from `monitor-handler.ts` so `feedback-invoke.ts` can reuse it. Tests in `monitor-handler.feedback-authz.test.ts` were rewritten to call the extracted handler directly — the old `handler.run(ctx)` shape no longer intercepts feedback, and `originalRun` was removed because the typed route is the dispatch point now. `MSTeamsAppOn` overload added with the typed `IMessageSubmitActionInvokeActivity` ctx, slotted between the SSO overloads and the `activity` catch-all so `activity` stays last. This leaves only `message`, `conversationUpdate`, and `messageReaction` flowing through `app.on("activity", ...)` → `handler.run`. Promoting those is the path to deleting the catch-all entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): fall back to block delivery when partial-mode stream fails mid-flight codex openclaw#5 / Galin F5 on PR openclaw#76262. `reply-stream-controller.ts` previously re-threw any non-cancel error from `stream.emit` during partial streaming and from `stream.emit`/`stream.close` during finalize. Combined with `preparePayload` suppressing block delivery once `tokensEmitted` was true, that meant a network blip or API error mid-stream produced a truncated reply with no recovery — the user saw the prefix that made it through and nothing else. Add a `streamFailed` latch parallel to `canceledLocally` / `tokensEmitted`: - `onPartialReply`: catch non-cancel errors, set `streamFailed = true`, log a warn, don't propagate (the pipeline must keep running so `preparePayload` can decide). - `preparePayload`: when `tokensEmitted && streamFailed`, fall through to block delivery instead of suppressing. The user may see a duplicate (streamed prefix + full block reply); intentional — matches the pre-migration `TeamsHttpStream.hasContent` recovery and is better than truncated-only. - `finalize`: same latch + warn on non-cancel close failure, swallow rather than throw. The streamed content already reached the user; the closing activity (AI-Generated marker, feedback channelData) is the only loss, not worth blowing up the dispatcher. - `isStreamActive` returns false once the stream has failed. New tests cover crash-mid-stream after tokens were emitted (assert block delivery payload is returned), happy-path no-duplicate behavior (assert `preparePayload` still suppresses when nothing failed), and finalize close-failure (assert no throw). The pre-existing "re-throws non-cancel" test was inverted to assert non-throwing latch behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): declare @microsoft/teams.api as a runtime dependency Type-only `import("@microsoft/teams.api/dist/...").TypeName` references in `sdk.ts` (added when typed `MSTeamsApp.on` overloads were introduced) are picked up by the `extension-runtime-dependencies` contract test as genuine runtime imports. Declaring `@microsoft/teams.api` as a direct dep makes the contract pass; the package was already coming in transitively via `@microsoft/teams.apps`. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]> * fix(msteams): keep SSO on SDK signin routes * test(msteams): avoid redundant signin handler assertion * docs(msteams): clarify Teams cloud support * fix(msteams): use current SDK string helper * fix(msteams): gate SDK invoke side effects * test(msteams): avoid implicit any in lifecycle tests * fix(msteams): preserve SDK user agent and matrix check * fix(msteams): expose SDK common dependency * fix(msteams): use SDK user agent merge * fix(msteams): fall back when stream close no-ops * chore(msteams): drop unrelated merge artifacts * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * chore(msteams): restore unrelated main files * test(msteams): type stream close mock result * fix(msteams): configure Teams cloud service URL * chore(msteams): refresh shrinkwrap * chore(deps): refresh shrinkwrap locks * chore(ci): rerun guards after main sync * chore(deps): refresh shrinkwrap for node 24 * chore(config): refresh docs baseline * fix(msteams): preserve Teams SDK proactive references * fix(msteams): harden SDK proactive sends * fix(msteams): align service url contract * test: fix bonjour beacon type narrowing * fix(msteams): ignore ambient service url * fix(msteams): fall through submit invokes * test: align shrinkwrap override policy with Teams SDK deps * fix(msteams): ack invoke routes promptly * fix(msteams): support china cloud boundaries * test: sync PR with current CI gates * test: isolate channel setup registry metadata --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
Summary
Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Real behavior proof (required for external PRs)
pnpm openclaw --dev gateway, Microsoft Teams dev app, Bot Framework OAuth connection, devtunnel on port 3978, local downstream OBO API, and the examplemsteams-graph-profileplugin.msteams-graph-profileplugin configured.hi, run teams whoamito the OpenClaw Teams dev app.hi, run teams whoamiagain.msteams_whoamicall returned the signed-in Teams user profile/email through the downstream OBO API.msteams_whoamireturnedMicrosoft Teams delegated auth unavailable: missing_consent.Root Cause (if applicable)
Regression Test Plan (if applicable)
N/A for a new feature, but the smallest reliable guardrails added/used are:
src/plugins/tools.optional.test.ts,extensions/msteams/src/delegated-auth.test.ts,extensions/msteams/src/monitor-handler.sso.test.ts,extensions/msteams/src/monitor-handler/message-handler.delegated-auth.test.ts,extensions/codex/src/app-server/run-attempt.test.ts,src/agents/pi-embedded-runner/run.delegated-auth-context.test.ts,src/auto-reply/reply/followup-runner.test.ts.User-visible / Behavior Changes
plugins.entries.<pluginId>.auth.delegatedAccess.examples/plugins/msteams-graph-profileandexamples/msteams-obo-downstream-api.Diagram (if applicable)
Security Impact (required)
Yes)Yes)Yes)Yes)Yes)Yes, explain risk + mitigation: Delegated Teams tokens are sensitive. This PR keeps access opt-in and policy-gated by plugin/tool allowlists plus provider, audience, scope, user, tenant, chat, and chat-type checks. Tokens are passed as hidden runtime context to plugin execution, not prompt text or transcript content. The example keeps Microsoft Graph access server-side in a downstream API so client secrets and Graph permissions stay behind an API boundary.Repro + Verification
Environment
Steps
pnpm openclaw --dev gateway.hi, run teams whoamiin Teams.hi, run teams whoami.Expected
Actual
missing_consent.Microsoft Teams delegated auth is connected. Retry the tool now.msteams_whoamireturned the signed-in Teams user profile/email.Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
msteams_whoami, and downstream OBO profile lookup.Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes)Yes)No)plugins.entries.<pluginId>.auth.delegatedAccessand the Teams Bot Framework OAuth connection described in the docs.Risks and Mitigations
Rationale And Design Decisions
Keep core generic
Delegated auth is modeled as a generic plugin capability instead of a Teams-specific special case in core. Core owns policy parsing, tool gating, token claim validation, and runtime propagation; the Teams plugin owns Bot Framework SSO behavior and token acquisition.
Make delegated auth explicit and policy-gated
Plugin delegated auth is opt-in through
plugins.entries.<pluginId>.auth.delegatedAccess.enabled, with optional provider, audience, scope, user, tenant, chat, and chat-type restrictions.Keep optional tools least-privilege
Optional plugin factories receive
ctx.authonly when the plugin itself,group:plugins,*, or a concrete optional tool name is allowlisted. The resolver also handles implicit optional registrations wherenames: []anddeclaredNamescarry the actual tool contract.Resolve auth per execution for cached descriptors
Cached plugin tool descriptors keep descriptor creation fast, but delegated auth is resolved when the cached tool executes, so a previous message’s auth context is never cached into a future run.
Propagate auth through run plumbing, not prompts
The Teams delegated auth context is passed through hidden runtime/run parameters and plugin tool context, including queued replies, follow-up runs, embedded runner attempts, OpenClaw tools, and Codex app-server tool calls.
Teams owns Bot Framework SSO details
The Teams plugin owns Bot Framework token exchange, verify-state, sign-in resource creation, sign-in-link fallback, six-digit code fallback, and Teams user-id fallback handling.
Keep Graph access server-side in the example
The example plugin calls a local downstream API. The downstream API performs Microsoft Graph OBO using its own app registration and delegated permission grant, which keeps Graph client secrets and broader Graph behavior behind a production-shaped API boundary.
Verification
pnpm test src/plugins/tools.optional.test.tspnpm tsgo:core --pretty falsepnpm tsgo:test:src --pretty falsepnpm test src/plugins/tools.optional.test.ts extensions/msteams/src/delegated-auth.test.ts extensions/msteams/src/monitor-handler.sso.test.ts extensions/msteams/src/monitor-handler/message-handler.delegated-auth.test.ts extensions/codex/src/app-server/run-attempt.test.ts src/agents/pi-embedded-runner/run.delegated-auth-context.test.ts src/auto-reply/reply/followup-runner.test.tspnpm tsgo:extensions --pretty falsepnpm tsgo:extensions:test --pretty falsepnpm test src/plugins/tool-descriptor-cache.test.ts src/plugins/tools.optional.test.ts extensions/msteams/src/delegated-auth.test.tspnpm exec oxfmt --check --threads=1 src/plugins/tool-descriptor-cache.ts src/plugins/tool-descriptor-cache.test.ts src/plugins/tools.ts src/plugins/tools.optional.test.ts extensions/msteams/src/delegated-auth.test.tsgit diff --checkpnpm dup:check:coveragemsteams_whoamireturned the signed-in user profile