Читать 7 мин

Универсальные HTTP-инструменты стирают границы доступа

Универсальные HTTP-инструменты скрывают много полномочий за одним вызовом. Разделяйте доступ по секрету, адресу, методу и деталям запроса.

Универсальные HTTP-инструменты стирают границы доступа

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

Из-за этого права на уровне инструмента образуют слабую границу безопасности. Разрешение использовать http_request не объясняет, что именно разрешено агенту. Полезное решение принимается уровнем ниже, где конкретный секрет можно связать с проверенным адресом, допустимым методом и ограниченными деталями запроса.

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

Один инструмент вызова дает много возможностей

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

Возьмем схему с пятью обычными полями: url, method, headers, body и credential_name. Хост может показать ее одним пунктом в списке инструментов. Но GET /projects/42 с токеном чтения и DELETE /projects/42 с токеном администратора требуют разных решений. То же касается запроса к публичному API и запроса, адрес которого разрешается во внутреннюю сеть.

Аннотации инструментов Model Context Protocol не устраняют эту проблему. Спецификация MCP называет сведения о чтении и разрушительных действиях подсказками и требует не доверять им, если они пришли не с доверенного сервера. Универсальный клиент также не может выставить всегда верное значение readOnlyHint: поведение зависит от еще не полученных аргументов.

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

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

Для разрешения нужны четыре координаты

Надежное решение по HTTP-запросу учитывает четыре координаты: секрет, проверенный адрес, метод и детали запроса. Если убрать любую из них, запрос сохранит одобренный вид, но сможет изменить результат.

Секрет определяет используемые полномочия. Токен развертывания и токен оплаты могут обращаться к одному хосту и пути, но открывать совсем разные действия. Адрес показывает, куда разрешено передать эти полномочия. Метод выражает общий смысл HTTP-операции. Остальные детали, включая путь, параметры, отдельные заголовки, тип содержимого и поля тела, задают реальную операцию.

Компактная запись разрешения может выглядеть так:

credential: issue-tracker-read
origin: https://api.example.test:443
path_prefix: /v2/issues/
methods:
  - GET
redirects: deny
headers:
  allow:
    - Accept
query:
  deny:
    - include_deleted
body: forbidden

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

Такой объект политики не подходит всем без изменений. Одним API нужны точные пути, другим нужен идентификатор арендатора в пути, третьи передают действие в теле POST. Важна форма решения: после разбора и нормализации оценить все четыре координаты вместе, а затем добавить секрет, только если запрос прошел проверку.

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

Полномочия задает секрет

Секреты должны давать отдельные наборы прав с минимальными полномочиями, которые поддерживает целевой API. Универсальный HTTP-инструмент намного безопаснее, когда исполнитель выбирает между именованными и ограниченными секретами, а не хранит один токен на всю организацию.

Области OAuth полезны, но область и аудитория отвечают на разные вопросы. Область может разрешать чтение контактов. Аудитория указывает, какой сервер ресурсов должен принять токен. RFC 8707 определяет параметр OAuth resource, с которым клиент запрашивает токен для конкретного защищенного ресурса, и рекомендует серверу авторизации ограничивать аудиторию выданного токена. Практический смысл прост: токен, переданный одному ресурсу, не должен работать на другом.

Текущая спецификация авторизации MCP применяет такое же разделение. Сервер MCP должен принимать только предназначенные ему токены и не может пересылать входящий MCP-токен следующему API. При вызове другого API сервер MCP действует как отдельный OAuth-клиент и использует отдельный токен. Универсальному HTTP-исполнителю стоит сохранить эту четкую границу, даже если агент не видит OAuth-процесс.

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

Выбор секрета должен быть виден и в окне подтверждения. Строка POST api.example.test не показывает, используется ли тестовый ключ или токен владельца аккаунта. Покажите понятную метку секрета и связанный аккаунт или арендатор, но никогда не выводите сам секрет. Если исполнитель не может определить этот контекст, разрешение слишком расплывчато для тихого повторного использования.

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

Адресом считается разрешенный конечный узел

