Читать 5 мин

Как AI-агентам открывать pull request с безопасными ограничениями

AI-агентам, открывающим pull request, нужны строгие ограничения репозитория, защищённые ветки, маршрутизация ревьюеров, свидетельства тестов и запись аудита для каждого изменения.

Как AI-агентам открывать pull request с безопасными ограничениями

AI-агенты, открывающие pull request через API, могут заметно экономить время инженеров, но только если репозиторий рассматривает каждое сгенерированное изменение как недоверенный вклад с отслеживаемым автором. Граница здесь простая: агент может подготовить предлагаемое изменение, а люди и правила репозитория решают, должно ли оно попасть в кодовую базу.

Я видел, как команды делали этот процесс небезопасным: сосредотачивались на качестве кода модели и забывали о правах вокруг неё. Обычно всё ломается по довольно прозаичным причинам. Задача для staging-сервиса оказывается в production-репозитории. Широкий токен незаметно даёт доступ на запись ко всем проектам. После тайм-аута агент открывает десять почти одинаковых pull request. Кто-то сливает один из них, потому что заголовок звучит правдоподобно.

Безопасная схема не рассчитывает на осторожность агента. Она ограничивает область его действий, пропускает изменение через обычную проверку и оставляет достаточно подробную запись для последующего расследования.

Область репозитория должна быть явной и проверяться автоматически

Агенту нужно выдавать доступ к названному набору репозиториев, а не к организации целиком с расчётом сузить область позже. Область репозитория отвечает на конкретный вопрос: какие кодовые базы этот процесс может читать, в какие ветки отправлять изменения и для каких репозиториев открывать pull request?

Храните список разрешений отдельно от инструкций агента. Промпты могут направлять поведение, но не обеспечивают авторизацию. Компонент, который хранит учётные данные репозитория или выполняет API-запрос, должен отклонять репозиторий, отсутствующий в списке разрешений.

Для каждого разрешённого репозитория задайте допустимые базовые ветки и пространство имён для записи. Например:

repositories:
  - name: acme/payments-api
    base_branches: ["main", "release/2025.1"]
    write_branch_prefix: "agent/"
    pull_request_drafts: true
  - name: acme/docs
    base_branches: ["main"]
    write_branch_prefix: "agent/"
    pull_request_drafts: false

Этот фрагмент предотвращает распространённую ошибку: агент получает запрос «исправить текст оформления заказа», находит похожий файл в доступном для поиска репозитории и записывает изменения туда просто потому, что его учётные данные это позволяют. Список разрешений превращает такую попытку в отказ, а не в чужую задачу по уборке.

Область доступа включает и операции с репозиторием. Большинству агентов нужно читать файлы, создавать ветку, отправлять коммиты, читать результаты проверок, а также создавать или обновлять pull request. Им редко требуется менять настройки репозитория, регистрировать вебхуки, изменять правила защиты веток, добавлять ключи развёртывания, управлять участниками или сливать изменения. Не выдавайте такие права только потому, что они входят в удобный широкий токен.

Используйте отдельную учётную запись бота, а не личный токен разработчика. Личный токен размывает авторство, плохо переживает смену задач и часто несёт права, о выдаче которых уже никто не помнит. Учётная запись бота даёт одного исполнителя, которого можно заблокировать при проблемах.

Доступ на чтение требует такого же внимания, как доступ на запись. Агент, который может просматривать каждый приватный репозиторий, способен раскрыть исходный код или конфигурацию через собственные журналы, контекст задачи и ответы. Дайте ему минимальный набор полезных репозиториев, даже если он не получает прямых прав на запись.

Префикс ветки задаёт границу выполнения, а не просто правило именования

Агент должен создавать ветки только внутри выделенного префикса, например agent/, а сервер репозитория должен проверять это ограничение. Правило, записанное в промпте, рано или поздно нарушится из-за неверного вызова инструмента, ошибки повтора или попытки агента выполнить слишком широкую задачу.

Защитите main, релизные ветки, ветки окружений и любые ветки, из которых выполняется автоматическое развёртывание. Агент не должен отправлять в них изменения, выполнять принудительную отправку или менять правила их защиты.

Имена веток должны быть достаточно детерминированными для расследования и достаточно уникальными, чтобы не возникали конфликты. Добавьте ссылку на задачу и короткий случайный суффикс или идентификатор запуска:

agent/OPS-1842-retry-payment-7f3a

Не позволяйте агенту напрямую использовать заголовки задач в именах веток. В заголовках могут оказаться секреты, имена клиентов, небезопасные символы или вводящие в заблуждение формулировки. Создавайте имя ветки в контроллере и передавайте его агенту как неизменяемое значение.

Для базового коммита тоже нужно явное правило. В начале запуска контроллер должен определить SHA одобренной базовой ветки и записать его. Агент создаёт ветку от этого SHA, а не от того, что main означает после долгой генерации кода. Это не устраняет расхождения, но делает их заметными и воспроизводимыми.

Ветка может содержать только коммиты, связанные с назначенной задачей. Никакого попутного форматирования, несвязанного обновления зависимости или попытки «навести порядок» в соседнем коде только потому, что он показался странным. Сгенерированные изменения часто выглядят убедительно, и ревьюеры могут пропустить посторонние правки внутри в остальном разумного патча.

Задайте лимит изменений до начала работы. Он может ограничивать число изменённых файлов, общее число строк или пути за пределами запрошенного компонента. Сам по себе лимит не оценивает риск. Это сигнал остановки: он подсказывает агенту запросить новую задачу, а не незаметно превратить небольшой ремонт в переписывание всего репозитория.

Создание pull request требует проверяемой транзакции API

Успешный HTTP-ответ ещё не доказывает, что агент открыл правильный pull request. Контроллер должен проверить репозиторий, head-ветку, базовую ветку, SHA коммита и возвращённый идентификатор pull request, прежде чем сообщать об успехе.

Для API в стиле GitHub важны поля запроса с предлагаемым заголовком, head, base, текстом и статусом draft. Конкретная конечная точка зависит от платформы, но проверки безопасности остаются теми же:

{
  "title": "OPS-1842: retry transient payment gateway failures",
  "head": "agent/OPS-1842-retry-payment-7f3a",
  "base": "main",
  "body": "Task: OPS-1842\nBase commit: 4b2c...\nTests: unit payment retry suite\nLimits: no configuration changes",
  "draft": true
}

Перед отправкой запроса получите сведения о ветке и убедитесь, что её конечный SHA совпадает с коммитом, записанным для этого запуска. После ответа снова запросите pull request и сравните его head, base и состояние с исходным запросом. Сохраняйте неизменяемый номер pull request или идентификатор узла, выданный платформой, а не только его URL.

Повторы требуют особого обращения. Сетевой тайм-аут создаёт классическую проблему дубликатов: сервер мог создать pull request 418, но клиент не получил ответ и повторил запрос. Храните в контроллере запись идемпотентности с ID задачи, репозиторием, веткой, базовым SHA и номером pull request. При повторе сначала ищите существующую ветку и открытый pull request, и только потом выполняйте запрос создания.

Не используйте заголовок задачи как единственное значение идемпотентности. Повторяющийся запрос вроде «обновить сгенерированную документацию» столкнётся с предыдущим запуском. Идентификатор запуска должен отличать один запуск от другого, а ID задачи помогает связать связанную работу.

Черновые pull request хорошо подходят для работы агентов по умолчанию. Они показывают ревьюерам, что изменение существует, но ещё не прошло заявленный автором этап готовности. Агент может перевести pull request в готовое состояние только после выполнения обязательных команд и сохранения их результата в записи запуска. Если репозиторий не использует draft, добавляйте метку вроде agent-created через доверенный контроллер, а не через текст, составленный агентом.

Назначение ревьюеров должно учитывать владельцев и риск

Первый ревьюер должен определяться правилами владения репозиторием, а не догадкой агента о том, кто лучше разбирается в теме. В документации CODEOWNERS от GitHub описано сопоставление файлов и владельцев, которое может запрашивать проверку изменённых путей. В GitLab есть сопоставимые механизмы одобрения и владельцев кода. Эти файлы полезны для маршрутизации, но сами по себе не делают ревью обязательным, если этого не требуют правила защиты ветки или слияния.

Это различие важно. Репозиторий может показать запрошенного владельца кода и всё равно разрешить слияние без его одобрения, в зависимости от настроек. Считайте маршрутизацию и принудительное выполнение отдельными мерами контроля. Прежде чем доверять политике, проверьте обе с помощью специально созданного тестового pull request без разрешения.

