Читать 6 мин

Может ли обратное давление MCP stdio заморозить агента?

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

Может ли обратное давление MCP stdio заморозить агента?

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

Для инструментов агентов это особенно важно. Инструмент может вернуть огромный результат поиска, закодированный файл, полный ответ API или подробный вывод команды. Затем агент может токенизировать результат, суммировать его, ждать подтверждения или решить, что контекста уже достаточно. Если хост в это время перестанет читать stdout сервера, сервер может заблокироваться посреди ответа. После этого он может не прочитать уведомление об отмене или следующий запрос из stdin.

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

Заблокированный канал легко принять за ошибку агента

Заблокированный канал stdio дает симптомы, которые часто направляют команду по ложному следу. Агент выглядит зависшим после вызова инструмента. Процесс сервера все еще работает. Загрузка CPU может быть низкой. Срабатывает тайм-аут, но повторный вызов тоже зависает. Вину возлагают на среду выполнения модели, MCP SDK или блокировку внутри инструмента.

Часто инструмент уже закончил работу. Он застрял в write(), пытаясь передать ответ, который хост больше не читает. Размер канала ограничен и зависит от платформы. В документации Node для дочерних процессов прямо сказано то, что давно известно разработчикам оболочек: если подпроцесс записывает больше данных, чем помещается в канал, а родительский процесс их не принимает, подпроцесс блокируется, пока канал снова не освободит место.

В таком инциденте есть две разные очереди, и путаница между ними приводит к плохим исправлениям.

  • Канал операционной системы хранит исходные байты stdout между MCP-сервером и хостом.
  • Очередь приложения хоста хранит разобранные сообщения JSON-RPC, ожидающие обработки агентом, интерфейсом, журналом или сборщиком контекста.

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

В официальных рекомендациях MCP по отладке есть простой диагностический ориентир: локальные stdio-серверы должны выводить журналы не в stdout. Используйте stderr. Если в stdout попали баннер, трассировка стека или строка прогресса, не являющаяся данными протокола, ошибка фрейминга возникла еще до проблемы с большим результатом.

stdout должен оставаться читаемым во время работы

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

Используйте транспортный цикл с узкими обязанностями:

  1. Читайте stdout как поток байтов, когда операционная система их предоставляет.
  2. Передавайте байты анализатору фреймов протокола.
  3. Отклоняйте некорректные или слишком большие фреймы как ошибки транспорта.
  4. Передавайте готовые сообщения ограниченному диспетчеру, отдельному от читателя.
  5. Продолжайте читать поток или намеренно завершайте дочерний процесс, если диспетчер не может принять новую работу.

Ключевое слово здесь - «отдельному». Читатель, который синхронно запускает дорогостоящую обработку сообщения, рано или поздно перестает читать. Сам разбор JSON тоже может стать проблемой для огромного сообщения, но обычно ошибка возникает раньше: читатель ждет callback, занятый посторонней работой.

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

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

Для результата нужны два ограничения

Задайте ограничение на транспорт и ограничение на содержимое. Один счетчик символов не защищает процесс: экранирование JSON, кодирование base64 и оболочка ответа меняют число байтов в stdout.

Транспортный предел, это максимальный размер в байтах одного полностью сериализованного сообщения JSON-RPC. Проверяйте его во фреймере до разбора произвольного JSON. Он защищает память и время разбора на стороне хоста. Предел содержимого, это максимальный полезный объем, который инструмент возвращает в content или structuredContent. Проверяйте его в обработчике инструмента до сериализации результата. Он защищает контекст агента и сохраняет смысл ответа.

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

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

{
  "jsonrpc": "2.0",
  "id": 41,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Returned 50 of 4,382 matching records. Results are sorted by updated time. Use cursor \"eyJvZmZzZXQiOjUwfQ\" to continue, or add a narrower path or query."
      }
    ],
    "structuredContent": {
      "items": [
        {"path": "src/auth.ts", "line": 18, "summary": "reads token from environment"}
      ],
      "nextCursor": "eyJvZmZzZXQiOjUwfQ",
      "truncated": true,
      "totalEstimate": 4382
    }
  }
}

Текст дает модели понятное объяснение. Структурированное содержимое дает клиенту стабильный токен продолжения и машиночитаемый признак усечения. Не указывайте выдуманное общее число, если подсчет всех записей дорог или невозможен. Укажите truncated: true и опустите количество. Ложная точность отнимает больше времени, чем честный неполный результат.

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

