Читать 7 мин

Защищают ли лимиты декодированного размера агента от сжатых API?

Лимиты декодированного размера не дают gzip- и Brotli-ответам, маленьким при передаче, исчерпать память клиента, парсеры, логи или контекст агента.

Защищают ли лимиты декодированного размера агента от сжатых API?

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

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

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

Размер при передаче показывает лишь часть картины

Content-Length обычно описывает тело HTTP-сообщения в том виде, в котором оно передаётся. Если ответ содержит Content-Encoding: gzip или Content-Encoding: br, этот счётчик относится к сжатым байтам. Тело размером 40 КБ после декодирования может вырасти до десятков или сотен мегабайт, если во входных данных много повторов. Точный коэффициент не так важен. Проблему вызовет любой коэффициент, который превышает ваш бюджет памяти или контекста.

RFC 9110 считает кодирование содержимого преобразованием, применённым к представлению. Это важно. Приложение использует декодированное представление, а транспорт передаёт закодированное. Клиент, который задаёт лимит приложения по транспортной форме, ставит защиту не с той стороны преобразования.

Transfer-Encoding ещё больше усложняет картину. При chunked-передаче нет полезного окончательного Content-Length, на который клиент может опереться, а HTTP/2 и HTTP/3 не используют chunked-передачу таким же образом. Даже при наличии заголовка сервер может указать неверное значение. Используйте заголовок только как раннюю подсказку для отклонения, но не как доказательство безопасности тела.

Для ответа стоит записывать три величины: прочитанные байты при передаче, полученные декодированные байты и байты, сохранённые для вызывающего кода. Иногда они совпадают, но часто нет. JSON-ответ может декодироваться до 12 МБ, при разборе занять гораздо больше 12 МБ памяти, а затем потребовать сокращения до 64 КБ, прежде чем агент сможет безопасно его использовать.

Это различие также предотвращает обычный спор об одной настройке «максимальный размер ответа». Один инженер имеет в виду байты сокета. Другой - декодированные байты. Третий - текст, вставленный в результат инструмента. Дайте каждому пределу своё имя и применяйте его на той границе, которую он описывает.

Декодируйте до неограниченного буферирования

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

Для gzip-ответа в Go ограничитель декодированного потока должен оборачивать gzip reader. Этот помощник намеренно читает на один байт больше разрешённого объёма. Без дополнительного байта ответ, фактический размер которого ровно равен лимиту, неотличим от большего ответа, обрезанного на лимите.

var ErrDecodedBodyTooLarge = errors.New("decoded response exceeds limit")

func readGzipBody(r io.Reader, limit int64) ([]byte, error) {
    zr, err := gzip.NewReader(r)
    if err != nil {
        return nil, err
    }
    defer zr.Close()

    bounded := &io.LimitedReader{R: zr, N: limit + 1}
    body, err := io.ReadAll(bounded)
    if err != nil {
        return nil, err
    }
    if int64(len(body)) > limit {
        return nil, ErrDecodedBodyTooLarge
    }
    return body, nil
}

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

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

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

Ответ может исчерпать контекст раньше памяти

Защита памяти необходима, но у инструментов агента есть и другой бюджет: объём текста результата, который можно безопасно поместить в диалог агента. JSON-ответ размером 2 МБ может быть безвреден для настольного процесса и при этом стать ужасным результатом инструмента. Он способен вытеснить задачу, заставить агента разбирать несущественные записи или вынудить модель рассуждать по неполному представлению, которое выглядит завершённым.

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

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

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

Здесь рушится распространённая мысль «модель же может это суммировать». Модель способна суммировать данные, которые вы намеренно выбрали. Она не может сделать безопасной неограниченную передачу и не должна решать, сколько необработанного удалённого вывода процесс выделит в память до проверки.

Кодирование содержимого должно быть явным в контракте клиента

Считайте Content-Encoding входными данными, которые выбирают декодер, а не украшением вокруг в остальном одинакового потока байтов. Принимайте только кодировки, которые реализует ваш клиент, и ясно отклоняйте неизвестные значения. Если сервис возвращает gzip, используйте путь gzip. Если он возвращает br, используйте Brotli-декодер, обёрнутый тем же ограничителем декодированных байтов. Если кодирование содержимого отсутствует, оберните исходное тело этим ограничителем, потому что в этом случае исходные байты одновременно декодированы.

Не принимайте несколько кодировок без тщательной реализации. HTTP допускает список кодировок, и указанные преобразования идут в определённом порядке. Поддержка gzip, br означает, что декодировать нужно в обратном порядке и ставить защиту размера после каждой стадии расширения. Один финальный лимит слабее, чем кажется: промежуточная стадия может сильно вырасти до того, как следующая её уменьшит. Если вашим интеграциям не нужны вложенные кодировки, отклоняйте их, пока не появятся тесты и продуманная реализация.

Автоматическую распаковку нужно проверить. Многие HTTP-библиотеки добавляют Accept-Encoding, декодируют gzip и скрывают это от кода приложения. Для обычных запросов это удобно, но из-за этого код может считать байты не на том уровне. Выясните, выдаёт ли тело ответа байты при передаче или декодированные байты, удаляет ли библиотека Content-Encoding и показывает ли счётчики сжатых байтов. Напишите тест, который докажет поведение для точной конфигурации клиента.

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

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

Проверяйте распаковку на понятных файлах

