Для AI-агентов: markdown этой страницы — /docs-content/infra/deploy/exec.md индекс документации — /llms.txt
Выполнить команду
POST /v1/infra/servers/:id/exec
Выполняет shell-команду на BLACKHOLE-сервере через агент туннеля. Полный стандартный вывод (stdout), поток ошибок (stderr) и код возврата передаются клиенту. По умолчанию ответ — JSON после завершения команды (рекомендуется для AI-агентов и скриптов). На STANDALONE для построчного прогресса передайте ?stream=true и читайте SSE-события stdout/stderr/exit. Отказ при выполнении команды приходит в потоковом режиме событием error.
Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без ?stream=true) — SSE они разбирать не умеют. Официальный MCP-клиент платформы Вайбкод (инструмент manage_server_deploy) снимает этот параметр сам и всегда работает в JSON-режиме.
Потоковый режим даёт живой прогресс, но не увеличивает предельное время выполнения: синхронный предел одинаков для JSON и потокового режима и равен timeout + 30 секунд, а сам timeout ограничен 600 секундами. На STANDALONE для более долгой работы с сохранением результата используйте async=true — см. «Durable exec — длительная команда» ниже. Другой вариант — самостоятельно управлять фоновой задачей.
На каких серверах работает
Тип сервера приходит в поле kind ответов GET /v1/infra/servers и GET /v1/infra/servers/:id.
kind |
Команда выполняется | Что учесть |
|---|---|---|
STANDALONE — отдельная виртуальная машина |
да | Контракт страницы действует целиком, включая workdir и env |
GALAXY_APP — приложение в галактике |
да, внутри контейнера приложения | Поля workdir и env не поддерживаются — 400 GALAXY_EXEC_NO_WORKDIR_ENV. Используйте cd /opt/app; FOO=bar node script.js только для несекретных значений. Секреты загружайте внутри контейнера из существующего защищённого файла или настроенного хранилища секретов приложения; не вставляйте их значения в текст команды. Ошибки во время выполнения приходят со статусом 502, а не 200. Исключений два: EXEC_BUSY приходит как 409, а EXEC_NO_EXIT — как 200 с success: false, потому что это не отказ канала, а отсутствие вердикта у отработавшего вызова (см. ниже) Поддерживается только синхронный exec с таймаутом до 600 секунд. async=true получает 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND без исполнения или перехода. |
GALAXY — машина, несущая контейнеры приложений |
нет — 403 GALAXY_HOST_EXEC_FORBIDDEN |
На машине стоят контейнеры разных ключей одного аккаунта Битрикс24, поэтому целью команд она не является. Выполняйте команду на приложении, по его собственному id (kind равен GALAXY_APP). Свободное место на машине смотрите в кабинете, на карточке галактики — см. Galaxy-приложение Длительные запросы получают 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND до отправки команды. |
Параметры
| Параметр | В | Тип | Обяз. | По умолч. | Описание |
|---|---|---|---|---|---|
id |
path | string (UUID) | да | — | ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED |
stream |
query | string | нет | — | true — синхронный SSE-режим на STANDALONE. Для JSON не передавайте. Exec Galaxy возвращает блокирующий JSON без живого SSE-потока |
async |
query | string | нет | false |
true — длительное исполнение отдельной машины с сохранённым результатом и ответом 202. Другие виды: 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND, без синхронного перехода. Несовместим с stream=true |
Поля запроса (body)
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
command |
string | да | Shell-команда. 1–10 000 символов |
timeout |
number | нет | Таймаут исполнения в секундах: 1–600 синхронно; 1–86400 с async=true только на STANDALONE. По умолчанию 300 |
workdir |
string | нет | Только отдельная машина. До 500 символов. Существующая директория, чей очищенный абсолютный путь — /opt, /srv, /tmp, /root, /var/log, /var/lib, /etc/systemd, /etc/nginx или вложенный в один из них. Относительные пути, оставшиеся .. в очищенном пути и чувствительные префиксы ниже запрещены. Без поля используется рабочая директория процесса исполнения |
env |
object | нет | Переменные окружения: { "KEY": "value" }. Только строковые значения |
Проверка по OpenAPI. Стандартный requestBody допускает timeout до 86400 секунд, чтобы обычные валидаторы и сгенерированные клиенты принимали поддерживаемые запросы async=true на STANDALONE. Схема тела не учитывает параметр async: синхронный API по-прежнему отклоняет timeout больше 600 секунд, даже если стандартная схема пропускает тело. Контракты по режимам описаны в #/components/schemas/InfraSyncExecRequest (до 600 секунд) и #/components/schemas/InfraDurableExecRequest (до 86400 секунд). Расширение x-vibe-request-schema проверяет собранные {query,body} с учётом режима; вид сервера проверяется во время исполнения.
Расширение x-vibe-request-schema проверяет собранный объект {query, body}: предел таймаута по режиму и допустимое сочетание async/stream. Вид сервера проверяется при вызове API; выбор схемы длительного тела не отменяет требование STANDALONE.
Для workdir очистка пути предшествует проверке. Следующие префиксы запрещены даже внутри разрешённого корня: /etc/shadow, /etc/passwd, /etc/sudoers, /etc/cron, /etc/ssh/, /root/.ssh/, /root/.gnupg/, /root/.bashrc, /root/.profile, /root/.bash_history. Запрещённый путь даёт EXEC_WORKDIR_DENIED, недоступная директория — EXEC_WORKDIR_NOT_FOUND, путь к файлу — EXEC_WORKDIR_NOT_DIR. Длительный запрос может получить HTTP 502 за запрещённый путь до запуска; принятая задача возвращает последующие проверки директории через операцию. См. коды отказа длительного exec.
Примеры
curl — личный ключ
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "ls -la /opt/app", "timeout": 30}'
curl — OAuth-приложение
curl -X POST "https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "npm ci --production", "workdir": "/opt/app", "timeout": 180}'
JavaScript — личный ключ
const res = await fetch(
`https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'node -v',
timeout: 10,
}),
}
)
const { data } = await res.json()
console.log(`exit ${data.exitCode} за ${data.duration}ms:\n${data.stdout}`)
JavaScript — OAuth-приложение
// Команда с переменными окружения и рабочей директорией
const res = await fetch(
`https://vibecode.bitrix24.tech/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'npx tsx scripts/seed.ts',
workdir: '/opt/app',
env: { DATABASE_URL: 'postgresql://localhost/mydb' },
timeout: 60,
}),
}
)
Поля ответа
| Поле | Тип | Режим | Описание |
|---|---|---|---|
success |
boolean | Оба | Синхронный 200: true, если команда выполнилась, включая ненулевой код. Длительный 202: запрос принят для сверки; это не доказательство запуска |
data.exitCode |
number | Синхронный 200 |
Код возврата процесса |
data.stdout |
string | Синхронный 200 |
Стандартный вывод |
data.stderr |
string | Синхронный 200 |
Поток ошибок |
data.duration |
number | Синхронный 200 |
Время выполнения в миллисекундах |
data.truncated |
boolean | Синхронный 200 |
Достигнут ли общий предел stdout+stderr 1 МиБ (1 048 576 байт) |
data.operationId |
string | Оба | Идентификатор запуска; при отказе — error.operationId. Обязателен в длительном 202: без сохранённой записи отправки нет. Синхронная команда может выполняться без него, если запись не удалось создать |
data.status |
string | Длительный 202 |
running: подтверждение запуска может быть потеряно; читайте ту же операцию |
data.statusUrl |
string | Длительный 202 |
URL чтения операции и сохранённого результата |
Длительный ответ 202 не содержит вывода команды или кода возврата. Сохранённые поля читайте через разрешённую ручку исхода операции.
Пример ответа
{
"success": true,
"data": {
"exitCode": 0,
"stdout": "Linux epd65hdv07p8g89c0c06 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux\n",
"stderr": "",
"duration": 5,
"truncated": false,
"operationId": "clz9k2m4x0001qw8h3f7d2n5q"
}
}
Длительная команда отдельной машины принята (202):
{
"success": true,
"data": {
"operationId": "clz9k2m4x0001qw8h3f7d2n5q",
"status": "running",
"statusUrl": "/v1/infra/operations/clz9k2m4x0001qw8h3f7d2n5q"
}
}
Пример ответа при ошибке
409 EXEC_BUSY — приём команд занят. Отказ платформенного лока возвращает сигналы повтора из этого примера:
{
"success": false,
"error": {
"code": "EXEC_BUSY",
"message": "Another operation is running on this server",
"retryable": true,
"retryAfter": 10
}
}
Если ответ содержит retryable: true и retryAfter, он также несёт заголовок Retry-After с тем же интервалом в секундах. Соблюдайте этот интервал; поле error.hint.autoExpiresInSeconds, если оно есть, задаёт полный верхний предел жизни лока. Агентский отказ длительному запуску возвращает error.operationId без retryable, retryAfter и Retry-After. Прочитайте эту операцию до нового запроса exec; неизвестный исход требует сверки состояния и независимого подтверждения остановки команды.
Ошибки
Ошибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа на отдельной виртуальной машине (kind: "STANDALONE"). У galaxy-приложения (kind: "GALAXY_APP") он приходит со статусом 502, и исключений там два: занятость exec-канала (EXEC_BUSY) приходит настоящим 409, потому что это отказ «занято», а не сбой, а EXEC_NO_EXIT — с 200 и success: false.
Ошибки до запуска команды
| HTTP | Код | Описание |
|---|---|---|
| 400 | VALIDATION_ERROR |
Нарушена схема запроса (пустая команда, неверный таймаут, workdir длиннее 500 символов, неверное значение async или async=true вместе с stream=true). |
| 400 | DURABLE_EXEC_UNSUPPORTED_SERVER_KIND |
async=true направлен на сервер вида, отличного от STANDALONE, включая GALAXY_APP и GALAXY. Отказ до блокировки, создания операции и отправки команды; идентификатора операции и синхронного перехода нет. В Galaxy доступен отдельный синхронный запрос с таймаутом до 600 секунд |
| 400 | NOT_BLACKHOLE |
Сервер в режиме OPEN — Deploy API недоступен |
| 400 | COMMAND_TOO_LONG |
Команда длиннее 10 000 символов. В ответе hint: большие данные и скрипты передавайте через /upload, затем bash /путь/скрипт.sh |
| 400 | GALAXY_EXEC_NO_WORKDIR_ENV |
В команде для galaxy-приложения (kind: "GALAXY_APP") переданы workdir или env. Контейнерный запуск их не принимает — используйте cd и только несекретные присваивания вроде FOO=bar; секреты загружайте внутри контейнера из существующего защищённого файла или настроенного хранилища секретов приложения |
| 401 | MISSING_API_KEY |
Не передан заголовок X-Api-Key |
| 401 | INVALID_API_KEY |
Ключ не опознан: такой строки на платформе нет |
| 402 | ACCOUNT_FROZEN |
Баланс Вайбкод заморожен. Запрос отбивается до операции, спящий сервер при этом не будится — пополните баланс и повторите |
| 403 | WRONG_KEY |
Сервер существует, но вызывающему не принадлежит. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — открыты по любому из трёх оснований: управляющий ключ сервера, ключ, приложение которого привязано к этому серверу (Application.serverId), либо членство в команде разработки этого сервера (обе роли, «Разработчик» и «Администратор»). Токены доступа и загрузка значка требуют именно управляющего ключа. Управление самой машиной открыто ещё и роли «Администратор» команды. В ответе — hint с восстановлением из двух шагов. Подробнее — Восстановление доступа к серверу |
| 403 | SERVER_WAKE_BLOCKED |
Сервер спит, а пробуждение запрещено — например, закончился доступ к платформе или действует административная блокировка. Команда такой сервер не поднимает, повтор не поможет, пока запрет не снят. Разбор запрета — Разбудить сервер |
| 403 | GALAXY_HOST_EXEC_FORBIDDEN |
Команда пришла на id машины-галактики (kind: "GALAXY"). Выполняйте её на приложении, по его собственному id — см. «На каких серверах работает» выше |
| 403 | INFRA_FORBIDDEN_FOR_COWORK_KEY |
Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — Проектный ключ для деплоя |
| 403 | EXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH |
Вызов сделан ключом внешнего участника разработки, а в адресе указан не тот сервер, для которого ключ выпущен. Такой ключ действует ровно на одном сервере, и ответ не сообщает, существует ли запрошенный сервер. Для работы с другим сервером нужен ключ того внешнего членства |
| 403 | EXTERNAL_COLLABORATOR_NOT_A_MEMBER |
Ключ внешнего участника обращается к своему серверу, но доступа сейчас нет: членство снято, истёк его срок либо для аккаунта Битрикс24, которому принадлежит сервер, отключены внешние участники разработки. Код эти причины не различает и сведений о сервере не раскрывает. Членство перечитывается на каждом запросе, поэтому снятый доступ перестаёт работать сразу |
| 404 | NOT_FOUND |
Сервера с таким id нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает 403 WRONG_KEY — код различает «нет такого сервера» и «нет прав на него» |
| 409 | EXEC_BUSY |
Приём занят другой командой или выкладкой. Отказ платформенного лока несёт Retry-After и retryable: true / retryAfter; соблюдайте этот интервал. Агентский отказ длительному запуску отдельной машины тоже приходит как 409 с error.operationId, но без этих полей повтора. Прочитайте операцию до повторного запроса. Работающая длительная задача отдельной машины штатно занимает канал после 202, без autoExpiresInSeconds; читайте её результат вместо /unstick или удаления лока. В Galaxy канал общий для всех приложений и выкладок хоста. При неизвестном исходе сверьте состояние и логи и обратитесь в поддержку; отсутствие error.hint.holder не доказывает простой. Восстановление лока описано в /unstick и DELETE /lock и требует независимо подтверждённого простоя. Владельцу galaxy-приложения принудительное восстановление недоступно |
| 409 | SERVER_NOT_READY |
Сервер не готов к операции: не запущен, туннель не подключён или успешный непустой снимок Gateway подтвердил отсутствие туннеля при сохранённом CONNECTED. Этот ответ выдаётся только когда repair не подтвердил восстановление; недоступный или пустой снимок не считается подтверждением отсутствия, и API использует сохранённый статус. Платформа пытается восстановить подтверждённо отсутствующий туннель до запуска команды; если это не удалось, ответ несёт hint. Спящий сервер этот код не описывает — его платформа будит сама. Восстановите туннель вызовом /repair и повторите запрос |
| 409 | WAKE_IN_PROGRESS |
Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите |
| 422 | VM_MISSING |
У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Сервер нужно заменить: удалить и создать новый. Адрес каждого шага error.hint называет, только если шаг доступен этому запросу, а для недоступного — причину отказа и что изменить. Поле error.userMessage приходит, только когда доступны оба шага |
| 422 | VM_POINTER_LOST |
У записи нет externalId, но разбор облака видит машину под облачным именем этого сервера: связь потеряна, сама машина скорее всего работает и тарифицируется. Удалять сервер и создавать новый НЕ нужно — тогда машина останется работать вообще без записи. Сервер в error не переводится, обратитесь в поддержку за восстановлением связи |
| 502 | WAKE_FAILED |
Во время пробуждения спящего сервера он перешёл в неожиданное состояние. Повторите запрос |
| 502 | PROVIDER_ERROR |
Облачный провайдер вернул ошибку при запуске виртуальной машины спящего сервера. Туннель здесь ни при чём — вызов /repair не поможет |
| 409 | GALAXY_APP_NOT_READY |
Только у galaxy-приложения (kind: "GALAXY_APP"): платформа не определила имя его контейнера на хосте и команду не запустила. Два условия. Имя контейнера не прошло проверку — в message приходит invalid on-host name. Либо у приложения нет субдомена или связи с galaxy-хостом — тогда в message приходит Galaxy app is missing its subdomain or host link. Текущее состояние приложения — GET /v1/infra/servers/:id |
| 429 | RATE_LIMITED |
Превышен лимит 10 операций в минуту на сервер. Точное значение — в заголовке x-ratelimit-limit (потолок делится на реплики) |
| 503 | EXEC_JOB_STORAGE_UNAVAILABLE |
Не удалось сохранить запись длительной операции или её адрес назначения; команда не отправлена |
| 502 | EXEC_JOB_CAPACITY |
На хосте достигнут предел 64 сохранённых каталогов длительных задач. Команда не запущена. Прочитайте error.operationId, дождитесь допустимой очистки после семи суток или обратитесь в поддержку. Автоматического повтора нет |
| 502 | EXEC_JOB_UNAVAILABLE |
Недоступно хранилище длительных задач, приём или запуск через systemd. Прочитайте error.operationId и обратитесь в поддержку; не заменяйте запрос синхронным запуском |
| 502 | EXEC_JOB_INVALID |
Агент отклонил параметры длительного запуска или истёкшее окно запуска. Команда не запущена. Прочитайте error.operationId; проверьте записанный отказ и обратитесь в поддержку при повторном отказе корректному запросу |
| 502 | EXEC_WORKDIR_DENIED |
Рабочая директория отдельной машины нарушает правила пути выше. Команда не выполнялась. Прочитайте error.operationId, затем выберите разрешённую существующую директорию |
| 502 | EXEC_JOB_REJECTED |
Агент отказал до исполнения без отдельного кода. Прочитайте отказ операции и обратитесь в поддержку; не заменяйте запрос синхронным запуском |
| 502 | UNKNOWN_TYPE |
Агент не поддерживает длительный exec; отказ до запуска, без перехода к синхронному исполнению |
| 503 | WAKE_TIMEOUT |
Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в sleeping, поэтому повторный запрос безопасен. См. «Известные особенности» |
Отказы после приёма длительной задачи
HTTP 202 может предшествовать отказу, обнаруженному до запуска пользовательской команды. Опрашивайте возвращённую операцию; status: "failed" и data.error.code обозначают доказанный отказ, а unknown не разрешает новый exec. Истёкшее окно запуска или отказ рабочей директории описаны в таблице отказов длительного exec. Неизвестный исход имеет data.error: null; отсутствие кода ошибки не означает успех.
Ошибки во время синхронного выполнения
HTTP-статус не отражает результат команды. На отдельной виртуальной машине (
kind: "STANDALONE") соединение удерживается на всё время выполнения, а статус200отправляется до его начала — поэтому и успешная, и провалившаяся команда приходят с кодом200. Признак отказа — полеsuccess: falseи объектerrorв теле ответа. Проверяйтеsuccessв теле, а не HTTP-статус: клиент, который ветвится по статусу, посчитает провалившуюся команду успешной. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом502. Исключений два, те же, что в таблице видов сервера выше:EXEC_BUSYприходит как409, аEXEC_NO_EXIT— как200сsuccess: false. Поэтому проверятьsuccessв теле нужно на обоих видах серверов.
| Код | Описание |
|---|---|
EXEC_TIMEOUT |
Превышен timeout (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе — объект hint, его состав разобран ниже |
EXEC_WORKDIR_DENIED |
Рабочая директория отдельной машины нарушает правила пути выше. Агент отказал до запуска команды; сверьте записанный исход перед использованием разрешённой директории |
EXEC_WORKDIR_NOT_FOUND |
Рабочая директория отдельной машины отсутствует или недоступна для проверки. Агент отказал до запуска команды; сверьте записанный исход перед использованием существующей доступной директории |
EXEC_WORKDIR_NOT_DIR |
Рабочий путь отдельной машины не является директорией. Агент отказал до запуска команды; сверьте записанный исход перед использованием директории |
EXEC_FAILED |
Ошибка выполнения на агенте |
EXEC_NO_EXIT |
Поток завершился, не прислав статус выхода. Что успела сделать команда на сервере — неизвестно, поэтому это не успех: агент мог перестать отвечать, туннель — оборваться в середине команды, а очень большой вывод — переполнить канал уже после того, как команда отработала. В data возвращается накопленный к моменту обрыва вывод (stdout, stderr). Полей exitCode, duration и truncated нет — их значения платформе неизвестны. Приходит на обоих видах серверов. На отдельной виртуальной машине (kind: "STANDALONE") ответ дополнительно несёт hint с путём восстановления, у galaxy-приложения его нет: подсказка ведёт на восстановление виртуальной машины, которой у приложения в галактике нет |
EXEC_BUSY |
Канал агента занят синхронной командой, а на отдельной машине — также длительной задачей. Синхронный отказ приходит как success: false с HTTP 200 на STANDALONE либо 409 с Retry-After и retryable: true / retryAfter на GALAXY_APP. Его hint.recoveryAction сначала указывает на GET /v1/infra/servers/:id/operations?kind=exec. Прочитайте исходы длительных задач и опрашивайте работающие; при неизвестном исходе нужны поддержка и подтверждение полной остановки. Отсутствие autoExpiresInSeconds не доказывает утечку мьютекса. Через /unstick восстанавливайте только подтверждённо зависший синхронный мьютекс независимо подтверждённой простаивающей отдельной машины. Владелец galaxy-приложения не может восстановить общий канал хоста; задача соседнего приложения может не быть видна в его списке, поэтому при устойчивой занятости обратитесь в поддержку |
CONTAINER_EXEC_OPTIONS_DENIED |
Синхронный exec Galaxy отклонил хостовые workdir или env до исполнения. Публичный API обычно отклоняет их раньше с 400 GALAXY_EXEC_NO_WORKDIR_ENV. Сверьте записанный исход и исправьте синхронный запрос |
CONTAINER_NOT_READY |
Хост галактики ответил, но контейнера приложения, в котором должна была выполниться команда, на нём ещё нет: слот заведён и ни разу не выложен, выкладка идёт прямо сейчас или контейнер перезапускается. Приходит со статусом 502. В ответе hint, который ведёт к выкладке приложения, а не к хосту — хост исправен, будить и чинить его незачем. Только у galaxy-приложения (kind: "GALAXY_APP") |
Состав `hint` у `EXEC_TIMEOUT`
| Поле | Тип | Описание |
|---|---|---|
hint.reason |
string | Что произошло: команда не уложилась в отведённое время, и вся процесс-группа завершена немедленно, без grace-паузы |
hint.recovery |
string | Что делать: в синхронном режиме верхняя граница timeout — 600 секунд, для более длинных операций запускайте фоновую задачу (systemd-run --unit=<name>) и следите за ней через GET /v1/infra/servers/:id/logs с параметром ?service=<имя>, а для прогресса в пределах таймаута передайте ?stream=true |
hint.recoveryAction |
string | Указатель на документацию: docs: /docs/infra/deploy/exec |
Подсказка приходит в JSON-ответе и SSE-событии error. Агентский EXEC_BUSY на обоих поддерживаемых видах сервера использует GET /v1/infra/servers/:id/operations?kind=exec как recoveryAction. Сначала прочитайте исход операции. /unstick нужен только при подтверждённой утечке синхронного мьютекса независимо подтверждённой простаивающей отдельной машины. На STANDALONE работающая длительная задача занимает канал после освобождения платформенного лока с 202. Опрашивайте её операцию; при неизвестном исходе нужны поддержка и независимое подтверждение полной остановки. Отсутствие autoExpiresInSeconds не доказывает утечку мьютекса. Восстановление туннеля и удаление лока не снимают ограничение приёма длительной задачи. В Galaxy команды выполняются синхронно с пределом 600 секунд и общим каналом для всех приложений и выкладок; задача соседнего приложения может не попасть в ваш список операций, поэтому пустой список не доказывает простой хоста.
Отказ связи с туннелем приходит в том же виде — со своим кодом в поле error.code (например TUNNEL_NOT_FOUND или GATEWAY_TIMEOUT: …). Часть gateway-кодов несёт детали после двоеточия (GATEWAY_UNREACHABLE: …, GATEWAY_TIMEOUT: …), поэтому сопоставляйте error.code по префиксу, а не по точному совпадению. В потоковом режиме (?stream=true) эти же ошибки приходят SSE-событием error. Данные события несут code и message, а у EXEC_TIMEOUT, GATEWAY_TIMEOUT и агентского EXEC_BUSY — ещё и hint. Подсказка у GATEWAY_TIMEOUT говорит главное: код возврата не получен, поэтому исход команды неизвестен и она может продолжать выполняться на сервере — не запускайте её повторно вслепую, сначала проверьте состояние. Поля success в них нет — признаком отказа служит само событие error.
Полный список общих ошибок API — Ошибки.
Известные особенности
- Спящую отдельную виртуальную машину команда будит сама. Если сервер (
kind: "STANDALONE") в статусеsleeping, платформа запускает пробуждение и ждёт его в том же запросе — до 6.5 минут, и только потом выполняет команду. Отдельный вызов/wakeне нужен, но клиентский таймаут запроса это время обязан пережить. Если сервер не поднялся, приходит503 WAKE_TIMEOUTи сервер возвращается вsleeping— команда не выполнялась, повторный запрос безопасен. - Команда исполняется через
/bin/sh. Полеcommandзапускается интерпретатором/bin/sh— это минимальный POSIX-шелл (dash), неbash. Конструкции, специфичные дляbash(set -o pipefail,[[ … ]], массивы), в нём недоступны и завершаются ошибкой. Запускайте их явно — черезcommand: "bash -c 'set -o pipefail; …'"либо файлом-скриптомcommand: "/bin/bash /opt/app/script.sh". - Блокировка на уровне сервера. Синхронный
/execи/deployберут платформенный лок; окно лока синхронного exec —timeout + 90секунд, deploy — 15 минут. Длительный exec отдельной машины освобождает этот лок после202, но занимает свой канал на всё время задачи с запрошенным таймаутом до 86400 секунд. Проверка завершения может продлить занятость сверх таймаута команды. Неизвестный исход требует проверки и подтверждения остановки;/unstickиDELETE /lockне снимают ограничение длительной задачи. Exec Galaxy синхронный, ограничен 600 секундами, а канал хоста общий для всех приложений и выкладок. Зависший синхронный лок восстанавливайте только после независимого подтверждения простоя по состоянию сервера и логам. - Общий максимум вывода — 1 МиБ (1 048 576 байт), суммарно на
stdoutиstderr. Это один бюджет на два потока, а не по 1 МиБ на каждый: если команда пишет и вstdout, и вstderr, обрезка считается по их сумме. Когда вывод больше лимита, приходитtruncated: trueи хвост обрезается. Команда при этом отработала,exitCodeкорректный. Для большого вывода перенаправляйте в файл:command: "my-cmd > /opt/app/output.log 2>&1"и потом читайте через/exec cat /opt/app/output.log. .envиз/deployне загружается автоматически. Systemd подгрузит.envпри старте сервиса, но не для разовых команд через/exec. Если нужныDATABASE_URL/ API-ключи — передавайте в полеenv:{ env: { DATABASE_URL: "..." } }.- Два таймаута в сумме дают
timeout + 30секунд. Агент убивает процесс ровно поtimeout. Gateway ждёт ещё 30 секунд на финальные события и только потом отдаётEXEC_TIMEOUT. - Поддержание соединения в JSON-режиме. Если команда выполняется дольше 15 секунд, сервер периодически шлёт пробелы в тело ответа — это предотвращает таймауты nginx и промежуточных прокси. Пробелы идут внутри уже открытого JSON-объекта, поэтому тело с первого байта выглядит как JSON (
{), а не начинается с пробелов: и строгие клиенты, и обычные парсеры читают его одинаково, снимать пробелы вручную не нужно. - Клиентский таймаут: убедитесь, что он межбайтовый, а не общий. Keepalive-пробелы сбрасывают таймауты «на тишину», поэтому классические сокет-таймауты Python (
urlopen(..., timeout=300), read-таймаутrequests) сами по себе долгую команду в JSON-режиме не оборвут. Обрывают две другие вещи. Первая — клиенты и обёртки с настоящим wall-clock-дедлайном на весь запрос (например,aiohttpсClientTimeout(total=…)или собственный дедлайн агент-фреймворка): keepalive против них бессилен. Вторая — серверный параметрtimeoutсамой команды (по умолчанию 300 секунд): по его истечении агент завершает процесс и приходитEXEC_TIMEOUTнезависимо от клиентских настроек — не путайте с обрывом на своей стороне. Практика: в Python задавайте раздельные таймауты —requests.post(url, json=body, headers=headers, timeout=(10, 60))— 10 секунд на connect и 60 на паузу между байтами. В синхронном режиме, если клиент оборвал соединение, лок на сервере держится ещё доtimeout + 90секунд (EXEC_BUSYдля повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже. На спящем сервере это правило вступает в силу не сразу: пробелы идут только после пробуждения, поэтому до первого байта ответа проходит до 6.5 минут полной тишины, и рецептtimeout=(10, 60)оборвёт запрос ещё до запуска команды. Либо поднимите межбайтовый таймаут выше этого окна, либо разбудите машину заранее вызовом/wakeсwait=trueи вызывайте/execуже на готовой.
Принудительное освобождение канала
На STANDALONE EXEC_BUSY без autoExpiresInSeconds может означать работающую длительную задачу, а не утечку мьютекса агента. Сначала прочитайте исход операции или найдите запуск в списке операций сервера с kind=exec. Пока задача работает, ждите. Исход unknown не подтверждает остановку: сверьте состояние и логи и обратитесь в поддержку. Не разрывайте туннель и не удаляйте лок работающей или неподтверждённой длительной задачи; /unstick не снимает её ограничение приёма. Используйте POST /v1/infra/servers/:id/unstick для восстановления лока только после независимого подтверждения простоя, с учётом ограничений этой ручки.
Как выбрать режим
Четыре режима вызова и когда каждый уместен:
| Когда | Как | Мониторинг |
|---|---|---|
| Команда укладывается в 600 секунд, прогресс не нужен | { "command": "...", "timeout": 600 } без ?stream=true — блокирующий JSON-ответ |
exitCode, stdout, stderr в теле ответа |
Команда отдельной машины укладывается в timeout, нужен живой прогресс |
тот же запрос с ?stream=true — SSE-события stdout / stderr / exit |
читать события по мере поступления |
| Сохранённый результат отдельной машины; её канал занят на всё время задачи | ?async=true, таймаут до 86400 только на STANDALONE |
Читайте statusUrl: исход и сохранённые stdout/stderr |
| Команда отдельной машины, которой нужно освободить канал до завершения | фоновая задача под управлением клиента systemd-run --unit=<name> — /exec возвращается сразу |
GET /v1/infra/servers/:id/logs?service=<name> |
Для синхронного exec общий дедлайн запроса на стороне клиента не ставьте меньше значения timeout команды — иначе клиент оборвёт соединение раньше, чем сервер успеет ответить. Спящий сервер добавляет к этому дедлайну до 6.5 минут на пробуждение — пример ниже рассчитан на уже запущенную машину. Пример раздельных таймаутов для Python:
import requests
r = requests.post(
f"{VIBE_URL}/v1/infra/servers/{SERVER_ID}/exec",
headers={"X-Api-Key": VIBE_API_KEY},
json={"command": "npm ci --production", "workdir": "/opt/app", "timeout": 600},
timeout=(10, 60), # 10 секунд на соединение, 60 на паузу между байтами
)
Поднять timeout у /exec шагу install не поможет — у шагов деплоя свой предел в 300 секунд, отдельный от синхронного предела 600 у /exec.
Важно: умолчание timeout — 300 секунд, а не 600. Синхронный потолок 600 существует, но чтобы им воспользоваться, поле надо передать явно. Команда, рассчитанная на девять минут и запущенная без timeout, будет снята на пятой.
Если ожидание оборвалось
Транспортный таймаут, разрыв соединения или перезапуск клиента прекращают только ожидание ответа. Команда на сервере при этом не останавливается и может доработать до конца уже после того, как клиент получил ошибку. Поэтому повторный запуск изменяющей команды может применить изменение второй раз.
Чтобы этого избежать, принятый запуск обычно получает идентификатор — но это не гарантия: если в синхронном режиме платформа не смогла завести запись, идентификатора не будет, а команда всё равно выполняется. В длительном режиме отдельной машины запись обязательна, и идентификатор возвращается с 202. Когда он есть, он приходит тремя каналами:
- заголовок ответа
X-Vibe-Operation-Id— на отдельной виртуальной машине уходит до начала выполнения команды, поэтому доживает до клиента даже когда тело ответа потеряно. У синхронного exec galaxy-приложения он приходит только с терминальным ответом: эта ветка не открывает поток, поэтому при обрыве ожидания идентификатор до клиента не доезжает и запуск ищется в списке операций сервера с?kind=exec, - первый кадр
event: operationпри?stream=true— только у синхронного exec на отдельной виртуальной машине. БраузерныйEventSourceзаголовков ответа не отдаёт, поэтому для него это единственный ранний канал, - поле
operationIdв теле —data.operationIdпри успехе,error.operationIdпри отказе.
Порядок восстановления:
- Не повторяйте изменяющую команду вслепую.
- Запросите
GET /v1/infra/operations/:operationId. Если идентификатор потерян, найдите запуск в списке операций сервера с параметром?kind=exec. - Статус
succeededозначает, что команда отработала до конца, а её собственный вердикт лежит вexitCode. Повторять не нужно. - Статус
failedплатформа ставит, только когда может доказать, что команда не запускалась, — например приEXEC_BUSY. Это доказывает только отказ этой попытке. ПриEXEC_BUSYсначала сверьте занявшую канал операцию, включая прежнюю попытку с потерянным подтверждением; неизвестный исход занятой операции не разрешает новый exec. - Статус
unknownозначает, что исход не доказан и команда может выполняться прямо сейчас. Сверьтесь с состоянием сервера и журналом до решения о повторе.
Для синхронного exec запись хранит исход попытки — статус, код возврата и причину отказа — без вывода команды. С async=true stdout/stderr сохраняются в операции; другой вариант — самостоятельно управлять фоновой задачей и читать журнал.
Фоновые задачи
Для синхронного JSON и потокового exec timeout ограничен 600 секундами, а по его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без grace-паузы). На отдельной машине для более долгих операций — установки пакетов, восстановления дампа БД, тяжёлой сборки — используйте долговременный async=true, если нужен сохранённый вывод. Другой вариант — самостоятельно управлять фоновой задачей: /exec вернётся мгновенно, канал освободится (второй вызов не упрётся в EXEC_BUSY), а процесс продолжит работать независимо от HTTP-соединения.
Рекомендуемый способ — транзиентный systemd-юнит
{ "command": "systemctl reset-failed restore-db 2>/dev/null; systemd-run --unit=restore-db /bin/bash /opt/data/restore.sh", "timeout": 30 }
Задача получает собственный cgroup и живёт независимо от exec-сессии. Управление — стандартными вызовами:
- статус:
{ "command": "systemctl is-active restore-db" }—active(идёт),inactive(завершилась успешно),failed(упала). - логи: journald подхватывает вывод юнита автоматически — читайте через
GET /v1/infra/servers/:id/logs?service=restore-db. systemctl reset-failed <имя>перед повторным запуском — иначе systemd откажет в создании юнита с именем упавшей задачи.
Лёгкий вариант — фон с редиректом вывода
{ "command": "(cd /opt/app && npm run build > /tmp/build.log 2>&1 &)", "timeout": 10 }
Редирект обоих потоков в файл обязателен — он освобождает пайпы exec-сессии. Прогресс: { "command": "tail -20 /tmp/build.log" }.
Анти-паттерн — голый `nohup`
nohup cmd & без редиректа не работает: в пайпе (а не терминале) nohup не перенаправляет вывод, фоновый процесс наследует пайп exec-сессии, агент ждёт его закрытия до самого timeout — и затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте > файл 2>&1.
Не вставляйте значения секретов в command. На отдельной машине передавайте их через env; в galaxy-приложении загружайте внутри контейнера из существующего защищённого файла или настроенного хранилища секретов приложения. Синхронный exec журналирует на агенте первые 200 символов команды. Длительный worker отключает этот журнал, но сама команда может раскрыть секреты через вывод или дочерние процессы. Помните также, что шаги install/preStart у /deploy имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый /exec или фоновую задачу, а не в install-скрипт.
Durable exec — длительная команда
Для долгой команды вызовите POST /v1/infra/servers/:id/exec?async=true только на сервере STANDALONE. Ответ: HTTP 202, X-Vibe-Operation-Id и data: {operationId, status: "running", statusUrl}. Опрашивайте GET /v1/infra/operations/:operationId; после потери подтверждения не отправляйте второй /exec. Если идентификатор потерян, найдите запуск в списке операций сервера с kind=exec.
Тело принимает timeout от 1 до 86400 секунд (по умолчанию 300). async=true несовместим с stream=true. Любой другой вид сервера получает 400 DURABLE_EXEC_UNSUPPORTED_SERVER_KIND до блокировки, создания операции и отправки команды, без синхронного перехода. Exec Galaxy остаётся синхронным с пределом 600 секунд. Исполнение, не поддерживающее длительные задачи, отказывает до запуска; синхронного перехода нет.
Задача переживает потерю HTTP-ответа и переподключение. Её канал занят на всё время задачи, хотя после 202 платформенный лок освобождён; другие exec и команды выкладки получают EXEC_BUSY. Читайте операцию вместо повторного exec или вызова /unstick. Завершённая операция отдаёт stdout, stderr, duration в миллисекундах, truncated и доказанный exitCode; общий предел вывода — 1 МиБ. Вывод хранится зашифрованным в семидневном окне исхода. Приватный вывод доступен только ключу в области операции, принадлежащему пользователю-создателю: ротация ключа того же владельца сохраняет доступ, передача другому владельцу — нет. succeeded включает ненулевой код выхода; unknown не разрешает повтор команды. Перезагрузка машины или потеря процесса могут оставить неизвестный исход, требующий проверки и подтверждения остановки. Чтение сверяет результат без запуска команды. Отмена и автоматический повтор недоступны.
Исполнение хранит не более 64 записей задач на отдельной машине, включая завершённые задачи в семидневном окне хранения. При заполнении новая команда отклоняется до исполнения с HTTP 502 EXEC_JOB_CAPACITY и error.operationId. Сначала прочитайте операцию, затем дождитесь допустимой очистки по сроку хранения или обратитесь в поддержку; автоматического повтора нет. Очистка при приёме запроса удаляет только просроченные результаты с подтверждённой остановкой. Если задача прервана до сохранения результата, подтверждённая остановка оставляет исход unknown, но через семь суток запись допускается к штатной очистке. Активные задачи и неподтверждённая остановка не очищаются только потому, что прошло семь суток. /unstick и удаление лока не снимают ограничение приёма длительной задачи.