Однократная проверка строки URL не контролирует адрес. Перед добавлением секрета исполнитель должен разобрать и нормализовать URL, разрешить имя хоста, применить сетевые правила и заново принять решение для каждого перенаправления.

Начните с точной схемы, хоста и фактического порта. https://api.example.test и https://api.example.test:8443 относятся к разным origin. Отклоняйте данные пользователя в URL, неоднозначную кодировку, неподдерживаемые схемы и имена, лишь похожие на разрешенный суффикс. Проверка, принимающая api.example.test.attacker.invalid, не считается списком разрешений.

Затем разрешите DNS-имя и проверьте каждый полученный адрес. Публичное с виду имя может вести на loopback, link-local, приватную сеть или метаданные облачного экземпляра. Между проверкой и подключением DNS-ответ также может измениться. Проверяющий компонент должен сам управлять соединением и сверять фактически использованный адрес, а не передавать URL другому клиенту для нового разрешения.

Памятка OWASP Server Side Request Forgery Prevention Cheat Sheet советует разрешать только известные доверенные адреса, если приложение может их заранее определить. Она также рекомендует отключить автоматические перенаправления, потому что они обходят проверку входных данных. Для инструментов агента совет особенно уместен: URL часто задает модель, а запрос может нести секрет, предназначенный только первому адресу.

Перенаправление требует нового решения. RFC 9110 призывает осторожно обращаться с автоматическими перенаправлениями небезопасных методов и советует удалять связанные с ресурсом поля, в том числе Authorization и Cookie. Безопасный исполнитель может поступить проще: по умолчанию запретить перенаправления для аутентифицированных вызовов либо показать новый адрес как новое действие и снова добавить секрет только после проверки.

Контроль пути важен и внутри разрешенного origin. Мультитенантные шлюзы, общие SaaS-хосты и административные маршруты могут находиться за одним именем. RFC 8707 отмечает, что путь с идентификатором арендатора иногда нужно включать в идентификатор ресурса. Список origin без ограничения пути или арендатора может оказаться намного шире, чем ожидает проверяющий.

HTTP-методы дают сигнал, но не ответ

Проверьте процесс до подтверждения
Карточка сессии сначала показывает владельца подписи процесса агента.

Ограничение методов убирает много ошибок, но имя метода не доказывает безвредность запроса. RFC 9110 определяет GET, HEAD, OPTIONS и TRACE как безопасные методы, потому что их заданная семантика сводится к чтению. PUT, DELETE и безопасные методы считаются идемпотентными: повтор операции должен дать тот же ожидаемый эффект, что и один вызов.

Безопасность и идемпотентность не совпадают. DELETE может быть идемпотентным и все равно уничтожать ресурс. POST обычно не безопасен и не идемпотентен, но API может использовать его для чтения, если поисковый запрос не помещается в URL. Система подтверждения, которая сводит риск к сравнению GET и POST, ошибется в обоих случаях.

Реальные API иногда нарушают семантику методов. RFC 9110 отдельно предупреждает о ресурсах, которые помещают удаление в параметры GET, и требует от владельца запретить небезопасное действие через безопасный метод. Исполнитель не может полагаться на соблюдение этого правила каждым сервисом. Если GET /jobs?id=7&action=cancel меняет состояние, разрешение всех GET не дает доступ только на чтение.

Учитывайте метод как один из входов. Связывайте его с путем и при необходимости с ограничениями конкретной операции. Для хорошо описанного API полезную карту дает OpenAPI: спецификация разрешает указывать требования безопасности для операции, а записи OAuth перечисляют нужные области. Импортируйте эти данные в конфигурацию, затем сравните с тем, что сервис реально выдает и принимает. Сам файл описания ничего не контролирует.

Повтор запросов тоже входит в это решение. Тайм-аут после POST не сообщает, применил ли сервер действие. Не повторяйте автоматически небезопасный неидемпотентный запрос, если API не дает механизм идемпотентности или клиент не знает, что первый запрос не сработал. Идемпотентность снижает операционный риск повтора, но не авторизует исходное действие.