Дайте агентам путь для действий
Встроенная прослойка sp mcp позволяет агентам с поддержкой MCP запрашивать действия, не получая API-ключи.

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

python3 -c 'open("repeat.txt", "wb").write(b"A" * (32 * 1024 * 1024))'
wc -c repeat.txt
gzip -9 -c repeat.txt > repeat.txt.gz
brotli -f repeat.txt -o repeat.txt.br
wc -c repeat.txt.gz repeat.txt.br

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

Создайте из того же источника граничные случаи: декодированное тело на один байт меньше настроенного лимита, ровно на лимите и на один байт больше. Прогоните каждый через локальный HTTP-обработчик, который правильно устанавливает Content-Encoding. Тест файлового декодера полезен, но он не поймает HTTP-библиотеку, которая автоматически распаковывает данные до срабатывания вашего ограничения.

Используйте второе семейство фикстур с реалистичной структурой. Создайте JSON с разделением по строкам, где у каждой записи есть повторяющееся поле полезной нагрузки, затем отдавайте его через gzip и Brotli. Это выявит код, который правильно работает со срезом байтов, но после декодирования позволяет JSON-парсеру или сканеру строк построить неограниченный массив. Добавьте одну запись длиннее лимита поля или строки: атакующим не обязательно повторять короткие строки, чтобы заставить парсер выделять память.

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

Сбой обычно начинается с удобного метода

Представьте агента, который вызывает API для получения данных об задачах. Обычно конечный пункт возвращает небольшую JSON-страницу. Клиент отправляет Accept-Encoding: gzip, получает ответ с Content-Length 18 КБ и записывает заголовок как доказательство умеренного размера. Затем его HTTP-помощник прозрачно распаковывает тело и вызывает ReadAll до разбора JSON.

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

Первое исправление часто добавляет if len(body) > limit после ReadAll. Такой тест делает модульный тест зелёным, но сохраняет всплеск выделения памяти. Во втором исправлении декодированный reader оборачивают ограничителем, что верно по направлению, но всё ещё передают агенту первые limit байтов как запасной вариант. Теперь агент может действовать по половине ответа, а журнал аудита не показывает чистой ошибки.

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

Это разделение важно, потому что загрузка отчёта и поиск через инструмент агента - разные операции. То, что обе используют HTTP-запрос, не делает одинаковыми их риски и бюджеты.

У лимитов должен быть владелец конечного пункта и причина

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

Записывайте лимит рядом с определением действия и объясняйте его. Фразы «в документации API указан размер страницы 100» недостаточно: одна запись всё равно может быть огромной. Полезное объяснение называет ожидаемое представление и назначение вызывающего кода, например «разобрать один объект статуса и вернуть выбранные поля» или «сохранить запрошенный пользователем архив после явного одобрения».

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

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

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

Отмена и тайм-ауты защищают от другого вида сбоев

Смотрите, какой запуск действовал
Журнал Sessions показывает запуски агента отдельно от вызовов, выполненных внутри них.

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

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

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

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

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

Лимиты разбора идут после декодирования, а не вместо него

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

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

Проверяйте данные до отображения. Поле с именем instructions, command или message, полученное от удалённого сервиса, всё равно остаётся удалённым содержимым. Его размер может укладываться в декодированный лимит, но оно всё равно может не подходить для помещения в управляющий поток агента. Выбирайте поля по схеме и передавайте их как данные. Лимит размера предотвращает один класс сбоев, но не подтверждает достоверность содержимого.

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

Считайте запись аудита отчётом о границе

Проверяйте журнал аудита
Проверяйте хеш-связанный журнал аудита Sallyport офлайн по шифротексту с помощью sp audit verify.

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

При раннем отклонении декодированное число может быть limit + 1, а не фактическим конечным размером, потому что клиент намеренно прекратил чтение. Честно записывайте это. Утверждение, что вы знаете полный декодированный размер, создаёт впечатление, будто вы прочитали весь поток, который должны были ограничить. Нижней границы достаточно, чтобы объяснить решение.

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

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

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

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

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

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

Проверяйте в ревью любые изменения, добавляющие новую HTTP-библиотеку, удобный помощник для получения данных или новый путь логирования ответов. Такие изменения регулярно обходят тщательно ограниченный reader, потому что по отдельности выглядят безобидными. Вопрос для ревью прост: где впервые становятся доступны декодированные байты и какой лимит оборачивает именно этот reader?

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

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

Защищает ли Content-Length мой клиент от бомбы распаковки?

Нет. Content-Length описывает байты, переданные в теле HTTP-сообщения, а они могут быть сжаты. Клиент должен ограничивать объём после применения Content-Encoding, а до начала декодирования также ограничивать сжатый поток.

Как ограничить максимальный размер распакованного HTTP-ответа?

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

Могут ли ответы Brotli быть бомбами распаковки?

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

Что должен делать агент, если ответ API превышает лимит размера?

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

Нужно ли всем конечным точкам API задавать один лимит размера ответа?

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

Достаточно ли потоковой распаковки, чтобы предотвратить исчерпание памяти?

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

Можно ли доверять заголовку Content-Length от API?

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

Какие фикстуры нужны для проверки лимитов gzip-ответов?

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

Почему инструментам агента нужен лимит меньше доступной памяти приложения?

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

Решает ли проблему слишком больших ответов отказ от передачи API-ключей агенту?

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

Sallyport

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

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