[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"docs-infra\u002Fdeploy\u002Fexec":3,"docs-tabs-infra\u002Fdeploy\u002Fexec":6},{"content":4,"lastmod":5},"\n## Выполнить команду\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fexec`\n\nВыполняет shell-команду на BLACKHOLE-сервере через агент туннеля. Полный стандартный вывод (`stdout`), поток ошибок (`stderr`) и код возврата передаются клиенту. По умолчанию ответ — JSON после завершения команды (рекомендуется для AI-агентов и скриптов). Если нужен построчный прогресс — передайте `?stream=true` и читайте SSE-события `stdout`\u002F`stderr`\u002F`exit`. Отказ при выполнении команды приходит в потоковом режиме событием `error`.\n\n**Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без `?stream=true`)** — SSE они разбирать не умеют.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n| `stream` | query | string | нет | `false` | `true` — SSE-режим, иначе — JSON-ответ |\n\n## Поля запроса (body)\n\n| Поле | Тип | Обяз. | Описание |\n|------|-----|:-----:|----------|\n| `command` | string | **да** | Shell-команда. 1–10 000 символов |\n| `timeout` | number | нет | Таймаут выполнения в секундах: 1–600. По умолчанию 300 |\n| `workdir` | string | нет | Рабочая директория. До 500 символов |\n| `env` | object | нет | Переменные окружения: `{ \"KEY\": \"value\" }`. Только строковые значения |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fexec?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"command\": \"ls -la \u002Fopt\u002Fapp\", \"timeout\": 30}'\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fexec?stream=false\" \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"command\": \"npm ci --production\", \"workdir\": \"\u002Fopt\u002Fapp\", \"timeout\": 180}'\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fexec?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      command: 'node -v',\n      timeout: 10,\n    }),\n  }\n)\nconst { data } = await res.json()\nconsole.log(`exit ${data.exitCode} за ${data.duration}s:\\n${data.stdout}`)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\n\u002F\u002F Команда с переменными окружения и рабочей директорией\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fexec?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      command: 'npx tsx scripts\u002Fseed.ts',\n      workdir: '\u002Fopt\u002Fapp',\n      env: { DATABASE_URL: 'postgresql:\u002F\u002Flocalhost\u002Fmydb' },\n      timeout: 60,\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` если команда выполнилась (включая ненулевой exitCode) |\n| `data.exitCode` | number | Код возврата процесса |\n| `data.stdout` | string | Стандартный вывод команды |\n| `data.stderr` | string | Поток ошибок команды |\n| `data.duration` | number | Время выполнения в секундах |\n| `data.truncated` | boolean | `true` если `stdout`\u002F`stderr` обрезаны по размеру (5 МБ на поток) |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"exitCode\": 0,\n    \"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\u002FLinux\\n\",\n    \"stderr\": \"\",\n    \"duration\": 5,\n    \"truncated\": false\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n409 — на сервере уже выполняется другая операция. Это отказ до запуска команды, поэтому HTTP-статус отражает результат:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"EXEC_BUSY\",\n    \"message\": \"Another operation is running on this server\"\n  }\n}\n```\n\n## Ошибки\n\nОшибки эндпоинта делятся на две группы, и отдаются они по-разному. Отказ до запуска команды приходит с настоящим HTTP-статусом. Отказ во время выполнения приходит в теле ответа.\n\n### Ошибки до запуска команды\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема запроса (пустая команда, неверный таймаут, `workdir` длиннее 500 символов) |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 400 | `COMMAND_TOO_LONG` | Команда длиннее 10 000 символов. В ответе `hint`: большие данные и скрипты передавайте через [`\u002Fupload`](.\u002Fupload.md), затем `bash \u002Fпуть\u002Fскрипт.sh` |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `EXEC_BUSY` | На сервере уже выполняется другая `\u002Fexec` или `\u002Fdeploy`. Подождите или снимите лок через [`\u002Flock`](.\u002Flock.md). Если `EXEC_BUSY` сохраняется после снятия лока — расклиньте канал через `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Funstick` (см. ниже) |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n\n### Ошибки во время выполнения\n\n> **HTTP-статус не отражает результат команды.** На отдельной виртуальной машине (`kind: \"STANDALONE\"`) соединение удерживается на всё время выполнения, а статус `200` отправляется до его начала — поэтому и успешная, и провалившаяся команда приходят с кодом `200`. Признак отказа — поле `success: false` и объект `error` в теле ответа. **Проверяйте `success` в теле, а не HTTP-статус:** клиент, который ветвится по статусу, посчитает провалившуюся команду успешной. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n\n| Код | Описание |\n|-----|----------|\n| `EXEC_TIMEOUT` | Превышен `timeout` (или стандартные 300 секунд). Агент завершает процесс-группу принудительно (SIGKILL, без grace-паузы). В ответе `hint` с путями решения — см. секцию «Фоновые задачи» ниже |\n| `EXEC_FAILED` | Ошибка выполнения на агенте |\n| `EXEC_BUSY` | Занят собственный exec-мьютекс агента — он уже выполняет другую команду. Отличается от платформенного лока (`409` до старта): приходит уже во время выполнения, в теле как `success: false` (на `STANDALONE` — с HTTP `200`, на `GALAXY_APP` — с `502`). В ответе `hint` с путём восстановления через [`\u002Funstick`](#расклинивание-зависшего-канала) |\n\nОтказ связи с туннелем приходит в том же виде — со своим кодом в поле `error.code` (например `TUNNEL_NOT_FOUND` или `GATEWAY_TIMEOUT: …`). Часть gateway-кодов несёт детали после двоеточия (`GATEWAY_UNREACHABLE: …`, `GATEWAY_TIMEOUT: …`), поэтому сопоставляйте `error.code` по префиксу, а не по точному совпадению. В потоковом режиме (`?stream=true`) эти же ошибки приходят SSE-событием `error`. Данные события несут `code` и `message`, поля `success` в них нет — признаком отказа служит само событие `error`.\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Блокировка на уровне сервера.** Пока идёт `\u002Fexec` или `\u002Fdeploy`, второй такой вызов вернёт 409 `EXEC_BUSY`. Если предыдущий `\u002Fdeploy` был прерван (например, упал на шаге `healthcheck` или клиент оборвал соединение по таймауту), серверный лок продолжает держаться до автоматического истечения — для `\u002Fdeploy` это до 15 минут, для `\u002Fexec` — пока не истечёт `timeout` команды плюс 90 секунд. Снять лок сразу можно вызовом [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md), после чего `\u002Fexec` снова работает. Пересоздавать сервер не нужно.\n- **Максимальный размер `stdout`\u002F`stderr` — 5 МБ на поток.** Если вывод команды больше, приходит `truncated: true` и хвост обрезается. Команда при этом отработала, `exitCode` корректный. Для большого вывода перенаправляйте в файл: `command: \"my-cmd > \u002Fopt\u002Fapp\u002Foutput.log 2>&1\"` и потом читайте через `\u002Fexec cat \u002Fopt\u002Fapp\u002Foutput.log`.\n- **`.env` из `\u002Fdeploy` не загружается автоматически.** Systemd подгрузит `.env` при старте сервиса, но не для разовых команд через `\u002Fexec`. Если нужны `DATABASE_URL` \u002F API-ключи — передавайте в поле `env`: `{ env: { DATABASE_URL: \"...\" } }`.\n- **Два таймаута в сумме дают `timeout + 30` секунд.** Агент убивает процесс ровно по `timeout`. Gateway ждёт ещё 30 секунд на финальные события и только потом отдаёт `EXEC_TIMEOUT`.\n- **Поддержание соединения в JSON-режиме.** Если команда выполняется дольше 15 секунд, сервер периодически шлёт пробелы в тело ответа — это предотвращает таймауты nginx и промежуточных прокси. JSON-парсер игнорирует начальные и конечные пробелы, так что клиент просто прочитает корректный JSON по окончании.\n- **Клиентский таймаут: убедитесь, что он межбайтовый, а не общий.** 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` для повторных вызовов). Команды дольше нескольких минут надёжнее запускать фоновой задачей — см. «Фоновые задачи» ниже.\n\n## Расклинивание зависшего канала\n\nЕсли после [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md) вызов `\u002Fexec` всё ещё возвращает `EXEC_BUSY` (а `\u002Fdeploy` — падает с кодом `DEPLOY_FAILED` и сообщением «Another command is running»), значит завис exec-мьютекс на стороне агента — фоновый процесс держит канал открытым, поэтому серверный лок снят, а канал остаётся занятым. `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Funstick` снимает серверный лок И разрывает туннель агента, который при переподключении завершает зависшую команду и освобождает мьютекс. Перезагрузка сервера не выполняется.\n\nЕсли в момент вызова на сервере всё ещё выполняется легитимная операция (деплой, exec или харден), эндпоинт по умолчанию отвечает `409 OPERATION_IN_PROGRESS` и НЕ трогает её — иначе он оборвал бы работающую команду. Расклинивать нужно только зависший канал. Если вы уверены, что он действительно завис, повторите с `?force=true`.\n\nОтвет: `{ \"success\": true, \"data\": { \"backendLockReleased\": true, \"agentBounced\": true, \"reconnected\": true } }`. Для galaxy-хостов и galaxy-приложений расклинивание не поддерживается (`409 GALAXY_UNSTICK_UNSUPPORTED`).\n\n## Фоновые задачи\n\n`timeout` у `\u002Fexec` ограничен 600 секундами, а по его истечении агент завершает всю процесс-группу принудительно (SIGKILL, без grace-паузы). Операции длиннее — установка пакетов, восстановление дампа БД, тяжёлая сборка — запускайте фоновой задачей: `\u002Fexec` вернётся мгновенно, канал освободится (второй вызов не упрётся в `EXEC_BUSY`), а процесс продолжит работать независимо от HTTP-соединения.\n\n### Рекомендуемый способ — транзиентный systemd-юнит\n\n```json\n{ \"command\": \"systemctl reset-failed restore-db 2>\u002Fdev\u002Fnull; systemd-run --unit=restore-db \u002Fbin\u002Fbash \u002Fopt\u002Fdata\u002Frestore.sh\", \"timeout\": 30 }\n```\n\nЗадача получает собственный cgroup и живёт независимо от exec-сессии. Управление — стандартными вызовами:\n\n- статус: `{ \"command\": \"systemctl is-active restore-db\" }` — `active` (идёт), `inactive` (завершилась успешно), `failed` (упала).\n- логи: journald подхватывает вывод юнита автоматически — читайте через [`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs?service=restore-db`](.\u002Flogs.md).\n- `systemctl reset-failed \u003Cимя>` перед повторным запуском — иначе systemd откажет в создании юнита с именем упавшей задачи.\n\n### Лёгкий вариант — фон с редиректом вывода\n\n```json\n{ \"command\": \"(cd \u002Fopt\u002Fapp && npm run build > \u002Ftmp\u002Fbuild.log 2>&1 &)\", \"timeout\": 10 }\n```\n\nРедирект обоих потоков в файл обязателен — он освобождает пайпы exec-сессии. Прогресс: `{ \"command\": \"tail -20 \u002Ftmp\u002Fbuild.log\" }`.\n\n### Анти-паттерн — голый `nohup`\n\n`nohup cmd &` **без редиректа** не работает: в пайпе (а не терминале) `nohup` не перенаправляет вывод, фоновый процесс наследует пайп exec-сессии, агент ждёт его закрытия до самого `timeout` — и затем принудительно завершает всю процесс-группу, включая ваш «фоновый» процесс. Всегда добавляйте `> файл 2>&1`.\n\nСекреты (пароли БД и т. п.) передавайте через поле `env`, а не внутри `command` — команды журналируются агентом (первые 200 символов). Помните также, что шаги `install`\u002F`preStart` у [`\u002Fdeploy`](.\u002Fdeploy.md) имеют собственный таймаут 300 секунд — тяжёлые установки выносите в разовый `\u002Fexec` или фоновую задачу, а не в install-скрипт.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy` — цепочка нескольких `\u002Fexec` с проверкой работоспособности.\n- [Снять зависший лок](.\u002Flock.md) — `DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock` — если `EXEC_BUSY` после обрыва.\n- [Логи сервиса](.\u002Flogs.md) — `GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs` — системный журнал через `journalctl` и мониторинг фоновых задач, запущенных через `systemd-run` (`?service=\u003Cимя юнита>`).\n- [Загрузить файл](.\u002Fupload.md) — `POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fupload` — записать файл на сервер.\n- [Восстановить туннель](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) — если `SERVER_NOT_READY`.\n- [Длинные операции через exec](\u002Fdocs\u002Frecipes\u002Flong-operations) — синхронный JSON, потоковый SSE и фоновый `systemd-run` под задачи дольше 600 секунд.\n","2026-07-14",{"deploy.md":7,"lock.md":8,"logs.md":9,"upload.md":10},"\n## Полный деплой приложения\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fdeploy`\n\nПолный цикл деплоя за один запрос: остановка предыдущего сервиса → скачивание архива → распаковка → установка рантайма → установка зависимостей → запись `.env` → команды `preStart` → создание systemd-юнита → запуск → проверка работоспособности. Используйте вместо цепочки `\u002Fupload` + `\u002Fexec` — быстрее и атомарно. По умолчанию ответ — JSON (рекомендуется для AI-агентов). Для построчного прогресса передайте `?stream=true` — сервер вернёт поток SSE (Server-Sent Events) с событиями на каждом шаге.\n\n**Для AI-агентов и MCP-клиентов обязательно используйте JSON-режим (без `?stream=true`).**\n\n## Galaxy-приложение — деплой сразу после создания\n\nЕсли сервер — galaxy-приложение (поле `createdVia` равно `galaxy` в ответе [`POST \u002Fv1\u002Finfra\u002Fservers`](..\u002Fservers\u002Fcreate.md)), деплой запускают **сразу после создания**, не дожидаясь `blackholeStatus: \"CONNECTED\"` — контейнер собирается самим деплоем. Модель размещения, жизненный цикл и стоимость — на странице [Galaxy-приложение](..\u002Fgalaxy.md).\n\nОтличия деплоя для galaxy-приложения:\n\n- `runtime` и `start` **обязательны**. Без `runtime` запрос вернёт `400 GALAXY_DEPLOY_RUNTIME_REQUIRED`.\n- Источник — только встроенный `source.content` (архив в base64). Варианты `source.url` и `source.versionId` для galaxy-приложения отклоняются с `400 GALAXY_DEPLOY_CONTENT_ONLY`.\n- `Dockerfile` в архиве не требуется — платформа сгенерирует его сама из `runtime`, `port` и `start`. Если положить свой `Dockerfile` в архив, он будет проигнорирован (платформа всегда генерирует свой). См. «Известные особенности» ниже.\n- Рабочая директория контейнера — **`\u002Fopt\u002Fapp`** (та же, что и `extractTo` у обычной виртуальной машины): код копируется туда, поэтому работают и относительная команда (`start: \"node server.js\"`), и абсолютный путь (`start: \"node \u002Fopt\u002Fapp\u002Fserver.js\"`). Путь `\u002Fapp` тоже ведёт на `\u002Fopt\u002Fapp` — это символьная ссылка для обратной совместимости. То есть один и тот же `start` подходит и для VM-, и для galaxy-деплоя.\n\nЖизненный цикл galaxy-приложения — на странице [Galaxy-приложение](..\u002Fgalaxy.md). Сценарий «деплой одним запросом при создании» — на странице [Создать сервер](..\u002Fservers\u002Fcreate.md).\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n| `stream` | query | string | нет | `false` | `true` — SSE-режим, иначе — JSON-ответ |\n\n## Поля запроса (body — JSON)\n\n| Поле | Тип | Обяз. | По умолч. | Описание |\n|------|-----|:-----:|-----------|----------|\n| `source` | object | **да** | — | Источник кода — ровно один из трёх вариантов ниже (`url`, `content` или `versionId`) |\n| `source.url` | string | да (или `content` \u002F `versionId`) | — | HTTPS-URL архива с кодом. До 500 МБ |\n| `source.content` | string | да (или `url` \u002F `versionId`) | — | Base64-строка архива. До 500 МБ на тело запроса |\n| `source.versionId` | string | да (или `url` \u002F `content`) | — | ID снапшота из депо приложения, например `v3` — см. [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n| `start` | string | **да** | — | Команда запуска приложения (пример: `cd \u002Fopt\u002Fapp && node server.js`) |\n| `port` | number | нет | `3000` | Порт, на котором приложение слушает. **Всегда 3000** для BLACKHOLE — если не передан, подставляется 3000. (В multipart-режиме порт указывать обязательно, см. таблицу ниже.) |\n| `extractTo` | string | нет | `\u002Fopt\u002Fapp` | Директория распаковки |\n| `runtime` | string | нет | — | ID рантайма: `node20`, `python311`, `php83`, `static` и т. д. Список — [`GET \u002Fv1\u002Finfra\u002Fruntimes`](.\u002Fruntimes.md). Сервер создаётся из стандартного образа Ubuntu 24.04 — без `runtime` на нём нет ни `node`, ни `python`, ни `php`, и `start` упадёт с `command not found`. Альтернатива — поставить тулчейн вручную через `preStart` (см. ниже). |\n| `install` | string | нет | — | Команда установки зависимостей (до 5000 символов) |\n| `preStart` | string | нет | — | Команда **до** запуска сервиса: миграции БД, seed-скрипты, сборка, ручная установка системных пакетов (например, `apt-get update && apt-get install -y nodejs npm` если не передан `runtime`). До 5000 символов |\n| `env` | object | нет | — | Переменные окружения `{ \"KEY\": \"value\" }` — будут записаны в `.env` и загружены systemd |\n| `systemd` | boolean | нет | `true` | Создавать systemd-юнит для автозапуска |\n| `cleanDeploy` | boolean | нет | `true` | Очистить `extractTo` перед распаковкой. Установите `false` для merge-деплоя |\n| `serviceName` | string | нет | `app` | Имя systemd-юнита. Только `a-z`, `0-9`, `-`. Создаёт `{serviceName}.service`. При смене имени между деплоями — см. «Известные особенности» |\n| `healthPath` | string | нет | `\u002F` | Путь для проверки работоспособности — например, `\u002Fhealth`, `\u002Fapi\u002Fstatus`. Только безопасные символы |\n| `debug` | boolean | нет | `false` | Добавить диагностический шаг `debug_info` — `ls -la`, `sha256sum` файлов, `file -i` для отладки шагов сборки |\n\n## Поля запроса (body — multipart\u002Fform-data)\n\nАльтернатива JSON — загрузка архива без base64. Полезно из скриптов bash\u002FPowerShell.\n\n| Поле формы | Тип | Обяз. | Описание |\n|------------|-----|:-----:|----------|\n| `archive` | файл | **да** | Архив (tar.gz \u002F zip), до 500 МБ |\n| `start` | string | **да** | Команда запуска |\n| `port` | string | **да** | Порт (строкой: `\"3000\"`) |\n| `extractTo` | string | нет | По умолчанию `\u002Fopt\u002Fapp` |\n| `runtime` | string | нет | ID рантайма |\n| `install` | string | нет | Команда установки |\n| `preStart` | string | нет | Команда до запуска |\n| `env` | string | нет | JSON-строка: `{\"KEY\":\"value\"}` |\n| `systemd` | string | нет | `\"true\"` \u002F `\"false\"` |\n| `cleanDeploy` | string | нет | **При multipart по умолчанию `\"false\"`** (merge). Укажите `\"true\"` для чистого деплоя |\n| `serviceName` | string | нет | Имя systemd-юнита |\n| `healthPath` | string | нет | Путь для проверки работоспособности |\n| `debug` | string | нет | `\"true\"` — добавить диагностический шаг `debug_info` |\n\n## Шаги деплоя\n\nШаги выполняются последовательно. Если какой-то провалился — деплой останавливается.\n\n| Шаг | Условие | Что делает | Таймаут |\n|-----|---------|------------|---------|\n| `stop_existing` | всегда | `systemctl stop {serviceName}` — останавливает сервис с текущим именем `serviceName` и освобождает порт от оставшихся процессов этого же деплоя. Посторонний процесс не завершается: шаг возвращает статус `warning`, деплой продолжается | 15с |\n| `clean` | `cleanDeploy: true` | Удаляет всё в `extractTo` | 30с |\n| `download` | всегда | Скачивает и распаковывает архив (1 повтор при ошибке) | 10 мин |\n| `cleanup_metadata` | после extract | Удаляет macOS-сайдкары (`._*`, `.DS_Store`) — молча если архив чистый | — |\n| `normalize_windows_paths` | после extract | Приводит пути с обратной косой чертой `\\` из zip, собранного PowerShell `Compress-Archive`, к обычному виду | — |\n| `debug_info` | `debug: true` | Диагностика: `ls -la`, `sha256sum` файлов, `file -i`, локаль. Читает до 32 КБ в `stdout` | — |\n| `runtime` | `runtime` передан | Устанавливает рантайм. При повторном деплое с тем же — переустанавливает | 300с |\n| `install` | `install` передан | Запускает команду установки зависимостей | 300с |\n| `env` | `env` передан | Пишет `.env` в `extractTo` с правами `0600` | — |\n| `pre_start` | `preStart` передан | Запускает команду до старта сервиса. Для миграций БД, seed-скриптов, сборки | 300с |\n| `systemd` | `systemd: true` | Создаёт `{serviceName}.service` и перечитывает конфигурацию systemd (`systemctl daemon-reload`) | 30с |\n| `start` | всегда | `systemctl enable --now {serviceName}` | 30с |\n| `healthcheck` | всегда | Проверка работоспособности — до 10 попыток `curl localhost:{port}{healthPath}` с интервалом 3 секунды, засчитывается ответ со статусом 200–399 от сервиса `serviceName`. Ответ 200–399 от постороннего процесса, удерживающего порт, шаг не засчитывает и завершается ошибкой | ~30с |\n| `tunnel_routing` | всегда | Проверяет, что туннель Gateway маршрутизирует на нужный порт (`set_port` + опрос агента). Статус `warning` (не `error`) — проверка не прошла, деплой не прерывается | ~10с |\n\n## Примеры\n\nПримеры с внешним `source.url` содержат заголовок `X-Skip-Source-Snapshot`: при включённом хранилище исходников деплой со стороннего адреса без него возвращает `409 SNAPSHOT_REQUIRED`. Подробности и альтернатива через хранилище — в разделе «Известные особенности».\n\n### curl — личный ключ\n\n```bash\n# JSON, Node.js с миграциями\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz\" },\n    \"runtime\": \"node20\",\n    \"install\": \"cd \u002Fopt\u002Fapp && npm install --production\",\n    \"preStart\": \"cd \u002Fopt\u002Fapp && npx prisma migrate deploy\",\n    \"start\": \"cd \u002Fopt\u002Fapp && node server.js\",\n    \"port\": 3000,\n    \"env\": { \"NODE_ENV\": \"production\", \"DATABASE_URL\": \"postgresql:\u002F\u002Flocalhost\u002Fmydb\" }\n  }'\n\n# Multipart — локальный архив\ntar -czf app.tar.gz -C .\u002Fmy-app .\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -F \"archive=@app.tar.gz\" \\\n  -F \"runtime=node20\" \\\n  -F \"install=cd \u002Fopt\u002Fapp && npm install --production\" \\\n  -F \"start=cd \u002Fopt\u002Fapp && node server.js\" \\\n  -F \"port=3000\"\n```\n\n### curl — inline-архив (минимальный набор полей)\n\n```bash\n# Канонический inline-деплой: достаточно source.content + start.\n# Тип архива определяется автоматически по сигнатуре первых байт —\n# tar.gz, zip и tar.bz2 поддерживаются, отдельное поле формата не нужно.\nB64=$(base64 \u003C app.tar.gz | tr -d '\\n')\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\n    \\\"source\\\": { \\\"content\\\": \\\"$B64\\\" },\n    \\\"start\\\": \\\"cd \u002Fopt\u002Fapp && node server.js\\\",\n    \\\"port\\\": 3000\n  }\"\n```\n\nОбязательны только два поля: `source` (один из `content` \u002F `url` \u002F `versionId`) и `start`. Архив можно завернуть и в zip (`zip -r app.zip .`), и в tar.gz (`tar -czf app.tar.gz -C .\u002Fmy-app .`) — платформа распознаёт оба по содержимому.\n\n### curl — произвольный стек без рантайма\n\n```bash\n# Деплой без runtime — любое ПО ставится через preStart\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fexample.com\u002Fmy-app.tar.gz\" },\n    \"preStart\": \"apt-get update -qq && apt-get install -y ffmpeg imagemagick\",\n    \"install\": \"cd \u002Fopt\u002Fapp && npm install --production\",\n    \"start\": \"cd \u002Fopt\u002Fapp && node server.js\",\n    \"port\": 3000\n  }'\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fdeploy?stream=false\" \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -H \"X-Skip-Source-Snapshot: deploy from external URL\" \\\n  -d '{\n    \"source\": { \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Ffastapi\u002Farchive\u002Fmain.tar.gz\" },\n    \"runtime\": \"python311\",\n    \"install\": \"cd \u002Fopt\u002Fapp && pip install -r requirements.txt\",\n    \"start\": \"cd \u002Fopt\u002Fapp && python -m uvicorn main:app --host 0.0.0.0 --port 3000\",\n    \"port\": 3000\n  }'\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fdeploy?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n      'X-Skip-Source-Snapshot': 'deploy from external URL',\n    },\n    body: JSON.stringify({\n      source: { url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz' },\n      runtime: 'node20',\n      install: 'cd \u002Fopt\u002Fapp && npm install --production',\n      start: 'cd \u002Fopt\u002Fapp && node server.js',\n      port: 3000,\n    }),\n  }\n)\nconst body = await res.json()\nif (body.success) {\n  console.log(`Приложение живёт: ${body.data.appUrl}`)\n} else {\n  console.error(`Упал на шаге ${body.error.step}: ${body.error.message}`)\n}\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fdeploy?stream=false`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n      'X-Skip-Source-Snapshot': 'deploy from external URL',\n    },\n    body: JSON.stringify({\n      source: { url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz' },\n      runtime: 'node20',\n      install: 'cd \u002Fopt\u002Fapp && npm install --production',\n      start: 'cd \u002Fopt\u002Fapp && node server.js',\n      port: 3000,\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` — все шаги выполнены, приложение отвечает на проверку работоспособности |\n| `data.steps` | array | Массив шагов процесса деплоя, в порядке выполнения |\n| `data.steps[].step` | string | Имя шага: `stop_existing`, `download`, `runtime`, `install`, `env`, `pre_start`, `systemd`, `start`, `healthcheck`, `tunnel_routing`, и т. д. |\n| `data.steps[].status` | string | `ok` — успешно, `error` — провал (только последний шаг может иметь этот статус) |\n| `data.steps[].duration` | number | Длительность шага в секундах. Присутствует не у всех шагов — только у тех, которые сами отмеряют время выполнения (`runtime`, `install`, `pre_start`, `normalize_windows_paths`, `cleanup_metadata`, `debug_info`) |\n| `data.steps[].stderr` | string | Стандартный поток ошибок (`stderr`) шага при `status: \"error\"` |\n| `data.steps[].stdout` | string | Стандартный вывод (`stdout`), заполняется для шага `debug_info` (до 32 КБ) |\n| `data.serviceName` | string | Имя systemd-юнита (эхо параметра или `\"app\"`) |\n| `data.status` | string | `\"running\"` после успешного деплоя |\n| `data.appUrl` | string | HTTPS-адрес приложения: `https:\u002F\u002F{subdomain}.vibecode.bitrix24.tech` |\n| `data.source` | object | Итог автосохранения исходников. Только при `success: true`. В SSE-режиме приходит отдельным событием `event: source`. Полный контракт — [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n| `data.source.autoSaved` | boolean | `true` — снимок исходников сохранён или привязан к деплою |\n| `data.source.savedVersionId` | string | Версия `vN` сохранённого снимка. Присутствует при `autoSaved: true` |\n| `data.source.skippedReason` | string \\| null | Причина, по которой снимок не сохранён, либо `null`. Значения — [Хранилище исходников](\u002Fdocs\u002Fsource-storage) |\n\nПри `success: false` есть дополнительно `error.code`, `error.message`, `error.step` с именем провалившегося шага, а `data.steps` содержит хронологию шагов до провала. На отдельной виртуальной машине (`kind: \"STANDALONE\"`) такой ответ приходит с HTTP-статусом 200 — соединение удерживается с начала деплоя, поэтому провал шага виден по полю `success`, а не по статусу. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n\n## Пример ответа\n\nУспешный деплой (JSON):\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"steps\": [\n      { \"step\": \"stop_existing\", \"status\": \"ok\" },\n      { \"step\": \"download\", \"status\": \"ok\" },\n      { \"step\": \"runtime\", \"status\": \"ok\", \"duration\": 45 },\n      { \"step\": \"install\", \"status\": \"ok\", \"duration\": 12 },\n      { \"step\": \"env\", \"status\": \"ok\" },\n      { \"step\": \"systemd\", \"status\": \"ok\" },\n      { \"step\": \"start\", \"status\": \"ok\" },\n      { \"step\": \"healthcheck\", \"status\": \"ok\" }\n    ],\n    \"serviceName\": \"app\",\n    \"status\": \"running\",\n    \"appUrl\": \"https:\u002F\u002Fapp-abc12345.vibecode.bitrix24.tech\",\n    \"source\": { \"autoSaved\": false, \"skippedReason\": \"deploy from external URL\" }\n  }\n}\n```\n\nБлок `data.source` — итог автосохранения исходников. В примерах выше деплой идёт с внешнего URL с заголовком `X-Skip-Source-Snapshot`, поэтому `autoSaved: false`, а `skippedReason` повторяет переданную причину. При деплое из хранилища — `source.content` или `source.versionId` — блок выглядит как `{ \"autoSaved\": true, \"savedVersionId\": \"vN\", \"skippedReason\": null }`. Полный контракт и значения `skippedReason` — [Хранилище исходников](\u002Fdocs\u002Fsource-storage).\n\nSSE-поток при `?stream=true`:\n\n```\nevent: step\ndata: {\"step\":\"stop_existing\",\"status\":\"ok\"}\n\nevent: step\ndata: {\"step\":\"download\",\"status\":\"ok\"}\n\nevent: step\ndata: {\"step\":\"runtime\",\"status\":\"ok\",\"duration\":45}\n\nevent: step\ndata: {\"step\":\"install\",\"status\":\"ok\",\"duration\":12}\n\nevent: step\ndata: {\"step\":\"healthcheck\",\"status\":\"ok\"}\n\nevent: done\ndata: {\"serviceName\":\"app\",\"status\":\"running\",\"appUrl\":\"https:\u002F\u002Fapp-abc12345.vibecode.bitrix24.tech\"}\n\nevent: source\ndata: {\"autoSaved\":false,\"skippedReason\":\"deploy from external URL\"}\n```\n\n## Пример ответа при ошибке\n\nДеплой упал на шаге install (HTTP 200, `success: false`):\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"DEPLOY_FAILED\",\n    \"message\": \"npm ERR! Missing dependencies\",\n    \"step\": \"install\"\n  },\n  \"data\": {\n    \"steps\": [\n      { \"step\": \"stop_existing\", \"status\": \"ok\" },\n      { \"step\": \"download\", \"status\": \"ok\" },\n      { \"step\": \"install\", \"status\": \"error\", \"stderr\": \"npm ERR! Missing dependencies\" }\n    ]\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема запроса (отсутствует `source` или `start`, `port` вне диапазона 1–65535 или не передан в multipart-режиме, неверный формат архива) |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен |\n| 402 | `BILLING_EXHAUSTED` | Баланс исчерпан — пополните |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `EXEC_BUSY` | Другая операция уже выполняется на сервере. Подождите или снимите лок через [`\u002Flock`](.\u002Flock.md) |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен, туннель не подключён, либо сервер числится подключённым, но у Gateway нет живого туннеля. В ответе — поле `hint` с причиной и следующим шагом. Платформа пытается восстановить туннель сама. Если это не удалось — разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос |\n| 409 | `SNAPSHOT_REQUIRED` | Деплой с внешнего `source.url` при включённом хранилище исходников. Передайте `X-Skip-Source-Snapshot: \u003Cпричина>` или загрузите архив в хранилище и разверните через `{source: {versionId}}` — см. «Известные особенности» |\n| 400 | `GALAXY_DEPLOY_RUNTIME_REQUIRED` | Деплой galaxy-приложения без `runtime`. Укажите рантайм, например `node20` |\n| 400 | `GALAXY_DEPLOY_CONTENT_ONLY` | Деплой galaxy-приложения с `source.url` или `source.versionId`. Для galaxy-приложения источник — только встроенный `source.content` |\n| 400 | `GALAXY_DEPLOY_INVALID_INSTALL` | Деплой galaxy-приложения с многострочной командой `install`. Для galaxy-приложения `install` — только однострочная команда, перевод строки ломает сборку |\n| 409 | `GALAXY_BUILD_BUSY` | На хосте galaxy сейчас собирается другое приложение. Повторите запрос через несколько секунд |\n| 409 | `GALAXY_APP_BUSY` | На этом galaxy-приложении уже выполняется другая операция. Повторите запрос через несколько секунд |\n| 502 | `GALAXY_APP_BUILD_FAILED` | Сборка контейнера galaxy-приложения не удалась. Хвост лога сборки — в поле `buildLog` |\n| 502 | `GALAXY_APP_START_FAILED` | Контейнер galaxy-приложения собрался, но упал или ушёл в перезапуск по нехватке памяти сразу после старта. Хвост лога — в поле `buildLog` |\n| 502 | `GALAXY_HOST_UNREACHABLE` | Хост galaxy недоступен — туннель хоста не в статусе `CONNECTED`. Состояние временное, повторите тот же запрос через 1–2 минуты — приложение и его данные сохранены |\n| 502 | `GALAXY_APP_DEPLOY_FAILED` | Не удалось развернуть galaxy-приложение — непредвиденный сбой деплоя. Повторите запрос |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 429 | `DEPLOY_BACKEND_BUSY` | Слишком много одновременных деплоев со встроенным телом запроса. Ответ содержит заголовок `Retry-After: 30` — повторите через указанное время либо передайте архив ссылкой в `source.url`, для загрузок по ссылке ограничения нет |\n| 200 | `DEPLOY_FAILED` | Упал один из шагов процесса деплоя. Поле `error.step` указывает, какой. `data.steps[-1].stderr` содержит подробности |\n| 200 | `DEPLOY_TIMEOUT` | Шаг деплоя не уложился в отведённое ему время |\n| 200 | `DEPLOY_CONNECTION_TERMINATED` | Соединение с сервером оборвалось посреди деплоя — агент или туннель отключились. Деплой мог примениться частично. Посмотрите, на каком шаге произошёл обрыв, и повторите деплой. Если обрыв повторяется — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите деплой |\n| 200 | `DEPLOY_TUNNEL_STALE` | У Gateway нет живого туннеля для сервера, хотя сервер числится подключённым. Вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите деплой — правка полезной нагрузки деплоя тут не поможет |\n| 502 | `RUNTIME_FAILED` | Установка рантайма на сервере провалилась. Попробуйте без `runtime` или через отдельный [`\u002Fexec`](.\u002Fexec.md) |\n| 503 | `RUNTIME_TIMEOUT` | Установка рантайма не завершилась за 10 минут. Она может завершиться позже — проверьте статус сервера перед повторным запросом. Тело ответа содержит поле `retryAfter` со значением 30, ответ несёт заголовок `Retry-After: 30` — это число секунд до повторного запроса |\n\n> **HTTP-статус не отражает результат деплоя.** На отдельной виртуальной машине (`kind: \"STANDALONE\"`) соединение удерживается на всё время деплоя, а статус `200` отправляется до его начала — поэтому и успешный, и провалившийся деплой приходят с кодом `200`. Признак отказа — поле `success: false` и объект `error` в теле ответа. **Проверяйте `success` в теле, а не HTTP-статус:** клиент, который ветвится по статусу, посчитает провалившийся деплой успешным. У galaxy-приложения (`kind: \"GALAXY_APP\"`) та же ошибка приходит со статусом `502`.\n>\n> В потоковом режиме (`?stream=true`) провал шага приходит SSE-событием `step` со `status: \"error\"` — оно несёт имя провалившегося шага. При исключении или обрыве транспорта отдельное событие `error` несёт `code` и `message`. При зависшем exec-мьютексе завершающее событие `error` несёт `code: \"DEPLOY_FAILED\"`, `step` и `hint`, но без `message`. Поля `success` в потоке нет — признак отказа шага — это `status: \"error\"` в его событии `step`.\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Статус `warning` на шаге `tunnel_routing` не означает провал деплоя.** После успешного `healthcheck` платформа проверяет, что туннель Gateway маршрутизирует публичный трафик на нужный порт (`set_port` + опрос агента). Если порт ещё не определён или агент работает в режиме фиксированного порта, шаг получает статус `warning` (не `error`) и деплой не прерывается. Публичный URL приложения может около 30 секунд отдавать страницу-заглушку Black Hole, пока сканер портов агента не определит рабочий порт — после этого заглушка пропадает автоматически. Если у агента нет автоопределения порта и `warning` сохраняется дольше — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair).\n- **Код приложения передаётся через `source.content`, а не через `start`\u002F`install`\u002F`preStart`.** Поля-команды (`start`, `install`, `preStart`) — это короткие shell-команды с лимитом 5000 символов каждое (`command` у [`\u002Fexec`](.\u002Fexec.md) — 10000). Они не предназначены для содержимого файлов: base64 целого архива туда не помещается и обрезается. Сам код (tar.gz \u002F zip, в том числе один файл) кладите в `source.content` (base64-строка архива, до 500 МБ на тело запроса) либо в `source.url` (ссылка на архив), а в `start` оставляйте только команду запуска (например `node server.js`). Для большого архива используйте `source.url`, чтобы не раздувать тело запроса base64'ом.\n- **Три стратегии для `.env`.** Файл пишется в `{extractTo}\u002F.env` (по умолчанию `\u002Fopt\u002Fapp\u002F.env`) и при `cleanDeploy: true` удаляется перед следующим деплоем:\n  1. Передавать `env` в каждом `\u002Fdeploy` — перегенерируется автоматически (рекомендуется).\n  2. Хранить `.env` вне `extractTo` — например, `\u002Fvar\u002Flib\u002Fmyapp\u002F.env`. Эта директория не трогается `cleanDeploy`. В `start` указать явный путь.\n  3. `cleanDeploy: false` — накопительный (merge) деплой, `.env` переживёт, но вместе с ним останутся устаревшие файлы прошлых деплоев.\n- **Автоматическая подгрузка `.env` в systemd.** В юните прописан `EnvironmentFile=-{extractTo}\u002F.env` (знак `-` делает файл опциональным). Приложение через systemd получит переменные само. Но при `systemd: false` или командах из `install`\u002F`preStart` env подгружать не будет — передавайте явно.\n- **Если смешаны `dependencies` и `devDependencies`**, `npm install --production` не поставит пакеты из `devDependencies`. Для cron-скриптов с `tsx`, `ts-node` и т. п. уберите `--production` или добавьте их отдельно через `preStart`.\n- **Повторный деплой с тем же `runtime` переустанавливает его.** Опустите параметр `runtime` при повторных деплоях — сэкономит 45+ секунд. Первый деплой с `runtime` обязателен для установки.\n- **Проверка работоспособности может не успеть.** До 10 попыток с интервалом 3 секунды — это около 30 секунд на всё. Если приложение стартует дольше, сделайте эндпоинт `\u002Fhealth`, отвечающий 200 сразу, даже до прогрева, или переместите долгую инициализацию в `preStart`.\n- **Фоновые приложения тоже должны слушать порт 3000.** Шаг `healthcheck` выполняется при каждом деплое — даже если приложение не обслуживает HTTP-трафик (бот, который только опрашивает события, cron-демон, обработчик очереди). Без процесса, слушающего порт 3000, шаг повторяет попытки и завершается ошибкой `attempt 10\u002F10: curl exit=7` (не удалось подключиться). Поднимите рядом с основной логикой минимальный HTTP-сервер, отвечающий кодом 200 на `GET \u002F`:\n\n    Node.js:\n\n    ```javascript\n    require('http').createServer((_req, res) => res.end('ok')).listen(3000)\n    \u002F\u002F дальше — основная логика приложения\n    ```\n\n    Python:\n\n    ```python\n    import http.server, threading\n\n    class Health(http.server.BaseHTTPRequestHandler):\n        def do_GET(self):\n            self.send_response(200)\n            self.end_headers()\n            self.wfile.write(b'ok')\n        def log_message(self, *args):\n            pass\n\n    threading.Thread(\n        target=lambda: http.server.HTTPServer(('', 3000), Health).serve_forever(),\n        daemon=True,\n    ).start()\n    # дальше — основная логика приложения\n    ```\n- **Лок на 15 минут.** Один `\u002Fdeploy` блокирует `\u002Fexec` и другие `\u002Fdeploy`. Если предыдущий `\u002Fdeploy` упал на шаге `healthcheck` или клиент оборвал соединение по таймауту, последующие `\u002Fexec` и `\u002Fdeploy` будут возвращать `409 EXEC_BUSY` до автоматического истечения лока — то есть до 15 минут с момента старта прерванного деплоя. Снять лок сразу можно вызовом [`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`](.\u002Flock.md). Пересоздавать сервер не нужно.\n- **Долгий деплой и таймауты HTTP-клиента.** Полный цикл — особенно с установкой `runtime` и большим `npm install` \u002F `pip install` — реально занимает 3–15 минут. В JSON-режиме сервер удерживает соединение, отправляя HTTP 200 + chunked-пробелы каждые 15 сек до готовности (поэтому `Content-Length` отсутствует, а ответ выглядит «висящим»). Если ваш HTTP-клиент установит более короткий таймаут (например, `curl --max-time 120` или дефолт SDK ~30–120 сек) — соединение оборвётся, но `orchestrateDeploy` на сервере продолжит работу. Симптомы такого обрыва: ответ обрывается на пробелах, последующие `\u002Fexec` и `\u002Fdeploy` отвечают `409 EXEC_BUSY` до завершения серверной части или до истечения 15-минутного лока. Что делать: (а) в curl — `--max-time 900` (15 мин) или без таймаута, (б) в любом SDK — настроить таймаут запроса ≥ 900 сек для этого эндпоинта, (в) или используйте `?stream=true` (SSE) — там события идут построчно по мере прохождения шагов, прокси и SDK таймауты ведут себя стабильнее. Снять застрявший лок руками — [`DELETE \u002Flock`](.\u002Flock.md).\n- **`apt-get` в `preStart` работает без `sudo`.** Агент и команды деплоя выполняются от имени `root` — `sudo` писать не нужно и это не вызовет ошибку.\n- **Команды `install`, `preStart` и `start` выполняются через POSIX-шелл `\u002Fbin\u002Fsh`.** Пишите их в POSIX-совместимом синтаксисе. Для остановки при первой же ошибке подойдёт `set -e`, для условий — `[ ... ]`. Опция `set -o pipefail` доступна только в bash — если она нужна, вызовите bash явно: `bash -c 'set -o pipefail; команды'`. То же для отдельного скрипта, запускаемого из `start`, — начните его строкой `#!\u002Fbin\u002Fbash` или запускайте через `bash script.sh`.\n- **`docker-compose-plugin` отсутствует в стандартных репозиториях Ubuntu — устанавливайте Docker через официальный скрипт.** Пакет `docker-compose-plugin` — это компонент Docker CE из репозитория docker.com, которого нет в стандартных Ubuntu 24.04 apt-источниках. `apt-get install docker-compose-plugin` завершится ошибкой `Unable to locate package`. Правильный способ поставить Docker Engine + Compose V2 одной командой:\n  ```\n  \"preStart\": \"curl -fsSL https:\u002F\u002Fget.docker.com | sh && systemctl enable --now docker\"\n  ```\n  После этого `docker compose` (без дефиса) доступен как плагин. Если нужен только Compose V1 (устаревший, только для совместимости), он есть в Ubuntu repos: `apt-get install -y docker-compose`.\n- **`cleanDeploy: true` очищает только `extractTo`, не всю систему.** По умолчанию удаляется директория `\u002Fopt\u002Fapp` перед распаковкой нового архива. Системные пакеты (`apt`), базы данных, файлы в `\u002Fvar\u002Flib\u002F`, `\u002Fetc\u002F`, `\u002Froot\u002F` не затрагиваются — они переживут любое число повторных деплоев.\n- **Системные пакеты сохраняются при sleep\u002Fwake, reboot и repair.** Все эти операции сохраняют диск виртуальной машины нетронутым. Повторно устанавливать ПО через `preStart` при каждом деплое не нужно — достаточно сделать это однажды. Последующие деплои без `runtime` и без нужных `preStart`-команд найдут пакеты на месте.\n- **`\u002Fetc\u002Fsystemd\u002Fsystem\u002F` доступен для записи.** Можно создавать кастомные systemd-юниты через `preStart` или `\u002Fexec`: `systemctl daemon-reload && systemctl enable --now myservice`. Стандартный юнит деплоя (`app.service`) не конфликтует с пользовательскими юнитами при разных именах.\n- **При смене `serviceName` между деплоями остановите старый сервис вручную.** Шаг `stop_existing` останавливает только сервис с текущим именем `serviceName`. Если предыдущий деплой прошёл с именем по умолчанию `app`, а следующий — с именем `dedup`, то команда `systemctl stop dedup` не находит прежний `app.service`. Прежний сервис продолжает работать и удерживает порт приложения — по умолчанию 3000. Новый сервис не может занять порт и уходит в цикл перезапусков. Шаг `healthcheck` распознаёт это состояние, включая случай, когда на запрос отвечает кодом 200 прежний процесс: деплой завершается ошибкой. Сообщение об ошибке называет номер порта, состояние сервиса, процесс-держатель порта и команду для освобождения порта — `fuser -k \u003Cпорт>\u002Ftcp`. Посторонний процесс платформа не завершает. Освободите порт командой `fuser -k \u003Cпорт>\u002Ftcp`, остановите прежний сервис командой `systemctl stop \u003Cстарое-имя>.service` через [`\u002Fexec`](.\u002Fexec.md) или верните прежнее значение `serviceName`, затем повторите деплой. Оговорка: когда диагностическая проверка порта не смогла отработать, ответ со статусом 200–399 засчитывается и деплой завершается успешно.\n- **Используйте `debug: true` при расхождениях `\u002Fdeploy` и ручного пути.** Добавляет шаг `debug_info` с `sha256sum` файлов и `file -i` — помогает сравнить состояние `\u002Fdeploy` с ручным `\u002Fupload` + `\u002Fexec` в байтах. Типичный случай — Tailwind v4 oxide падает на неожиданном UTF-8.\n- **Тип архива определяется автоматически, отдельного поля формата нет.** Для `source.content` платформа читает сигнатуру первых байт (`PK` → zip, `1F 8B` → tar.gz, `BZ` → tar.bz2), для `source.url` — расширение пути. Нераспознанный архив трактуется как tar.gz. Передавать тип явным полем не нужно и негде.\n- **Для загрузки кода предпочтителен tar.gz.** Оба формата распаковываются, но zip, собранный средством `Compress-Archive` из PowerShell, хранит пути с обратной косой чертой `\\`. Вложенные файлы из такого архива могут оказаться в корне `extractTo` вместо своих папок, и часть кода не попадёт в нужное место. Шаг `normalize_windows_paths` приводит такие пути к обычному виду, но чтобы исключить проблему, соберите код в tar.gz: `tar -czf app.tar.gz -C .\u002Fmy-app .`. Если нужен zip — упакуйте его Unix-инструментом: `zip -r app.zip .`.\n- **Крупные inline-архивы конкурируют за слот.** `source.content` больше ~10 КБ держит запрос в памяти на всё время деплоя, поэтому такие запросы ограничены небольшим числом одновременных (по умолчанию 2 на бэкенд). При превышении приходит `429` с подсказкой переключиться на `source.url` или `source.versionId` — у них этого лимита нет. Мелкие inline-архивы и `versionId` слот не занимают.\n- **Деплой с внешнего URL требует снимок исходников или явный отказ.** Когда на портале включено [хранилище исходников](\u002Fdocs\u002Fsource-storage), деплой с `source.url`, который указывает не на хранилище Вайбкод, возвращает `409 SNAPSHOT_REQUIRED` — так история версий приложения не теряется при деплое со стороннего адреса. Есть два пути. Передать заголовок `X-Skip-Source-Snapshot: \u003Cпричина ≤200 символов>` — деплой продолжится без сохранения снимка. Либо сначала загрузить архив через `POST \u002Fv1\u002Fapps\u002F:id\u002Fsources` и развернуть его через `{ \"source\": { \"versionId\": \"vN\" } }` — тогда платформа сохранит версию. Источник из самого хранилища — `source.versionId` или `source.content` — этого заголовка не требует. Подробнее — [Хранилище исходников](\u002Fdocs\u002Fsource-storage).\n- **Galaxy-приложению свой `Dockerfile` не нужен — платформа генерирует его сама.** Из полей деплоя `runtime`, `port` и `start` платформа собирает `Dockerfile` с базовым образом рантайма, строкой `EXPOSE \u003Cport>` и командой `CMD`, выводимой из `start`. Класть собственный `Dockerfile` в архив не требуется и не нужно. Это касается только galaxy-приложения — поле `createdVia` равно `galaxy`.\n- **Ошибка сборки `published no host port for \u003Cport>` означает, что контейнер не удержался на `port`, а не отсутствие `EXPOSE`.** У galaxy-приложения этот текст приходит, когда контейнер упал на старте, команда `start` неверна или приложение слушает другой порт — то есть на `port` никто не отвечает. Поправьте `start` и `port` так, чтобы приложение действительно слушало `port`. Добавлять `EXPOSE` вручную не нужно: платформа уже выставила его из `port`.\n- **Переменные `env` galaxy-приложения подставляются в контейнер при запуске, а не вшиваются в образ.** Платформа передаёт их флагом `docker run --env` на старте контейнера, поэтому секреты из `env` не попадают в слои образа и не видны в его истории. Это касается только galaxy-приложения — поле `createdVia` равно `galaxy`.\n\n## Смотрите также\n\n- [Список рантаймов](.\u002Fruntimes.md) — `GET \u002Fv1\u002Finfra\u002Fruntimes` — все доступные ID.\n- [Выполнить команду](.\u002Fexec.md) — ручные команды вместо автоматической цепочки.\n- [Логи сервиса](.\u002Flogs.md) — `?service={serviceName}` для проверки старта.\n- [Снять зависший лок](.\u002Flock.md) — при `EXEC_BUSY` после обрыва.\n- [Задать порт](.\u002Fport.md) — если приложение слушает не 3000 (не рекомендуется).\n- [Хранилище исходников](\u002Fdocs\u002Fsource-storage)\n","\n## Снять зависший лок\n\n`DELETE \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flock`\n\nПринудительно снимает зависший лок операции на сервере. Используйте, когда [`\u002Fexec`](.\u002Fexec.md) или [`\u002Fdeploy`](.\u002Fdeploy.md) оборвались со стороны клиента (потеряно соединение, локальный таймаут), но на сервере всё ещё активен лок и последующие вызовы возвращают `EXEC_BUSY`. Эндпоинт всегда возвращает 200: если лок был — снят и в ответе `released: true`, если его не было — вернётся `released: false` (тоже успех).\n\n> **Используйте осторожно.** Снятие лока активной операции может привести к непредсказуемому состоянию сервера — одновременно запущенные `\u002Fexec` и `\u002Fdeploy` могут конкурировать за файлы и systemd-юниты.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID сервера |\n\nТело запроса пустое.\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\ncurl -X DELETE -H \"X-Api-Key: YOUR_API_KEY\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flock\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X DELETE -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flock\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flock`,\n  {\n    method: 'DELETE',\n    headers: { 'X-Api-Key': 'YOUR_API_KEY' },\n  }\n)\nconst { data } = await res.json()\n\nif (data.released) {\n  console.log(`Снят лок ${data.operation}, держался ${data.ageMs}ms, до истечения было ${data.expiresInMs}ms`)\n} else {\n  console.log(data.message)  \u002F\u002F \"No active lock on this server\"\n}\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flock`,\n  {\n    method: 'DELETE',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n    },\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при вызове на существующем сервере |\n| `data.released` | boolean | `true` — был активный лок, и он снят. `false` — лок отсутствовал |\n| `data.operation` | string | Тип снятой операции: `\"exec\"` или `\"deploy\"`. Присутствует только при `released: true` |\n| `data.ageMs` | number | Сколько миллисекунд лок был активен. Присутствует только при `released: true` |\n| `data.expiresInMs` | number | Через сколько миллисекунд лок истёк бы сам. Присутствует только при `released: true` |\n| `data.message` | string | Сообщение `\"No active lock on this server\"`. Присутствует только при `released: false` |\n\n## Пример ответа\n\nЛок был — снят:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"released\": true,\n    \"operation\": \"deploy\",\n    \"ageMs\": 320000,\n    \"expiresInMs\": 580000\n  }\n}\n```\n\nЛока не было:\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"released\": false,\n    \"message\": \"No active lock on this server\"\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n404 — сервер не найден:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Server not found\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Один лок на сервер.** Защита от гонок между `\u002Fexec` и `\u002Fdeploy` — они делят один общий лок. Невозможно заблокировать отдельную команду.\n- **Автосброс при нормальном завершении.** Успех или ошибка `\u002Fexec`\u002F`\u002Fdeploy` снимают лок сами. `DELETE \u002Flock` нужен только когда клиент оборвал соединение до завершения или таймаут агента прошёл без отправки финального события.\n- **TTL лока — автоистечение без вызова.** `\u002Fexec` ставит лок на `timeout + 30 + 60` секунд. `\u002Fdeploy` — на 15 минут. Даже без `DELETE \u002Flock` зависший лок истечёт сам.\n- **`released: false` — это успех**, не ошибка. Эндпоинт не возвращает 404 при отсутствии лока, потому что идея в том, чтобы «гарантировать, что лока нет». Клиенту не важно, был он или не был.\n- **Работает в любом режиме сервера.** В отличие от большинства Deploy API эндпоинтов, `DELETE \u002Flock` не требует BLACKHOLE — лок держится в памяти платформы, не зависит от агента.\n\n## Смотрите также\n\n- [Выполнить команду](.\u002Fexec.md) — основной источник `EXEC_BUSY`.\n- [Полный деплой](.\u002Fdeploy.md) — долгая операция, лок на 15 минут.\n- [Метрики туннеля](.\u002Fmetrics.md) — `available: true` + `httpConnections: 0` показывает, идёт ли реальная работа.\n","\n## Логи сервиса\n\n`GET \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Flogs`\n\nВозвращает логи BLACKHOLE-сервера через системную утилиту `journalctl`. Без параметра `service` читается весь системный журнал (все сервисы и общесистемные сообщения). Для логов конкретного systemd-юнита — передайте `service` (по умолчанию при деплое создаётся `app.service`, если не указали другое имя). Поддерживает фильтрацию по количеству строк, временному окну и подстроке.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | По умолч. | Описание |\n|----------|---|-----|:-----:|-----------|----------|\n| `id` | path | string (UUID) | да | — | ID BLACKHOLE-сервера |\n| `service` | query | string | нет | — | Имя systemd-юнита: `app`, `crm-dashboard`, и т. п. Не указывайте расширение `.service`. По умолчанию читается весь системный журнал |\n| `lines` | query | number | нет | 50 | Количество строк, 1–500 |\n| `since` | query | string | нет | — | Временная метка в формате утилиты `journalctl`: `\"1 hour ago\"`, `\"2026-04-22 10:00:00\"`, `\"10 minutes ago\"` |\n| `grep` | query | string | нет | — | Подстрока для фильтра, до 200 символов |\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\n# Логи приложения за последний час с фильтром по \"error\"\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?service=app&lines=100&since=1%20hour%20ago&grep=error\"\n\n# Весь системный журнал, последние 10 строк (для отладки cloud-init)\ncurl -H \"X-Api-Key: YOUR_API_KEY\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?lines=10\"\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  \"https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Flogs?service=app&lines=50\"\n```\n\n### JavaScript — личный ключ\n\n```javascript\nconst params = new URLSearchParams({\n  service: 'app',\n  lines: '100',\n  since: '30 minutes ago',\n})\n\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flogs?${params}`,\n  { headers: { 'X-Api-Key': 'YOUR_API_KEY' } }\n)\nconst { data } = await res.json()\ndata.logs.forEach(line => console.log(line))\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Flogs?service=app&lines=50`,\n  {\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n    },\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | Всегда `true` при успехе |\n| `data.service` | string \\| null | Эхо параметра `?service=`, или `null` если не передавался |\n| `data.logs` | array\\\u003Cstring\\> | **Основное поле.** Строки логов в порядке поступления (новые — в конце массива) |\n| `data.lines` | array\\\u003Cstring\\> | Алиас `data.logs` (тот же массив). Сохранён для обратной совместимости — в новом коде используйте `data.logs` |\n| `data.requestedLines` | number | Эхо `?lines=` параметра (валидировано 1–500, default 50) |\n| `data.returnedLines` | number | Длина `data.logs` |\n| `data.since` | string \\| null | Эхо `?since=`, или `null` |\n| `data.grep` | string \\| null | Эхо `?grep=`, или `null` |\n| `data.hint` | string | Опционально. Диагностика когда `data.logs.length === 0` |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"service\": null,\n    \"logs\": [\n      \"Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root\",\n      \"Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026\u002F04\u002F22 11:17:27 [exec] id= timeout=10s workdir=\\\"\\\" command=\\\"uname -a\\\"\",\n      \"Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists...\"\n    ],\n    \"lines\": [\n      \"Apr 22 11:17:02 epd65hdv07p8g89c0c06 CRON[1406]: pam_unix(cron:session): session closed for user root\",\n      \"Apr 22 11:17:27 epd65hdv07p8g89c0c06 vibe-agent[1098]: 2026\u002F04\u002F22 11:17:27 [exec] id= timeout=10s workdir=\\\"\\\" command=\\\"uname -a\\\"\",\n      \"Apr 22 11:17:28 epd65hdv07p8g89c0c06 cloud-init[975]: Reading package lists...\"\n    ],\n    \"requestedLines\": 50,\n    \"returnedLines\": 3,\n    \"since\": null,\n    \"grep\": null\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n400 — сервер в OPEN-режиме:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"NOT_BLACKHOLE\",\n    \"message\": \"Deployment API only available for BLACKHOLE servers\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема: `lines` вне 1–500, `grep` длиннее 200 символов |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 502 | `TUNNEL_NOT_FOUND` \u002F `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия, поэтому сопоставляйте `error.code` по префиксу, а не по точному совпадению |\n| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код тоже несёт детали после двоеточия — сравнивайте по префиксу |\n| 502 | `LOGS_UNAVAILABLE` | Агент не смог прочитать системный журнал: нет такого юнита, неверный фильтр, отказано в доступе или `journalctl` вернул ошибку |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Формат строк — как выдаёт `journalctl`.** Каждая строка уже содержит время, имя хоста, имя процесса и его PID — дополнительная обработка на клиенте не нужна, выводите как есть.\n- **Фильтр `grep` — простая подстрока, не регулярное выражение.** Для сложных паттернов выгружайте больше строк и фильтруйте на клиенте.\n- **Логи агента туннеля видны без `service`** — под именем `vibe-agent`. Полезно для диагностики, когда приложение не отвечает через Deploy API. Просто не передавайте параметр `service` — увидите весь системный журнал, включая `vibe-agent`, cloud-init и другие общесистемные сообщения.\n- **Лимит 500 строк помножен на ограничение 10 вызовов в минуту** — за минуту можно прочитать максимум 5000 строк. Для большего объёма логов — несколько вызовов с разными `since` и аккуратно к лимиту.\n\n## Galaxy-приложения\n\nЕсли сервер — galaxy-приложение (`kind=GALAXY_APP`), эндпоинт возвращает не системный журнал хоста, а stdout\u002Fstderr самого контейнера приложения (`docker logs`). Параметр `service` для galaxy-приложений не применяется.\n\n- **Чтение read-only.** Запрос логов не будит спящую галактику. Если хост-галактика спит или недоступен, ответ — пустой массив `data.logs` плюс диагностическое поле `data.hint`. Разбудите галактику (любым обращением к приложению) и повторите запрос.\n- **`since` — длительность или RFC3339, не относительная текстовая форма.** Для galaxy-приложений `since` принимает только длительность в секундах\u002Fминутах\u002Fчасах (`10m`, `2h`, `24h`) или метку времени RFC3339 (`2026-06-24T10:00:00Z`) — для интервала в сутки используйте `24h`. Относительные текстовые формы `journalctl` (`\"1 hour ago\"`, `\"10 minutes ago\"`) допустимы только для Black Hole-серверов — для galaxy-приложения такое значение вернёт `400 VALIDATION_ERROR`.\n- **`grep`** работает так же — простая подстрока, фильтрация на стороне платформы.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — посмотреть, какое имя сервиса создаётся.\n- [Выполнить команду](.\u002Fexec.md) — `journalctl -u app -f` для потокового чтения логов в реальном времени через `exec`.\n- [Метрики туннеля](.\u002Fmetrics.md) — активность агента и HTTP-соединения.\n","\n## Загрузить файл\n\n`POST \u002Fv1\u002Finfra\u002Fservers\u002F:id\u002Fupload`\n\nЗаписывает файл на BLACKHOLE-сервер через агент туннеля. Два варианта источника: встроенное base64-содержимое в теле запроса (до 500 МБ) или URL, с которого агент скачает файл сам (до 500 МБ). Поддерживается автоматическая распаковка архивов `tar.gz` \u002F `tar.bz2` \u002F `zip` — формат определяется по сигнатуре файла (magic bytes) или по расширению URL.\n\n## Параметры\n\n| Параметр | В | Тип | Обяз. | Описание |\n|----------|---|-----|:-----:|----------|\n| `id` | path | string (UUID) | да | ID BLACKHOLE-сервера, `status: running`, `blackholeStatus: CONNECTED` |\n\n## Поля запроса (body)\n\n| Поле | Тип | Обяз. | Описание |\n|------|-----|:-----:|----------|\n| `path` | string | **да** | Путь на сервере, 1–500 символов. Для архивов — куда положить загруженный архив до распаковки |\n| `content` | string | да (или `url`) | Base64-содержимое файла. Максимум 500 МБ на тело запроса |\n| `url` | string | да (или `content`) | HTTPS-URL, откуда агент скачает файл. До 500 МБ |\n| `mode` | string | нет | Права файла в восьмеричном формате: `0644`, `0755`. Применяется к файлу по пути `path` |\n| `extract` | boolean | нет | Распаковать архив после загрузки. По умолчанию `false`. Требует `content` или `url` с архивом |\n| `extractTo` | string | нет | Директория для распаковки (при `extract: true`). По умолчанию — директория из `path` |\n\nНужно передать ровно одно из: `content` или `url`.\n\n## Примеры\n\n### curl — личный ключ\n\n```bash\n# Загрузка файла по URL с автоматической распаковкой\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\n    \"url\": \"https:\u002F\u002Fgithub.com\u002Fuser\u002Frepo\u002Farchive\u002Fmain.tar.gz\",\n    \"path\": \"\u002Fopt\u002Fapp\u002Fsource.tar.gz\",\n    \"extract\": true,\n    \"extractTo\": \"\u002Fopt\u002Fapp\"\n  }'\n\n# Встроенное base64-содержимое — маленький файл (package.json)\nCONTENT=$(echo '{\"name\":\"my-app\",\"version\":\"1.0.0\"}' | base64)\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_API_KEY\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d \"{\\\"content\\\":\\\"$CONTENT\\\",\\\"path\\\":\\\"\u002Fopt\u002Fapp\u002Fpackage.json\\\",\\\"mode\\\":\\\"0644\\\"}\"\n```\n\n### curl — OAuth-приложение\n\n```bash\ncurl -X POST https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002FSERVER_ID\u002Fupload \\\n  -H \"X-Api-Key: YOUR_APP_KEY\" \\\n  -H \"Authorization: Bearer USER_SESSION_TOKEN\" \\\n  -H \"Content-Type: application\u002Fjson\" \\\n  -d '{\"url\":\"https:\u002F\u002Fexample.com\u002Fconfig.json\",\"path\":\"\u002Fetc\u002Fmyapp\u002Fconfig.json\"}'\n```\n\n### JavaScript — личный ключ\n\n```javascript\n\u002F\u002F Загрузка архива и распаковка\nconst res = await fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fupload`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_API_KEY',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      url: 'https:\u002F\u002Fgithub.com\u002Fuser\u002Fapp\u002Farchive\u002Fmain.tar.gz',\n      path: '\u002Fopt\u002Fapp\u002Fsource.tar.gz',\n      extract: true,\n      extractTo: '\u002Fopt\u002Fapp',\n    }),\n  }\n)\nconst { data } = await res.json()\nconsole.log(`Записано ${data.size} байт по пути ${data.path}${data.extracted ? ', распаковано' : ''}`)\n```\n\n### JavaScript — OAuth-приложение\n\n```javascript\n\u002F\u002F Встроенное base64-содержимое — маленький конфиг\nconst content = Buffer.from('NODE_ENV=production\\n').toString('base64')\nawait fetch(\n  `https:\u002F\u002Fvibecode.bitrix24.tech\u002Fv1\u002Finfra\u002Fservers\u002F${serverId}\u002Fupload`,\n  {\n    method: 'POST',\n    headers: {\n      'X-Api-Key': 'YOUR_APP_KEY',\n      'Authorization': 'Bearer USER_SESSION_TOKEN',\n      'Content-Type': 'application\u002Fjson',\n    },\n    body: JSON.stringify({\n      content,\n      path: '\u002Fopt\u002Fapp\u002F.env',\n      mode: '0600',\n    }),\n  }\n)\n```\n\n## Поля ответа\n\n| Поле | Тип | Описание |\n|------|-----|----------|\n| `success` | boolean | `true` при успешной записи |\n| `data.path` | string | Итоговый путь файла на сервере |\n| `data.size` | number | Размер в байтах |\n| `data.extracted` | boolean | `true` если архив был распакован после загрузки |\n\n## Пример ответа\n\n```json\n{\n  \"success\": true,\n  \"data\": {\n    \"path\": \"\u002Fopt\u002Fapp\u002Fsource.tar.gz\",\n    \"size\": 1048576,\n    \"extracted\": true\n  }\n}\n```\n\n## Пример ответа при ошибке\n\n403 — путь запрещён для загрузки:\n\n```json\n{\n  \"success\": false,\n  \"error\": {\n    \"code\": \"UPLOAD_PATH_DENIED\",\n    \"message\": \"Upload to system path is not allowed\"\n  }\n}\n```\n\n## Ошибки\n\n| HTTP | Код | Описание |\n|------|-----|----------|\n| 400 | `VALIDATION_ERROR` | Нарушена схема: одновременно `content` и `url`, либо ни одного, неверный `path`, тело больше 500 МБ |\n| 400 | `NOT_BLACKHOLE` | Сервер в режиме OPEN — Deploy API недоступен |\n| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |\n| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |\n| 403 | `UPLOAD_PATH_DENIED` | Загрузка в системные директории (`\u002Froot\u002F.ssh`, `\u002Fetc\u002Fpasswd`, `\u002Fboot`, …) запрещена |\n| 404 | `NOT_FOUND` | Сервер не существует, удалён или принадлежит другому API-ключу |\n| 409 | `SERVER_NOT_READY` | Сервер не готов к операции: не запущен или туннель не подключён. В ответе — поле `hint` с причиной и следующим шагом. Разбудите сервер или вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите запрос. Случай «числится подключённым, но у Gateway нет живого туннеля» на этом маршруте приходит как `502 TUNNEL_NOT_FOUND` (см. ниже) |\n| 429 | `RATE_LIMITED` | Превышен лимит 10 операций в минуту на сервер |\n| 429 | `DEPLOY_BACKEND_BUSY` | Слишком много одновременных загрузок с телом запроса. Платформа ограничивает число одновременных `\u002Fupload` и `\u002Fdeploy` со встроенным содержимым, чтобы не исчерпать память. Ответ несёт заголовок `Retry-After: 30` — повторите через 30 секунд. Либо передайте архив ссылкой (`url`): для загрузок по ссылке ограничения на одновременность нет |\n| 502 | `UNZIP_PREFLIGHT_FAILED` | Не удалось установить `unzip` на сервере перед распаковкой zip-архива. Чтобы не зависеть от этого шага, используйте архив `.tar.gz` |\n| 500 | код агента | Ошибка записи файла на сервере: нехватка места, права доступа и т. п. Конкретный код приходит от агента в поле `error.code` |\n| 502 | `TUNNEL_NOT_FOUND` \u002F `GATEWAY_UNREACHABLE: …` | Нет живого туннеля до сервера либо Gateway недоступен — вызовите [`\u002Frepair`](\u002Fdocs\u002Finfra\u002Flifecycle\u002Frepair) и повторите. `GATEWAY_UNREACHABLE` несёт детали после двоеточия — сопоставляйте `error.code` по префиксу |\n| 503 | `GATEWAY_TIMEOUT: …` | Gateway не ответил вовремя. Код несёт детали после двоеточия — сравнивайте по префиксу |\n\nПолный список общих ошибок API — [Ошибки](\u002Fdocs\u002Ferrors).\n\n## Известные особенности\n\n- **Автоопределение формата архива при `extract: true`.** Агент определяет формат по сигнатуре файла (magic bytes, функция `detectArchiveExt()`): поддерживает `.tar.gz`, `.zip`, `.tar.bz2`. Формат можно не указывать явно. Для URL — агент также проверяет расширение.\n- **macOS-архивы чистятся автоматически.** Если архив содержит AppleDouble-сайдкары (`._*`) или `.DS_Store`, агент удаляет их после распаковки — это предотвращает ложные срабатывания сканеров вроде Tailwind v4 oxide. При создании архива на macOS советуем использовать `COPYFILE_DISABLE=1 tar -czf ...` чтобы сайдкары вообще не попадали в архив.\n- **Windows PowerShell ZIP — работает на агенте ≥ 1.2.3.** `Compress-Archive` в Windows пишет ZIP с литеральными `\\` в именах файлов. Агент с версии 1.2.3 автоматически нормализует эти пути через шаг `normalize_windows_paths`. Для старых версий агента используйте `tar.gz` (через WSL \u002F Git Bash) либо выкладывайте готовый tar-архив и загружайте через `url`.\n- **Список запрещённых путей.** Агент блокирует загрузку в: `\u002Froot\u002F.ssh\u002F` (защита SSH-ключей), `\u002Fboot`, `\u002Fetc\u002Fshadow`, `\u002Fetc\u002Fpasswd`, системные сервис-файлы. Для приложения используйте `\u002Fopt\u002F\u003Capp>\u002F` или `\u002Fvar\u002Flib\u002F\u003Capp>\u002F`.\n- **`mode` применяется к архиву, не к распакованным файлам.** При `extract: true` права распакованных файлов определяются содержимым архива, а `mode` задаётся только сохранённому архиву. Если нужно изменить права внутри — сделайте через [`\u002Fexec`](.\u002Fexec.md) c `chmod`.\n- **Таймаут 10 минут на загрузку через `url`.** Для 500 МБ должно уложиться. Если источник медленный — предзагрузите архив, положите на свой CDN.\n\n## Смотрите также\n\n- [Полный деплой](.\u002Fdeploy.md) — за один запрос вместо цепочки `upload` + `exec`.\n- [Выполнить команду](.\u002Fexec.md) — после загрузки можно распаковать\u002Fустановить вручную.\n- [Логи сервиса](.\u002Flogs.md) — проверить, что сервис применил новые файлы.\n"]