Як розгорнути OpenTag на VPS для @згадувань агентів
Покрокове розгортання OpenTag v0.9.0 на VPS: TLS ingress, перевірка підписів webhook, scopes токенів Slack і GitHub та безпечні налаштування.
Що робить OpenTag, коли ви згадуєте агента
OpenTag перетворює @згадування в гілці Slack або issue на GitHub на запуск coding agent на вашій машині. Хтось додає коментар @opentag investigate this до issue. Listener отримує подію від платформи, перевіряє її підпис, зіставляє згадування з прив’язаним проєктом, запускає coding agent у локальній копії репозиторію та публікує результат у тій самій гілці.
Проєкт поширюється за ліцензією MIT і розміщений у amplifthq/opentag. Станом на August 2026 найновіший tagged release — v0.9.0, опублікований 28 July 2026; він постачається як npm package. Офіційного container image немає, тому фіксувати потрібно версію npm. У кожній наведеній нижче команді цю версію зафіксовано.
Це проєкт для VPS, а не для laptop, через інтеграцію з GitHub. GitHub надсилає події репозиторію HTTP-запитом на URL, який ви реєструєте один раз. Тому цей URL має залишатися доступним за тією самою адресою і надалі.
Чотири компоненти
Слухач отримує події платформи, і для кожної платформи використовується окремий слухач. Слухач GitHub — це HTTP endpoint на порту 3050 за шляхом /github/webhooks. Слухач Slack Events API працює на порту 3040 за адресою /slack/events. Slack також може працювати в Socket Mode. У цьому режимі застосунок відкриває вихідне WebSocket-з’єднання, тому вхідний порт не потрібен.
Диспетчер координує роботу. За замовчуванням він прослуховує порт 3030, зберігає стан запусків у локальному файлі бази даних, заданому через OPENTAG_DATABASE_PATH, і записує журнал аудиту для кожного запуску. Жоден зовнішній вузол не повинен мати доступу до цього порту.
Runner — це локальний daemon. Він опитує систему на наявність роботи, захоплює запуск, утримує lease для нього та за замовчуванням надсилає heartbeat кожні 15 секунд, поки запуск активний. Він відхиляє будь-який захоплений запуск, якщо target проєкту відсутній або не входить до allowlist у його власній конфігурації. Саме ця перевірка не дає події GitHub вказати вашому агенту репозиторій, до якого ви його не прив’язали.
Executor — це сам coding agent. OpenTag запускає його через ACP (agent client protocol) — протокол JSON-RPC, який працює через стандартні потоки введення та виведення. Тому агент запускається як дочірній процес у робочому каталозі, який передає йому OpenTag. Вбудовані імена включають echo, codex, claude-code, cursor, opencode, hermes і openclaw. Почніть із echo — executor, указаний у прикладі конфігурації. Це дає змогу перевірити весь ланцюжок до того, як модель почне змінювати ваш код.
Порядок завжди незмінний: подія платформи, перевірка підпису, запис запуску, захоплення, агент, відповідь у thread.
Чому ноутбука й тунелю недостатньо
Посібник із налаштування GitHub пропонує виконати ngrok http 3050 і вставити адресу тунелю у webhook репозиторію. Це працює протягом перших десяти хвилин. Адреса безкоштовного тунелю змінюється щоразу після перезапуску процесу й перестає існувати, коли ноутбук переходить у режим сну. GitHub зберігає стару URL-адресу для доставки payload і продовжує надсилати запити на неї, тому вкладка Recent Deliveries у налаштуваннях webhook заповнюється помилками, а гілка залишається без відповіді. Протягом тижня цього ніхто не помічає, оскільки webhook, який нічого не робить, зовні не відрізняється від бота, про якого ніхто не повідомив.
VPS усуває дві причини збоїв. DNS-ім’я не змінюється, тому URL-адреса для доставки payload, яку ви вставили один раз, залишається правильною. Машина не переходить у режим сну, тому коментар о 02:00 отримає відповідь. Спочатку належно налаштуйте сервер: у розділі перші десять хвилин на новому VPS описано користувача для входу та firewall, які передбачає цей посібник.
Slack є винятком. У Socket Mode він встановлює вихідне з’єднання й не потребує публічної URL-адреси, тому розгортання лише для Slack може залишатися закритим. У GitHub немає еквівалентного режиму. Webhook репозиторію використовує вхідні HTTP-запити, а це означає потребу в публічній кінцевій точці, TLS (transport layer security) і перевірці підпису.
Розміщення OpenTag на Ubuntu з фіксованим релізом
Для OpenTag v0.9.0 потрібен Node.js 22 або новіший. Ubuntu 24.04 постачається з Node 18 у власному репозиторії, тому встановіть Node.js із NodeSource.
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -vnode -v має вивести v22 або новішу версію. У Node 20 під час встановлення з’являється попередження EBADENGINE, а CLI може завершитися з помилкою після запуску.
Створіть для сервісу окремий обліковий запис. Агент працює з дозволами цього користувача, тому не використовуйте свій обліковий запис для входу і не запускайте його від імені root. У матеріалі Користувачі з мінімальними привілеями на VPS пояснюється, чому цей поділ вартий додаткового кроку.
sudo adduser --disabled-password --gecos "" opentag
sudo loginctl enable-linger opentag
sudo npm install -g @opentag/cli@0.9.0
command -v opentagcommand -v opentag має вивести шлях на кшталт /usr/bin/opentag. Параметр linger важливий у Linux: OpenTag встановлює фоновий сервіс через systemd, а користувацький сервіс без linger зупиняється одразу після закриття SSH-сеансу.
Запустіть налаштування від імені цього користувача.
sudo -iu opentag opentag setupПід час налаштування потрібно вказати шість параметрів: мову CLI, локальну адресу прослуховування, coding agent, локальний проєкт для роботи, облікові дані платформи для збереження та спосіб запуску. Залиште адресу прослуховування на 127.0.0.1, оскільки nginx завершує TLS і пересилає запити на цю адресу. Тому такі listeners не потрібно робити доступними ззовні. Для GitHub також потрібно вказати репозиторій у форматі owner/repo, дозволити або заборонити відкривати pull requests, указати порт webhook (типово 3050) і токен. Наприкінці виберіть режим фонового сервісу. Якщо конфігурація вже існує і потрібно встановити сервіс без запитів, це виконує opentag setup --service.
Конфігурація зберігається в /home/opentag/.config/opentag/config.json, а стан виконання — у /home/opentag/.local/state/opentag. Після створення файлу варто вручну перевірити ці параметри.
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3030",
"runnerToken": "...",
"approvalMode": "ask",
"repositories": []
}Надавайте перевагу runnerToken — bearer token для окремого runner — перед старішим спільним pairingToken. За замовчуванням конфігураційний файл зберігає облікові дані у відкритому тексті. Щоб цього уникнути, замініть їх посиланням на секрет. Таке посилання зчитує значення зі змінної середовища або з файлу на диску під час запуску. У будь-якому разі цей файл є найчутливішим об’єктом на сервері: він має мати режим 600, належати opentag і ніколи не зберігатися в git-репозиторії. Докладніше про це йдеться в матеріалі Як не передавати секрети AI-агентам.
Перевірте встановлення, перш ніж відкривати доступ до сервісу.
sudo -iu opentag opentag doctor
sudo -iu opentag opentag statusopentag doctor перевіряє dispatcher, bindings, checkouts і executors. opentag status виводить конфігурацію та стан виконання. Після появи запусків цю команду можна обмежити одним запуском. Виправте все, що повідомляє doctor, перш ніж підключати платформу до цього сервера.
Налаштуйте TLS спереду й відкрийте лише два шляхи
nginx завершує TLS і пересилає рівно два шляхи. Усі інші запити повертають 404, тому сканер, який виявить хост, не дізнається, що працює за ним.
Створіть звичайний блок server для порту 80 у /etc/nginx/sites-available/opentag з двома наведеними нижче location, а потім дозвольте Certbot додати частину для TLS.
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/opentag /etc/nginx/sites-enabled/opentag
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d opentag.example.comnginx -t виводить syntax is ok і test is successful. Це єдиний захист від помилки в конфігурації, яка може призвести до перезавантаження з недоступним сайтом. У Certbot в Ubuntu 24.04 з nginx описано поновлення сертифіката та причини помилок перевірки ACME (середовища автоматичного керування сертифікатами). Готовий блок має такий вигляд.
server {
listen 443 ssl;
server_name opentag.example.com;
ssl_certificate /etc/letsencrypt/live/opentag.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/opentag.example.com/privkey.pem;
client_max_body_size 2m;
location = /github/webhooks {
proxy_pass http://127.0.0.1:3050;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location = /slack/events {
proxy_pass http://127.0.0.1:3040;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location / {
return 404;
}
}= у location = /github/webhooks використовує точний збіг, а proxy_pass без нічого після порту передає вихідний URI без змін. Якщо прибрати =, також пересилатиметься кожен шлях під /github/webhooks/. Це створює більшу поверхню атаки, ніж потрібно listener.
Правила firewall залишаються обмеженими.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw statusПорти 3030, 3040 і 3050 ніколи не відкриваються. Переконайтеся, що вони прив’язані до loopback, а не до всіх інтерфейсів.
sudo ss -tlnpУ кожному рядку OpenTag має бути 127.0.0.1:3030 або подібне значення. Рядок зі значенням 0.0.0.0:3050 означає, що listener доступний усьому інтернету, а його захищає лише ufw. Одна помилка у firewall — і trigger агента стане відкритим. У розділі основи firewall ufw пояснено, як насправді працює це правило заборони за замовчуванням.
Дві перевірки підтверджують роботу зовнішнього доступу. curl -I https://opentag.example.com/ повертає 404 від nginx, що підтверджує дійсність сертифіката та закриття catch-all. Запит до /slack/events або /github/webhooks без підпису не повинен повертати 200.
Перевіряйте кожен підпис, оскільки URL є публічним
URL для payload може знайти будь-хто. Він зберігається в налаштуваннях репозиторію, історії браузера або на скриншоті, доданому до тікета. Підпис — єдине, що відрізняє справжню доставку GitHub від запиту, введеного вручну.
GitHub підписує кожну доставку за допомогою секрету webhook і надсилає результат у заголовку x-hub-signature-256. OpenTag перевіряє цей заголовок за допомогою platforms.github.webhookSecret. У примітках щодо hardening проєкту це правило сформульовано безпосередньо: не приймайте непідписані події source на /github/webhooks. Slack підписує кожен запит за допомогою SLACK_SIGNING_SECRET і додає timestamp, тому перехоплене тіло запиту не можна повторно відтворити через кілька годин.
Пропуск цієї перевірки створює значний ризик. Неперевірений endpoint приймає вручну сформований payload issue_comment, що містить @opentag, після чого OpenTag запускає coding agent із вашим токеном у вашому checkout за інструкціями сторонньої особи. Відповідь надсилається в thread, указаний у підробленому payload.
OpenTag додає ще два рівні захисту. Source deliveries відстежуються за delivery ID, тому повторна доставка тієї самої події не запускає другий run. Runner calls приймають idempotency keys, тому повторне відтворення такого запиту повертає успішний результат, не додаючи ще одну audit event.
Rate limits можна налаштувати, і їх потрібно вмикати. OPENTAG_RATE_LIMIT_WINDOW_MS та OPENTAG_RATE_LIMIT_MAX_REQUESTS обмежують частоту запитів, OPENTAG_MAX_REQUEST_BODY_BYTES обмежує розмір body, а надто великий payload відхиляється з кодом 413 request_body_too_large. OPENTAG_RATE_LIMIT_DISABLED=true призначений для локальної розробки, і йому не місце на публічному сервері. Ще одне правило з тих самих приміток: публічний relay URL має використовувати HTTPS, а CLI дозволяє plain HTTP лише для localhost.
Які області дозволів насправді потрібні боту?
У GitHub OpenTag використовує fine-grained personal access token, а не GitHub App. У документації зазначено, що варіант із App запланований і наразі не є стандартним налаштуванням CLI. Це має наслідок, про який часто забувають: бот публікує коментарі від імені людини, яка створила token. Створюйте його в обліковому записі, ім’я якого ви готові бачити в кожній відповіді під час triage.
Налаштуйте дозволи так вузько, як зазначено в посібнику зі встановлення. Виберіть Only select repositories і вкажіть один репозиторій. Надайте дозволи Issues: Read and write і Pull requests: Read and write. Цього достатньо, щоб прочитати згадку та відповісти в обговоренні.
Зверніть увагу, чого тут немає: дозволу на запис до коду. OpenTag не надсилає гілки, якщо preparePullRequestBranch не має значення true. Окремий githubApplyToken дає змогу не використовувати один token для запису коду та публікування коментарів. Розділіть їх і не вмикайте token із дозволом на запис, доки шлях читання та коментування не пропрацює кілька тижнів.
Не використовуйте token із дозволом Contents: Read and write для All repositories. Тепер кожен, хто може залишати коментарі в будь-якому з цих репозиторіїв, може керувати агентом із правами на внесення комітів, а в журналі аудиту буде зазначено, що це зробив власник token. Розширюйте область дії по одному репозиторію за раз, після того як агент доведе свою надійність.
У Slack області дозволів бота — app_mentions:read, chat:write, reactions:write і channels:history. Для приватних каналів також потрібен groups:history і підписка на подію message.groups. Для Socket Mode потрібен app-level token із connections:write — той, що починається з xapp-. channels:history читає історію повідомлень у загальнодоступних каналах, до яких додано бота, тому додавайте бота лише до потрібних каналів, а не до всіх.
Наскрізний маршрут для одного запиту
Спочатку налаштуйте webhook. У репозиторії відкрийте Settings, потім Webhooks і Add webhook. URL для payload — https://opentag.example.com/github/webhooks, тип вмісту — application/json, а секрет — це секрет, згенерований під час налаштування. Підпишіться лише на Issue comments і Pull request review comments.
GitHub надсилає ping delivery одразу після збереження налаштувань. Відкрийте Recent Deliveries і перевірте, чи запит взагалі досяг сервера. Код 502 означає, що nginx не зміг підключитися до listener. Це локальна проблема, а не проблема GitHub.
Тепер скористайтеся налаштуванням. Відкрийте issue з описом помилки та додайте коментар:
@opentag triage this. Reproduce the report against the current main branch, then reply with the file and function most likely responsible, plus the test you would write first.Очікуваний порядок дій такий. Recent Deliveries фіксує delivery issue_comment з відповіддю 2xx. Dispatcher реєструє запуск. Runner отримує його та починає надсилати heartbeat. Executor відкриває checkout і виконує роботу. Відповідь надходить як коментар у тій самій гілці issue. sudo -iu opentag opentag status показує запуск, поки він виконується, тому його можна відстежувати, а не робити припущення.
Перед першим реальним запуском встановіть approvalMode у значення ask. У режимі ask запуск призупиняється та очікує підтвердження людини, перш ніж виконувати будь-які дії, що змінюють стан. Режими auto і autonomous також доступні. Їх доцільно використовувати пізніше в репозиторії, де ви протягом місяця переглядали журнали виконання.
У Slack той самий запуск починається з /bind owner/repo у каналі, а потім згадування. Бот також відповідає на /help, /status, /doctor, /stop і /unbind confirm. Обмежте коло користувачів, які можуть змінювати прив’язки, за допомогою OPENTAG_SLACK_BINDING_ADMIN_USER_IDS — списку ідентифікаторів користувачів Slack, розділених комами. Прив’язка визначає відповідність між публічним каналом і checkout на вашому сервері.
Triage — хороший перший маршрут, оскільки він читає дані, але не змінює їх, а результат легко перевірити. Наступний рівень — Review: агент коментує diff, а не issue. self-hosted агент для перевірки pull request використовує ту саму архітектуру, але працює з pull request. Якщо агент має під час роботи звертатися до ваших систем, для цього потрібні MCP-сервери на VPS. Вебпошук — ще одна можливість, якої часто потребує triage. підключення агента до власного екземпляра SearXNG дає змогу виконувати такі пошукові запити на обладнанні під вашим керуванням, але додає ще один канал, через який текст від стороннього користувача потрапляє до агента.
Що станеться, якщо агент помилиться на очах у всіх?
Він помилиться. Питання в тому, якою буде ціна цієї помилки.
Неправильна відповідь у публічному issue — це коментар від імені, яке впізнає ваша команда, а GitHub надсилає його електронною поштою всім підписаним одразу після публікації. Видалення коментарю не відкликає електронний лист. Те саме стосується сповіщення Slack. Плануйте роботу з урахуванням того, що відповідь може бути неправильною публічно, а не того, що приватно вона буде правильною.
Чотири рішення обмежують можливу шкоду. Вони важливіші за будь-який prompt, який ви напишете.
- Запускайте в режимі
ask: агент пропонує дію, людина її схвалює, а неправильний план коштує одного натискання. - Залиште
preparePullRequestBranchзі значенням false за замовчуванням. Тоді найгіршим наслідком невдалого запуску буде неправильний коментар, а не неправильна гілка. - Для початку прив’яжіть один репозиторій і один канал. Runner відхиляє будь-який запуск, цільовий проєкт якого не входить до його локального списку дозволених, тому непов’язаний репозиторій не зможе залучити агента до себе.
- Зберігайте токен для коментування окремо від будь-якого токена для apply, щоб відкликання доступу на запис не зупинило triage.
У Slack є команда /stop для запуску, який розвивається в неправильному напрямку. Кожен запуск також залишає аудиторський запис із mention, що його ініціював, і виконаними агентом діями. Саме цей запис потрібно переглянути пізніше, щоб з’ясувати, де сталася помилка.
Соціальна сторона не менш важлива за конфігурацію. Розмістіть бота в одному каналі, де люди очікують на роботу машини та розуміють, що вона може помилятися. Упевнена неправильна відповідь у каналі з сорока людьми, які вважають, що її перевірила людина, коштує дорожче за заощаджений час на triage. Укажіть в описі каналу, хто відповідає за бота і хто перевіряє його результати.
Резервні копії, оновлення та фіксація версії
Два шляхи містять усе: /home/opentag/.config/opentag/config.json і /home/opentag/.local/state/opentag. У першому зберігаються облікові дані, у другому — історія запусків і файл бази даних. Створюйте резервні копії обох із правами доступу 600 і зберігайте їх поза сервером. У разі втрати доведеться повторно створити токени та прив’язки, але не перебудовувати сервер.
Оновлення складається зі зміни версії та перезапуску.
sudo npm install -g @opentag/cli@0.9.0
sudo -iu opentag opentag service stop
sudo -iu opentag opentag service start
sudo -iu opentag opentag doctorФіксуйте версію, а не відстежуйте @latest. Це програмне забезпечення запускає coding agent у вашому репозиторії з активним токеном, тому реліз, опублікований уночі, є неперевіреною зміною цієї конфігурації. Політика безпеки не передбачає backport виправлень, а виправлення потрапляють лише до найновішого релізу. Тому фіксація версії означає, що ви читаєте changelog і свідомо переходите на нову версію. Це не означає залишатися на v0.9.0 назавжди. Історія до July 2026 показує кілька релізів на місяць, тож перед кожною зміною версії варто читати release notes.
FAQ
Чи потрібен VPS для запуску OpenTag, чи достатньо ноутбука?
Для самого Slack достатньо ноутбука, оскільки Socket Mode відкриває вихідне WebSocket-з’єднання й не потребує вхідного порту. Для GitHub ситуація інша. Webhook-и репозиторію доставляються через вхідний HTTP-запит на URL, який ви реєструєте один раз, тому адреса має залишатися незмінною та відповідати на запити, поки ви спите. Адреса tunnel host із безкоштовного облікового запису змінюється після кожного перезапуску, а GitHub продовжує надсилати запити на стару адресу. Це відображається як невдалі записи на вкладці Recent Deliveries репозиторію та як відсутність повідомлень у гілці. VPS із фіксованим DNS-ім’ям і сертифікатом усуває обидві проблеми.
Які дозволи GitHub потрібні OpenTag?
Токен доступу fine-grained для персонального облікового запису, обмежений параметром Only select repositories, із дозволами Issues: Read and write та Pull requests: Read and write. Цього достатньо, щоб прочитати згадку та відповісти в гілці. Доступ на запис до коду не потрібен, якщо ви не встановите preparePullRequestBranch у значення true, щоб OpenTag надсилав гілки. Окремий githubApplyToken дає змогу зберігати токен для запису коду окремо від токена для коментування. Не використовуйте токен для всіх репозиторіїв із дозволом contents write, оскільки будь-хто, хто може коментувати будь-який із цих репозиторіїв, зможе керувати агентом, який має право створювати коміти.
Як зупинити запуск, якщо він виконується неправильно?
Slack має команду /stop саме для цього. На сервері opentag status показує, що виконується, а opentag service stop зупиняє daemon і завершує весь pipeline, а не лише один запуск. Щоб не використовувати жодну з цих команд, установіть approvalMode у значення ask, щоб перед внесенням змін запуск призупинявся для перевірки людиною. Залиште preparePullRequestBranch зі значенням false, щоб невдалий запуск створював коментар, а не гілку.
Чому мій webhook повертає 502, а в гілці немає повідомлень?
Код 502 повертає nginx, а не OpenTag. Це означає, що proxy не зміг підключитися до listener. /var/log/nginx/error.log покаже connect() failed (111: Connection refused) while connecting to upstream. Або listener зупинено, або він працює на іншому порту, ніж указано в рядку proxy_pass. Виконайте sudo ss -tlnp і перевірте, що щось прослуховує 127.0.0.1:3050 для GitHub та 127.0.0.1:3040 для Slack. Потім виконайте opentag doctor, щоб перевірити bindings і executors.