Deploy API

Развёртывание приложений на BLACKHOLE-серверах без SSH. Все операции идут через агент туннеля: команды выполняются на сервере, файлы загружаются через туннель, логи читаются из системного журнала (утилита journalctl). AI-агенты должны использовать эту группу эндпоинтов для полного цикла «взять код из git → установить зависимости → запустить сервис → читать логи».

Требования: сервер в режиме BLACKHOLE, статус running, blackholeStatus: CONNECTED. В OPEN-режиме Deploy API вернёт NOT_BLACKHOLE.

Это контракт для Black Hole VM (kind: "STANDALONE"). Для galaxy-приложения (kind: "GALAXY_APP") требование CONNECTED не действует — деплой сам собирает контейнер. Полная модель и контракт деплоя galaxy-приложения — Galaxy-приложение.

OOM galaxy-приложения → выпуск на выделенный сервер. Если деплой galaxy-приложения падает с 502 GALAXY_APP_START_FAILED именно по причине OOM (приложение переросло лимит памяти контейнера 512 МБ), ответ содержит структурную подсказку error.hint с recoveryAction: "graduate-to-dedicated-vm". Рекомендованное действие — пересоздать приложение на отдельной виртуальной машине: `POST /v1/infra/servers` с placement: "dedicated" и graduateFrom (идентификатор упавшего galaxy-приложения, который будет удалён после создания сервера), затем POST /v1/infra/servers/:id/deploy с тем же исходным кодом. Защита от повторного создания через заголовок Idempotency-Key на этот выпуск не распространяется — вместе с graduateFrom он вернёт 400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION. Допустимо, когда политика serverCreation портала разрешает создание серверов и вы в пределах квоты, выделенная виртуальная машина тарифицируется (засыпает при простое). Подсказка добавляется только при причине OOM, а не при обычном краше.

Формат ответов: все эндпоинты по умолчанию отдают JSON (201/200 со {"success": true, ...}). Для /deploy и /exec доступен потоковый режим через ?stream=true — возвращается поток SSE (Server-Sent Events) с построчным прогрессом. Для AI-агентов и MCP-клиентов всегда используйте JSON (не добавляйте ?stream=true) — они не умеют разбирать SSE.

Ограничения частоты:

Ограничение Значение
Операций в минуту на сервер 10
Одновременных exec/deploy 1 на сервер
Таймаут exec 1–600 секунд (по умолчанию 300)
Размер встроенного тела (base64 в /upload, /deploy source.content) 500 МБ
Размер файла через URL (/upload url, /deploy source.url) 500 МБ
Размер multipart-архива в /deploy 500 МБ

Скоуп: vibe:infra

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

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 они разбирать не умеют.

Параметры

Параметр В Тип Обяз. По умолч. Описание
id path string (UUID) да ID BLACKHOLE-сервера, status: running, blackholeStatus: CONNECTED
stream query string нет false true — SSE-режим, иначе — 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?stream=false" \
  -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?stream=false" \
  -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?stream=false`,
  {
    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?stream=false`,
  {
    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-статус отражает результат:

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

Ошибки

Ошибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа.

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

HTTP Код Описание
400 VALIDATION_ERROR Нарушена схема запроса (пустая команда, неверный таймаут, workdir длиннее 500 символов)
400 NOT_BLACKHOLE Сервер в режиме OPEN — Deploy API недоступен
400 COMMAND_TOO_LONG Команда длиннее 10 000 символов. В ответе hint: большие данные и скрипты передавайте через `/upload`, затем bash /путь/скрипт.sh
401 MISSING_API_KEY Не передан заголовок X-Api-Key
401 INVALID_API_KEY Неверный или просроченный API-ключ
404 NOT_FOUND Сервер не существует, удалён или принадлежит другому API-ключу
409 EXEC_BUSY На сервере уже выполняется другая /exec или /deploy. Подождите или снимите лок через `/lock`. Если EXEC_BUSY сохраняется после снятия лока — расклиньте канал через POST /v1/infra/servers/:id/unstick (см. ниже)
409 SERVER_NOT_READY Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле hint с причиной и следующим шагом. Разбудите сервер или вызовите `/repair` и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как 502 TUNNEL_NOT_FOUND (см. ниже)
429 RATE_LIMITED Превышен лимит 10 операций в минуту на сервер

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

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_BUSY Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (409 до старта): приходит уже во время выполнения, в теле как success: false (на STANDALONE — с HTTP 200, на GALAXY_APP — с 502). В ответе hint с путём восстановления через `/unstick`

Отказ связи с туннелем приходит в том же виде — со своим кодом в поле error.code (например TUNNEL_NOT_FOUND или GATEWAY_TIMEOUT: …). Часть gateway-кодов несёт детали после двоеточия (GATEWAY_UNREACHABLE: …, GATEWAY_TIMEOUT: …), поэтому сопоставляйте error.code по префиксу, а не по точному совпадению. В потоковом режиме (?stream=true) эти же ошибки приходят SSE-событием error. Данные события несут code и message, поля success в них нет — признаком отказа служит само событие error.

Полный список общих ошибок API — Ошибки.

Известные особенности

  • Блокировка на уровне сервера. Пока идёт /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 для повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже.

Расклинивание зависшего канала

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

Если в момент вызова на сервере всё ещё выполняется легитимная операция (деплой, exec или харден), эндпоинт по умолчанию отвечает 409 OPERATION_IN_PROGRESS и НЕ трогает её — иначе он оборвал бы работающую команду. Расклинивать нужно только зависший канал. Если вы уверены, что он действительно завис, повторите с ?force=true.

Ответ: { "success": true, "data": { "backendLockReleased": true, "agentBounced": true, "reconnected": true } }. Для galaxy-хостов и galaxy-приложений расклинивание не поддерживается (409 GALAXY_UNSTICK_UNSUPPORTED).

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

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-скрипт.

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