Дата публикации
business

Как подключить свой 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‑ресурсы вызывались от имени пользователя.

Архитектура и ключевые идеи

Слои и их ответственность

Архитектура делится на четыре уровня:

  1. Microsoft 365 Copilot

    • хранит UX диалогов и identity пользователя;
    • не хранит ваше состояние сессий агента.
  2. Gateway (M365 Gateway)

    • адаптер протокола Bot Framework;
    • валидирует токены канала (Copilot, Teams);
    • делает OBO #1: меняет пользовательский токен на сервисный токен для вашего Agentic Service;
    • маппит conversation.id Copilot на session_id в вашем сервисе.
  3. Agentic Service

    • держит бизнес‑логику, оркестрацию, выбор LLM, промпты, инструменты;
    • валидирует сервисный bearer, пришедший от gateway;
    • делает OBO #2 к downstream‑API (Graph, Databricks, SQL, MCP и т.д.);
    • хранит сессии и историю диалога.
  4. Downstream‑сервисы

    • базы данных, REST‑API, MCP‑серверы;
    • принимают делегированный токен пользователя.

Почему два сервиса, а не один

  1. Граница доверия

    • gateway общается с Copilot и Teams, держит Bot‑учётку и знает протокол Bot Framework;
    • Agentic Service не хранит Bot‑секреты, не знает ничего про Teams и Bot Framework.
  2. Масштабирование

    • gateway можно масштабировать вширь, чтобы выдерживать пик запросов;
    • Agentic Service можно держать в одном экземпляре, если сессии в памяти и это MVP.
  3. Свобода фреймворка

    • в сервисе вы можете использовать любой стек: 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: как ходит идентичность пользователя

Принципы

  1. Токен пользователя не выходит за пределы OBO‑цепочки.

    • каждый сервис получает ограниченный assertion и меняет его на свой токен;
    • пароли пользователя нигде не появляются.
  2. JWT‑валидация обязательна на каждом уровне.

    • gateway валидирует токен канала (Copilot/Teams);
    • Agentic Service валидирует сервисный bearer перед тем, как использовать его для OBO, сессий и авторизации.
  3. Внутренний ingress — плюс, но не замена валидации.

    • даже если сервис доступен только из приватной сети, он всё равно обязан проверять bearer.
  4. ContextVar для assertion.

    • сервис привязывает валидированный JWT к ContextVar в middleware;
    • любой downstream‑вызов внутри того же запроса может получить assertion без протаскивания аргументов через весь стек.

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.id Copilot → 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-*.

Ключевые детали реализации

  1. 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"],
    ),
}
  1. Invoke‑activity

Copilot шлёт invoke‑сообщения во время SSO‑handshake. Gateway должен обрабатывать их без ошибок, иначе вход сломается.

  1. Маппинг сессий

Используйте context.activity.conversation.id как ключ сессии при обращении к сервису. Так один диалог Copilot всегда попадает в одну и ту же сессию агента.

  1. Health‑проверки

Проба ACA ходит на /healthz. JWT‑middleware должен игнорировать этот путь, иначе probe упадёт.

  1. Долгие запросы

Если вы используете долгие операции и 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-*.

Минимальные требования к сервису

  1. Валидация bearer

    • прочитать и проверить Authorization: Bearer <token>;
    • не использовать токен для OBO или авторизации без проверки подписи, issuer, audience и срока.
  2. Извлечение claims

    • получить oid, tid, upn/preferred_username, scp (scopes);
    • использовать их для привязки сессий к пользователю и аудита.
  3. Сессии

    • уметь держать состояние диалога между запросами;
    • Copilot ожидает многотуровые диалоги.
  4. Message exchange

    • endpoint, который принимает текст пользователя и возвращает ответ агента.
  5. OBO #2 (если нужно)

    • использовать acquire_token_on_behalf_of() MSAL с assertion из ContextVar;
    • запрашивать scopes вида <downstream-resource-id>/.default.
  6. 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 (см. пример выше).

Как запустить

Ниже — ключевые шаги, чтобы собрать всё воедино.

  1. Подготовьте Entra ID

    • создайте две app‑регистрации: для Agentic Service и для Gateway/Bot;
    • настройте scopes access_as_user и user_impersonation;
    • получите admin consent для обоих направлений;
    • сохраните client id/secret и tenant id.
  2. Настройте 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"
  1. Разверните gateway в Azure Container Apps

    • используйте FastAPI + microsoft-agents-hosting-fastapi;
    • настройте endpoint POST /api/messages;
    • подключите Auth‑handlers для agentic/connector‑сценариев;
    • реализуйте service client для своего Agentic Service.
  2. Разверните Agentic Service

    • реализуйте HTTP‑API с сессиями и endpoint'ом обмена сообщениями;
    • добавьте middleware для валидации bearer и привязки assertion к ContextVar;
    • реализуйте OBO #2 для downstream‑API, если это нужно.
  3. Соберите M365 App Package

    • создайте манифест, который указывает на ваш Azure Bot (gateway);
    • заверните манифест и иконки в ZIP;
    • загрузите пакет в Microsoft 365, чтобы Copilot увидел ваш агент.

После этого ваш существующий агентный сервис начинает жить внутри Microsoft 365 Copilot — с сохранением вашей логики, вашего стека и строгой моделью делегированного доступа через цепочку OBO.


Читайте также