Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
820cf7c
feat(apps): add Socket Mode protocol models
Sep 22, 2026
f641476
feat(apps): parse Socket Mode envelopes and replies
Sep 22, 2026
bd665ea
feat(apps): add Socket Mode negotiation
Sep 22, 2026
059705c
feat(apps): define Socket Mode connection abstraction
Sep 22, 2026
3c0e902
docs(apps): document Socket Mode transport
Sep 23, 2026
ab6f062
fix(apps): allow Socket Mode websocket URLs
Sep 23, 2026
9eea9be
fix(apps): require HTTP SignalR negotiate URLs
Sep 23, 2026
b994c3f
fix(apps): include region in Socket Mode endpoint
Sep 23, 2026
ac2a031
fix(apps): time out Socket Mode token acquisition
Sep 23, 2026
dd68942
test(apps): cover Socket Mode token timeout
Sep 23, 2026
2ebe3cf
fix(apps): tighten Socket Mode validation
Sep 23, 2026
6802ad6
feat(apps): define default Socket Mode geographies
Sep 24, 2026
99e8c6f
feat(apps): add SignalR Socket Mode connection
Sep 22, 2026
66c82d5
refactor(apps): clarify SignalR connection factory
Sep 23, 2026
55717e0
fix(apps): dispose late Socket Mode connection
Sep 23, 2026
8b43826
Update OnActivity to await readiness
teddyam Sep 23, 2026
99b4454
fix(apps): identify planned Socket Mode closure
Sep 23, 2026
fc99cb5
fix(apps): dispose failed Socket Mode startup
Sep 23, 2026
d9f5233
fix(apps): detach SignalR close handlers
Sep 23, 2026
a629457
fix(apps): isolate Socket Mode stop cancellation
Sep 24, 2026
92ac697
feat(apps): add per-geo Socket Mode supervisor
Sep 24, 2026
5e4c28f
feat(apps): add multi-geo Socket Mode transport
Sep 24, 2026
f73599e
feat(apps): add per-geo Socket Mode supervisor
Sep 24, 2026
8953d5e
feat(apps): add multi-geo Socket Mode transport
Sep 24, 2026
7d2e7b1
Merge branch 'teddyam-socket-mode-geo-supervisor' of https://github.c…
Sep 24, 2026
e5a0b3f
marked each geoStatus as stopped in StopAsync
Sep 24, 2026
ab2a6ae
fix(apps): harden Socket Mode shutdown
Sep 24, 2026
f760306
fix(apps): keep Socket Mode stop from rethrowing supervisor faults
Sep 24, 2026
959f98b
Merge branch 'teddyam-socket-mode-geo-supervisor' of https://github.c…
Sep 25, 2026
28ad9e2
test
Sep 25, 2026
aef3092
feat(core): transport-neutral ProcessAsync returning CoreInvokeResponse
Sep 28, 2026
c567a09
refactor: keep invoke responses in Apps; Core overload returns Task
Sep 28, 2026
a05e53e
feat: add Socket Mode options and hosted-service registration
Sep 28, 2026
6462344
feat(apps): run Socket Mode on a headless host; validate options at s…
Sep 28, 2026
25c7128
fix(apps): don't re-log handler failures in Socket Mode dispatch
Sep 28, 2026
ebb87e5
docs(samples): add SocketModeBot echo sample
Sep 28, 2026
22b7c69
fix(apps): let Socket Mode hosts start in Development; use launch pro…
Sep 28, 2026
420f055
fix(apps): default Socket Mode negotiate to the canary ring
Sep 28, 2026
3401b6a
docs(samples): quote the user's message in SocketModeBot replies
Sep 28, 2026
5370703
feat(apps): stop retrying Socket Mode on negotiate 401/403; add integ…
Sep 28, 2026
793e754
feat(apps): select Socket Mode with UseTeamsBotApplication(socket: true)
Sep 29, 2026
821efcf
fix(apps): keep production negotiate default; use canary in the sample
Sep 29, 2026
7e224a8
refactor(apps): make UseSocketMode the only Socket Mode switch
Sep 29, 2026
39861c2
feat(apps): mark Socket Mode experimental
Sep 29, 2026
1f20c53
fix(apps): stop retrying Socket Mode on negotiate 401/403
Oct 1, 2026
ba44d0e
Merge remote-tracking branch 'origin/teddyam-socket-mode-geo-supervis…
Oct 1, 2026
7e8f5fe
removed delayFirstAttempt
Oct 1, 2026
0b2b7ba
Merge remote-tracking branch 'origin/teddyam-socket-mode-geo-supervis…
Oct 1, 2026
c0641e9
Merge origin/main into teddyam-socket-mode-app-integration
Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
appsettings.Local.json
appsettings.Development.json
launchsettings.json
launchSettings.json

