- Дата публикации
Как подключить свой AI‑агент к Microsoft 365 Copilot и сохранить полный контроль
Что произошло
Microsoft показала детальный паттерн для интеграции существующих агентных сервисов с Microsoft 365 Copilot.
Ключевая идея:
- вы не переписываете своего агента под M365 Copilot Agents;
- вы ставите перед ним шлюз (M365 Gateway) на Azure Container Apps;
- доступ к данным и API идёт от имени пользователя через цепочку двух On-Behalf-Of (OBO) потоков в Entra ID.
Архитектура разбита на два сервиса:
- Gateway — статeless, понимает Bot Framework, проверяет токены, делает первый OBO и проксирует запросы;
- Agentic Service — stateful, хранит сессии, управляет логикой агента, валидирует токен сервиса и делает второй OBO к downstream‑сервисам.
Отдельно используются две регистрации приложений в Entra ID, Azure Bot с OAuth‑подключением и стандартные компоненты Azure (Azure Container Apps, Azure Bot Service). Дата выхода гайда в источнике не указана, но он описывает актуальный подход для Microsoft 365 Copilot и Microsoft Agent Framework.
Зачем это нужно
Почему это вообще проблема
Если у вас уже есть агентный сервис на LangChain, Semantic Kernel, Microsoft Agent Framework или самописной логике, первый очевидный шаг — переписать его под M365 Copilot Agents (declarative или custom engine).
Но это больно по нескольким причинам:
- вы теряете контроль над оркестрацией: как вызываются инструменты, как устроен loop, как обрабатываются ошибки;
- вы привязываетесь к конкретному фреймворку Copilot Agents;
- вы меняете сессию и хранение состояния под требования Copilot;
- вы усложняете схему делегированного доступа к своим API и базам.
Новый паттерн решает это так:
- ваш существующий агент остаётся как есть;
- вы добавляете тонкий шлюз, который понимает протокол Bot Framework и Copilot;
- все токены и права доступа выстраиваются в прозрачную OBO‑цепочку от пользователя до ваших downstream‑сервисов.
Что это даёт разработчикам и бизнесу
Для продуктовых и платформенных команд:
- можно подключить уже существующие агентные сервисы к Microsoft 365 Copilot без их переписывания;
- проще поддерживать единый стек: свой LLM, свои промпты, свои инструменты, свой стор сессий;
- можно аккуратно мигрировать: сначала добавить шлюз, потом при желании переносить части логики в Copilot Agents.
Для безопасности и комплаенса:
- чёткое разделение границ доверия: шлюз общается с Copilot и Teams, сервис — только с вашими данными;
- делегированный доступ (OBO) полностью прозрачен: каждый шаг валидирует токен и работает со scoped‑токенами;
- можно ограничить ingress для сервисного слоя, оставив наружу только gateway.
Для архитекторов и SRE:
- gateway и сервис масштабируются независимо;
- gateway можно переиспользовать для нескольких агентов, меняя только адаптер клиента;
- проще автоматизировать деплой и проверку админского consent: без него сервис просто не стартует.
Что меняет для рынка
Новый стандарт интеграции с Copilot
До этого многие команды смотрели на M365 Copilot как на платформу, которая требует пересборки всего под свой формат агентов. Новый паттерн показывает другой путь: Microsoft 365 Copilot становится фронтендом для уже существующих корпоративных AI‑сервисов.
Это меняет несколько вещей:
- крупные компании могут подключать свои LLM‑агенты к Copilot без смены стека и без отказа от собственного оркестратора;
- появляется повторно используемый gateway‑слой, который можно тиражировать по всем агентам внутри организации;
- downstream‑сервисы (Graph, Databricks, собственные API, MCP‑серверы) получают строгую модель делегированного доступа через цепочку OBO.
Как это бьёт по конкурентам
Для альтернативных корпоративных ассистентов это сигнал: Microsoft 365 Copilot глубже встраивается в существующие AI‑архитектуры, а не конкурирует с ними лоб в лоб.
Если у вас уже есть внутренний ассистент на LangChain или Semantic Kernel, теперь проще сделать Copilot‑фронтенд поверх него, чем строить отдельный UX или бота в Teams.
Это усиливает позицию Microsoft 365 как единый интерфейс к корпоративным данным и агентам. Инструменты, которые предлагают только собственный UI без глубокой интеграции с M365 и Entra ID, становятся менее привлекательными для крупных заказчиков.
Что меняется для пользователей
Пользователи Microsoft 365 Copilot получают доступ к:
- вашим внутренним базам и API;
- вашим MCP‑серверам;
- вашим доменным агентам (финансы, логистика, продажи, DevOps);
— прямо из Copilot, без переключения на отдельные чат‑интерфейсы.
При этом все запросы идут от имени пользователя, а не от сервисного аккаунта. Это значит:
- соблюдаются существующие права доступа в Entra ID;
- аудиторский след остаётся понятным: кто и к каким данным обращался;
- можно использовать уже настроенные RBAC‑модели и политики условного доступа.
Что это значит для вас
Если вы разрабатываете свои AI‑агенты
Этот паттерн подходит вам, если:
- у вас уже есть агент на LangChain, Semantic Kernel, Microsoft Agent Framework или самописном фреймворке;
- вы хотите, чтобы он появился в Microsoft 365 Copilot без переписывания;
- вам нужен делегированный доступ к базам, REST‑API, MCP‑серверам от лица пользователя;
- вы хотите полностью контролировать оркестрацию, промпты, выбор LLM и хранение сессий.
Он вряд ли подойдёт, если:
- вы делаете простой FAQ‑бот без доступа к защищённым данным;
- вам не нужен кастомный оркестратор, и вас устраивает декларативный M365 Copilot Agent;
- вы не готовы управлять Entra ID, OBO и валидацией JWT.
Если вы архитектор или руководите платформенной командой
На что обратить внимание:
- вам понадобятся две регистрации приложений в Entra ID + знание resource app ID для downstream‑API;
- без админского consent для делегированных прав ничего не заработает — это нужно зашить в bootstrap;
- gateway можно сделать общим для нескольких агентов, а сервисную часть — специфичной под каждую доменную область.
Если вы уже используете Azure Container Apps и Azure Bot, входной порог ниже: паттерн хорошо ложится на существующую инфраструктуру.
Если вы отвечаете за безопасность и комплаенс
Важные моменты:
- пароли пользователей нигде не хранятся;
- каждый сервис в цепочке видит только scoped‑токен и обменивает его по OBO на следующий;
- gateway и сервис обязаны валидировать JWT на своей границе;
- можно ограничить ingress для сервисного слоя и оставить наружу только gateway.
Если вы внедряете Copilot в среде с жёстким комплаенсом, этот паттерн даёт понятную историю:
- кто инициировал запрос;
- какие токены и с какими scope использовались;
- какие downstream‑ресурсы вызывались от имени пользователя.
Архитектура и ключевые идеи
Слои и их ответственность
Архитектура делится на четыре уровня:
-
Microsoft 365 Copilot
- хранит UX диалогов и identity пользователя;
- не хранит ваше состояние сессий агента.
-
Gateway (M365 Gateway)
- адаптер протокола Bot Framework;
- валидирует токены канала (Copilot, Teams);
- делает OBO #1: меняет пользовательский токен на сервисный токен для вашего Agentic Service;
- маппит
conversation.idCopilot наsession_idв вашем сервисе.
-
Agentic Service
- держит бизнес‑логику, оркестрацию, выбор LLM, промпты, инструменты;
- валидирует сервисный bearer, пришедший от gateway;
- делает OBO #2 к downstream‑API (Graph, Databricks, SQL, MCP и т.д.);
- хранит сессии и историю диалога.
-
Downstream‑сервисы
- базы данных, REST‑API, MCP‑серверы;
- принимают делегированный токен пользователя.
Почему два сервиса, а не один
-
Граница доверия
- gateway общается с Copilot и Teams, держит Bot‑учётку и знает протокол Bot Framework;
- Agentic Service не хранит Bot‑секреты, не знает ничего про Teams и Bot Framework.
-
Масштабирование
- gateway можно масштабировать вширь, чтобы выдерживать пик запросов;
- Agentic Service можно держать в одном экземпляре, если сессии в памяти и это MVP.
-
Свобода фреймворка
- в сервисе вы можете использовать любой стек: Microsoft Agent Framework, LangChain, Semantic Kernel, собственный код;
- gateway — просто HTTP‑прокси с переводом протокола.
Что можно переиспользовать в gateway
Gateway почти целиком можно сделать общим для нескольких агентов. Переиспользуемые части:
- хост для
POST /api/messagesи bootstrap Bot‑адаптера; - вся логика Bot/channel‑аутентификации;
- OBO #1 для получения токена сервиса по нужному scope;
- маппинг
conversation.id → session_id; - обработка долгих запросов и отложенных ответов;
- отказ при параллельных запросах в одну и ту же сессию;
- стандартная обработка ошибок и retry;
- форма деплоя в ACA + Azure Bot + app‑package.
Специфично для каждого сервиса:
- клиент для downstream‑сервиса (как именно вы вызываете свой Agentic Service);
- перевод Bot‑activity в payload сервиса;
- конкретные scopes и app‑id в Entra ID;
- телеметрия и fallback‑тексты;
- контракт старта сессии:
create_session()или сразуPOSTпервого сообщения.
Token Flow: как ходит идентичность пользователя
Принципы
-
Токен пользователя не выходит за пределы OBO‑цепочки.
- каждый сервис получает ограниченный assertion и меняет его на свой токен;
- пароли пользователя нигде не появляются.
-
JWT‑валидация обязательна на каждом уровне.
- gateway валидирует токен канала (Copilot/Teams);
- Agentic Service валидирует сервисный bearer перед тем, как использовать его для OBO, сессий и авторизации.
-
Внутренний ingress — плюс, но не замена валидации.
- даже если сервис доступен только из приватной сети, он всё равно обязан проверять bearer.
-
ContextVar для assertion.
- сервис привязывает валидированный JWT к
ContextVarв middleware; - любой downstream‑вызов внутри того же запроса может получить assertion без протаскивания аргументов через весь стек.
- сервис привязывает валидированный JWT к
Entra ID: какие приложения нужны
Вам нужны две регистрации в Entra ID и знание resource app ID для downstream‑API.
1. Agentic Service app
Настройки:
- Display name:
your-service-api - Identifier URI:
api://<service-client-id> - Sign-in audience:
AzureADMyOrg(single tenant) requestedAccessTokenVersion:2
Exposed scope:
access_as_user(delegated, User consent)
Required permissions:
- downstream API:
user_impersonation(delegated)
Нужен client secret — для ConfidentialClientApplication в MSAL.
2. Gateway / Bot app
Настройки:
- Display name:
your-bot - Identifier URI:
api://botid-<bot-client-id>(Bot SSO convention) - Sign-in audience:
AzureADMyOrg requestedAccessTokenVersion:2
Exposed scope:
access_as_user(delegated)
Required permissions:
- Service app:
access_as_user(delegated)
Redirect URI:
https://token.botframework.com/.auth/web/redirect
Preauthorized clients:
- Teams Desktop:
1fec8e78-... - Teams Web:
5e3ce6c0-...
Нужен client secret — для Bot Framework auth.
3. Обязательный admin consent
Нужны два делегированных grant'а с админским consent:
- Gateway/Bot app → Service app:
access_as_user(delegated); - Service app → Downstream API:
user_impersonation(delegated).
Без этого OBO вернёт AADSTS65001 и токен не получится. Операционный совет: при автоматизации деплоя проверяйте наличие admin consent и считайте его блокирующим условием. Не пытайтесь запускать сервис в надежде, что вход потом «сам заработает».
4. Azure Bot OAuth‑подключение
Создайте OAuth‑подключение для Azure Bot, чтобы Microsoft Agents SDK мог делать silent token exchange для перехода gateway → сервис.
Полная команда:
az bot authsetting create \
-g $RESOURCE_GROUP \
-n $BOT_RESOURCE_NAME \
-c SERVICE_CONNECTION \
--service Aadv2 \
--client-id $BOT_APP_ID \
--client-secret $BOT_APP_PASSWORD \
--provider-scope-string "$SERVICE_API_SCOPE offline_access openid profile" \
--parameters TenantId="$TENANT_ID" TokenExchangeUrl="api://botid-$BOT_APP_ID"
Компонент 1: Gateway для M365 (stateless, протокольный адаптер)
Задача gateway
Gateway должен:
- принять Bot‑activity от Copilot/Teams;
- валидировать токен канала по правилам Bot Framework;
- сделать OBO #1: получить сервисный токен для Agentic Service;
- перевести Copilot‑диалог в формат API вашего сервиса и отправить запрос.
Сервис при этом может ничего не знать о Bot Framework и Teams, но он обязан валидировать сервисный bearer на своём входе.
Валидация токенов в gateway — не опция, а требование
Если пропустить проверку токена канала, любой анонимный клиент может добраться до вашего forwarding‑пути. Gateway должен:
- использовать middleware Bot/Agents SDK или эквивалентные проверки
issuer/ подписи /audienceдля входящего токена; - после OBO #1 получить сервисный токен и передать его дальше как есть в
Authorization: Bearer ...; - по желанию вытащить поля
oid,tid,upnилиpreferred_usernameдля логов и заголовков, но считать их неавторитетной мета‑информацией.
Адаптация к API вашего сервиса
Внутри gateway есть service client — маленький HTTP‑адаптер, который переводит модель диалога Copilot в API вашего сервиса.
Маппинг обычно такой:
conversation.idCopilot →session_id/ thread ID / chat ID в сервисе;- текст сообщения пользователя → поле
prompt/text/inputв теле запроса; - ответ сервиса → поле
reply/response/outputи т.п.; - аутентификация →
Authorization: Bearer <token>или кастомный заголовок.
Пример клиента для сервиса с POST /chat/{thread_id}:
# Service client for a service with POST /chat/{thread_id}
class MyServiceClient:
async def send_turn(self, session_id: str, text: str, token: str) -> str:
resp = await self.http.post(
f"{self.base_url}/chat/{session_id}",
json={"prompt": text, "stream": False},
headers={"Authorization": f"Bearer {token}"},
)
return resp.json()["response"] # extract from service's response shape
И клиент для сервиса с POST /v1/conversations/{id}/messages и нестандартным заголовком токена:
# Service client for a service with POST /v1/conversations/{id}/messages
class AnotherServiceClient:
async def send_turn(self, session_id: str, text: str, token: str) -> str:
resp = await self.http.post(
f"{self.base_url}/v1/conversations/{session_id}/messages",
json={"content": text, "role": "user"},
headers={"X-Api-Token": token},
)
return resp.json()["choices"][0]["message"]["content"]
Gateway‑хендлер сообщений в обоих случаях одинаковый:
async def handle_message(context):
token = await agent_auth.get_token(context)
session_id = context.activity.conversation.id
reply = await service_client.send_turn(session_id, context.activity.text, token)
await context.send_activity(reply)
Что требуется от любого сервиса
Gateway сможет подружить Copilot с любым сервисом, если у сервиса есть:
- идентификатор сессии: session/thread/chat ID, по которому можно продолжать диалог;
- endpoint для обмена сообщениями: принимает текст пользователя и возвращает ответ;
- механизм аутентификации: bearer‑токен или другой формат, куда можно положить токен из OBO #1.
Всё остальное — детали реализации и формат URL. Главное — сервис должен сам валидировать bearer и не доверять только заголовкам вида X-User-*.
Ключевые детали реализации
- Auth‑handlers
Gateway должен уметь различать два сценария:
- agentic path — когда запрос идёт через M365 Copilot agentic channel;
- connector path — когда запрос идёт через обычный Teams / Bot connector.
Пример конфигурации:
auth_handlers = {
"service_agentic": AuthHandler(
auth_type="AgenticUserAuthorization",
abs_oauth_connection_name="SERVICE_CONNECTION",
obo_connection_name="SERVICE_OBO_CONNECTION", # may be same as abs
scopes=["api://<service-client-id>/access_as_user"],
),
"service_connector": AuthHandler(
auth_type="UserAuthorization",
abs_oauth_connection_name="SERVICE_CONNECTION",
obo_connection_name="",
scopes=["api://<service-client-id>/access_as_user"],
),
}
- Invoke‑activity
Copilot шлёт invoke‑сообщения во время SSO‑handshake. Gateway должен обрабатывать их без ошибок, иначе вход сломается.
- Маппинг сессий
Используйте context.activity.conversation.id как ключ сессии при обращении к сервису. Так один диалог Copilot всегда попадает в одну и ту же сессию агента.
- Health‑проверки
Проба ACA ходит на /healthz. JWT‑middleware должен игнорировать этот путь, иначе probe упадёт.
- Долгие запросы
Если вы используете долгие операции и proactive‑ответы, можно держать включённый long‑running режим SDK, но добавить маленький локальный bridge, который сохраняет исходное сообщение пользователя и использует proactive‑context только для отправки ответа. Логику агента при этом оставляйте в сервисе, а не в gateway.
Технологический стек gateway
В примере используется:
microsoft-agents-hosting-fastapi— адаптер Bot SDK под FastAPI;microsoft-agents-authentication-msal— MSAL‑аутентификация и OBO;httpx— HTTP‑клиент для вызова Agentic Service.
Можно использовать другой язык и SDK, если вы реализуете тот же протокол Bot Framework и ту же схему OBO.
Компонент 2: Agentic Service (stateful, фреймворк‑агностичный)
Роль сервиса
Agentic Service — это обычный HTTP‑API:
- он не знает про Bot Framework, Teams или Copilot‑activity;
- он принимает сервисный bearer, валидирует его и извлекает claims;
- он выполняет вашу агентную логику и при необходимости делает OBO #2 к downstream‑сервисам;
- он возвращает ответ в своём формате.
Сервис — это граница доверия для данных. Он должен доверять только валидированному bearer, а не заголовкам X-User-*.
Минимальные требования к сервису
-
Валидация bearer
- прочитать и проверить
Authorization: Bearer <token>; - не использовать токен для OBO или авторизации без проверки подписи, issuer, audience и срока.
- прочитать и проверить
-
Извлечение claims
- получить
oid,tid,upn/preferred_username,scp(scopes); - использовать их для привязки сессий к пользователю и аудита.
- получить
-
Сессии
- уметь держать состояние диалога между запросами;
- Copilot ожидает многотуровые диалоги.
-
Message exchange
- endpoint, который принимает текст пользователя и возвращает ответ агента.
-
OBO #2 (если нужно)
- использовать
acquire_token_on_behalf_of()MSAL с assertion из ContextVar; - запрашивать scopes вида
<downstream-resource-id>/.default.
- использовать
-
Ingress‑защита
- по возможности сделать сервис доступным только из внутренней сети или через приватный ingress;
- но всё равно проверять bearer.
ContextVar для assertion
Чтобы не протаскивать токен через все функции, сервис использует contextvars.ContextVar:
from contextvars import ContextVar, Token as CtxToken
_USER_ASSERTION: ContextVar[str | None] = ContextVar("user_assertion", default=None)
_USER_CLAIMS: ContextVar[TokenClaims | None] = ContextVar("user_claims", default=None)
def bind_identity(assertion: str, claims: TokenClaims):
return _USER_ASSERTION.set(assertion), _USER_CLAIMS.set(claims)
def reset_identity(a_tok: CtxToken, c_tok: CtxToken):
_USER_ASSERTION.reset(a_tok)
_USER_CLAIMS.reset(c_tok)
# В middleware, после валидации bearer:
a_tok, c_tok = bind_identity(raw_jwt, validated_claims)
try:
response = await call_next(request)
finally:
reset_identity(a_tok, c_tok) # всегда чистим контекст
ContextVar безопасен для async: каждый запрос получает свой контекст, и downstream‑код может достать assertion в любой точке стека.
OBO #2 к downstream‑сервисам
Когда агенту нужно вызвать downstream‑API от имени пользователя:
import msal
app = msal.ConfidentialClientApplication(
client_id=SERVICE_CLIENT_ID,
client_credential=SERVICE_CLIENT_SECRET,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
)
result = app.acquire_token_on_behalf_of(
user_assertion=_USER_ASSERTION.get(), # из ContextVar
scopes=["<downstream-resource-id>/.default"], # Graph, Databricks, свой API и т.д.
)
downstream_token = result["access_token"]
Примеры scopes:
- Microsoft Graph:
https://graph.microsoft.com/.default; - Azure Databricks:
2ff814a6-3304-4ab8-85cb-cd0e6f879c1d/.default; - Azure SQL:
https://database.windows.net/.default; - кастомный API:
api://<their-app-id>/.default; - MCP‑сервер за Entra:
api://<mcp-app-id>/.default.
Управление сессиями
Для MVP можно использовать простое in‑memory‑хранилище в одном реплике сервиса.
Базовый контракт:
- Create:
POST /api/chat/sessions→ возвращаетsession_id; - Send message:
POST /api/chat/sessions/{id}/messages→ возвращает ответ; - Get session:
GET /api/chat/sessions/{id}→ возвращает историю.
Рекомендации:
- каждая сессия имеет
owner_idизoidтокена; - любой доступ не от владельца —
403; - ограничивайте число turn'ов в истории (например,
max_turns * 2), чтобы память не росла бесконечно; - используйте
asyncio.Lock()на сессию, чтобы не было пересечения запросов.
Где живёт ваша агентная логика
Внутри сервиса вы свободны:
- Microsoft Agent Framework:
HandoffBuilder,ConcurrentBuilderи т.д.; - LangChain / LangGraph: chains, graphs, tool calling;
- Semantic Kernel: planners, plugins;
- прямые вызовы OpenAI / Azure OpenAI SDK;
- другие фреймворки: CrewAI, AutoGen и т.п.
Сервис — это чистая граница: снаружи HTTP‑контракт и токены, внутри — любая агентная логика.
Рекомендованный контракт, если вы строите сервис с нуля
Удобный вариант, который хорошо стыкуется с gateway:
POST /api/chat/sessions/{session_id}/messages
Authorization: Bearer <service-scoped-token>
Content-Type: application/json
{"text": "user message"}
Ответы:
200—{"session_id": "...", "reply": "...", "turns": [...]};401— неверный или отсутствующий токен;404— сессия не найдена (gateway может сам создать и повторить запрос).
Если у вас уже есть другой API, менять его не нужно — достаточно адаптировать service client в gateway (см. пример выше).
Как запустить
Ниже — ключевые шаги, чтобы собрать всё воедино.
-
Подготовьте Entra ID
- создайте две app‑регистрации: для Agentic Service и для Gateway/Bot;
- настройте scopes
access_as_userиuser_impersonation; - получите admin consent для обоих направлений;
- сохраните client id/secret и tenant id.
-
Настройте Azure Bot
- создайте Bot‑ресурс для gateway;
- добавьте OAuth‑подключение командой:
az bot authsetting create \
-g $RESOURCE_GROUP \
-n $BOT_RESOURCE_NAME \
-c SERVICE_CONNECTION \
--service Aadv2 \
--client-id $BOT_APP_ID \
--client-secret $BOT_APP_PASSWORD \
--provider-scope-string "$SERVICE_API_SCOPE offline_access openid profile" \
--parameters TenantId="$TENANT_ID" TokenExchangeUrl="api://botid-$BOT_APP_ID"
-
Разверните gateway в Azure Container Apps
- используйте FastAPI +
microsoft-agents-hosting-fastapi; - настройте endpoint
POST /api/messages; - подключите Auth‑handlers для agentic/connector‑сценариев;
- реализуйте service client для своего Agentic Service.
- используйте FastAPI +
-
Разверните Agentic Service
- реализуйте HTTP‑API с сессиями и endpoint'ом обмена сообщениями;
- добавьте middleware для валидации bearer и привязки assertion к ContextVar;
- реализуйте OBO #2 для downstream‑API, если это нужно.
-
Соберите M365 App Package
- создайте манифест, который указывает на ваш Azure Bot (gateway);
- заверните манифест и иконки в ZIP;
- загрузите пакет в Microsoft 365, чтобы Copilot увидел ваш агент.
После этого ваш существующий агентный сервис начинает жить внутри Microsoft 365 Copilot — с сохранением вашей логики, вашего стека и строгой моделью делегированного доступа через цепочку OBO.