Результат определяют детали запроса

Два запроса с одним секретом, origin, путем и методом могут дать противоположные результаты. Значимое действие часто скрывается в теле, параметрах и отдельных заголовках.

Платежный endpoint может принимать POST /v1/subscriptions/update и для уменьшения числа мест, и для перехода на дорогой тариф. Endpoint репозитория может использовать один маршрут изменения с полем operation для архивации, переноса или удаления. Поиск может вернуть скрытые или удаленные записи при include_deleted=true. Разрешение маршрута без ограничений этих полей разрешает все его режимы.

К заголовкам нужно относиться так же внимательно. Исполнитель должен владеть Authorization, Proxy-Authorization, Host и всеми специальными заголовками с секретами. Обычно агенту нельзя разрешать их задавать. Заголовки, выбирающие аккаунт, подменяющие пользователя или метод, запрашивающие асинхронное выполнение либо передающие условие записи, могут менять полномочия или эффект. Произвольная пересылка заголовков скрывает второй универсальный инструмент внутри первого.

Тип содержимого управляет разбором. Если политика проверяет JSON, но клиент может отправить форму, multipart или сжатые байты, агент вынесет важное поле за пределы анализатора. Требуйте заявленный тип, ограничивайте размер до буферизации, отклоняйте повторяющиеся и неоднозначные поля и авторизуйте то же представление байтов, которое уйдет в сеть. Проверка одного объекта с последующей сериализацией другого создает разрыв.

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

Точные схемы тела дорого поддерживать, поэтому тратьте усилия на маршруты с большими полномочиями. Для чтения с низким риском может хватить запрета тела и ограничения имен параметров. Для изменения аккаунта, развертывания, смены секретов, денежных операций или удаления проверяйте поля объекта, арендатора, суммы, среды и требуемого перехода. Если API предлагает более узкий endpoint или секрет, выберите его вместо сложного локального правила.

Подтверждение должно описывать готовое действие

Записывайте готовый API-вызов
Каждый HTTP-запрос виден в Activity и не скрывается за одним именем инструмента.

Полезное окно подтверждения показывает, что доверенный исполнитель отправит после нормализации, а не какой вызов предложила модель. Проверяющему нужны метка секрета, аккаунт или арендатор, разрешенный адрес, метод, путь, значимые параметры и поля тела, а также поведение перенаправлений.

Не нужно выводить в диалоге сырой JSON. В полном объекте опасное поле теряется среди меток времени и значений по умолчанию. Сначала покажите краткое описание операции, а ниже дайте открыть канонический запрос. Для развертывания можно написать, что секрет production-deployer создаст версию 2026.07.24 в рабочем арендаторе, а затем показать точный хост, путь POST и изменения тела.

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

Осознанно выберите границу повторного использования. Подтверждение на сессию может подойти для повторного чтения с узким секретом и адресом. Секрет, способный удалять ресурсы, требует подтверждения каждого вызова или намного более узкой заранее одобренной операции. Не заменяйте ограничение прав постоянными окнами. Люди перестают читать однообразные уведомления.

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

Безобидный план может привести к опасному вызову

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

Секрет чтения не работает с POST, поэтому планировщик выбирает другую запись хранилища, где упоминается автоматизация проектов. Этот токен также управляет webhook. В полученном комментарии агенту предлагают уведомить внешний callback, и планировщик передает его URL как адрес статуса. У инструмента остается разрешение на сессию, имя секрета звучит уместно, метод остается POST. Права на инструмент не замечают пересечения границы.

Первый callback отвечает перенаправлением 307 на приватный адрес. Удобная HTTP-библиотека сохраняет тело POST при таком статусе. Она может убрать автоматически созданный Authorization, но специальный заголовок из вызывающего кода останется, если клиент не обработает его явно. Теперь запрос несет полномочия проекта и содержимое задачи на непроверенный адрес. Даже если внутренний сервис отклонит секрет, тело может раскрыть данные или запустить действие без аутентификации.

