NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · Microsoft 365 maintainer map
Developer reference

msteams — maintainer documentation

Read out of the code across several sessions; the pinned commit these notes once carried no longer exists in history. Personal-chat sign-in, the Microsoft 365 delegated tools and their m365AccessMode setting, and TeamsSendFile were added after the original reading. Line references below are pointers into the named file, not exact anchors — if one no longer resolves, search the file instead of trusting the number. For the complete, current config and tool reference, see the published Microsoft Teams & Microsoft 365 page.

Four files, no overlap:

  • README.md (this file) — what the plugin is, what it needs, what it exposes, which file owns what.
  • architecture.md — how a message actually travels through the code, and what is persisted.
  • operations.md — Azure/Teams setup, config semantics, failure modes, redeploy behaviour.
  • permissions.md — every Microsoft Graph and Teams permission the plugin can need, which of the four consent surfaces it belongs to, and the checklist for standing a new tenant up the same way.

What it is

A Microsoft Teams platform adapter built on the Azure Bot Framework. Inbound Bot Framework activities arrive on the daemon webhook /hooks/msteams/messages; outbound replies, typing indicators, card edits and images go out through the Bot Connector REST API. It registers itself as an Elowen PlatformAdapter (plugins/msteams/index.mjs:223), so a Teams conversation becomes a brain channel session the same way a Discord channel or a Telegram chat does.

On top of plain chat it provides: a live tool trace edited in place, AskUserQuestion rendered as Adaptive Cards, slash commands with card pickers, per-chat model/reasoning/display overrides, image round-trips, proactive (host-initiated) pushes, and a downloadable Teams app package.

Since commit 701144dd it can also write to a person first rather than only reply: it builds a people directory out of traffic it already sees, and TeamsMessagePerson opens the 1:1 chat itself. See "Proactive messaging" below and the detail in architecture.md.

What it needs to run

Credential gate

The plugin registers the app-package API route unconditionally, then requires all three of appId, appPassword and tenantId before it does anything else (plugins/msteams/index.mjs:187-193). With any of them missing it logs enabled but appId/appPassword/tenantId are not all configured — not connecting and returns: no platform, no webhook, no tools. Pinned by tests/plugins/msteamsPlugin.test.ts:130 (no platform / no http routes without credentials) and :136 (both appear once configured).

Azure / Teams prerequisites

Summarised here, with the click-path in operations.md:

  1. An Entra app registration (single tenant) — its Application (client) ID is appId, the directory ID is tenantId, and a client secret is appPassword.
  2. An Azure Bot resource bound to that app id, with the messaging endpoint set to https://<your-domain>/hooks/msteams/messages and the Microsoft Teams channel enabled.
  3. The Teams app package (downloadable from this plugin — see below) uploaded to the org's Teams app catalog so users can install the bot.

Nothing beyond the bot credentials is required for normal chat: the member and roster lookups the plugin uses for UPN/e-mail resolution are Bot Connector calls, not Graph calls (plugins/msteams/lib/connector.mjs:73 for one member, :78 for the full roster). Microsoft Graph is a separate, optional layer — see graphLookup below.

Config fields

Declared in the configSchema of plugins/msteams/elowen-plugin.json; Czech and Slovak translations of every label and hint live in plugins/msteams/i18n/cs.json and sk.json.

KeyTypeDefaultWhat it does
appIdstring, required—Entra Application (client) ID; also the bot id. Used as the JWT audience (lib/auth.mjs:38) and to build the bot member id 28:<appId> (lib/adapter.mjs:198, :652).
appPasswordsecret, required—Client secret for the client-credentials token (lib/token.mjs:36).
tenantIdstring, required—Tenant-scoped OAuth token endpoint (lib/token.mjs:25) and the tenant stamped on a newly opened 1:1 chat (lib/adapter.mjs:655-656).
notifyConversationIdstringemptyDefault target for proactive pushes. A conversation id, or a person (e-mail / Entra object ID / display name) whose 1:1 chat the bot opens (lib/adapter.mjs:764-786). Empty = pushes are dropped with a warning.
graphLookupbooleanfalseEnables the optional Microsoft Graph layer. Off means the directory adapter never constructs a GraphClient (lib/adapter.mjs:102); the channel-history reader builds its own client only when channelMessagesRsc is on (lib/transcript.mjs:134-135).
graphCatalogAppIdstringemptyThe app's id in the org Teams catalog, needed to install the app for a user (lib/graph.mjs:80-93). Only visible when graphLookup is on.
respondWithoutMentionbooleantrueWhen false, a non-personal conversation is answered only if the bot is @mentioned (lib/adapter.mjs:391).
toolActivityenum off/status/livestatusWhat the progress message shows while the agent works.
answerModeenum final/livefinalOne reply at the end, or the answer streamed into a message.
toolOutputenum hidden/summary/tailsummaryHow much of a finished tool's result the progress message keeps.
toolMessageModeenum single/per_toolsingleOne edited progress message, or one message per tool call.
runtimeFooterbooleantrueAppends the model · context % line (lib/format.mjs:27).
showReasoningbooleanfalseStreams model reasoning into the progress message. Also forces a live stream to exist (elowen-plugin-shared/display, observesLiveEvents).
languageenum en/cs/skenLanguage of the bot's own service texts (lib/messages.mjs). Not the agent's answer language.
historyLimitnumber 0–1000How many remembered messages seed a brand-new conversation. 0 means nothing is written to disk (lib/adapter.mjs:236-240).
visionModelmodelemptyModel used for turns carrying image attachments (lib/adapter.mjs:430-432).
maxImageBytesnumber5242880Largest inbound image the bot downloads, clamped to 1 MiB–20 MiB (lib/adapter.mjs:470).
maxImagesnumber 1–104Inbound image attachments sent to the vision model.
maxUploadImagesnumber 1–104Images shared with ShareImage attached to one outgoing reply.
rolePoliciesrolePolicies—Sender → allowed projects + role prompt + tool allowlist. First match wins; unmapped senders are ignored (lib/adapter.mjs:338-343).

