diff --git a/.gitignore b/.gitignore index cf87964f..b2cef02b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ docker-compose.override.yml # Beever Atlas specific bot/node_modules/ bot/dist/ -bot/teams-app/beever-atlas-bot.zip +bot/teams-app/beever-atlas-teams.zip web/node_modules/ web/dist/ diff --git a/bot/README.md b/bot/README.md index 0a197f98..deb29f94 100644 --- a/bot/README.md +++ b/bot/README.md @@ -40,7 +40,7 @@ See [`.env.example`](../.env.example) for the canonical list and descriptions. K |---|---| | `SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET` | Slack Events API adapter | | `DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, `DISCORD_APPLICATION_ID` | Discord interactions adapter | -| `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD` | Microsoft Teams / Azure Bot adapter | +| `TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_APP_TENANT_ID` | Microsoft Teams / Azure Bot adapter. The bot detects SingleTenant vs MultiTenant from the presence of `TEAMS_APP_TENANT_ID` (see `registerTeamsFromEnvIfPresent` in `src/index.ts`) — SingleTenant is the supported path; MultiTenant requires extra MSAL configuration. Tenant id is also required for any call into `fetchMessages`. | | `TELEGRAM_BOT_TOKEN` | Telegram Bot API adapter | | `MATTERMOST_BASE_URL`, `MATTERMOST_BOT_TOKEN` | Mattermost outgoing-webhook adapter | diff --git a/bot/src/bridge.ts b/bot/src/bridge.ts index 6de7cc4a..9323be9a 100644 --- a/bot/src/bridge.ts +++ b/bot/src/bridge.ts @@ -2913,8 +2913,42 @@ async function handleValidateAdapter( appTenantId: credentials.appTenantId, appType: credentials.appType || "MultiTenant", }); - // Teams adapter creation validates credentials format; no simple ping API - jsonResponse(res, 200, { valid: true, message: "Adapter created successfully. Verify messaging endpoint is configured in Azure." }); + // Real validation: actually mint a Graph token via MSAL. The previous + // construct-only check let users typo the App ID (e.g. paste a display + // name like "Teams" instead of the GUID) and only catch it later when + // channel enumeration silently returned []. We exercise the token mint + // by hitting a minimal Graph endpoint: + // • 2xx → credentials valid + // • AADSTS / unauthorized_client → credentials wrong (400) + // • other (403/404/network) → creds look valid; soft accept + try { + const graph = (tempAdapter as any).app?.graph; + if (!graph) { + jsonResponse(res, 200, { + valid: true, + message: "Adapter constructed. Token mint could not be exercised; verify the messaging endpoint and Graph admin consent in Azure.", + }); + return; + } + await graph.http.get("/applications?$top=1"); + jsonResponse(res, 200, { valid: true, message: "Credentials valid (Graph token minted successfully)." }); + } catch (err) { + const msg = safeErrorMessage(err); + const lower = msg.toLowerCase(); + const isAuthFailure = + /aadsts\d{4,6}/i.test(msg) || + lower.includes("unauthorized_client") || + lower.includes("invalid_client") || + lower.includes("was not found in the directory"); + if (isAuthFailure) { + jsonResponse(res, 200, { valid: false, error: `Microsoft rejected the credentials: ${msg}` }); + } else { + jsonResponse(res, 200, { + valid: true, + message: `Credentials look valid but a Graph probe failed: ${msg}. Check Channel.ReadBasic.All admin consent if channel listing fails later.`, + }); + } + } } else if (platform === "mattermost") { const baseUrl = (credentials.baseUrl || credentials.server_url || "").replace(/\/+$/, ""); const botToken = credentials.botToken || credentials.bot_token || ""; diff --git a/bot/teams-app/build-package.mjs b/bot/teams-app/build-package.mjs index bb0a07d3..380e8e23 100644 --- a/bot/teams-app/build-package.mjs +++ b/bot/teams-app/build-package.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// Builds beever-atlas-bot.zip from manifest.json + generated placeholder icons. +// Builds beever-atlas-teams.zip from manifest.json + generated placeholder icons. // Replace outline.png / color.png with real artwork before production release. import { writeFileSync } from "node:fs"; @@ -67,10 +67,10 @@ writeFileSync(join(here, "outline.png"), solidPng(32, 32, 0xff, 0xff, 0xff)); console.log("✓ generated color.png (192x192) and outline.png (32x32)"); execSync( - `cd "${here}" && rm -f beever-atlas-bot.zip && zip -j beever-atlas-bot.zip manifest.json color.png outline.png`, + `cd "${here}" && rm -f beever-atlas-teams.zip && zip -j beever-atlas-teams.zip manifest.json color.png outline.png`, { stdio: "inherit" }, ); -console.log(`\n✓ built ${join(here, "beever-atlas-bot.zip")}`); +console.log(`\n✓ built ${join(here, "beever-atlas-teams.zip")}`); console.log( "\nSideload: Teams → Apps → Manage your apps → Upload a custom app → pick this zip", ); diff --git a/bot/teams-app/manifest.json b/bot/teams-app/manifest.json index 70d3a48d..537caebc 100644 --- a/bot/teams-app/manifest.json +++ b/bot/teams-app/manifest.json @@ -1,8 +1,8 @@ { - "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.16/MicrosoftTeams.schema.json", - "manifestVersion": "1.16", - "version": "1.0.1", - "id": "eefc03cb-3132-46a1-ac50-e200792849b6", + "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.25/MicrosoftTeams.schema.json", + "manifestVersion": "1.25", + "version": "1.0.3", + "id": "fb24e83f-52e6-40a4-bafb-764160e4ca7a", "packageName": "ai.beever.atlas", "developer": { "name": "Beever AI", @@ -23,12 +23,43 @@ "color": "color.png" }, "accentColor": "#5B4ECC", + "staticTabs": [ + { + "entityId": "conversations", + "scopes": ["personal"] + }, + { + "entityId": "about", + "scopes": ["personal"] + } + ], "bots": [ { - "botId": "eefc03cb-3132-46a1-ac50-e200792849b6", - "scopes": ["personal", "team", "groupchat"], + "botId": "fb24e83f-52e6-40a4-bafb-764160e4ca7a", + "scopes": ["personal", "team", "groupChat"], "supportsFiles": false, + "supportsCalling": false, + "supportsVideo": false, "isNotificationOnly": false } - ] + ], + "validDomains": ["*.botframework.com"], + "webApplicationInfo": { + "id": "fb24e83f-52e6-40a4-bafb-764160e4ca7a" + }, + "authorization": { + "permissions": { + "resourceSpecific": [ + { + "name": "ChannelMessage.Read.Group", + "type": "Application" + }, + { + "name": "ChatMessage.Read.Chat", + "type": "Application" + } + ] + } + }, + "supportsChannelFeatures": "tier1" } diff --git a/docs/content/getting-started/teams-setup.mdx b/docs/content/getting-started/teams-setup.mdx index 28616452..300f8b65 100644 --- a/docs/content/getting-started/teams-setup.mdx +++ b/docs/content/getting-started/teams-setup.mdx @@ -7,6 +7,10 @@ description: Connect Beever Atlas to Microsoft Teams Connect Beever Atlas to Microsoft Teams to automatically ingest conversations and build a searchable knowledge base. + +**Preferred path: the UI wizard.** Beever Atlas Settings → Integrations → Connect Microsoft Teams walks through every step below with inline guidance, the right App Type default (SingleTenant), and a runnable ngrok / endpoint snippet. Use this doc for context and as a reference for `.env`-based provisioning. + + **Beta Status**: Teams integration is in beta. Features may change as we improve the integration. Requires Azure AD admin access. @@ -57,17 +61,18 @@ In the application overview: 3. Select **Microsoft Graph** 4. Select **Application permissions** (for background sync): -Add the following permissions: +Add at minimum: | Permission | Purpose | |------------|---------| -| `ChannelMessage.Read.All` | Read all channel messages | -| `Chat.Read` | Read chat messages | -| `User.Read.All` | Read user information | -| `Files.Read.All` | Access file attachments | +| `Channel.ReadBasic.All` | **Required.** Channel enumeration via `GET /teams/{id}/channels` so workspaces and channels appear in the sidebar without an @mention. Beever Atlas falls back to a Redis-cached identity scan if this is missing, but loses the workspace whenever the cache is wiped. | +| `User.Read.All` | Optional. Look up author display names + emails when receiving messages. | +| `Chat.Read.All` | Optional. Fetch DM message history. | + +The historical permissions table (`ChannelMessage.Read.All`, `Chat.Read`, `Files.Read.All`) is **superseded** — the live bot uses RSC (Resource Specific Consent) declared in the Teams app manifest at `bot/teams-app/manifest.json` for per-message reads, so those tenant-wide Application permissions are no longer required. -**Application vs Delegated**: We use Application permissions for background sync (no user interaction required). This requires admin consent. +**Application vs Delegated**: We use Application permissions for background sync (no user interaction required). This requires admin consent — **only a Global Administrator can grant consent for Microsoft Graph application permissions**. Application Administrator and Cloud Application Administrator both return `Authorization_RequestDenied`. ### 2.2 Grant Admin Consent @@ -120,12 +125,20 @@ ADAPTER_MOCK=false Add the following to your `.env`: ```bash -# Microsoft Teams / Graph API -TEAMS_TENANT_ID=your_tenant_id_here -TEAMS_CLIENT_ID=your_client_id_here -TEAMS_CLIENT_SECRET=your_client_secret_here +# Microsoft Teams / Azure Bot adapter (env vars used by the bot's +# `registerTeamsFromEnvIfPresent` startup path — see bot/src/index.ts). +# These match the keys the UI wizard writes into the encrypted +# `platform_connections` document, with snake_case here vs camelCase +# inside the Chat SDK adapter. +TEAMS_APP_ID=your_microsoft_app_id # Azure AD Application (client) ID +TEAMS_APP_PASSWORD=your_azure_client_secret # Client secret value (NOT the secret id) +TEAMS_APP_TENANT_ID=your_azure_tenant_id # Directory (tenant) ID — required ``` + +**Historical names removed.** Earlier revisions of this doc referenced `TEAMS_TENANT_ID`, `TEAMS_CLIENT_ID`, `TEAMS_CLIENT_SECRET`. Those variables are **not read by the current bot** — the code uses the `TEAMS_APP_*` names above. If your `.env` still has the old names, rename them or you'll get a "no connections found" startup log even with correct values. + + **Alternative**: Add credentials through the web UI (Settings → Connections) for encrypted storage. diff --git a/web/src/components/settings/ConnectionWizard.tsx b/web/src/components/settings/ConnectionWizard.tsx index e1893f4c..69ff8945 100644 --- a/web/src/components/settings/ConnectionWizard.tsx +++ b/web/src/components/settings/ConnectionWizard.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { X, ArrowLeft, ArrowRight, CheckCircle2, Loader2, AlertCircle, ExternalLink, Zap } from "lucide-react"; import { cn } from "@/lib/utils"; import { ChannelSelector } from "./ChannelSelector"; @@ -47,13 +47,27 @@ const DISCORD_INSTRUCTIONS = [ ]; const TEAMS_INSTRUCTIONS = [ - { text: "Go to", link: "https://portal.azure.com/#create/Microsoft.AzureBot", linkText: "Azure Portal" }, - { text: "Create a new Azure Bot resource — choose SingleTenant or MultiTenant" }, - { text: "Under Configuration, copy the Microsoft App ID" }, - { text: "Click Manage Password → New client secret and copy the value" }, - { text: "Note your Azure AD Tenant ID from the Azure Active Directory overview (required for SingleTenant)" }, - { text: "Under Channels, add the Microsoft Teams channel and save" }, - { text: "Set the messaging endpoint to your bot's webhook URL" }, + { + text: "Create an Azure Bot resource", + link: "https://portal.azure.com/#create/Microsoft.AzureBot", + linkText: "in Azure Portal", + details: ["App type: SingleTenant (recommended) or MultiTenant"], + }, + { + text: "Expose this bridge over HTTPS, then set the Bot's Messaging endpoint to your URL + /api/teams", + details: [ + "Local dev: ngrok http 3001", + "https:///api/teams", + ], + }, + { text: "Copy the Microsoft App ID from Bot → Configuration" }, + { text: "On the linked App Registration → Manage Password, create a client secret and copy the VALUE (shown once)" }, + { text: "Copy the Tenant ID from Azure Active Directory → Overview" }, + { text: "On the Bot resource → Channels, add the Microsoft Teams channel" }, + { + text: "On API permissions, add Microsoft Graph application permission, then Grant admin consent — a Global Admin must do this", + details: ["Channel.ReadBasic.All"], + }, ]; const TELEGRAM_INSTRUCTIONS = [ @@ -70,7 +84,43 @@ const MATTERMOST_INSTRUCTIONS = [ { text: "Add the bot user to any channels where it should read from. The bot will only receive events from channels it is a member of" }, ]; -const CREDENTIAL_FIELDS: Record = { +interface CredentialField { + key: string; + label: string; + placeholder: string; + type?: string; + optional?: boolean; + /** When present, render as a ` onChange(field.key, e.target.value)} - placeholder={field.placeholder} - className="w-full h-9 px-3 rounded-lg border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 font-mono" - autoComplete="off" - spellCheck={false} - /> + {field.enum ? ( + + ) : ( + onChange(field.key, e.target.value)} + placeholder={field.placeholder} + className="w-full h-9 px-3 rounded-lg border border-border bg-background text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 font-mono" + autoComplete="off" + spellCheck={false} + /> + )} + {(() => { + const trimmed = (values[field.key] ?? "").trim(); + const err = trimmed && field.validate ? field.validate(trimmed) : null; + if (err) { + return

