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, avec TLS, signatures webhook, scopes de tokens et réglages sûrs.
Ce que fait OpenTag lorsque vous mentionnez un agent
OpenTag transforme une @mention dans un thread Slack ou une issue GitHub en exécution d’un agent de codage sur une machine que vous contrôlez. Quelqu’un ajoute le commentaire @opentag investigate this sur une issue. Un listener reçoit l’événement de la plateforme, vérifie sa signature, associe la mention à un projet lié, démarre un agent de codage sur un checkout local, puis publie le résultat dans le même thread.
Le projet est sous licence MIT et se trouve dans amplifthq/opentag. En août 2026, la dernière release marqué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. Vous devez donc verrouiller la version npm. Chaque commande ci-dessous la verrouille.
Ce projet nécessite un VPS plutôt qu’un laptop à cause de l’intégration avec GitHub. GitHub envoie les événements du dépôt en effectuant une requête HTTP vers 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. Chaque plateforme possède le sien. Le listener GitHub est un endpoint HTTP sur le port 3050, à l’emplacement /github/webhooks. Le listener de la Slack Events API écoute sur le port 3040, à l’emplacement /slack/events. Slack peut également fonctionner en Socket Mode. Dans ce mode, l’application ouvre 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. Aucun élément extérieur au serveur ne doit jamais pouvoir atteindre ce port.
Le runner est le daemon local. Il recherche les tâches, prend en charge une exécution, conserve son lease et envoie par défaut un heartbeat toutes les 15 secondes pendant toute la durée de l’exécution. 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 repository que vous n’avez jamais associé.
L’executor est l’agent de codage lui-même. OpenTag le lance via ACP (agent client protocol), un protocole JSON-RPC qui utilise 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 transmet. Les noms intégrés incluent echo, codex, claude-code, cursor, opencode, hermes et openclaw. Commencez par echo, l’executor fourni par la configuration d’exemple. Vous vérifierez ainsi 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 ordinateur portable et un tunnel ne suffisent pas
Le guide de configuration de 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 par un tunnel gratuit change à chaque redémarrage du processus et cesse d’exister lorsque l’ordinateur portable se met en veille. GitHub conserve l’ancienne URL de payload 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. L’URL de payload que vous collez une seule fois reste donc valide. La machine ne se met pas en veille. Un commentaire publié à 02:00 reçoit ainsi 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 firewall 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 utilisant uniquement Slack peut donc rester fermé. GitHub ne propose aucun équivalent. Les webhooks de dépôt sont des requêtes HTTP entrantes. Il faut donc un endpoint public, TLS (transport layer security) et une vérification de signature.
Héberger OpenTag sur Ubuntu à partir d’une version 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 valeur supé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. La page 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.
Exécutez la configuration avec cet utilisateur.
sudo -iu opentag opentag setupL’assistant de configuration pose six questions : la langue de la CLI, l’adresse d’écoute locale, l’agent de programmation, le projet local à traiter, les identifiants de plateforme à enregistrer et le mode d’exécution. Conservez l’adresse d’écoute sur 127.0.0.1, car nginx termine TLS et lui transmet les requêtes. Les listeners n’ont donc jamais besoin d’être accessibles depuis l’extérieur. Pour GitHub, l’assistant 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. Sélectionnez le mode de service d’arrière-plan à la fin. 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 par l’assistant.
{
"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 de secret. Celle-ci lit la valeur dans l’environnement ou dans un fichier sur disque au démarrage. Dans tous les cas, ce fichier est l’élément le plus sensible du serveur : définissez le mode 600, faites-le posséder par opentag et ne le placez jamais dans un dépôt git. L’argument général est présenté dans Garder les secrets hors des 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. Vous pouvez aussi limiter son périmètre à une seule exécution lorsque des exécutions existent. Corrigez tout ce que doctor signale avant de connecter une plateforme à ce serveur.
Placer TLS devant le service et n’exposer que deux chemins
Nginx termine TLS et transmet exactement deux chemins. Toutes les autres requêtes renvoient 404. Ainsi, un scanner qui trouve l’hôte n’apprend rien sur les services exécutés 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 la seule protection entre une faute de frappe et un rechargement qui rende le site indisponible. L’article 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 pare-feu 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 être de la forme 127.0.0.1:3030 ou équivalente. Une ligne 0.0.0.0:3050 signifie que le listener s’expose à tout Internet et que seul ufw le bloque. Une erreur de configuration du pare-feu suffit alors à ouvrir un déclencheur d’agent. L’article notions de base du pare-feu ufw explique le fonctionnement réel de 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
N’importe qui 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 hardening du projet énoncent directement la règle : n’acceptez pas les événements source non signés sur /github/webhooks. Slack signe chaque requête avec SLACK_SIGNING_SECRET et inclut un timestamp. Un body capturé ne peut donc pas être rejoué plusieurs heures plus tard.
Ignorer cette vérification n’est pas un risque mineur. Un endpoint non vérifié accepte un payload issue_comment rédigé manuellement contenant @opentag. OpenTag lance 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 par leur delivery ID. La rediffusion du même événement ne déclenche donc pas un second run. Les appels du runner acceptent des clés d’idempotence. La rediffusion d’un appel renvoie ainsi un succès sans ajouter un nouvel é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 n’a pas sa place sur un serveur public. Une autre règle figure dans ces mêmes notes : 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 indique que l’option App est prévue, mais qu’elle n’est pas le mode de configuration CLI par défaut aujourd’hui. Cela a 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 l’indique 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 fil.
Remarquez 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 ainsi de ne pas utiliser le même token pour écrire du code et publier des commentaires. Séparez-les et laissez le token d’écriture désactivé jusqu’à ce que le fonctionnement en lecture et en commentaire ait été éprouvé pendant quelques semaines.
Évitez une configuration dans laquelle un token dispose de 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, après que l’agent a démontré qu’il le méritait.
Dans 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, celui qui commence par xapp-. channels:history lit 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 problème 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 script d’installation. 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. Une réponse 502 signifie que nginx n’a pas pu atteindre le listener. Le problème est donc 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 deviner.
Définissez approvalMode sur ask avant la première exécution réelle. En mode ask, l’exécution s’interrompt et attend l’intervention d’une personne avant toute modification d’état. Les modes auto et autonomous existent également. Ils sont raisonnables plus tard, dans un dépôt dont vous avez lu les transcriptions pendant un mois.
Côté Slack, la même exécution commence par /bind owner/repo dans le channel, puis par 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 une bonne première étape, 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 alors un diff au lieu d’une issue. un agent de review de pull request auto-hébergé repose sur cette même architecture, appliquée aux pull requests. Si vous voulez que l’agent accède à vos propres systèmes pendant son travail, utilisez des serveurs MCP sur un VPS. La recherche Web est l’autre capacité que le triage demande souvent. Connecter l’agent à votre propre instance SearXNG permet de conserver ces recherches sur du matériel que vous administrez, au prix d’un canal supplémentaire par lequel le texte d’un tiers peut atteindre l’agent.
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 dans 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 rappelle pas l’e-mail. Il en va de même pour une notification Slack. Préparez-vous à ce que la réponse soit erronée en public, plutôt qu’à ce qu’elle soit correcte en privé.
Quatre choix limitent les dégâts. 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 associer un seul repository et un seul channel. Le runner rejette toute exécution dont la cible du projet ne figure pas dans son allowlist local. Un repository non associé ne peut donc pas appeler l’agent.
- Gardez le token de commentaire séparé de tout token apply. Ainsi, la révocation des droits d’écriture n’interrompt pas le triage.
Slack fournit une commande /stop pour interrompre une exécution qui prend la mauvaise direction. Chaque exécution laisse également un enregistrement d’audit contenant la mention qui l’a déclenchée et les actions de l’agent. Vous pouvez le consulter ensuite pour déterminer où l’agent s’est trompé.
L’aspect social est aussi important que la configuration. Placez le bot dans un channel 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 channel 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 channel qui est responsable du bot et qui vérifie ses résultats.
Sauvegardes, mises à niveau et version figée
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 d’exécution et le fichier de base de données. Sauvegardez-les tous les deux avec le mode 600 et stockez les sauvegardes hors du serveur. Leur perte vous obligerait à recréer les tokens et les associations, mais pas à reconstruire un serveur.
Les mises à niveau consistent à changer de version, puis à redémarrer le service.
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 doctorFigez la version au lieu de suivre @latest. Ce logiciel exécute un agent de code sur votre dépôt avec un token actif. Une release publiée pendant la nuit constitue donc une modification non révisée de ce dépôt. La politique de sécurité ne rétroporte aucun correctif. Les correctifs sont disponibles uniquement dans la dernière release. Figer la version vous oblige donc à lire le changelog et à effectuer les mises à niveau volontairement. Cela ne signifie pas rester indéfiniment sur v0.9.0. L’historique jusqu’en juillet 2026 montre plusieurs releases par mois. Il est donc recommandé de lire les release notes avant chaque changement de version.
FAQ
OpenTag a-t-il besoin d’un VPS 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 de dépôt sont envoyés en 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 hébergé avec 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 d’absence de réponse dans le fil de discussion. Un VPS avec un nom DNS fixe et un certificat élimine ces deux problèmes.
De quelles permissions GitHub OpenTag a-t-il besoin ?
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 fil de discussion. L’accès en écriture au code n’est pas nécessaire, sauf si vous définissez preparePullRequestBranch sur true afin qu’OpenTag pousse des branches. Un paramètre githubApplyToken distinct permet de séparer le token qui peut écrire dans le code de celui qui sert à commenter. É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 fournit une commande /stop prévue pour cela. Sur le serveur, opentag status affiche ce qui est en cours d’exécution et opentag service stop arrête le daemon, ce qui met fin à tout le pipeline et pas seulement à une exécution. Pour éviter d’avoir recours à l’une ou l’autre commande, définissez approvalMode sur ask afin que les exécutions soient mises en pause et attendent l’intervention d’une personne avant toute modification. Laissez preparePullRequestBranch sur false afin qu’une exécution incorrecte produise un commentaire plutôt qu’une branche.
Pourquoi mon webhook renvoie-t-il 502 alors que le fil de discussion reste silencieux ?
L’erreur 502 vient de nginx, pas d’OpenTag. Elle 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 est en écoute sur 127.0.0.1:3050 pour GitHub et sur 127.0.0.1:3040 pour Slack. Exécutez ensuite opentag doctor pour vérifier les bindings et les executors.