Для AI-агентов: markdown этой страницы — /docs-content/infra/deploy/exec.md индекс документации — /llms.txt

Выполнить команду

POST /v1/infra/servers/:id/exec

Выполняет shell-команду на BLACKHOLE-сервере через агент туннеля. Полный стандартный вывод (stdout), поток ошибок (stderr) и код возврата передаются клиенту. По умолчанию ответ — JSON после завершения команды (рекомендуется для AI-агентов и скриптов). Если нужен построчный прогресс — передайте ?stream=true и читайте SSE-события stdout/stderr/exit. Отказ при выполнении команды приходит в потоковом режиме событием error.

Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без ?stream=true) — SSE они разбирать не умеют. Официальный MCP-клиент платформы Вайбкод (инструмент manage_server_deploy) снимает этот параметр сам и всегда работает в JSON-режиме.

Потоковый режим даёт живой прогресс, но не увеличивает предельное время выполнения: оно одинаково в обоих режимах и равно timeout + 30 секунд, а сам timeout ограничен 600 секундами. Для работы, которая заведомо в это не укладывается, запускайте фоновую задачу — см. «Фоновые задачи» ниже.

На каких серверах работает

Тип сервера приходит в поле 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 (см. ниже)
GALAXY — машина, несущая контейнеры приложений нет — 403 GALAXY_HOST_EXEC_FORBIDDEN На машине стоят контейнеры разных ключей одного аккаунта Битрикс24, поэтому целью команд она не является. Выполняйте команду на приложении, по его собственному id (kind равен GALAXY_APP). Свободное место на машине смотрите в кабинете, на карточке галактики — см. Galaxy-приложение

Параметры

Параметр В Тип Обяз. По умолч. Описание
id path string (UUID) да ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED
stream query string нет true — потоковый режим SSE. Для JSON-ответа параметр не передавайте: JSON возвращается по умолчанию

Поля запроса (body)

Поле Тип Обяз. Описание
command string да Shell-команда. 1–10 000 символов
timeout number нет Таймаут выполнения в секундах: 1–600. По умолчанию 300
workdir string нет Рабочая директория. До 500 символов
env object нет Переменные окружения: { "KEY": "value" }. Только строковые значения

Примеры

curl — личный ключ

Terminal
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-приложение

