Представьте, что вы дежурный инженер, который в 2 часа ночи смотрит на неудачный запуск CI и задается вопросом, сталкивался ли кто-нибудь с этой ошибкой раньше.
А теперь представьте, что вы менеджер по продажам, у которого на руках опросник по безопасности от клиента, который нужно сдать в пятницу, а ответы разбросаны по десятку документов.
Наконец, представьте, что вы человек, ответственный за то, чтобы позволить программному обеспечению отвечать на оба этих запроса, имея доступ к системам, в которых хранится истина. Вы должны решить, что оно может читать, что может изменять, от чьего имени действует и как вы узнаете, когда оно ошибается.
Этот пост посвящен именно этому третьему человеку.
Два инженера начали создавать Nebula, нашу внутреннюю платформу агентов, в июле. Сегодня на ней работают 82 агента, принадлежащие командам по всему Starburst, и некоторые из тех, кто их создал, не пишут код профессионально. В остальной части этого поста мы: а) объясним, почему мы создали платформу, а не набор агентов, б) изложим принципы, которыми руководствовались, в) разберем стек и примитивы, которые агенты получают «из коробки», г) покажем, что история наших pull-реквестов говорит о распределении работы, и д) расскажем, что сломалось и что мы еще не решили.
Тезис умещается в одно предложение. Без доверия агент приносит больше вреда, чем пользы. Все, что написано ниже, — это следствие серьезного отношения к этому утверждению.
Зачем нужна платформа
К середине года команды здесь уже создали агентов для поддержки дежурств, операций по безопасности и управления затратами. Каждый из них работал. Но каждый также создал свою собственную среду выполнения, систему развертывания, идентификации и логирования, потому что не было ничего общего. Никто не мог ответить на базовые вопросы о «парке»: что запущено, к чему это имеет доступ, сколько это стоит и кому придет уведомление, если что-то пойдет не так.
Благодаря моделям писать агентов стало дешево, поэтому каждая команда собиралась создать своего, а проверка индивидуальных агентов по одному не масштабируется до размеров компании. Работа команды платформы — это рычаг. Создайте сложные части один раз и сделайте безопасный путь самым простым — «проторенную дорожку», которую команды выбирают, потому что это быстрее, чем создавать свое собственное.
Поэтому мы сначала определили зоны ответственности, и это умещается в одну строку. Мы, центральная AI Infra, владеем «плоскостью» агентов. Все остальные команды в Starburst владеют «парком». Плоскость — это общий уровень, на котором работает каждый агент, созданный один раз одной командой. Парк — это агенты, созданные всеми остальными. Платформа владеет всем, что не является доменной логикой: Slack, доступом к моделям, учетными данными инструментов, идентификацией, развертыванием, планированием и телеметрией. Агент владеет своим промптом, своими инструментами и своими суждениями.
Ментальная модель — это аренда (tenancy). Trino рассматривает коннектор как управляемую сущность, которая живет внутри движка, обновляется вместе с ним и наблюдается через него. Мы относимся к агенту так же. Платформа и агенты используют один репозиторий и одну границу развертывания, поэтому исправление в общей библиотеке доходит до каждого агента в одном коммите, без необходимости фиксировать версии SDK и без бэклога обновлений.
Принципы проектирования
Работу направляли четыре принципа. Каждый из них — это способ покупки доверия.
Держите агентов узкоспециализированными. Агент — это небольшая директория: манифест, фабрика, промпты и тесты. Манифест объявляет идентификатор, модель, описание, среду выполнения, побочные эффекты, принимает ли агент запланированные запуски и какие права он запрашивает. Он намеренно сделан коротким, чтобы читаться как декларация о намерениях. Привилегии, такие как таблица памяти, префикс общего хранилища или доступ к базе знаний, устанавливаются в конфигурации инфраструктуры рядом с IAM, который их ограничивает. Большинство агентов работают только на чтение или только в режиме черновика, и каждое изменение подтверждается человеком.
Сделайте идентификацию задачей платформы. Инструмент никогда не должен принимать «кто спрашивает» в качестве аргумента, потому что модель можно убедить передать неверный ответ. Платформа привязывает личность спрашивающего к каждому шагу, и инструменты считывают ее оттуда.
Относитесь к внешнему контенту как к данным, а не как к инструкциям. Хранимые объекты по умолчанию защищены, получение данных разрешено только из белого списка и защищено от подделки запросов на стороне сервера (SSRF), а защитный механизм отменяет публикацию в Slack, если пункт назначения не является тем, который указал пользователь, или тем, который вернул доверенный инструмент.
Сделайте безопасный путь самым простым. Учетные данные находятся на шлюзе, доступ к инструментам осуществляется через именованные права, и каждый вызов записывает аудит-лог. Инженер получает все это, просто описав агента, и ему пришлось бы постараться, чтобы обойти это.
Стек
Интерфейс разработчика. CLI нет. Интерфейс — это Claude Code, работающий на основе руководств (runbooks), зафиксированных в репозитории. Инженер вводит /nebula new и отвечает на несколько вопросов о цели, уровне модели, среде выполнения, владельце и правах доступа к инструментам. Claude пишет агента, запускает его локально и исправляет ошибки, а та же точка входа позволяет перечислять, запускать, останавливать и модернизировать агентов. Локальные запуски используют собственную облачную сессию разработчика, поэтому учетные данные провайдера никогда не попадают на ноутбук. Знания платформы живут в ее собственном репозитории: область применения, подход, выводы, утвержденный словарь, журнал сессий и рабочие задачи. Каждая сессия загружает их как контекст, благодаря чему новая сессия продолжает работу с того места, где закончилась предыдущая. Разрыв между идеей и развернутым, управляемым агентом составляет несколько часов.
Сборка и развертывание. Каждый pull-реквест запускает тесты, линтер, сканирование безопасности и предварительный просмотр инфраструктуры. Слияние с основной веткой (main) выполняется через GitHub Actions и Pulumi по протоколу OIDC, без хранения учетных данных. Конвейер обнаруживает новые стеки и перестраивает реестр агентов на основе манифестов, поэтому добавление агента сводится практически к одному слиянию.
Среда выполнения. Каждый агент поставляется как один контейнерный образ с контрактом из двух маршрутов: POST /invocations и GET /ping. Один и тот же образ работает локально, как долгоживущий сервис на ECS Fargate или на Bedrock AgentCore в виде посекундных сессий microVM, которые почти ничего не стоят в простое. Платформа выбирает, где запускать каждого агента, основываясь на том, что заявлено в манифесте агента. Агент, которому нужно оставаться активным долгое время, например, тот, с которым общаются в Slack, работает на Fargate как выделенная задача, которая живет столько же, сколько и агент. Агент, который просыпается по расписанию, выполняет одну задачу и завершает работу, запускается на AgentCore, что дает каждому запуску свою изолированную машину и тарифицируется только за секунды работы агента. Агенты, которым не нужна постоянная задача, ее не получают, именно здесь достигается экономия средств. Агенты используют Strands на Bedrock с моделью, объявленной в манифесте.
Парадный вход. Люди обращаются к каждому агенту через одно приложение Slack. Оно определяет, кто спрашивает, отвечает на запросы обнаружения («с какими агентами я могу поговорить?») и направляет каждый поток по иерархии, начиная с самого дешевого. Явный префикс to: побеждает. В противном случае поток, уже привязанный к агенту, остается на нем, что делает потоки «липкими». В противном случае один вызов классификатора считывает описания из реестра агентов, и если он не уверен, запрос переходит к агенту по умолчанию, а не к случайному выбору. Один общий парадный вход означает, что N агентам нужно N+1 контейнеров, а не 2N, а также один путь идентификации и один аудиторский след вместо множества.
Доступ: инструменты, идентификация и модели
Доступ имеет три уровня, и мы держим их раздельно, потому что они выходят из строя по-разному.
Инструменты предоставляются через именованные гранты. Грант сопоставляется с целевым объектом на одном управляемом шлюзе инструментов — шлюзе MCP. Шлюз хранит учетные данные провайдера, а агент — никогда, поэтому в коде или среде агента нет ничего, что могло бы привести к утечке. У нас 31 грант в девяти системах, включая GitHub, Atlassian, Datadog и Slack. Основную работу выполняют несколько соглашений:
- Чтение и запись — это отдельные гранты, а права на запись дополнительно разделены. Создание ветки, открытие пулл-реквеста и его рецензирование — это три разных гранта, поэтому агент, который только обновляет существующие пулл-реквесты, не может создавать произвольные ветки. Слияние (merge) не входит ни в один из них.
- Списки разрешенных объектов (allowlists) закреплены в каталоге, и каталог отказывается загружаться, если два гранта на один целевой объект претендуют на один и тот же инструмент, поэтому никакое изменение не может незаметно расширить возможности агента.
- Некоторые инструменты невозможно ограничить. Инструмент, который выполняет действия в зависимости от аргументов (чтение, запись или удаление), делает список разрешенных бесполезным, поскольку никто не может заранее определить при проверке, что именно он выполнит во время запроса. Мы полностью исключаем такие инструменты.
- Для каждой границы арендатора (tenant boundary) выделяется отдельный грант. У нас один грант на учетную запись Galaxy, потому что агент, которому нужна телеметрия продукта, не должен обладать учетными данными для чтения данных о выручке.
- Запрос гранта — это одна проверенная строка в манифесте в пулл-реквесте, как и любое другое изменение.
Идентификация основана на Okta. «Парадный вход» сопоставляет каждого пользователя Slack с его личностью в Okta, каждый запрос несет эту личность в контексте, привязанном к платформе, а каждый вызов инструмента записывает аудит, содержащий имя пользователя, агента, инструмента, результат и способ подтверждения личности.
Модели объявляются в манифесте и доступны через управляемый доступ к моделям на Bedrock, поэтому ни один агент не хранит ключ провайдера. Чего у нас пока нет, так это контроля над путем к модели. Каждый агент создает своего собственного клиента, и в разделе о том, что пошло не так, объясняется, во что нам это обошлось.
Примитивы, вызываемые агентами
Помимо доступа, платформа предоставляет небольшой набор функций, которые агент вызывает из своих инструментов. Каждая из них появилась, когда реальному агенту потребовалась соответствующая возможность.
Состояние. Вызовы не сохраняют состояние, что создает проблему, когда агенту нужно что-то запомнить. Один из нас столкнулся с этим при работе агента мониторинга, чьи запланированные запуски каждый раз начинались с чистого листа. Первая версия хранила состояние в хранилище секретов и столкнулась с ограничением в 64 КБ, отсутствием возможности запросов и политикой доступа, которая блокировала разработчику доступ к его собственному секрету. Это трение превратилось в примитив: одна таблица на агента, подключаемая через конфигурацию инфраструктуры, представленная в виде простых функций get и set, чтобы модель никогда не касалась базы данных. Позже мы добавили операцию compare-and-swap, после того как два процесса записи одновременно попытались выдать один и тот же идентификатор бэклога.
Общее хранилище. Агентам, которые работают с одним корпусом данных, нужно место для его размещения. Общее объектное хранилище предоставляет каждому агенту, который выбрал эту опцию, доступ на чтение или запись только к именованным префиксам. Поскольку это пересекает границы агентов, чтение по умолчанию ограничено.
Извлечение данных. Агент, который указывает базу знаний, получает доступ к извлечению данных строго в рамках этой базы. Мы предоставляем доступ к извлечению, но запрещаем генерацию, потому что позволить базе знаний вызывать модель означало бы выйти за пределы области ресурсов, которую мы ограничили.
Файлы. Вложения перемещаются между Slack и инструментами через именованные экстракторы, объявленные в манифесте, с ограничением по размеру и изолированным каналом для каждого вызова, чтобы параллельные запуски не видели файлы друг друга. Содержимое файлов не проходит через модель.
Планирование. Планирование — это способ вызова агента, а не отдельный тип агента. Планировщик и служба выполнения вызывают один и тот же маршрут /invocations по таймеру. Агент подключается через одно поле в манифесте, которое по умолчанию отключено, а запланированный вызов несет свой собственный класс идентификации, чтобы последующие записи могли отличить системный запуск от инициированного пользователем.
Одноранговые вызовы. Агент может вызывать одноранговые узлы, указанные в его манифесте, через один инструмент на каждый узел. На данный момент эти узлы должны быть агентами с доступом только для чтения на Fargate.
Потоковая передача. Вызывающие стороны, которые запрашивают это, получают кадры прогресса и сигналы heartbeat вместо ожидания одного ответа.
Наблюдаемость. С конца сентября каждый вызов отправляет метрики в Datadog из одного места — слушателя «парадного входа», который является единственным процессом, видящим каждый запрос в обеих средах выполнения. Планировщик отправляет те же метрики для запланированных запусков. Агенты не пишут код инструментирования. Платформа прикрепляет телеметрию к каждому вызову (токены, циклы, причина остановки, модель и длительность), а слушатель оценивает стоимость по локальной таблице, так как провайдер модели не возвращает данные о стоимости. Вызовы, результаты, использование и предполагаемые расходы разбиваются по агентам, средам выполнения, моделям и владельцам, при этом вызывающие стороны псевдонимизируются. Каждая строка лога содержит имя агента, а если запрос был от человека — имя этого человека. Новый агент появляется на дашбордах при первом же его вызове.
Что говорит история наших пулл-реквестов
С момента первого пулл-реквеста 15 июля было объединено 569 изменений. 55% — это работа агентов, 24% — работа над платформой. За последние 30 дней было объединено 230 изменений, 40 из которых затронули платформу. Четверть того, что мы выпускаем спустя три месяца, — это все еще сама платформа. Мы не построили платформу и не закончили ее. Мы продолжаем ее строить, потому что парк агентов постоянно требует большего.
Что пошло не так
Одобрения. Мы создали механизм защиты, который пропускает одобрение человека только в том случае, если одобряющий — это тот же человек, который сделал запрос. Первая версия молча отклоняла все реальные одобрения. Исправление проверяет одобряющего на соответствие пользователю, сделавшему запрос, и блокирует действие, если личность не подтверждена. Блокировка при ошибке — это правильное поведение, но оно выглядит так, будто ничего не происходит, поэтому вам нужен тест, доказывающий, что одобрение может пройти успешно.
«Здоровый» и ошибочный. Во время сбоя шлюза агенты с неработающими грантами на инструменты продолжали отвечать на проверки работоспособности, что они «здоровы». Мы разделили работоспособность гранта и доступность службы. Три последовательные ошибки по гранту интерпретируются как деградация, и этот сигнал исключается из решения о перезапуске, чтобы сбой провайдера не превратился в цикл перезагрузок.
Проверки, которые прошли на синтетических данных. Агент, работающий с конфиденциальными записями, прошел все проверки, которые мы написали на синтетических входных данных, а затем потерпел неудачу на первых же реальных. Проверка, которая видела только вымышленные данные, не является протестированной.
Общая служба, которую никто не смог внедрить. До появления платформы мы создали прокси-сервер для моделей — службу, через которую мог проходить каждый вызов модели, чтобы мы могли видеть стоимость и контролировать маршрутизацию в одном месте. Ни один агент ее не использовал. Отправка вызовов через нее означала изменение нескольких строк в каждом агенте, которые делают вызов модели, а у каждого агента была своя копия этих строк. Никто не собирался редактировать десятки агентов ради внедрения общей службы. Урок в том, что общая служба внедряется только тогда, когда платформа владеет путем к ней. Мы стандартизировали, какую модель агент объявляет в манифесте, но не то, как выполняется вызов. Теперь мы берем этот путь под свой контроль.
Что мы еще не решили
Действуя от лица пользователя. Сегодня журнал аудита фиксирует, кто запросил каждое действие. Система на другом конце, например GitHub или Jira, знает только то, что вызов поступил от Nebula, поэтому она применяет разрешения платформы и записывает имя платформы. Мы создаем путь, по которому вызов осуществляется от лица того, кто его запросил, чтобы нисходящая система применяла разрешения этого человека, а в ее собственном аудите указывалось имя человека.
Стоимость мы можем оценить, но не доказать. Поставщик модели не возвращает стоимость за вызов, поэтому мы сами рассчитываем цену каждого обращения на основе количества токенов и таблицы цен. Мы не сверяли эти оценки со счетами. Мы не распределяем затраты на инфраструктуру по агентам, не существует единого представления о состоянии всего портфеля, и ничто не сигнализирует о скачках расходов, поскольку информационные панели были настроены вручную без мониторов.
Оценка (Evals). Оценка — это тест, подтверждающий, что агент по-прежнему хорошо отвечает на реальные входные данные, который запускается перед развертыванием. Строительные блоки сегодня существуют внутри одного агента: обвязка, которая запускает реального агента с реальной моделью и реальными данными, при этом каждая запись отправляется в песочницу, а Slack заблокирован, а шаблоны калибруются на реальных входных данных перед отправкой. Это еще не функция платформы. Она должна работать на реальных входных данных, поставляться с каждым агентом по умолчанию и блокировать развертывание, а не просто сообщать о результатах.
Дрейф. Агент — это промпт, набор прав доступа и модель, и каждый из них может меняться самостоятельно. Кто-то редактирует промпт, добавляется право доступа, обновляется модель. Со временем агент может перестать делать то, что указано в его кратком описании. Главный вход маршрутизирует вопросы, используя эти описания, поэтому агент, подвергшийся дрейфу, также получает неверные вопросы. Имея более восьмидесяти агентов, у нас пока нет систематического способа это отследить.
Если агенты множатся и в вашей компании, вот пять вопросов, на которые мы хотели бы получить ответы для любого из них. Кто владелец. К чему он имеет доступ. От чьего имени он действует. Сколько это стоит. Как его отключить. На некоторые мы можем ответить четко, а остальные еще прорабатываем. Стоимость — это то, на что мы пока не можем ответить. Мы хотели бы обменяться опытом с теми, кто продвинулся дальше.
Спасибо различным инженерным командам и каждому инженеру, который создал агента, включая тех, кто не пишет код профессионально.