Возвращайте решения, а не необработанный поток данных

Большинство слишком больших ответов появляется у инструментов, чья модель вывода скопирована из командной строки. grep -R, git diff, API списка облачных ресурсов и запрос к базе данных удобны человеку в терминале. Они не становятся хорошими интерфейсами для агента только потому, что их обернули в JSON.

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

Для поиска в репозитории возвращайте пути к файлам, диапазоны строк, короткие фрагменты и использованный запрос. Не отдавайте каждую совпавшую строку в монорепозитории. Для HTTP-клиента возвращайте статус, выбранные заголовки, ограниченный фрагмент тела и дескриптор ответа, если продукт может безопасно его хранить. Не помещайте произвольную загрузку в base64 внутри content. Для SSH возвращайте ограниченный конец stdout и stderr вместе с кодом завершения. Если команда вывела огромный сгенерированный файл, она уже сообщила, что создала огромный файл. Агенту редко нужны все эти байты в текущем контексте.

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

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

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

Воспроизведите зависание до того, как заявлять об исправлении

Отслеживайте каждое действие инструмента
Журнал Activity записывает отдельные HTTP- и SSH-вызовы в зашифрованном аудиторском журнале.

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

Эта небольшая заготовка на Node записывает корректный JSON-RPC-ответ с достаточно большим содержимым, чтобы превысить обычную емкость канала, если родительский процесс игнорирует stdout. Она получает одну строку запроса из stdin, а затем пишет ответ частями. Ожидание drain показывает, когда среда выполнения применила обратное давление к записи stdout сервера.

// oversized-server.mjs
import readline from "node:readline";
import { once } from "node:events";

const rl = readline.createInterface({ input: process.stdin });

for await (const line of rl) {
  const request = JSON.parse(line);
  const text = "x".repeat(8 * 1024 * 1024);
  const response = JSON.stringify({
    jsonrpc: "2.0",
    id: request.id,
    result: { content: [{ type: "text", text }] }
  }) + "\n";

  for (let start = 0; start < response.length; start += 16 * 1024) {
    const chunk = response.slice(start, start + 16 * 1024);
    if (!process.stdout.write(chunk)) {
      process.stderr.write("stdout backpressure observed\n");
      await once(process.stdout, "drain");
    }
  }
}

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

// blocked-parent.mjs
import { spawn } from "node:child_process";

const child = spawn(process.execPath, ["oversized-server.mjs"], {
  stdio: ["pipe", "pipe", "pipe"]
});

child.stderr.setEncoding("utf8");
child.stderr.on("data", chunk => process.stderr.write(chunk));

child.stdin.write(JSON.stringify({
  jsonrpc: "2.0",
  id: 1,
  method: "tools/call",
  params: { name: "large", arguments: {} }
}) + "\n");

setTimeout(() => {
  console.error("child still running:", child.exitCode === null);
  child.kill("SIGTERM");
}, 1000);

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

Проверяйте данные с кавычками, многобайтовыми символами и длинными строками без переносов. Такие случаи выявляют фреймеры, которые считают символы JavaScript вместо байтов UTF-8, или предполагают, что каждый фрагмент чтения заканчивается на границе сообщения.

Отклоняйте огромный фрейм, не создавая новую блокировку

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

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

MCP stdio использует сообщения JSON-RPC поверх локального потока байтов. Относитесь к фреймингу как к транспортному коду, а не как к удобному вызову split("\n") после сбора неограниченного текста. Считайте байты во время накопления предполагаемого сообщения. Для каждого входящего фрагмента либо находите границы готовых фреймов и передаете их дальше, либо завершайте работу, как только кандидат превышает максимум.

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

Если вы контролируете обе стороны и используете JSON, разделенный переводами строк, требуйте один JSON-объект на строку, отклоняйте буквальные переводы строк внутри строк средствами обычного JSON-кодирования и записывайте ровно один разделитель после каждого сериализованного объекта. Сервер не должен выводить диагностические сообщения в stdout. В документации MCP по отладке сказано, что локальные серверы должны отправлять журналы в stderr, поскольку stdout предназначен для протокола.

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

Отмена работает только тогда, когда доходит до производителя данных

Используйте SSH без раскрытия ключей
Встроенный помощник sp-ssh выполняет SSH-команды, пока ключи остаются в зашифрованном хранилище Sallyport.

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

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