Четыре согласованные проверки останавливают цепочку в разных местах. Секрет чтения не разрешает POST. Секрет автоматизации не может обращаться к незарегистрированному callback. Перенаправление требует нового решения по адресу. Правила тела запрещают произвольный callback. Ни одна проверка не держит всю защиту, а понятные имена инструмента и секрета вообще ее не дают.

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

Та же цепочка ломается и без вредного комментария. Агент может угадать endpoint по старой документации, скопировать URL из ошибки или выбрать похожий секрет после ответа 403 от нужного. Для планировщика это обычное восстановление. Защита должна ожидать такое поведение и удерживать новую попытку в исходных правах.

Каноникализация выполняется до сравнения. Декодируйте процентные сегменты по одному описанному правилу, отклоняйте точки, выходящие из разрешенного префикса, нормализуйте фактический порт и определите обработку повторных параметров. Если политика считает /v2/issues/%2e%2e/admin путем задачи, а сервер превращает его в /v2/admin, они авторизуют разные ресурсы. Не угадывайте трактовку каждого посредника, а отклоняйте неоднозначную форму.

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

Для ошибки тоже нужен закрытый путь. Если выбранный секрет получил 401 или 403, верните результат агенту и не перебирайте автоматически все записи хранилища. Резервный перебор превращает узкую ошибку в разведку полномочий. Следующая попытка должна явно назвать другой секрет и пройти новое решение с учетом его прав и аккаунта.

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

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

Журнал должен хранить решение и вызов

Подтверждайте каждое использование
Для чувствительного ключа API можно требовать клик или Touch ID при каждом запросе.

Журнал вызовов инструментов слишком груб для расследования. Запись о вызове http_request не отвечает на главные вопросы: какой секрет использован, куда ушел запрос, какая операция разрешена и совпал ли отправленный запрос с одобренным.

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

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

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

Замените широкое разрешение в исполнителе

Универсальный HTTP-интерфейс можно сохранить без универсальных полномочий. Перенесите добавление секретов и контроль в доверенный исполнитель, а от модели принимайте запрос без секретов и непрозрачную ссылку на нужную запись.

Практический переход начинается с инвентаризации реальных вызовов, а не предполагаемых. Сгруппируйте недавние запросы по секрету, origin, методу и операции. Широкие секреты и адреса, которые никогда не встречаются вместе, стоит разделить. Маршрутам с несколькими разрушительными режимами в одном теле нужны собственные ограничения или отдельные секреты сервиса.

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

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

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

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

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

Почему универсальный HTTP-инструмент опаснее специализированного API-инструмента?

Универсальный клиент меняет свои полномочия через URL, метод, секрет, заголовки и тело. Специализированный инструмент обычно фиксирует больше вариантов, но итоговый запрос все равно требует контроля.

Станет ли универсальный HTTP-инструмент безопасным, если разрешить только GET?

Нет. По семантике HTTP метод GET безопасен, но некоторые API помещают меняющие состояние действия в параметры, а ответы могут раскрыть данные или вредные инструкции. Свяжите GET с секретом, адресом, путем и допустимыми параметрами.

Нужно ли превращать каждый endpoint API в отдельный инструмент агента?

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

Что показывать в окне подтверждения HTTP-инструмента?

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

Как агент должен передавать секрет API HTTP-исполнителю?

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

Достаточно ли областей OAuth для ограничения доступа агента к API?

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

Должен ли аутентифицированный HTTP-инструмент автоматически идти по перенаправлению?

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

Как предотвратить SSRF в HTTP-инструменте агента?

Разрешайте известные схемы и origin, разрешайте DNS в контролирующем компоненте, отклоняйте запрещенные диапазоны и проверяйте фактический адрес соединения. Повторяйте контроль для перенаправлений и изменений DNS.

Что хранить в аудите HTTP-вызовов агента?

Храните идентификаторы сессии и секрета, канонический адрес, разрешенный IP, метод, отредактированное описание, данные политики и подтверждения, хеш действия, статус и итог. Записывайте отказы, но не секретные байты.

Когда каждый HTTP-вызов нужно подтверждать отдельно?

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

Sallyport

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

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