The table above covers the fields present when this page was last touched. Personal-chat sign-in (accountLinking, oauthConnectionName), Microsoft sign-in on the web login page (ssoEnabled and the sso* provisioning keys), Microsoft 365 access (m365AccessMode, m365MaxTransferBytes), Graph channel history (channelMessagesRsc) and the Teams app branding keys (agentName, productName, appIconPath) were added later; their current defaults and purpose are documented in microsoft-365-plugin.md rather than duplicated here.

Two further keys are read by the code but are not in configSchema, so the settings UI never offers them. Both are E2E seams used by tests/e2e/msteams/:

  • openIdMetadataUrl (lib/auth.mjs:16), the Bot Framework OpenID metadata URL.
  • oauthTokenUrl (lib/connector.mjs:58, token seam in lib/token.mjs:25), the token endpoint override.

agentName is in configSchema; see operations.md for how empty values fall back.

Tools it exposes

Registered in plugins/msteams/lib/tools.mjs and declared in elowen-plugin.json. Tool availability is decided by the linked account's plugin grants and per-user tool deny-list in the users modal.

ToolPlan-safeWhat it does
TeamsSendnoPost into a conversation by id.
TeamsMessagePersonnoMessage a person by e-mail / Entra object id / 29:… id / display name; opens the 1:1 chat if needed.
TeamsFindPersonyesRead-only directory lookup; sends nothing. Lists at most 25 matches.
TeamsChatInfoyesConversation type, tenant, member count.
TeamsMembersyesFresh roster read, at most 50 listed; also feeds the people directory.
TeamsMemberInfoyesOne member's name / Entra id / UPN.
TeamsListConversationsyesConversations on the current service host, paged.
TeamsSendFilenoOffer a file in a 1:1 chat; uploaded to the recipient's OneDrive only after they accept the card.
TeamsApinoRaw Bot Connector REST: any method + path. Output truncated at 4000 characters.

That is 9 Teams* tools; the plugin also exposes 8 Microsoft* tools for the signed-in person's delegated Microsoft 365 access (directory, SharePoint, files, Outlook, tasks, OneNote, Excel, Teams as that person) — see microsoft-365-plugin.md for the full list and their access mode. planSafe covers exactly the five read-only Teams* tools listed above the table split. The tools that write are deliberately absent, so plan mode withholds them. The manifest also makes every Teams tool's output visible in the transcript.

The permission behavior is pinned by tests in tests/plugins/msteamsPlugin.test.ts: a project-scoped, non-owner session can reach curated and raw connector tools when the registry exposes them, while input validation such as requiring a named recipient still applies.

Proactive messaging (since 701144dd)

The Bot Connector cannot be addressed by e-mail — it only takes a conversationId. So the plugin builds a people directory (lib/directory.mjs) out of what it already sees: every inbound activity and every conversation roster carries a 29: account id, an Entra aadObjectId, a display name and (from the roster) a UPN/e-mail. No extra Microsoft permission is involved. Once a person is known, their 1:1 chat is opened once and remembered.

graphLookup is a second, optional layer (lib/graph.mjs), off by default. It exists only for people the bot has never met: it resolves an e-mail against the tenant directory and installs the Teams app for that user so a chat can be opened. It requires the application permissions User.ReadBasic.All and TeamsAppInstallation.ReadWriteSelfForUser.All with tenant admin consent, plus graphCatalogAppId for the install. What an admin must do, and what happens when they have not done it, is in operations.md.

Test tests/plugins/msteamsPlugin.test.ts:720 asserts that with the switch off nothing leaves the process on an unknown e-mail — the stubbed global fetch records zero calls.

File map