Для tools/call клиент отправляет через тот же сеанс stdio уведомление вроде этого:

{
  "jsonrpc": "2.0",
  "method": "notifications/cancelled",
  "params": {
    "requestId": 41,
    "reason": "result exceeded the client budget"
  }
}

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

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

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

Для проверки отмены нужен второй запрос

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

Надежная последовательность выглядит так:

  1. Запустите tools/call, обработчик которого выдает большой ответ частями или вызывает намеренно медленный источник данных.
  2. Дождитесь, пока стенд увидит достаточно байтов stdout и поймет, что ответ уже начался.
  3. Отправьте notifications/cancelled с идентификатором этого запроса.
  4. Убедитесь, что источник данных завершился или сообщил об отмене в пределах установленного срока.
  5. Отправьте небольшой запрос с новым идентификатором, например проверку состояния или ограниченный echo-инструмент.

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

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

Не требуйте, чтобы отмена всегда предотвращала ответ. Спецификация MCP допускает гонки. Проверяйте, что хост остается корректным при ответе после отмены, а сервер останавливает работу, если отмена пришла достаточно рано.

Вывод процесса требует отдельного пути чтения

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

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

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

Держите stderr отдельно от stdout. Инструмент может записывать полезную диагностику в stderr и при этом завершаться успешно. Ограничивайте и читайте оба потока независимо. Никогда не смешивайте произвольный вывод процесса с stdout MCP-сервера. У внешнего stdout ровно одна задача: сериализованные сообщения MCP.

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

Включите эти ошибки в критерии выпуска

Ограничения результатов и отмену легко случайно убрать. Рефакторинг может заменить потоковое чтение на readFile, превратить постраничный вызов API в неограниченный или перенести разбор в callback интерфейса. Оставьте тест с большим результатом в наборе проверок.

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

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

Если можно сделать только одно изменение, отделите чтение stdout от обработки агентом, а затем проверьте его сервером, который пишет намного больше, чем должен возвращать любой разумный инструмент. Такой тест превращает расплывчатую жалобу «агент завис» в ошибку, которую можно воспроизвести, измерить и не допустить в следующем выпуске.

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

Почему MCP-сервер зависает после возврата большого результата инструмента?

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

Какой максимальный размер результата MCP-инструмента можно считать безопасным?

Единого безопасного значения в MCP нет: полезный предел зависит от клиента, бюджета контекста модели, формата результата и выполняемой работы. Задайте предел в байтах для всего сериализованного JSON-RPC-ответа и меньший смысловой предел для содержимого инструмента. Явно сообщайте об усечении и давайте вызывающей стороне курсор, путь, запрос или отдельный инструмент для получения остального.

Поможет ли увеличение буфера stdout при обратном давлении MCP?

Нет. Увеличение буфера лишь откладывает проблему и может превратить заблокированный вывод в резкий рост потребления памяти. Хост должен постоянно читать канал, а серверу не следует заранее создавать чрезмерно большие объекты ответа.

Как отмена MCP-инструмента работает через stdio?

Уведомление MCP об отмене называется notifications/cancelled и содержит исходный идентификатор запроса, а также необязательную причину. Оно носит рекомендательный характер и может прийти одновременно с завершением. Поэтому сервер должен связать его с настоящим сигналом отмены, а клиент должен уметь обработать ответ, который уже был записан.

Должен ли MCP-хост читать stdout, пока агент занят?

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

Можно ли выводить отладочные сообщения в stdout MCP-сервера?

Операционные сообщения записывайте в stderr, а не в stdout. В stdout должны находиться только сообщения протокола MCP, иначе даже одна строка журнала может нарушить границы сообщений. Официальная документация MCP по отладке прямо говорит об этом для локальных stdio-серверов.

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

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

Как проверить отмену MCP, а не просто тайм-аут?

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

Как MCP-инструментам возвращать большие файлы или ответы API?

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

Устраняет ли шлюз действий риск обратного давления MCP stdio?

Sallyport выполняет HTTP- и SSH-действия через локальное приложение и прослойку sp mcp, поэтому агент не получает исходные учетные данные. Это защищает секреты, но не меняет физику stdout. Инструментам и клиентам по-прежнему нужны ограничения результатов, отмена и тесты, доказывающие, что большой ответ не может заблокировать сеанс.

Sallyport

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

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