Terminal
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 — личный ключ

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}s:\n${data.stdout}`)

JavaScript — OAuth-приложение

javascript
// Команда с переменными окружения и рабочей директорией
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 true если команда выполнилась (включая ненулевой exitCode)
data.exitCode number Код возврата процесса
data.stdout string Стандартный вывод команды
data.stderr string Поток ошибок команды
data.duration number Время выполнения в секундах
data.truncated boolean true если stdout/stderr обрезаны по размеру (5 МБ на поток)

Пример ответа

JSON
{
  "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
  }
}

Пример ответа при ошибке

409 — на сервере уже выполняется другая операция. Это отказ до запуска команды, поэтому HTTP-статус отражает результат. С этим же статусом у galaxy-приложения приходит занятость общего exec-канала хоста — она возникает уже во время выполнения, но по смыслу это тоже «занято, повторите»:

JSON
{
  "success": false,
  "error": {
    "code": "EXEC_BUSY",
    "message": "Another operation is running on this server",
    "retryable": true,
    "retryAfter": 10
  }
}

Ответ также несёт HTTP-заголовок Retry-After (в секундах, совпадает с полем retryAfter) — это короткий интервал опроса: повторяйте вызов с ним, пока блокировка не снимется. Полный верхний предел «когда лок снимется сам» лежит в error.hint.autoExpiresInSeconds.

Ошибки

Ошибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа — с одним исключением: занятость exec-канала (EXEC_BUSY) у galaxy-приложения приходит настоящим 409, потому что это отказ «занято», а не сбой.

Ошибки до запуска команды

HTTP Код Описание
400 VALIDATION_ERROR Нарушена схема запроса (пустая команда, неверный таймаут, workdir длиннее 500 символов)
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 /opt/app; FOO=bar node script.js
401 MISSING_API_KEY Не передан заголовок X-Api-Key
401 INVALID_API_KEY Неверный или просроченный API-ключ
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 — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — Проектный ключ для деплоя
404 NOT_FOUND Сервера с таким id нет или он удалён. Сервер, который существует, но вам не принадлежит, отвечает 403 WRONG_KEY — код различает «нет такого сервера» и «нет прав на него»
409 EXEC_BUSY На сервере уже выполняется другая /exec или /deploy. Ответ несёт заголовок Retry-After и поля retryable: true / retryAfter (секунды) — повторяйте с этим интервалом. Либо снимите лок через /lock. Если EXEC_BUSY сохраняется после снятия лока — освободите канал принудительно через POST /v1/infra/servers/:id/unstick. У galaxy-хоста и galaxy-приложения принудительное освобождение владельцу недоступно (409 GALAXY_UNSTICK_UNSUPPORTED): exec-канал общий для всех приложений хоста — повторите через 30-60 с, при устойчивом отказе обратитесь в поддержку
409 SERVER_NOT_READY Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле hint с причиной и следующим шагом. Спящий сервер этот код не описывает — его платформа будит сама. Запустите остановленный сервер вызовом /start либо восстановите туннель вызовом /repair и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как 502 TUNNEL_NOT_FOUND (см. ниже)
409 WAKE_IN_PROGRESS Спящий сервер уже будит другой запрос. Дождитесь его завершения и повторите
422 VM_MISSING У записи сервера нет виртуальной машины у облачного провайдера, поэтому будить нечего. Удалите сервер и создайте новый
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 операций в минуту на сервер
503 WAKE_TIMEOUT Спящий сервер не поднялся за отведённые 6.5 минуты — машина не запустилась или туннель не подключился. Сервер возвращается в sleeping, поэтому повторный запрос безопасен. См. «Известные особенности»

Ошибки во время выполнения

HTTP-статус не отражает результат команды. На отдельной виртуальной машине (kind: "STANDALONE") соединение удерживается на всё время выполнения, а статус 200 отправляется до его начала — поэтому и успешная, и провалившаяся команда приходят с кодом 200. Признак отказа — поле success: false и объект error в теле ответа. Проверяйте success в теле, а не HTTP-статус: клиент, который ветвится по статусу, посчитает провалившуюся команду успешной. У galaxy-приложения (kind: "GALAXY_APP") та же ошибка приходит со статусом 502.

Код Описание
EXEC_TIMEOUT Превышен timeout (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе — объект hint, его состав разобран ниже
EXEC_FAILED Ошибка выполнения на агенте
EXEC_NO_EXIT Поток завершился, не прислав статус выхода. Что успела сделать команда на сервере — неизвестно, поэтому это не успех: агент мог перестать отвечать, туннель — оборваться в середине команды, а очень большой вывод — переполнить канал уже после того, как команда отработала. В data возвращается накопленный к моменту обрыва вывод (stdout, stderr). Полей exitCode, duration и truncated нет — их значения платформе неизвестны. В ответе hint с путём восстановления. Только на отдельной виртуальной машине (kind: "STANDALONE"). У galaxy-приложения этот случай пока приходит как success: true с exitCode: -1
EXEC_BUSY Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (409 до старта): приходит уже во время выполнения, в теле как success: false (на STANDALONE — с HTTP 200, на GALAXY_APP — с 409, вместе с заголовком Retry-After и полями retryable: true / retryAfter). В ответе hint с путём восстановления: на STANDALONE — через /unstick, на GALAXY_APP — повтор и обращение в поддержку, потому что общий канал хоста владелец приложения освободить не может
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 потокового режима. Такой же по составу объект hint несёт агентский EXEC_BUSY — на отдельной виртуальной машине (kind: "STANDALONE") его recoveryAction указывает на POST /v1/infra/servers/:id/unstick. У galaxy-приложения этот путь владельцу закрыт, поэтому там recoveryAction описывает повтор и обращение в поддержку.

Отказ связи с туннелем приходит в том же виде — со своим кодом в поле 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, второй такой вызов вернёт 409 EXEC_BUSY. Если предыдущий /deploy был прерван (например, упал на шаге healthcheck или клиент оборвал соединение по таймауту), серверный лок продолжает держаться до автоматического истечения — для /deploy это до 15 минут, для /exec — пока не истечёт timeout команды плюс 90 секунд. Снять лок сразу можно вызовом DELETE /v1/infra/servers/:id/lock, после чего /exec снова работает. Пересоздавать сервер не нужно.
  • Максимальный размер stdout/stderr — 5 МБ на поток. Если вывод команды больше, приходит 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 уже на готовой.

Принудительное освобождение канала

Если после DELETE /v1/infra/servers/:id/lock вызов /exec всё ещё возвращает EXEC_BUSY/deploy — падает с кодом DEPLOY_FAILED и сообщением «Another command is running»), значит завис exec-мьютекс на стороне агента: серверный лок снят, а канал остаётся занятым фоновым процессом. Этот случай лечит POST /v1/infra/servers/:id/unstick — там разобраны параметры, ответ и все коды отказа.

Как выбрать режим

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

Когда Как Мониторинг
Команда укладывается в 600 секунд, прогресс не нужен { "command": "...", "timeout": 600 } без ?stream=true — блокирующий JSON-ответ exitCode, stdout, stderr в теле ответа
Команда укладывается в timeout, нужен живой прогресс тот же запрос с ?stream=true — SSE-события stdout / stderr / exit читать события по мере поступления
Команда дольше 600 секунд или канал не должен быть занят на всё её время фоновая задача systemd-run --unit=<name>/exec возвращается сразу GET /v1/infra/servers/:id/logs?service=<name>

Общий дедлайн запроса на стороне клиента не ставьте меньше значения timeout команды — иначе клиент оборвёт соединение раньше, чем сервер успеет ответить. Спящий сервер добавляет к этому дедлайну до 6.5 минут на пробуждение — пример ниже рассчитан на уже запущенную машину. Пример раздельных таймаутов для Python:

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.

Важно: умолчание timeout300 секунд, а не 600. Потолок 600 существует, но чтобы им воспользоваться, поле надо передать явно. Команда, рассчитанная на девять минут и запущенная без timeout, будет снята на пятой.

Фоновые задачи

timeout у /exec ограничен 600 секундами, а по его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без grace-паузы). Операции длиннее — установка пакетов, восстановление дампа БД, тяжёлая сборка — запускайте фоновой задачей: /exec вернётся мгновенно, канал освободится (второй вызов не упрётся в EXEC_BUSY), а процесс продолжит работать независимо от HTTP-соединения.

Рекомендуемый способ — транзиентный systemd-юнит

JSON
{ "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 откажет в создании юнита с именем упавшей задачи.

Лёгкий вариант — фон с редиректом вывода

JSON
{ "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.

Секреты (пароли БД и т. п.) передавайте через поле env, а не внутри command — команды журналируются агентом (первые 200 символов). Помните также, что шаги install/preStart у /deploy имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый /exec или фоновую задачу, а не в install-скрипт.

Смотрите также