Назад в блог

Tenant-изоляция для AI-агентов: один агент — много клиентов без утечек

Как изолировать контексты клиентов у общего агента: tenant-scoped инструменты, привязка токенов к tenant и тесты на перекрёстные утечки.

Иллюстрация к материалу: Tenant-изоляция для AI-агентов: один агент — много клиентов без утечек

SaaS-компания запускает одного support-агента на всех клиентов: дешевле, чем по агенту на tenant. Архитектура выглядит безобидно — пока одна ошибка в tenant_id не отдаёт в ответ документы соседнего клиента. У обычного API такая ошибка видна в логах запроса; у агента она растворяется в неструктурированном контексте: ретривер вернул чужое, модель пересказала своими словами, и следов «чужого tenant» в ответе уже нет.

Почему общий агент опаснее общего API

API возвращает строки из выборки WHERE tenant_id = X — ошибка видна и воспроизводима. Агент смешивает источники: векторный поиск без tenant-фильтра, кэш диалогов, tool-ответы, few-shot примеры из чужих тикетов. Каждый слой может подмешать чужое, а финальный ответ уже не содержит машиночитаемого указания на источник. Поэтому изоляция строится на каждом слое, а не «проверкой tenant в одном месте».

5 правил изоляции

  1. Tenant резолвится сервером, а не приходит от агента. tenant_id извлекается из аутентифицированной сессии вызывающего, а не из аргументов tool-вызова. Агент не может «попросить» чужой tenant — поле для него read-only.
  2. Инструменты tenant-scoped. Каждый инструмент выполняется в контексте одного tenant: поиск, листинги и выборки всегда с фильтром, bulk-операции через tenant запрещены архитектурно.
  3. Retrieval с tenant-фильтром до ранжирования. Фильтр по tenant применяется на этапе выборки, а не после rerank: иначе чужой документ с высоким скором вытеснит свой со средним.
  4. Память и кэш сегментированы. История диалогов, эмбеддинги пользовательских preference и кэш ответов — по namespace на tenant. Общий кэш «частых ответов» допустим только для tenant-нейтрального контента.
  5. Ответ проверяется перед выдачей. DLP-постпроверка ищет в ответе маркеры чужих tenant (ID, названия, суммы из чужого скоупа) — как последний рубеж, а не основной механизм.

Привязка токенов к tenant

Токен агента привязывается к паре agent × tenant через resource indicators: токен, выданный для tenant A, не принимается инструментами tenant B даже при подмене аргументов. Это закрывает confused deputy, когда скомпрометированный или ошибающийся агент пытается переиспользовать credential в чужом контексте. Ротация — короткоживущие токены, чтобы украденный токен протухал раньше, чем его заметят в чужом скоупе.

Тесты на cross-tenant утечки

  • Канарейки: в данные каждого тестового tenant кладётся уникальная метка-фраза; после прогона типовых диалогов проверяется, что метка tenant A никогда не всплывает в сессии tenant B.
  • Перепутанный tenant_id: интеграционный тест подменяет tenant в аргументах — сервер обязан проигнорировать его и ответить данными сессионного tenant.
  • Отравленный сосед: в tenant A кладётся документ с высокой релевантностью к запросам tenant B — убеждаемся, что фильтр выборки его отсекает.
  • Кэш-промывка: одинаковый запрос от двух tenant подряд — второй ответ не должен содержать данных первого.

Эти тесты — в CI-target для агентских изменений, рядом с PII-ловушками.

Что логировать

Каждое действие: agent_id × tenant_id × tool × результат, плюс каким tenant_id резолвилась сессия и был ли конфликт с аргументами. Тогда вопрос «трогал ли агент данные клиента X» закрывается запросом, а попытка подмены tenant видна как аномалия до утечки.

Где Codenik

Codenik резолвит tenant из сессии на каждый tools/call и игнорирует tenant из аргументов агента. Токены привязаны к паре агент × tenant, retrieval и листинги идут с серверным фильтром, а постпроверка режет чужеродные маркеры до попадания в контекст. Один агент обслуживает всех клиентов — изоляцию держит шлюз, а не аккуратность промптов.

Короткий вывод

Общий агент без tenant-изоляции — это общая БД без WHERE: вопрос не «утечёт ли», а «когда заметят». Серверный tenant, scoped-инструменты, фильтр до ранжирования, сегмент памяти и канарейки в CI — пять слоёв, каждый из которых дешёвый по отдельности и дорогой в цене утечки все вместе.

Источники и дальнейшее чтение