{err}

; + } + if (field.hint) { + return

{field.hint}

; + } + return null; + })()} ))} @@ -529,22 +646,56 @@ function StepChannels({ } function StepWebhookMode({ platform }: { platform: Platform }) { - const label = platform === "telegram" ? "Telegram" : "Microsoft Teams"; + if (platform === "teams") return ; return (

Webhook-driven ingestion

- {label} bots receive messages via webhook and have no channel listing API. Channels appear + Telegram bots receive messages via webhook and have no channel listing API. Channels appear automatically once the bot receives its first message from a chat it's been added to.

- {platform === "telegram" - ? "Make sure the bot is added to your group and, for privacy-enabled bots, granted admin permission so it can read messages." - : "Make sure the Teams channel is configured in Azure Bot Service and the messaging endpoint points to this bridge."} + Make sure the bot is added to your group and, for privacy-enabled bots, granted admin permission so it can read messages. +

+
+
+ ); +} + +/** Teams' post-validation step. Endpoint + Graph consent are covered in + * the Setup step list (they happen in Azure before credentials), so this + * panel is just the one remaining action that has to happen in TEAMS + * itself — uploading the app package so the bot appears in channels. */ +function TeamsWebhookMode() { + return ( +
+
+

One step left in Teams

+

+ Credentials validated. To make the bot appear in channels, a Teams admin uploads the app package: +

+
+ +
+

+ Teams Admin Center → Manage apps → Upload custom app: +

+ + bot/teams-app/beever-atlas-teams.zip + +

+ Once uploaded, add the app to the team(s) and channels you want Beever Atlas to read. +

+
+ +
+ +

+ Channels appear automatically — no @mention required.