# User-specific files
*.rsuser
Expand Down
1 change: 1 addition & 0 deletions Microsoft.Teams.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Project Path="samples/OAuthFlowBot/OAuthFlowBot.csproj" />
<Project Path="samples/ObservabilityBot/ObservabilityBot.csproj" />
<Project Path="samples/PABot/PABot.csproj" Id="ef8f29ef-fe59-4edf-8a50-6e7ab6699a45" />
<Project Path="samples/SocketModeBot/SocketModeBot.csproj" />
<Project Path="samples/StateBot/StateBot.csproj" Id="59061ea6-7fb8-4f94-908d-e32e323cafe4" />
<Project Path="samples/StreamingBot/StreamingBot.csproj" />
<Project Path="samples/SuggestedActionBot/SuggestedActionBot.csproj" />
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ dotnet samples/scenarios/middleware.cs -- --urls "http://localhost:3978"
|--------|-------------|
| [CoreBot](samples/CoreBot/) | Lowest-level sample using `Microsoft.Teams.Core` directly |
| [CustomHosting](samples/CustomHosting/) | Custom `TeamsBotApplication` subclass and hosting |
| [SocketModeBot](samples/SocketModeBot/) | Echo bot that receives activities over Socket Mode, with no web server |

### Messaging and lifecycle

Expand Down
25 changes: 25 additions & 0 deletions samples/SocketModeBot/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Extensions.Hosting;
using Microsoft.Teams.Apps;

// Socket Mode receives activities over an outbound WebSocket, so the bot runs on a
// generic host with no web server and no public messaging endpoint.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTeamsBotApplication(options => options.UseSocketMode(socket =>
{
// Socket Mode is only available on the canary ring for now.
socket.NegotiateBaseUrl = new Uri("https://canary.botapi.skype.com");
}));
IHost host = builder.Build();

TeamsBotApplication teamsApp = host.UseTeamsBotApplication();

teamsApp.OnMessage(async (context, cancellationToken) =>
{
// ReplyAsync quotes the user's message above the reply.
await context.ReplyAsync($"You said: {context.Activity.Text}", cancellationToken);
});

host.Run();
15 changes: 15 additions & 0 deletions samples/SocketModeBot/Properties/launchSettings.TEMPLATE.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"profiles": {
"SocketModeBot": {
"commandName": "Project",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development",
Comment thread
teddyam marked this conversation as resolved.
"AzureAd__TenantId": "",
"AzureAd__ClientId": "",
"AzureAd__ClientCredentials__0__SourceType": "ClientSecret",
"AzureAd__ClientCredentials__0__ClientSecret": "",
"AzureAd__Instance": "https://login.microsoftonline.com/"
}
}
}
}
30 changes: 30 additions & 0 deletions samples/SocketModeBot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SocketModeBot

This sample is an echo bot that receives activities over Socket Mode instead of an HTTP messaging endpoint. The bot opens outbound WebSocket connections to the Teams service, so it needs no public URL or tunnel.

## Prerequisites

- Bot registered in the public cloud and installed in Teams, with Socket Mode enabled for the bot.
- Bot credentials: copy `Properties/launchSettings.TEMPLATE.json` to `Properties/launchSettings.json` (git-ignored) and fill in `AzureAd__TenantId`, `AzureAd__ClientId`, and `AzureAd__ClientCredentials__0__ClientSecret`.

## What it shows

- `UseSocketMode(...)` on `AddTeamsBotApplication` to receive activities over Socket Mode, with `NegotiateBaseUrl` set to the canary ring, the only ring where Socket Mode is available today.
- `<NoWarn>$(NoWarn);ExperimentalTeamsSocketMode</NoWarn>` in the project file, because Socket Mode is experimental.
- A generic host (`Host.CreateApplicationBuilder`) with no web server, and `UseTeamsBotApplication()` to get the app.

## Commands / Flows

| Flow | Behavior |
|---|---|
| send any message | Bot quotes your message and replies `You said: <your text>` |