Используйте список изменённых файлов после последнего коммита, а не пути, которые агент планировал изменить. Сгенерированный патч может в конце затронуть общую библиотеку, каталог развёртывания или папку миграций. Расчёт ревьюеров должен учитывать фактические изменения.

Добавьте ответственного человека, если задача затрагивает области, для которых правила владения слишком широки или отсутствуют. Этот человек отвечает за смысл задачи. Владелец кода может подтвердить соответствие реализации компоненту, а ответственный человек проверяет, подходит ли запрошенное поведение продукту. Не назначайте десяток людей только потому, что diff пересёк границы. Длинный список ревьюеров приводит к знакомой ситуации: каждый считает, что сложную часть проверит кто-то другой.

Некоторые пути должны автоматически направлять изменение на более строгую проверку. Например, миграции баз данных, код авторизации, определения сборки и релиза, манифесты зависимостей, конфигурация инфраструктуры и сгенерированные артефакты. Правильный ответ не всегда «запретить агенту». Часто нужно «назначить владельца, который понимает последствия». Миграция может пройти модульные тесты и при этом сделать откат невозможным.

Назначайте ревьюеров через API только после создания pull request и проверяйте результат. Если группе владельцев нельзя отправить запрос, контроллер должен пометить pull request как заблокированный, а не молча заменить её случайным разработчиком. Тихая подмена превращает правило владения в декорацию.

Тесты описывают свидетельства, а одобрение решает, принимать ли изменение

Выбирать фиксированные элементы управления
Используйте лестницу решений Sallyport из трёх элементов вместо собственных правил для действий агентов.

Агент должен точно указывать, что он запустил, что не запускал и почему. Фраза «тесты прошли» бесполезна без команд, кодов завершения и SHA коммита, который проверялся. Храните эти свидетельства также вне текста pull request, поскольку агент может изменить текст позже.

Используйте небольшой структурированный отчёт для каждого запуска:

{
  "run_id": "run_01J...",
  "repository": "acme/payments-api",
  "head_sha": "8c71...",
  "commands": [
    {"command": "npm test -- payment-retry", "exit_code": 0},
    {"command": "npm run lint", "exit_code": 0}
  ],
  "not_run": ["integration suite requires payment sandbox approval"]
}

Контроллер должен отклонять перевод в состояние готовности к ревью, если в свидетельстве указан SHA, отличный от конечного SHA ветки. Это обнаруживает распространённую последовательность: агент запускает тесты, делает ещё одну «небольшую» правку и открывает pull request без повторного запуска.

Обязательные проверки статуса должны оставаться на стороне репозитория. У агента не должно быть права пропускать проверки, одобрять собственный pull request, менять защиту веток или выполнять слияние. Проверка может подтвердить, что известная команда завершилась успешно. Она не подтверждает правильность нового правила авторизации, понимание требования или саму необходимость выполнять задачу в этом репозитории.

Не заменяйте проверку diff сгенерированным резюме. Хорошее резюме помогает ревьюерам сориентироваться, но это утверждения автора. Ревьюерам нужны реальные изменения файлов, тесты, контекст соответствующей задачи и все намеренные пропуски.

Для каждого созданного изменения нужна история аудита, способная пережить инцидент

Храните ключи репозитория вне агента
Sallyport подставляет HTTP-учётные данные из зашифрованного хранилища, поэтому агент получает результаты, а не ключи репозитория.

URL pull request не является записью аудита. Он исчезает, когда репозитории перемещают, ветки удаляют, доступ меняют или кто-то редактирует описание. Храните журнал событий только с добавлением, чтобы расследование могло установить, кто запустил процесс, какой процесс выполнил каждый вызов, какой репозиторий изменился и что вернула система.

Минимально записывайте следующие поля:

  • ID запуска и исходную ссылку на задачу или тикет
  • учётную запись действующего бота и идентификатор аутентифицированного процесса агента
  • репозиторий, базовую ветку, базовый SHA, head-ветку и SHA каждого созданного коммита
  • тип API-запроса, неизменяемый идентификатор pull request, временные метки и статус результата
  • запросы на ревью, одобрения, результаты проверок, события закрытия, слияния или отклонения

