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 server was updated. Reloading the page to pick up the latest version.