FileWhat it owns
index.mjsRegistration: the app-package API route, the credential gate, the StateStore, the adapter, the /hooks mount and the tools.
elowen-plugin.jsonManifest: description, provides, icons, showOutput, planSafe, the whole configSchema.
icon.svgThe plugin's icon in the Elowen UI.
i18n/cs.json, i18n/sk.jsonCzech/Slovak translations of the manifest description and every config label/hint/option. Enforced by scripts/check-languages.mjs.
lib/adapter.mjsWebhook dispatch, admission, brain turn, transport and per-turn targeted audiences; wires the protocol owners with explicit dependencies.
lib/transcript.mjsOpt-in rolling history, bounded Graph thread projection and recorded-history fallback.
lib/files.mjsPersonal-chat file offers, the shared 20 MiB tool/upload cap, pending bytes, consent, expiration and shared-file delivery.
lib/proactive.mjsPerson resolution, roster sweep, personal routes, recipient relay and notifications.
lib/asks.mjsQuestion owner checks, shared ordered answer projection and core-owned ask_resolved lifecycle; no local expiry clock.
lib/commands.mjsCatalog classification, local/control pickers, shared display axes and live model/reasoning validation.
lib/accountLinking.mjsOAuth admission, activity validation, one Entra-before-Teams identity resolver and fresh durable-account delegated sessions.
lib/connector.mjsBot Connector REST client: reply/send/update/delete/typing/members/conversations/createConversation/download, with a single 429 retry.
lib/token.mjsEntra client-credentials tokens, one cached bearer per scope, refreshed ~60 s before expiry.
lib/auth.mjsInbound JWT verification against Microsoft's JWKS, pinned to our appId and the connector issuer, with a serviceUrl cross-check.
lib/directory.mjsThe people directory: remember/list/evict/resolve, persisted under one reserved StateStore key.
lib/graph.mjsThe optional Microsoft Graph layer: e-mail → tenant user, and app installation for that user.
lib/tools.mjsThe nine Teams* tools and their admin/owner gates.
lib/messages.mjsThe bot's own service texts in en/cs/sk.
lib/cards.mjsAdaptive Card builders: the ask card, the paged picker card, the settled one-liner.
lib/stream.mjsTeams binding for the shared live-message engine: transport closures, markdown style, image strategy.
lib/format.mjsTeams sizing and the runtime-footer markup (CHUNK = 20000).
lib/ids.mjsIdentity helpers: matchesId, senderIds, senderIsAdmin, displayNameOf.
lib/appPackage.mjsThe sideloadable Teams app package: hand-rolled stored ZIP + solid-colour PNG icons + the Teams manifest.
lib/state.mjsOne line: re-exports the shared StateStore.
lib/config.mjsConfig normalization and defaults for the adapter and tools.
lib/microsoftTools.mjsRegisters the eight Microsoft* tools and dispatches each call to its product owner.
lib/m365/*.mjsOne owner per Microsoft 365 product (directory, SharePoint, files, Outlook, tasks, OneNote, Excel, Teams), plus schema.mjs and shared.mjs.
lib/identityControl.mjsThe microsoftIdentity control other plugins can use.
lib/delegatedGraph.mjsDelegated Graph client helpers, including HTML-to-text for thread bodies.
prompt/10-surface.mdPrompt fragment describing the Teams surface to the model.
web-src/Browser sources for the Microsoft Teams workspace (TeamsWorkspace.tsx, PolicyEditor.tsx, policyModel.ts).

index.mjs:17-21 also re-exports matchesId, senderIds, senderIsAdmin, displayNameOf, splitContent, footerLine, CHUNK, makeTokenVerifier and ConnectorClient from the plugin entry. That is the surface the unit tests import (tests/plugins/msteamsPlugin.test.ts:148, :382) — treat it as load-bearing rather than incidental.

Verify it works

In the repo, from the registry repository root:

npx vitest run tests/plugins/msteamsPlugin.test.ts   # 52 tests, the behavioural contract
npx vitest run tests/plugins/manifest.test.ts        # every bundled manifest parses
node scripts/check-languages.mjs                     # i18n covers every manifest string
npm run test:e2e:msteams                             # builds, then a real daemon + a fake Bot Framework

The E2E scenario (tests/e2e/msteams/run.mjs) drives a signed activity into the real webhook, checks the async threaded reply and the in-place live-trace edits, and asserts that a garbage JWT bounces with 401 without ever reaching the brain.

On a running instance:

  1. The daemon log should carry msteams platform registered (webhook /hooks/msteams/messages + Teams and delegated Microsoft 365 tools) (index.mjs:232) and then msteams connected (app <appId>) (lib/adapter.mjs:162). A msteams credential check failed: … instead means the token call failed but the adapter is still up.
  2. GET /plugins/msteams/app-package (admin) returns the ZIP; it returns 503 while the plugin is enabled but not configured (index.mjs:105) — deliberately 503 rather than 404. Any non-empty sub-path under that mount is a 404 (index.mjs:104).
  3. Send the bot a direct message from a mapped sender. You should see a typing indicator, then a reply threaded under your message.
  4. /help in that chat answers with the command list (lib/commands.mjs:98); /status reports the live model and context.