Installer OpenTag sur un VPS pour les mentions @agent
Déployez OpenTag v0.9.0 sur un VPS pour relayer les mentions Slack et GitHub vers votre agent, avec TLS, signatures webhook et scopes de tokens maîtrisés.
Ce que fait OpenTag lorsque vous mentionnez un agent
OpenTag transforme une @mention dans un fil Slack ou une issue GitHub en une exécution d’agent de programmation sur une machine que vous contrôlez. Quelqu’un ajoute @opentag investigate this dans un commentaire d’issue. Un listener reçoit l’événement de la plateforme, vérifie sa signature, associe la mention à un projet lié, lance un agent de programmation sur un checkout local, puis publie le résultat dans le même fil.
Le projet est sous licence MIT et se trouve sur amplifthq/opentag. En août 2026, la dernière release taguée est v0.9.0, publiée le 28 juillet 2026, et le projet est distribué sous forme de package npm. Il n’existe pas d’image de conteneur officielle : l’élément à figer est donc la version npm. Chaque commande ci-dessous la fixe.
Ce projet nécessite un VPS plutôt qu’un laptop à cause de GitHub. GitHub transmet les événements du dépôt en envoyant une requête HTTP à une URL que vous enregistrez une seule fois. Cette URL doit donc répondre à la même adresse demain.
Les quatre composants
Le listener reçoit les événements de la plateforme, et chaque plateforme en possède un. Le listener GitHub est un endpoint HTTP sur le port 3050, à l’emplacement /github/webhooks. Le listener de l’API Slack Events est disponible sur le port 3040, à l’emplacement /slack/events. Slack peut également fonctionner en Socket Mode : l’application ouvre alors un WebSocket sortant et n’a besoin d’aucun port entrant.
Le dispatcher assure la coordination. Il écoute par défaut sur le port 3030, conserve l’état des exécutions dans un fichier de base de données local défini par OPENTAG_DATABASE_PATH et enregistre une piste d’audit pour chaque exécution. Rien à l’extérieur du serveur ne doit jamais accéder à ce port.
Le runner est le daemon local. Il recherche les tâches, prend en charge une exécution, conserve un lease dessus et envoie par défaut un heartbeat toutes les 15 secondes tant que l’exécution est active. Il refuse toute exécution prise en charge dont la cible du projet est absente ou ne figure pas dans l’allowlist de sa propre configuration. Ce contrôle empêche un événement GitHub de diriger votre agent vers un dépôt que vous n’avez jamais associé.
L’executor est l’agent de code lui-même. OpenTag le lance via ACP (agent client protocol), un protocole JSON-RPC qui communique sur l’entrée et la sortie standard. L’agent s’exécute donc comme un processus enfant dans un répertoire de travail qu’OpenTag lui fournit. Les noms intégrés comprennent echo, codex, claude-code, cursor, opencode, hermes et openclaw. Commencez avec echo, l’executor fourni par la configuration d’exemple. Cela permet de vérifier que l’ensemble du flux fonctionne avant qu’un modèle ne modifie votre code.
L’ordre ne change jamais : événement de la plateforme, vérification de la signature, enregistrement de l’exécution, prise en charge, agent, réponse dans le thread.
Pourquoi un laptop et un tunnel ne suffisent pas
Le guide de configuration GitHub vous demande d’exécuter ngrok http 3050, puis de coller l’hôte du tunnel dans le webhook du dépôt. Cela fonctionne pendant les dix premières minutes. L’hôte fourni gratuitement par le tunnel change à chaque redémarrage du processus et cesse d’exister lorsque le laptop se met en veille. GitHub conserve l’ancienne payload URL et continue de l’utiliser. L’onglet Recent Deliveries des paramètres du webhook se remplit donc d’échecs, tandis que le thread reste silencieux. Personne ne s’en aperçoit pendant une semaine, car un webhook qui ne fait rien ressemble exactement à un bot dont personne n’a parlé.
Un VPS corrige les deux problèmes. Le nom DNS ne change pas. La payload URL que vous collez une seule fois reste donc valide. La machine ne se met pas en veille. Un commentaire publié à 02:00 reçoit donc une réponse. Configurez d’abord correctement le serveur : les dix premières minutes sur un nouveau VPS couvrent l’utilisateur de connexion et le pare-feu supposés par ce guide.
Slack fait exception. En Socket Mode, il établit une connexion sortante et n’a besoin d’aucune URL publique. Un déploiement limité à Slack peut donc rester fermé. GitHub n’a pas d’équivalent. Les repository webhooks utilisent des requêtes HTTP entrantes. Ils nécessitent donc un endpoint public, TLS (transport layer security) et une vérification de signature.
Héberger OpenTag soi-même sur Ubuntu depuis une release figée
OpenTag v0.9.0 nécessite Node.js 22 ou une version ultérieure. Ubuntu 24.04 fournit Node 18 dans son propre dépôt. Installez donc Node.js depuis 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 doit afficher v22 ou une version ultérieure. Avec Node 20, l’installation affiche un avertissement EBADENGINE et la CLI peut échouer au démarrage.
Créez un compte dédié pour le service. L’agent s’exécute avec les permissions de cet utilisateur. Ce compte ne doit donc être ni votre compte de connexion ni root. Utilisateurs avec privilèges minimaux sur un VPS explique pourquoi cette séparation justifie cette étape supplémentaire.
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 doit afficher un chemin tel que /usr/bin/opentag. Le paramètre linger est important sous Linux : OpenTag installe son service d’arrière-plan via systemd, et un service utilisateur sans linger s’arrête dès que votre session SSH se ferme.
Lancez la configuration avec ce compte.
sudo -iu opentag opentag setupLa configuration vous pose six questions : la langue de la CLI, l’adresse d’écoute locale, l’agent de programmation, le projet local sur lequel travailler, les identifiants de la plateforme à enregistrer et le mode d’exécution. Conservez l’adresse d’écoute 127.0.0.1, car nginx termine TLS et transmet les requêtes à cette adresse. Les listeners n’ont donc jamais besoin d’être accessibles depuis l’extérieur. Pour GitHub, la configuration demande également le dépôt au format owner/repo, l’autorisation d’ouvrir des pull requests, le port du webhook (3050 par défaut) et le token. À la fin, choisissez le mode service d’arrière-plan. Si vous disposez déjà d’une configuration et souhaitez installer le service sans invite, opentag setup --service effectue cette opération.
La configuration est enregistrée dans /home/opentag/.config/opentag/config.json et l’état d’exécution dans /home/opentag/.local/state/opentag. Vérifiez manuellement ces clés après l’écriture du fichier de configuration.
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3030",
"runnerToken": "...",
"approvalMode": "ask",
"repositories": []
}Préférez runnerToken, le bearer token limité au runner, à l’ancien pairingToken partagé. Le fichier de configuration contient les identifiants en clair, sauf si vous les remplacez par une référence à un secret. Cette référence lit la valeur dans l’environnement ou dans un fichier sur le disque au démarrage. Dans tous les cas, ce fichier est l’élément le plus sensible du serveur : permissions 600, propriétaire opentag, et jamais dans un dépôt git. L’explication générale se trouve dans Ne pas exposer les secrets aux agents IA.
Vérifiez l’installation avant d’exposer quoi que ce soit.
sudo -iu opentag opentag doctor
sudo -iu opentag opentag statusopentag doctor vérifie le dispatcher, les bindings, les checkouts et les executors. opentag status affiche la configuration et l’état d’exécution. Il peut aussi être limité à un seul run une fois que des runs existent. Corrigez tout ce que doctor signale avant de connecter une plateforme à ce serveur.
Placer TLS en frontal et n’ouvrir que deux chemins
nginx termine TLS et transmet exactement deux chemins. Toutes les autres requêtes renvoient 404. Ainsi, un scanner qui découvre l’hôte n’apprend rien sur les services qui se trouvent derrière.
Écrivez un bloc server simple sur le port 80 dans /etc/nginx/sites-available/opentag, avec les deux emplacements ci-dessous, puis laissez Certbot ajouter la partie 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 affiche syntax is ok et test is successful. C’est le seul élément entre une faute de frappe et un rechargement qui rend le site indisponible. Certbot sur Ubuntu 24.04 avec nginx explique le renouvellement et les cas d’échec d’un challenge ACME (automatic certificate management environment). Le bloc final ressemble à ceci.
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;
}
}Le = dans location = /github/webhooks est une correspondance exacte. proxy_pass, sans rien après le port, transmet l’URI d’origine sans la modifier. Supprimez le = et tous les chemins sous /github/webhooks/ seront également transmis. Cela expose davantage de surface que nécessaire pour ce listener.
Le firewall reste restrictif.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw statusLes ports 3030, 3040 et 3050 ne sont jamais ouverts. Vérifiez qu’ils sont liés à loopback et non à toutes les interfaces.
sudo ss -tlnpChaque ligne OpenTag doit contenir 127.0.0.1:3030 ou une valeur similaire. Une ligne contenant 0.0.0.0:3050 signifie que le listener s’expose à tout Internet et que seul ufw le bloque. Une simple erreur de configuration du firewall suffirait alors à ouvrir un déclencheur d’agent. Bases du firewall ufw explique ce que fait réellement ce refus par défaut.
Deux vérifications suffisent pour valider le point d’entrée. curl -I https://opentag.example.com/ renvoie 404 depuis nginx, ce qui confirme que le certificat est valide et que le catch-all est fermé. Une requête vers /slack/events ou /github/webhooks sans signature ne doit jamais renvoyer 200.
Vérifiez chaque signature, car l’URL est publique
Tout le monde peut trouver l’URL du payload. Elle figure dans les paramètres de votre dépôt, dans l’historique du navigateur ou dans une capture d’écran collée dans un ticket. La signature est le seul élément qui distingue une véritable livraison GitHub d’une requête saisie manuellement.
GitHub signe chaque livraison avec le secret du webhook et envoie le résultat dans l’en-tête x-hub-signature-256. OpenTag vérifie cet en-tête par rapport à platforms.github.webhookSecret. Les notes de durcissement du projet énoncent directement la règle : n’acceptez pas d’événements source non signés sur /github/webhooks. Slack signe chaque requête avec SLACK_SIGNING_SECRET et inclut un horodatage. Ainsi, un body intercepté ne peut pas être rejoué plusieurs heures plus tard.
Ignorer cette vérification présente un risque important. Un endpoint non vérifié accepte un payload issue_comment rédigé manuellement contenant @opentag. OpenTag exécute alors un coding agent, avec votre token, dans votre checkout, en suivant les instructions d’un inconnu. La réponse est envoyée vers le thread indiqué par le faux payload.
OpenTag ajoute deux niveaux de protection. Les livraisons source sont suivies avec leur delivery ID. La rediffusion du même événement ne démarre donc pas une deuxième exécution. Les appels du runner acceptent des clés d’idempotence. La répétition d’un appel renvoie donc un succès sans ajouter un autre événement d’audit.
Les limites de débit sont configurables et doivent être activées. OPENTAG_RATE_LIMIT_WINDOW_MS et OPENTAG_RATE_LIMIT_MAX_REQUESTS limitent le débit des requêtes, OPENTAG_MAX_REQUEST_BODY_BYTES limite la taille du body et un payload trop volumineux est rejeté avec 413 request_body_too_large. OPENTAG_RATE_LIMIT_DISABLED=true est prévu pour le développement local et ne doit pas être utilisé sur un serveur public. Les mêmes notes ajoutent une dernière règle : une URL de relay publique doit utiliser HTTPS, et la CLI n’autorise HTTP en clair que pour localhost.
Quels scopes de token le bot doit-il réellement avoir ?
Sur GitHub, OpenTag utilise un personal access token à granularité fine plutôt qu’une GitHub App. La documentation précise que l’option App est prévue, mais qu’elle n’est pas le mode de configuration CLI par défaut aujourd’hui. Cela entraîne une conséquence souvent oubliée : le bot publie ses commentaires au nom de la personne qui a créé le token. Créez-le avec un compte dont le nom peut apparaître dans chaque réponse de triage.
Limitez les scopes comme indiqué dans le guide d’installation. Sélectionnez Only select repositories et choisissez-en un. Accordez Issues: Read and write et Pull requests: Read and write. Cela suffit pour lire une mention et répondre dans le thread.
Notez ce qui manque : l’accès en écriture au code. OpenTag ne pousse pas de branches, sauf si preparePullRequestBranch est défini sur true. Un paramètre distinct, githubApplyToken, permet d’utiliser un token pour écrire le code et un autre pour publier des commentaires. Gardez-les séparés et laissez le token d’écriture désactivé jusqu’à ce que le parcours en lecture et en commentaire ait fonctionné pendant quelques semaines.
Évitez d’utiliser un token avec Contents: Read and write sur All repositories. Toute personne pouvant commenter dans l’un de ces dépôts peut alors piloter un agent disposant des droits de commit, tandis que la piste d’audit attribue l’action au propriétaire du token. Élargissez la portée dépôt par dépôt, une fois que l’agent a démontré sa fiabilité.
Sur Slack, les scopes du bot sont app_mentions:read, chat:write, reactions:write et channels:history. Les canaux privés nécessitent également groups:history ainsi qu’un abonnement à l’événement message.groups. Socket Mode nécessite un token au niveau de l’application avec connections:write, c’est-à-dire celui qui commence par xapp-. channels:history permet de lire l’historique des messages dans les canaux publics auxquels le bot a été ajouté. Ajoutez donc le bot aux canaux où il doit intervenir, plutôt qu’à tous les canaux.
Traiter un incident de bout en bout
Le webhook vient en premier. Dans le dépôt, ouvrez Settings, puis Webhooks, puis Add webhook. L’URL de payload est https://opentag.example.com/github/webhooks, le type de contenu est application/json et le secret est celui généré par le setup. Abonnez-vous uniquement à Issue comments et Pull request review comments.
GitHub envoie une livraison ping dès que vous enregistrez la configuration. Ouvrez Recent Deliveries et vérifiez que la requête a bien atteint le serveur. Un code 502 à cet endroit signifie que nginx n’a pas pu atteindre le listener. Le problème est local, pas lié à GitHub.
Testez maintenant le fonctionnement. Ouvrez une issue qui décrit un bug et ajoutez le commentaire suivant :
@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.Voici ce qui doit se produire, dans l’ordre. Recent Deliveries enregistre la livraison issue_comment avec une réponse 2xx. Le dispatcher enregistre une exécution. Le runner la prend en charge et commence à envoyer des heartbeats. L’executor ouvre le checkout et travaille. La réponse arrive sous forme de commentaire dans le même fil d’issue. sudo -iu opentag opentag status affiche l’exécution pendant son traitement. Vous pouvez donc la suivre au lieu de faire des suppositions.
Définissez approvalMode sur ask avant la première exécution réelle. En mode ask, l’exécution se met en pause et attend l’intervention d’une personne avant toute modification d’état. Les modes auto et autonomous existent également. Ils conviennent plus tard, sur un dépôt dont vous avez lu les transcriptions pendant un mois.
Côté Slack, la même exécution commence avec /bind owner/repo dans le channel, puis avec une mention. Le bot répond également à /help, /status, /doctor, /stop et /unbind confirm. Limitez les personnes autorisées à modifier les bindings avec OPENTAG_SLACK_BINDING_ADMIN_USER_IDS, une liste d’identifiants utilisateur Slack séparés par des virgules. Un binding associe un channel public à un checkout sur votre serveur.
Le triage constitue un bon premier parcours, car il lit les données sans les modifier et sa réponse est facile à évaluer. La review est l’étape suivante : l’agent commente un diff au lieu d’une issue. un agent de review de pull request auto-hébergé repose sur la même architecture, orientée vers les pull requests. Si vous voulez que l’agent accède à vos propres systèmes pendant son exécution, utilisez des serveurs MCP sur un VPS.
Que se passe-t-il lorsque l’agent se trompe devant tout le monde ?
Il se trompera. La question est de savoir ce que cela coûte.
Une réponse erronée sur une issue publique devient un commentaire publié sous un nom que votre équipe reconnaît, et GitHub l’envoie par e-mail à toutes les personnes abonnées dès sa publication. Supprimer le commentaire ne permet pas de rappeler l’e-mail. Il en va de même pour une notification Slack. Préparez-vous à ce que la réponse soit erronée publiquement, plutôt qu’à ce qu’elle soit correcte en privé.
Quatre choix limitent les dégâts, et ils comptent davantage que n’importe quel prompt que vous pourriez rédiger.
- Exécutez l’agent en mode
ask: l’agent propose, une personne approuve, et un plan erroné ne coûte qu’un clic. - Laissez
preparePullRequestBranchà sa valeur par défaut, false, afin que le pire résultat d’une mauvaise exécution soit un commentaire erroné plutôt qu’une mauvaise branche. - Commencez par lier un seul dépôt et un seul canal. Le runner rejette toute exécution dont la cible du projet ne figure pas dans sa liste d’autorisation locale. Un dépôt non lié ne peut donc pas appeler l’agent.
- Gardez le token de commentaire séparé de tout token d’application, afin que la révocation des droits d’écriture n’interrompe pas le triage.
Slack dispose d’une commande /stop pour une exécution qui prend une mauvaise direction. Chaque exécution laisse également un enregistrement d’audit contenant la mention qui l’a déclenchée et les actions effectuées par l’agent. C’est ce que vous consultez ensuite pour comprendre où l’exécution a dévié.
L’aspect social compte autant que la configuration. Placez le bot dans un canal où les utilisateurs s’attendent à la présence d’une machine et savent qu’elle peut se tromper. Une réponse erronée et formulée avec assurance dans un canal de quarante personnes qui pensent qu’un humain l’a vérifiée coûte plus cher que le temps gagné par le triage. Indiquez dans la description du canal qui est responsable du bot et qui vérifie ses résultats.
Sauvegardes, mises à niveau et épinglage de version
Deux chemins contiennent tout : /home/opentag/.config/opentag/config.json et /home/opentag/.local/state/opentag. Le premier contient vos identifiants, le second l’historique des exécutions et le fichier de base de données. Sauvegardez-les tous les deux avec le mode 600 et conservez les sauvegardes hors du serveur. Leur perte impose de recréer les tokens et les associations, mais pas de reconstruire un serveur.
Les mises à niveau consistent à changer de version et à redémarrer.
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Épinglez la version au lieu de suivre @latest. Ce logiciel exécute un coding agent sur votre dépôt avec un token actif. Une release publiée pendant la nuit constitue donc une modification non vérifiée de ce dépôt. La security policy ne backporte aucun correctif, et les correctifs sont disponibles uniquement dans la dernière release. L’épinglage vous oblige donc à lire le changelog et à effectuer la mise à niveau volontairement. Cela ne signifie pas rester indéfiniment sur v0.9.0. L’historique jusqu’à July 2026 montre plusieurs releases par mois. C’est une bonne raison de lire les release notes avant chaque changement de version.
FAQ
Ai-je besoin d’un VPS pour exécuter OpenTag, ou un ordinateur portable suffit-il ?
Un ordinateur portable suffit pour Slack seul, car Socket Mode ouvre un WebSocket sortant et ne nécessite aucun port entrant. GitHub fonctionne différemment. Les webhooks des dépôts sont transmis via HTTP entrant vers une URL que vous enregistrez une fois. Cette adresse doit donc rester identique et répondre même lorsque vous dormez. L’adresse d’un tunnel fournie par un compte gratuit change à chaque redémarrage. GitHub continue alors d’envoyer les requêtes vers l’ancienne adresse. Cela apparaît sous forme d’échecs dans l’onglet Recent Deliveries du dépôt et sous forme de silence dans le thread. Un VPS avec un nom DNS fixe et un certificat élimine ces deux problèmes.
Quelles permissions GitHub sont nécessaires à OpenTag ?
Un personal access token à granularité fine, limité à Only select repositories, avec Issues: Read and write et Pull requests: Read and write. Cela suffit pour lire une mention et y répondre dans le thread. L’accès en écriture au code n’est pas nécessaire, sauf si vous définissez preparePullRequestBranch sur true pour qu’OpenTag pousse des branches. Un githubApplyToken distinct permet de séparer le token qui écrit dans le code de celui qui publie les commentaires. Évitez un token valable pour tous les dépôts avec l’autorisation contents write. Toute personne pouvant commenter dans l’un de ces dépôts pourrait alors piloter un agent capable de créer des commits.
Comment arrêter une exécution qui se passe mal ?
Slack possède une commande /stop prévue à cet effet. Sur le serveur, opentag status affiche ce qui est en cours d’exécution et opentag service stop arrête le daemon. Cela termine tout le pipeline, et pas une seule exécution. Pour éviter ces interventions, définissez approvalMode sur ask afin que les exécutions s’interrompent avant toute modification et attendent l’intervention d’une personne. Laissez preparePullRequestBranch sur false afin qu’une exécution défectueuse produise un commentaire plutôt qu’une branche.
Pourquoi mon webhook renvoie-t-il 502 alors que le thread reste silencieux ?
Le code 502 vient de nginx, et non d’OpenTag. Il signifie que le proxy n’a pas pu joindre le listener. /var/log/nginx/error.log affichera connect() failed (111: Connection refused) while connecting to upstream. Le listener est soit arrêté, soit configuré sur un port différent de celui indiqué par la ligne proxy_pass. Exécutez sudo ss -tlnp et vérifiez qu’un processus écoute sur 127.0.0.1:3050 pour GitHub et sur 127.0.0.1:3040 pour Slack. Exécutez ensuite opentag doctor pour afficher les bindings et les executors.