Не записывайте по умолчанию в журнал аудита открытые учётные данные, полные исходные файлы или произвольные тексты задач. Для расследования нужны надёжные факты действий, а не ещё одна неконтролируемая копия чувствительных материалов. Если вы храните содержимое патча, записывайте digest содержимого и применяйте обычные правила хранения и доступа.

Полезно разделять журнал активности и журнал решений. Журнал активности говорит, что API-вызов создал pull request 418. Журнал решений показывает, кто одобрил эту сессию агента, кто изменил его права и кто их отозвал. Во время инцидента важны оба. Нужно знать, что произошло и почему в тот момент у исполнителя были соответствующие полномочия.

Sallyport может хранить сессии агентов и отдельные HTTP- или SSH-вызовы в журналах, сформированных из его зашифрованного аудита с цепочкой хешей, а sp audit verify проверяет цепочку офлайн поверх шифротекста. Это подходит для схемы, в которой агент запрашивает действие, но никогда не получает саму учётную запись репозитория.

Связывание хешами делает последующее изменение заметным, но не превращает неполную запись событий в полноценную. Записывайте идентификаторы репозитория и коммитов на границе действия. Идеально проверенная запись «HTTP-запрос отправлен» не скажет, создал ли запрос неправильный pull request.

Храните учётные данные вне агента и отделяйте создание от слияния

Агент никогда не должен получать широкий токен репозитория в промпте, окружении, файле рабочего пространства или выводе инструмента. Оказавшись в этом контексте, токен может утечь через журналы, историю shell, сообщения об ошибках, скопированные расшифровки или инструкцию, которая убедит агента вывести его. Последующее сокрытие не возвращает секрет под надёжный контроль.

Используйте шлюз действий или узкий контроллер, который принимает конкретный запрос: создать ветку в этом репозитории, отправить эти коммиты в разрешённый префикс, создать черновой pull request относительно одобренной базовой ветки или запросить этих ревьюеров. Шлюз сам подставляет учётные данные и возвращает результат. Он должен отклонять вызовы, не соответствующие объявленной области.

Права на создание и права на слияние различаются. Сервис, способный создавать pull request, может предложить тысячи плохих изменений. Сервис, способный выполнять слияние, может отправить одно плохое изменение в production. Не объединяйте эти права ради более гладкой демонстрации. Оставьте действие слияния защищённым правилами ветки и ответственным человеком, даже если агент создал безупречный патч.

Авторизация на один запуск тоже лучше, чем постоянное доверие к процессу агента. Инструменты агентов могут запускать дочерние процессы, неожиданно использоваться в других задачах и оставаться активными после завершения работы, ради которой был выдан доступ. Выдавайте процессу одну ограниченную сессию, записывайте одобрение и обеспечивайте немедленный отзыв.

Учебный сбой обнаруживает пробелы, которые скрывает текст политики

Идентифицировать каждый запуск агента
Журнал Sessions сохраняет запуски агентов и позволяет мгновенно отозвать сессию.

Проведите контролируемый учебный сбой, прежде чем полагаться на автоматическое создание pull request. Используйте тестовый репозиторий или временную политику веток и дайте агенту задачу, которая пытается пересечь каждую границу. Цель состоит в проверке отказов и записи событий, а не в демонстрации идеального сценария.

Начните с разрешённого и запрещённого репозитория. Убедитесь, что контроллер создаёт ожидаемый черновой pull request в первом случае и отклоняет второй до появления любой ветки. Затем попробуйте отправить изменения в main, выполнить принудительную отправку в ветку agent/ и открыть pull request относительно неразрешённой релизной ветки. Изучайте события репозитория, а не только сообщения контроллера.

Затем смоделируйте тайм-аут после того, как запрос создания достиг API репозитория. Перезапустите процесс и убедитесь, что он находит исходный pull request, а не открывает новый. Измените ветку после записи результатов тестов и проверьте, что переход в состояние готовности останавливается из-за несовпадения SHA.

Наконец, отзовите сессию агента во время запуска и выполните ещё один API-вызов. Вызов должен завершиться отказом, отказ должен появиться в журнале решений, а учётные данные не должны попасть в вывод агента. Если хотя бы одна из этих проверок зависит от того, заметит ли человек сообщение в чате, такого контроля пока нет.

