feat: Microsoft Teams connector#1518
Open
vuyiv wants to merge 34 commits into
Open
Conversation
Add a Microsoft Teams platform connector as a Bot Framework bot: - Inbound webhook with Bot Framework JWT + serviceURL-claim validation and an allow_from AAD-object-id allowlist. - Outbound Bot Connector REST client with AAD client-credentials auth. - Streaming Adaptive Card replies (working card -> answer, native "AI generated" label), rendering uniformly in channels and 1:1. - Engagement: @mention engages a reply thread, then follows it without re-mentioning; messages @mentioning others-but-not-the-bot are ignored; the bot never acts on its own activity. Engagement persists across restarts. - session_scope (thread default | channel | user) selects session grouping; channel scope keys on the channel root. - Teams-Markdown FormattingInstructions; text-based permission prompts. Contained entirely in platform/teams/ plus plugin registration, Makefile, config.example, docs, and the JWT/OAuth2 deps — no core/ changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Bound the AAD token fetch with a timeout-bearing HTTP client — it runs before the connector's own http timeout, so an unreachable login endpoint could otherwise hang a turn goroutine indefinitely. - Give the webhook server a bounded shutdown context. - Fix the self-message guard to match the bot's "28:<appId>" channelAccount form (the bare-appID compare never matched); harden its test accordingly. - Remove dead fields: activity.ReplyToID and replyContext.sessionKey. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Ack 202 and run the agent turn on a background goroutine (JWT validation stays synchronous before the ack). A slow turn under a synchronous ack could exceed Bot Framework's ~15s window and trigger a retry + duplicate dispatch; acking first matches the M365 Agents SDK's background-queue model. - Add an optional service_url_allowlist (comma-separated hosts, empty = any JWT-validated host). When set, an activity whose serviceUrl host is off the list is dropped before the bot's bearer token is POSTed to it — defense in depth against a forged serviceURL. Host-based match so regional paths pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security hardening from the Fable security re-analysis: - Accept only the Bot Framework channel issuer (api.botframework.com) for inbound activity tokens; drop the AAD tenant-issuer path. That path exists in the SDK for agent-to-agent/skill invocation, which this messaging connector does not offer. Restricting to the channel issuer keeps the activity `From` channel-signed, so allow_from and the serviceURL binding rest on a trusted identity instead of caller-supplied body fields (closes the spoof + fail-open residuals). tenant_id remains required for the outbound token authority. - Cap concurrent async dispatch with a semaphore (maxConcurrentDispatch); at capacity the webhook sheds with 503 so the Bot Connector retries, rather than spawning unbounded agent turns — matching the SDK's bounded background queue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… = none) Make the placeholder-card label a config option instead of a hardcoded constant. Empty (the default) renders a label-less card — no built-in fallback string. config.example suggests "💭 Thinking…". Drops the defaultCardWorkingText constant and the loadingCard empty-string fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add inboundAttachment parsing (file-download-info + image classification) and an isPersonal() gate on the activity, so a 1:1 file/image can be surfaced to the connector. Distinct from the outbound card attachment type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add connector.fetch (size-capped GET, optional bearer) + fetchOutcome so inbound attachments can be downloaded best-effort; add max_attachment_bytes config (default 20 MiB). The bearer token is attached only on request, never to a pre-authed file downloadUrl. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In a personal chat, download file/image attachments and populate Message.Images/Files so the engine hands them to the agent; a file-only message (empty text) now dispatches. Images fetch the bot token only when the contentUrl shares the JWT-bound serviceURL host; data: URIs decode inline. Channel/group attachments stay ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a 1:1 attachment download fails or exceeds max_attachment_bytes, reply a brief notice instead of dropping it silently; the turn still proceeds with whatever text and other attachments succeeded. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… cap Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d media Code-review follow-ups (both P3): - A file-download attachment with an empty/unparseable downloadUrl is now counted as a failed download, so the user gets a notice instead of a silently dropped turn. - Dispatch now gates on a processable attachment (file or image), so a 1:1 message whose only attachment is an unhandled type no longer forces an empty-content turn. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…efault - teams.md: native AI label is present from the first card render, not only at finalize - config.example.toml: allow_from default is unset/empty (behaves like "*"), not "*" - CLAUDE.md: add no_teams to the build-tag list Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bot now sends images back to Teams as inline base64 attachments, threaded to the originating activity. Oversize images degrade to a text notice instead of failing the turn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gation test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eSender Capture the non-obvious constraint surfaced this session: images/files reach platforms only via the external send socket, never through agent reply Events, and the 0600 daemon-owned socket blocks run_as_user agents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nder/FileSender" This reverts commit eb59c3c.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Permission and single-select AskUserQuestion prompts now render as Action.Submit buttons folded into the live streaming card (or a standalone card when none is active), instead of leaking as a separate plain-text message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inbound Action.Submit perm:* values now map to allow/deny/allow-all text so 'allow all' is not mis-resolved as a one-time allow; askq:* forwarded verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SendImage now lets Teams define the size limit: it attempts the send and, on a 413 (errActivityTooLarge), degrades to the oversize notice. The former 1 MiB notice-trigger became a generous 20 MiB pathological-size guard, so images Teams would accept (observed working well above 1 MiB) are no longer pre-rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lded Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nothing folds prompts into the streaming card anymore, so the activeCards registry, promptUpdate, and their lifecycle wiring are unused. Removed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Users can now drive their coding agent from Microsoft Teams.
What
session_scope, inbound 1:1 files/images, outbound inline images, and interactive permission / AskUserQuestion prompts as buttons.docs/teams.md, config inconfig.example.toml.Risks
platform/teams/— no changes tocore/or other platforms, so blast radius is limited to the new adapter.Decisions
413and degrading to a notice, not a guessed byte cap — matches Microsoft's own SDK guidance. A host-a-URL path for very large images is deferred.tenant_idrequired) — Azure deprecated multi-tenant bot creation after 2025-07-31; inbound JWT is restricted to the Bot Framework channel issuer.serviceUrlis bound to the request's JWT claim, with an optional host allowlist for defense-in-depth.AI Attribution
platform/teams/connector, its tests, and docs.Commit message
#lovelyreviewdesc | base: a23fb90 | head: 0a6bfe7