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 — личный ключ
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-приложение
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 — личный ключ
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-приложение
// Команда с переменными окружения и рабочей директорией
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 МБ на поток) |
Пример ответа
{
"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-статус отражает результат:
{
"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, второй такой вызов вернёт 409EXEC_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-юнит
{ "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.
Секреты (пароли БД и т. п.) передавайте через поле env, а не внутри command — команды журналируются агентом (первые 200 символов). Помните также, что шаги install/preStart у `/deploy` имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый /exec или фоновую задачу, а не в install-скрипт.
Смотрите также
- Полный деплой —
POST /v1/infra/servers/:id/deploy— цепочка нескольких/execс проверкой работоспособности. - Снять зависший лок —
DELETE /v1/infra/servers/:id/lock— еслиEXEC_BUSYпосле обрыва. - Логи сервиса —
GET /v1/infra/servers/:id/logs— системный журнал черезjournalctlи мониторинг фоновых задач, запущенных черезsystemd-run(?service=<имя юнита>). - Загрузить файл —
POST /v1/infra/servers/:id/upload— записать файл на сервер. - Восстановить туннель — если
SERVER_NOT_READY. - Длинные операции через exec — синхронный JSON, потоковый SSE и фоновый
systemd-runпод задачи дольше 600 секунд.