Первое практическое действие состоит в проверке токена, который ваш агент использует сегодня. Перечислите все репозитории, которых он может коснуться, все ветки, которые может обновлять, а также возможность выполнять слияние или менять настройки. Большинство команд обнаруживают, что токен шире, чем требует задача. Сузьте область доступа, прежде чем просить агента создать ещё один pull request.

Вопросы и ответы

Какие права нужны AI-агенту для открытия pull request?

Агенту нужна отдельная учётная запись бота только с теми разрешениями репозитория и API, которые необходимы для создания веток, отправки коммитов в разрешённое пространство имён и открытия pull request. Не выдавайте ему права мейнтейнера, администратора или слияния изменений. Граница между агентом и учётными данными важна не меньше, чем текст инструкции.

Можно ли разрешить AI-агентам отправлять изменения напрямую в основную ветку?

Выделите агенту префикс веток, например agent/, и отклоняйте отправку изменений в любые другие ветки. Защитите основные и релизные ветки, чтобы обновлять их можно было только обычным путём слияния. Так агент не сможет обойти проверку, выбрав удобную целевую ветку.

Какого размера должен быть pull request, созданный AI?

В pull request должна быть одна цель, узкий diff и понятные тесты. Если задача требует несвязанного рефакторинга, обновления зависимостей и изменения поведения, разделите её. Качество проверки резко падает, когда ревьюеру приходится одновременно восстанавливать несколько решений.

Как назначать ревьюеров для pull request, созданных AI?

Используйте правила владения репозиторием для первоначального назначения, а затем добавьте ответственного человека, если изменения затрагивают сгенерированные файлы, миграции, права доступа, определения развёртывания или чувствительный к безопасности код. CODEOWNERS удобно использовать для маршрутизации, но этот механизм не заменяет обязательную проверку. Требование к ревьюеру должно задаваться правилами защиты ветки или слияния.

Что нужно записывать, когда агент создаёт pull request?

Записывайте сессию агента, личность инициатора, репозиторий, SHA коммита, имена веток, параметры запроса, URL pull request, временные метки, события одобрения и результат слияния. Сохраняйте исходную ссылку на задачу и digest патча, если это допускают правила хранения. Одних заголовка и URL недостаточно для полноценного аудита.

Безопасно ли разрешать AI-агенту автоматически создавать pull request?

Это может быть безопасно, если у агента нет прав на слияние, он работает только в разрешённых репозиториях и не может отправлять изменения в защищённые ветки. Требуйте человеческой проверки, успешного прохождения проверок и понятной записи каждого действия. Относитесь к агенту как к недоверенному участнику с быстрыми руками, а не как к мейнтейнеру.

Может ли AI-агент открывать pull request через API?

Используйте API платформы репозитория: создайте ветку от явно указанного базового коммита, отправьте коммиты и создайте pull request относительно одобренной базовой ветки. Проверьте ответ, сохраните возвращённый идентификатор и остановитесь при сбое любого запроса. Не извлекайте данные из веб-интерфейса и не считайте успех доказанным только из-за сгенерированного URL.

Должен ли агент обновлять существующий pull request или создавать новый?

Создавайте новый pull request, если ожидаемый результат изменился, исходная ветка устарела или ревьюерам нужно принять отдельное решение. Обновляйте существующий pull request, только если задача и ответственные остаются прежними. Использование старого pull request, чтобы скрыть новый объём работ, означает провал проверки.

Как предотвратить появление дубликатов pull request от агента?

Используйте данные идемпотентности в собственном контроллере: до повторной попытки сохраните ID задачи, репозиторий, базовый SHA, имя ветки и номер созданного pull request. При повторе сначала найдите ветку и существующий открытый pull request. Большинство API репозиториев примут повторные запросы, но это не делает дубликаты безвредными.

Достаточно ли успешных тестов, чтобы безопасно слить pull request, созданный AI?

Нет. Чистый diff мало говорит о том, выбрал ли агент правильное поведение, тот ли репозиторий изменил и разрешена ли выбранная зависимость. Тесты и правила ревью должны оставаться обязательными, а у людей должно быть достаточно контекста для оценки запрошенного изменения.

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov