ИИ-агенты для безопасности
Зачем автоматизировать recon и тесты на API NotHoly
Модель, которая не отказывает, плюс CVE, advisory и чтение публичных страниц без оплаты токенами. Активную работу делают ваши инструменты.
Как устроен tool use: модель возвращает структурированный вызов, ваша программа-хост запускает инструмент, а правила подтверждения определяют, что разрешено.
Чат-модель выдаёт текст. Агент — это система, которая даёт модели больше: она читает запрос, решает, что ей нужны сведения или действие извне разговора, просит программу-хост выполнить работу и продолжает с полученным результатом. Почти каждый агент построен на одном механизме: tool use, он же function calling.
В статье этот механизм разобран по шагам на формах запросов и ответов из публичной документации API. Вы увидите, почему решение о том, что действительно запустится, принимает хост, а не модель, почему действиям с побочными эффектами нужно решение человека и что список OWASP называет excessive agency.
Лаборатория не нужна. Короткий локальный набросок ближе к концу прогоняет цикл с заглушкой вместо модели, так что поток управления виден без единого обращения к API.
У агента три части.
Модель принимает текст и выдаёт текст. При tool use она может выдать ещё и структурированный запрос вида «вызови эту функцию с такими аргументами».
Хост — это программа, которую вы пишете или запускаете. Он отправляет запросы модели, хранит разговор, читает вывод и решает, что с ним делать. Настоящий код каждого инструмента находится у хоста.
Инструменты — это функции, которые хост согласен выполнять по просьбе модели: найти файл, сделать запрос к базе данных, скачать страницу, отправить сообщение. У каждого инструмента есть имя, описание и схема аргументов.
Модель предлагает, хост распоряжается. Большинство вопросов безопасности дальше в статье вытекает из этого разделения.
Модель без инструментов может отвечать только из своего контекста. Спросите у неё сегодняшний курс валют, и она станет гадать или скажет, что не знает. Дайте ей инструмент погоды, и она сможет запросить значение у хоста, получить его и ответить на его основе.
Цикл допускает и несколько шагов. Модель может выполнить поиск, прочитать результат, решить, что этого мало, перейти по найденной ссылке и затем подвести итог. Каждый шаг идёт отдельным обменом с моделью, и хост управляет каждым из них: сколько их разрешено, какие инструменты предложены и что происходит при сбое вызова.
Любой обмен с tool use идёт в одной и той же последовательности, какой бы ни был API.
Цикл заканчивается, когда модель возвращает обычный текст, когда хост доходит до лимита ходов или когда хост его останавливает.
В определении инструмента есть имя, описание для модели и схема входных данных. Схема на JSON Schema объявляет тип каждого аргумента и то, какие аргументы обязательны. Модель выбирает инструменты, читая их описания, поэтому расплывчатое описание приводит к неверным вызовам.
Сама модель ничего не запускает. Она возвращает данные с именем функции и аргументами в виде объекта JSON. Вывод структурирован, поэтому хосту не приходится угадывать имя функции из свободного текста.
Структурированный вызов может быть неверным. Модель может назвать несуществующий инструмент или передать значение, которое подходит под схему, но не подходит к ситуации.
Этот шаг принадлежит только хосту. Хост ищет имя в своём реестре. Если имени нет, он возвращает ошибку. Если имя есть, хосту всё равно нужно решить, можно ли запустить этот вызов с этими аргументами для этого пользователя. Только после этого он выполняет код.
Результат уходит сообщением, в котором указан ID исходного вызова. Модель читает его как данные и решает, что делать дальше. Относитесь к этим данным как к данным и не исполняйте их как инструкции. На скачанной странице могут быть предложения, похожие на команды. В этом суть prompt injection, и хост не должен позволять возвращённому содержимому менять свои правила.
Формы ниже следуют двум публичным справочникам по tool use, проверено 2026-11-08. Идентификаторы моделей здесь условные. Имена полей точные.
{
"model": "<model-id>",
"max_tokens": 1024,
"tools": [
{
"name": "get_weather",
"description": "Get the current weather for a given location.",
"input_schema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and state, e.g. San Francisco, CA"
}
},
"required": ["location"]
}
}
],
"messages": [
{ "role": "user", "content": "What's the weather in San Francisco?" }
]
}tools — полный список функций, которые может вызвать модель. Всё, чего в нём нет, для модели не существует.name — то, что вернёт модель и по чему хост выберет функцию.description читает модель, поэтому там нужно написать, что делает инструмент и когда его использовать.input_schema — это JSON Schema. required сообщает модели, какие аргументы она обязана передать.{
"stop_reason": "tool_use",
"content": [
{
"type": "tool_use",
"id": "toolu_01A09q90qw90lq917835lq9",
"name": "get_weather",
"input": { "location": "San Francisco, CA" }
}
]
}stop_reason со значением tool_use означает, что модель ждёт результат. Хост, который читает только текст, пропустит вызов.type, равным tool_use, и есть структурированный вызов. Его id нужно вернуть обратно.input должен соответствовать схеме. Всё равно проверяйте его, потому что модель не умеет гарантировать соответствие схеме.{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A09q90qw90lq917835lq9",
"content": "15 degrees Celsius, partly cloudy"
}
]
}tool_use_id должен совпадать с id вызова. При несовпадении связь теряется.content коротким. Большие дампы расходуют контекст и могут нести внедрённый текст.Во втором справочнике у той же идеи другие имена. У инструмента type равен function, а модель возвращает элемент function_call, поле arguments которого содержит строку JSON, и хост должен её разобрать.
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"parameters": {
"type": "object",
"properties": {
"location": { "type": "string" }
},
"required": ["location"],
"additionalProperties": false
},
"strict": true
}{
"type": "function_call",
"id": "fc_12345xyz",
"call_id": "call_12345xyz",
"name": "get_weather",
"arguments": "{\"location\":\"Paris, France\"}"
}{
"type": "function_call_output",
"call_id": "call_12345xyz",
"output": "{\"temperature\":\"25\",\"unit\":\"C\"}"
}В этом справочнике правило сказано прямо: когда модель вызывает функцию, ваше приложение должно её выполнить и вернуть результат. Имена полей различаются между эндпоинтами и версиями, так что проверяйте их, прежде чем копировать форму.
Легко представить, что модель «пользуется» инструментом, как человек калькулятором. Эта картинка скрывает главное. Модель пишет текст с описанием вызова, а превратится ли этот текст в действие, решает хост.
Отсюда три задачи хоста. Код выполняется только здесь, поэтому функция удаления запустится, только если её запустит хост. Только здесь можно применить политику: строка в промпте «не записывай файлы» остаётся просьбой, а отсутствие инструмента записи или роль базы данных только для чтения уже служат средством контроля. И хост управляет охватом, потому что выбирает, какие инструменты предложить на каждом ходу. Предложить меньше инструментов — самое дешёвое средство контроля из доступных.
Спецификация MCP описывает то же разделение на языке протокола. Хост — это LLM-приложение, клиенты — коннекторы внутри него, а серверы предоставляют инструменты. Согласно спецификации, хосты должны получить явное согласие пользователя перед вызовом любого инструмента, а описания инструментов, например аннотации, следует считать недоверенными, если они не получены от доверенного сервера. Страница спецификации, которую я проверял 2026-11-08, указывает на редакцию от 2026-07-28, так что прочитайте текущую редакцию, прежде чем строить на ней что-либо.
Некоторые вызовы инструментов только читают. Поиск или справка ничего не меняют за пределами хоста. Другие что-то меняют: отправляют сообщение, правят запись, тратят деньги или удаляют данные.
Ошибочное чтение даёт неверный ответ, который кто-то может проверить. Ошибочная запись может оказаться необратимой, затронуть других людей и какое-то время оставаться незамеченной. Поэтому разумная схема такова: разделить инструменты по эффекту и требовать явного решения человека перед каждым вызовом с побочным эффектом. Хост показывает действие простыми словами с настоящими аргументами и ждёт согласия.
Подтверждайте конкретный вызов, сессию целиком подтверждать не стоит. Разовое согласие на «отправить письмо» не должно незаметно распространяться на следующие сто сообщений другим адресатам. Подтверждение перестаёт работать и тогда, когда проверяющий не вчитывается, поэтому дополняйте его видимыми аргументами, ограничениями на то, что может сделать один вызов, и логами.
OWASP Top 10 for LLM Applications называет excessive agency под номером LLM06:2025. Там описана уязвимость, из-за которой действия, наносящие ущерб, выполняются в ответ на неожиданный, неоднозначный или подстроенный вывод модели, независимо от причины. Частые триггеры — галлюцинации и prompt injection, прямая или косвенная.
Названы три первопричины: excessive functionality (инструменты, которые задаче не нужны), excessive permissions (инструменты, которые могут больше, чем требует задача) и excessive autonomy (действия с большими последствиями без проверки). Меры защиты следуют из этих причин. Предлагайте только те инструменты, которые нужны задаче. Предпочитайте узкие инструменты универсальным оболочкам и загрузчикам произвольных URL. Применяйте минимальные права в нижележащей системе, например через роли базы данных или узкие скоупы OAuth. Выполняйте действия с правами самого пользователя. Требуйте подтверждения человека для действий с большими последствиями. Проверяйте авторизацию в нижележащей системе и не полагайтесь на решение модели.
Логирование, мониторинг и лимиты запросов ограничивают ущерб. Проблему они не предотвращают.
В этом наброске нет ни провайдера, ни сетевых обращений, ни секретов. «Модель» здесь — фиксированная заглушка. На первом ходу она возвращает вызов инструмента, а после получения результата выдаёт текст. Задача наброска — показать, где находится хост. Замените заглушку настоящим вызовом API в своей среде, а логику хоста оставьте.
Сохраните это как loop_demo.py:
# Local sketch: no provider, no network. The "model" is a fixed stub that
# returns the same shape a provider returns for a tool call.
import json
def get_weather(location: str) -> str:
return json.dumps({"location": location, "temp_c": 15, "sky": "partly cloudy"})
def delete_file(path: str) -> str:
return json.dumps({"deleted": path}) # never reached in this demo
# The host decides what may run. The model only names a tool.
REGISTRY = {"get_weather": get_weather} # delete_file is NOT registered
REQUIRES_APPROVAL = {"delete_file"}
def fake_model(messages):
# Stub: first turn asks for weather, second turn answers with text.
last = messages[-1]
if last["role"] == "user" and isinstance(last["content"], str):
return {"stop_reason": "tool_use",
"content": [{"type": "tool_use", "id": "toolu_demo_1",
"name": "get_weather", "input": {"location": "Lisboa, PT"}}]}
return {"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]}
messages = [{"role": "user", "content": "Weather in Lisboa?"}]
for step in range(5): # hard cap on turns
reply = fake_model(messages)
messages.append({"role": "assistant", "content": reply["content"]})
if reply["stop_reason"] != "tool_use":
print("final:", reply["content"][0]["text"])
break
results = []
for block in reply["content"]:
if block["type"] != "tool_use":
continue
name, args = block["name"], block["input"]
if name not in REGISTRY:
out, err = f"unknown tool {name}", True
elif name in REQUIRES_APPROVAL:
out, err = "approval required", True
else:
out, err = REGISTRY[name](**args), False
print(f"host ran {name}({args}) -> {out}")
results.append({"type": "tool_result", "tool_use_id": block["id"],
"content": out, "is_error": err})
messages.append({"role": "user", "content": results})Запускайте из папки, где лежит только этот файл:
python -I loop_demo.pyФлаг -I запускает Python в изолированном режиме. Переменные окружения игнорируются, а текущая папка не попадает в путь импорта. Это полезная привычка для кода, который писали не вы.
На машине с Windows 11, Git Bash и Python 3.12.10 запуск вывел:
host ran get_weather({'location': 'Lisboa, PT'}) -> {"location": "Lisboa, PT", "temp_c": 15, "sky": "partly cloudy"}
final: doneЗаглушка вернула причину остановки tool_use и вызов get_weather. Хост нашёл имя в реестре, запустил функцию и записал tool_result обратно в разговор. Затем заглушка ответила текстом, и цикл закончился. delete_file не запускался, потому что хост его не регистрировал. Чтобы увидеть ветку подтверждения, заставьте заглушку запросить delete_file и посмотрите, как хост откажет.
Предлагать все инструменты из кодовой базы. Модель использует то, что видит, и этим же может воспользоваться любой текст, который она читает. Начните с короткого списка и добавляйте инструменты по одному.
Писать расплывчатые описания. «Что-то делает с файлами» провоцирует неверные вызовы. Напишите, что инструмент читает или записывает и когда его использовать не надо.
Доверять аргументам, потому что они прошли схему. Проверка схемы подтверждает форму, смысл она не проверяет. Правильно оформленный путь всё равно может вести за пределы разрешённой папки.
Доверять возвращённому содержимому. Результат инструмента — это данные. Если вставить его в следующий промпт как инструкции, веб-страница получит возможность управлять вашим хостом.
Не ставить лимит ходов. Цикл без ограничения может повторять сбойный вызов, пока это не обойдётся в реальные деньги.
Подтверждать сессию целиком. Подтверждайте конкретные вызовы с видимыми аргументами.
Логировать не то. Записывайте имена инструментов, решения и результаты, следя за персональными данными. В полных промптах и ответах в логах могут оказаться данные, которые вы не должны были хранить.
Соберите цикл с настоящим API-ключом и одним инструментом только для чтения, например функцией, которая возвращает текущее время. Изучите тела запроса и ответа и найдите блок tool_use и возвращаемый вами tool_result.
Затем добавьте инструмент, который пишет во временный файл в вашей папке, и поставьте перед ним шаг подтверждения. Попросите модель указать путь вне этой папки и проверьте, что хост останавливает вызов.
Прочитайте разделы спецификации MCP об инструментах и безопасности и статью OWASP об excessive agency целиком. Сопоставьте каждую меру защиты со строкой в своём коде.
Проверено 2026-11-08. Страницы документации меняются, поэтому уточните текущую версию, прежде чем полагаться на имя поля.
ИИ-агенты для безопасности
Модель, которая не отказывает, плюс CVE, advisory и чтение публичных страниц без оплаты токенами. Активную работу делают ваши инструменты.
ИИ-агенты для безопасности
Устройство агента для bug bounty: модель, инструменты, память и ограничители в цикле на вашей машине под вашим подтверждением.
ИИ-агенты для безопасности
Небольшой агент bug bounty на Python с вызовом инструментов на API NotHoly: предлагает команды recon, ждёт подтверждения, читает вывод и ведёт заметки.