## Running the Sample

~~~bash
dotnet run --project samples/SocketModeBot/SocketModeBot.csproj
~~~

`dotnet run` and IDEs apply the launch profile, which sets `DOTNET_ENVIRONMENT=Development` and the credentials.

The bot is ready when the log shows `Socket Mode ready across 3 geo(s).` (amer, emea, apac). Startup fails if any geo cannot connect within the startup timeout.
16 changes: 16 additions & 0 deletions samples/SocketModeBot/SocketModeBot.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<NoWarn>$(NoWarn);ExperimentalTeamsSocketMode</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Teams.Apps\Microsoft.Teams.Apps.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions samples/SocketModeBot/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.Teams": "Information"
}
}
}
54 changes: 54 additions & 0 deletions src/Microsoft.Teams.Apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A high-level framework for building Microsoft Teams bots in .NET. Built on top o
- **Targeted Messages** &mdash; Send messages visible only to a selected participant in supported conversations
- **Observability** &mdash; OpenTelemetry spans, metrics, and Agent 365 baggage propagation
- **Fluent Configuration** &mdash; Chainable handler registration and options-based service configuration
- **Socket Mode** &mdash; Receive activities over an outbound WebSocket during development, with no public endpoint or tunnel

## Installation

Expand Down Expand Up @@ -123,6 +124,59 @@ builder.Services.AddTeamsBotApplication(options =>
`IDistributedCache` implementation, such as Redis, for state that must survive
process restarts or be shared across instances.

## Socket Mode

Socket Mode receives activities over outbound WebSocket connections that the bot opens to the Teams service,
instead of an HTTP messaging endpoint, so there is no public URL or dev tunnel to expose. Only inbound delivery
changes: handlers and outbound sends work the same way.

WebSocket is only recommended for use when developing agents. Socket Mode bots should not be submitted to
Marketplace for publishing.

Socket Mode is experimental, and its API may change. `UseSocketMode` and `SocketModeOptions` report the
`ExperimentalTeamsSocketMode` diagnostic, which fails the build until you suppress it, for example with
`<NoWarn>$(NoWarn);ExperimentalTeamsSocketMode</NoWarn>` in the project file.

Socket Mode runs on a generic host with no web server:

```csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Teams.Apps;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTeamsBotApplication(options => options.UseSocketMode());

var host = builder.Build();
var teams = host.UseTeamsBotApplication();

teams.OnMessage(async (context, ct) =>
{
await context.ReplyAsync($"You said: {context.Activity.Text}", ct);
});

host.Run();
```

- **One connection per geo** &mdash; The bot connects to `amer`, `emea`, and `apac` by default. Startup waits until
every geo is ready and fails if any geo cannot connect within `StartupTimeout`. Dropped connections reconnect
automatically, and connection tokens are rotated before they expire.
- **No web server** &mdash; `UseSocketMode` is the only switch; `UseTeamsBotApplication()` is the same call for both
transports. With Socket Mode enabled it throws on a `WebApplication`, and a host that includes a web server fails
to start. Tabs, OAuth callbacks, health endpoints, and other HTTP routes are unavailable.
- **Public cloud only** &mdash; Registration fails for bots configured for another cloud.
- **Classic bot identity only** &mdash; The connection is negotiated with the bot's app ID and credentials. Agentic
identities are not supported.
- **Canary endpoint** &mdash; Socket Mode is currently available only on the canary ring. Set `NegotiateBaseUrl` to
`https://canary.botapi.skype.com` until it is enabled on the default production endpoint.
- **Rejected credentials are not retried** &mdash; If negotiation returns HTTP 401 or 403, startup fails immediately.
After startup, the affected geo stops reconnecting until the app restarts.
- **Idempotent handlers** &mdash; The service can redeliver an activity, for example after a reconnect, so handlers
should tolerate running more than once for the same activity.

`UseSocketMode(configure)` accepts a `SocketModeOptions` callback to change the geos, negotiate URL, and connection
timeouts. `UseSocketMode(bool)` turns Socket Mode on with the defaults, or off with `false`, which clears any earlier
configuration so the bot uses HTTP.

## Main Types

| Type | Description |
Expand Down
41 changes: 41 additions & 0 deletions src/Microsoft.Teams.Apps/SocketMode/SocketModeHostedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace Microsoft.Teams.Apps.SocketMode;

/// <summary>
/// Runs the Socket Mode transport for the lifetime of the host.
/// </summary>
/// <remarks>
/// Startup waits until every geo is ready, so the host does not report started while the bot cannot receive
/// activities, and a startup failure surfaces from <c>host.Run()</c> instead of being swallowed in the background.
/// </remarks>
/// <param name="services">The host's service provider, used to create the transport and detect a web server.</param>
internal sealed class SocketModeHostedService(IServiceProvider services) : IHostedService
{
private readonly IServiceProvider _services = services ?? throw new ArgumentNullException(nameof(services));
private SocketModeTransport? _transport;

/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken)
{
// Socket Mode replaces inbound HTTP rather than running beside it, so a host that would also start a web
// server (for example a WebApplication) is rejected before any socket opens.
if (_services.GetService<IServiceProviderIsService>()?.IsService(typeof(IServer)) == true)
{
throw new InvalidOperationException(TeamsBotApplicationHostingExtensions.SocketWithWebServerMessage);
}

// Created here rather than injected, so invalid options fail when the host starts, as in the other SDKs.
_transport = _services.GetRequiredService<SocketModeTransport>();
return _transport.StartAsync(cancellationToken);
}

/// <inheritdoc />
public Task StopAsync(CancellationToken cancellationToken)
=> _transport is null ? Task.CompletedTask : _transport.StopAsync().WaitAsync(cancellationToken);
}
72 changes: 72 additions & 0 deletions src/Microsoft.Teams.Apps/SocketMode/SocketModeOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Teams.Apps.SocketMode;

/// <summary>
/// Configures Socket Mode, where the bot receives activities over outbound WebSocket connections instead of an
/// inbound HTTP endpoint.
/// </summary>
/// <remarks>
/// Socket Mode opens one connection per geo and waits until every geo is ready before the host finishes starting.
/// It is supported only in the public cloud, and runs on a host without a web server
/// (<c>Host.CreateApplicationBuilder</c>). Settings are validated when the host starts.
/// Socket Mode is experimental: the API is in preview and may change, and it is recommended only for developing agents.
/// </remarks>
[Experimental("ExperimentalTeamsSocketMode")]
public sealed class SocketModeOptions
{
/// <summary>
/// Gets or sets the base URL used to negotiate each geo connection. Must use HTTPS unless it targets loopback.
/// Defaults to <c>https://botapi.skype.com</c>.
/// </summary>
public Uri NegotiateBaseUrl { get; set; } = new(SocketModeProtocol.DefaultNegotiateBaseUrl);

/// <summary>
/// Gets or sets the geos to connect, one connection each. An empty string connects to the base URL without a geo
/// segment. Defaults to <c>amer</c>, <c>emea</c>, and <c>apac</c>.
/// </summary>
public IReadOnlyList<string> Geos { get; set; } = SocketModeProtocol.DefaultGeos;

/// <summary>
/// Gets or sets how long each geo has to establish its initial connection. Defaults to 30 seconds.
/// </summary>
public TimeSpan StartupTimeout { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Gets or sets an explicit reconnect delay schedule; the last delay repeats. When <c>null</c> or empty, capped
/// exponential backoff with jitter is used.
/// </summary>
public IReadOnlyList<TimeSpan>? ReconnectDelays { get; set; }

/// <summary>
/// Gets or sets how long a new connection waits for the service to report it ready. Defaults to 30 seconds.
/// </summary>
public TimeSpan ReadinessTimeout { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Gets or sets the interval between keep-alive messages sent to the service. Defaults to 15 seconds.
/// </summary>
public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(15);

/// <summary>
/// Gets or sets how long without a message from the service before the connection is considered lost.
/// Defaults to 30 seconds.
/// </summary>
public TimeSpan ServerTimeout { get; set; } = TimeSpan.FromSeconds(30);

/// <summary>
/// Snapshots the transport-level settings, so later changes to these options do not affect a running transport.
/// Invalid values are passed through for the transport to reject when it starts.
/// </summary>
/// <returns>The transport options.</returns>
internal SocketModeTransportOptions ToTransportOptions() => new()
{
NegotiateBaseUri = NegotiateBaseUrl,
Geos = Geos is null ? null! : [.. Geos],
StartupTimeout = StartupTimeout,
ReconnectDelays = ReconnectDelays is { Count: > 0 } delays ? [.. delays] : null,
};
}
Loading
Loading