Connecting Microsoft Teams

The Teams bot is a bidirectional channel: a person messages the bot in Teams, a Memex agent answers in the same chat. It reuses the same pipeline as the email channelinbound message → find-or-create a thread → agent → reply — so a Teams chat is just another front end onto an agent thread. It ships inert and turns on only when an admin provisions an Azure Bot and sets Teams:Enabled.

How it works

Teams user message
      │
      ▼
POST /api/teams/messages  ──(Bot Framework JWT)──►  ITeamsClient.ValidateInboundAsync   ── reject forged ──► 401
      │ (validated)
      ▼
TeamsInboundProcessor
      │  • map Teams user → Memex user by AAD object id (User.objectId)
      │  • find-or-create ONE thread per Teams conversationId (TeamsConversation link)
      │      new  → hub.StartThread  (first message seeded into PendingUserMessages)
      │      exists → hub.SubmitMessage (appended to pending)
      ▼
agent runs as that user  ──►  TeamsReplySender
                                  • ThreadFlow.ObserveResponses(threadPath)  ← shared read-side primitive
                                  • on each completed assistant reply → ITeamsClient.SendMessageAsync
                                  • send-once via TeamsConversation.LastDeliveredMessageId
                                  ▼
                           reply appears in the Teams chat

Key points:

Components

Piece Role
TeamsBotController (/api/teams/messages) Validates the inbound JWT, parses the message activity, routes it
ITeamsClient / TeamsClient Inbound JWT validation + outbound reply via an app-only connector token; test seam (fake on CI)
TeamsInboundProcessor Map user → find-or-create thread per conversation → StartThread/SubmitMessage
TeamsConversation (NodeType) Links a thread ↔ Teams conversation (serviceUrl, conversationId, LastDeliveredMessageId)
TeamsReplySender (hosted) ObserveResponsesSendMessageAsync; send-once; registered only when enabled

Configuration

Key Meaning
Teams:Enabled master switch — false = inert (endpoint NotFound, no reply sender)
Teams:AppId the Azure Bot / app registration id (MicrosoftAppId)
Teams:AppPassword the bot app client secret (MicrosoftAppPassword) — keep in Key Vault
Teams:TenantId Entra tenant id for a single-tenant bot (optional; multi-tenant when empty)

Configured on the Deployment record's advanced rung (portal configuration keys), and the secret handed over separately — never on the record:

builder.AddMemex("memex")
    // …
    .WithPortalConfig("Teams__Enabled", "true")
    .WithPortalConfig("Teams__AppId", "<bot app id>")
    .WithPortalConfig("Teams__TenantId", "<tenant>")
    .WithSecret("Teams__AppPassword", builder.AddParameter("teams-app-password", secret: true));

Deploy parameters (Memex.Deploy.AppHost): teams-enabled, teams-app-id, teams-app-password, teams-tenant-id → emitted as Teams__*. On AKS the secret comes from Key Vault through the record's keyVaultSecrets map (teams-apppassword → Teams__AppPassword), like the email client secret (ConfiguringAnInstanceFromAspire).

Azure setup (one-time, by an admin)

  1. Create an Azure Bot resource (Azure Portal → Azure Bot). Use a multi-tenant or single-tenant Microsoft App; note the App ID and create a client secret.
  2. Messaging endpoint: set it to {BaseUrl}/api/teams/messages (e.g. https://portal.example.com/api/teams/messages).
  3. Enable the Teams channel on the Bot resource.
  4. Teams app manifest: create a Teams app whose bots[0].botId is the App ID, with the personal (and optionally team/groupchat) scopes, and sideload/publish it to your tenant so users can chat with it.
  5. Configure Memex: set Teams:Enabled=true, Teams:AppId, Teams:TenantId, and supply Teams:AppPassword via Key Vault. Redeploy; the endpoint goes live and validates inbound activities.
  6. User mapping: ensure Memex User nodes carry the user's AAD object id (content.objectId) so the bot can run as the right person. Unmapped users get a "no account" reply.

Until step 5, the bot is completely inert — the code ships with Teams:Enabled=false, the endpoint returns NotFound, and no reply sender runs.

What the bot does NOT do

The bot reads nothing on a user's behalf: it cannot list a user's teams, enumerate channels, read channel history or chats, or post as a user. Its outbound credential is an app-only Bot Framework connector token, valid only for conversations the bot is part of. Reading or sending a user's Teams content is a different integration — delegated Graph scopes on the Executive Assistant's consent link plus tools on its plugin — and a team the user reaches as a guest (another company's tenant) is not reachable on a home-tenant token at all. An agent that answers "I have no Teams access" is describing this accurately; the engineering notes live with the repository's /teams skill.

Notifications over Teams

The notification system already models a Teams channel kind; once the bot is connected, a recipient's notification rules can escalate notifications into Teams the same way they do email.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.