Aller au contenu

Passerelles de clavardage

Une passerelle de clavardage fait d’un produit de clavardage externe — Slack aujourd’hui — un moyen de premier ordre d’entrer dans un espace de travail Control Center. Mentionnez le robot et un agent s’éveille ; répondez dans le fil Slack et la conversation se poursuit ; tapez une commande slash et un ticket est créé. En dessous, les mêmes espaces, transcriptions, tickets et attributions que ceux qu’utilise l’application de bureau. Le clavardage est une surface sur l’espace de travail, et non une copie de celui-ci.

Slack est le seul fournisseur livré. Il n’existe aucune implémentation de Discord, Telegram, Teams ou Matrix nulle part dans le produit. La fonctionnalité est écrite sans égard au fournisseur, à dessein — ajouter un deuxième produit, c’est une valeur d’énumération, un adaptateur et un greffon — mais aujourd’hui, « clavardage » veut dire Slack.

Deux surfaces la couvrent, et elles appartiennent à des personnes différentes. Relier un espace de travail à Slack et personnaliser le robot sont des actions d’administrateur sous Paramètres → Espace de travail → Général. Lier votre propre compte de clavardage à votre identité Control Center est une action de membre sous Paramètres → Espace de travail → Profil et identité.

Trois décisions façonnent toute la fonctionnalité : dans quelle direction la connexion s’établit, ce qui autorise un message qui arrive de l’extérieur et où le savoir propre au produit a le droit de vivre.

La passerelle n’écoute jamais le fournisseur. Slack est joint par Socket Mode : le serveur demande à Slack une URL de socket et établit la connexion vers l’extérieur, puis reçoit les mentions, les messages privés et les commandes slash par cette connexion.

C’est exactement là l’intérêt. Un ordinateur portatif derrière un NAT, un serveur domestique sans domaine, une machine qui passe d’un réseau à l’autre — tous peuvent faire tourner la passerelle sans port entrant, sans tunnel, sans certificat TLS et sans URL publique à permuter quand elle change. Il n’y a aucun point de terminaison webhook à protéger, parce qu’il n’y a aucun point de terminaison webhook. Lorsque Control Center compose la définition de l’application, il impose Socket Mode et retire toute URL de requête, car une application configurée pour les deux serait une application qui fonctionne à moitié.

Le prix à payer est une connexion à maintenir en vie, et c’est la tâche du serveur : la reconnexion est une temporisation exponentielle avec gigue plafonnée à 30 secondes, une rotation de socket amorcée par le fournisseur se reconnecte sans faire grimper cette temporisation, et un identifiant rejeté est définitif — réessayer indéfiniment un jeton révoqué ne rapporterait qu’une limite de débit, alors la passerelle s’arrête et attend de nouveaux jetons tout en rapportant l’erreur du fournisseur telle quelle dans les paramètres.

Un espace de travail sans application de clavardage ne coûte rien du tout : aucun socket, aucune minuterie, aucun trafic. Et le tout démarre après la bannière de disponibilité du serveur, parce qu’une application de clavardage lente à répondre ne doit jamais retarder le démarrage.

L’appartenance autorise, et non la possession du jeton

Section intitulée “L’appartenance autorise, et non la possession du jeton”

La personne qui a installé l’application n’est pas celle qui envoie le message. Chaque événement entrant est donc résolu en un principal avant que quoi que ce soit ne se produise, et il échoue en mode fermé avec une explication :

  • Un compte de clavardage non lié apprend comment le lier, dans le fil Slack où il a écrit.
  • Un compte lié dont l’utilisateur n’est pas membre de cet espace de travail est invité à demander une invitation à un administrateur.
  • Un membre dont le rôle est en lecture seule (viewer, guest) apprend que son rôle ne peut pas lancer de travail.

Aucun de ces cas ne va plus loin. Un message de clavardage n’est jamais attribué à quelqu’un qui ne l’a pas envoyé — c’est ce qui garde honnêtes la piste de vérification, les limites de débit par principal et les déclarants des tickets. Voir Multijoueur — identité, appartenance et présence pour le modèle d’appartenance sur lequel cela s’appuie.

Lier est une résolution d’identité, pas un octroi d’accès. L’appartenance est revérifiée à chaque événement, de sorte qu’une liaison appartenant à quelqu’un qui a été retiré depuis aboutit à un refus plutôt qu’à ses anciens privilèges. Deux voies établissent une liaison : un code à usage unique de courte durée que le membre tape dans le clavardage, ou une correspondance automatique sur le courriel vérifié du fournisseur — et la voie du courriel exige encore que l’utilisateur correspondant soit déjà membre, de sorte qu’un inconnu dont l’adresse se trouve par hasard sur le serveur ne se lie à rien.

Les codes sont volontairement gardés en mémoire, avec une durée de vie de 15 minutes. En conserver un créerait une ligne durable en forme d’identifiant pour quelque chose censé s’évaporer, et qu’un redémarrage du serveur invalide tous les codes en circulation est le comportement correct, pas une régression — le membre appuie de nouveau sur « Lier mon compte Slack ».

Un refus est répondu dans le fil Slack qu’a ouvert le message, comme une vraie réponse. Une réponse en fil marque d’un badge le message auquel elle répond, touche la seule personne qui en a besoin et garde malgré tout « vous n’êtes pas lié » hors du canal lui-même. Publier un message est aussi la seule capacité que tous les fournisseurs possèdent, donc l’explication ne dépend jamais d’une fonctionnalité que l’application pourrait ne pas avoir.

Une passerelle qui reflète dans les deux sens bouclera si rien ne distingue les deux directions. Chaque message que la passerelle fait entrer est estampillé de metadata['chat'] — fournisseur, équipe, conversation, fil, message et expéditeur. Le miroir sortant saute tout message qui porte cette estampille.

Cette règle unique est ce qui rend la mise en miroir bidirectionnelle sûre : un message humain tapé dans Control Center est relayé vers le fil lié (précédé du nom de l’auteur), tandis qu’un message venu du clavardage s’arrête là où il est entré.

Ce qui traverse vers l’extérieur est volontairement étroit. La réponse de l’agent est relayée comme du texte de clavardage ordinaire. Une courte réflexion peut apparaître sur la ligne Thinking… de la carte. La sortie des outils est un mur de diffs et reste dans Control Center, qui est aussi l’endroit vers lequel pointe une réponse tronquée.

Une carte rend compte du travail ; le texte reste le message

Section intitulée “Une carte rend compte du travail ; le texte reste le message”

Là où le fournisseur les affiche, un tour porte une carte de tâche, modifiée sur place aussi longtemps que le tour dure et posée au-dessus de la réponse qu’elle explique.

La carte est un plan regroupé, pas un paragraphe écrasé. Slack concatène details et output sur la même tâche, donc les détails d’une ligne sont envoyés une fois (la réflexion sur Thinking…) et output n’est jamais diffusé en continu. La préparation est une ligne dont le titre est remplacé (Working on it…, Cloning …). Le raisonnement ajoute une ligne Thinking…. Chaque outil a sa propre ligne (Read lib/main.dart). La réponse est du texte ordinaire sous la carte — la vue de plan de Slack la lit dans un ajout markdown ultérieur, et non dans un segment mélangé aux tâches. Rien d’autre : aucune sortie d’outil, et seulement un argument d’outil tiré d’une courte liste d’autorisation, parce que les arguments d’un outil inconnu peuvent être tout le corps d’un fichier et qu’une ligne, c’est une ligne.

La carte couvre aussi l’attente avant le tour. Une première mention crée la conversation, et sa création clone chaque dépôt dans lequel l’agent travaillera — ce qui, sur un vrai dépôt, représente des minutes pendant lesquelles on n’a encore rien demandé à l’agent et où le fil donne l’impression que le robot a ignoré la demande. La carte s’ouvre donc au moment où la mention est acceptée (Working on it…), puis cette ligne de préparation se met à jour avec les mêmes mots que ceux qu’affiche l’application de bureau — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… —, puis une ligne Thinking… dès que l’agent commence à raisonner, puis une ligne par outil. Une carte, un message, de « je vous ai entendu » jusqu’à la réponse en texte ordinaire en dessous. La préparation n’est signalée que là où quelqu’un dans le clavardage l’attend : une conversation en cours de préparation dans l’application, ou une conversation abandonnée reprise au démarrage du serveur, est préparée sans aucune carte.

Elle porte la seule chose que le clavardage ne peut pas donner au lecteur : un lien View in Control Center vers la conversation derrière la réponse. Les produits de clavardage n’acceptent que http(s) dans un lien et rien d’autre, alors le bouton pointe vers une petite page que ce serveur sert, qui redirige le navigateur vers le lien control-center:// de l’application de bureau. Cette page ne lit rien et ne résout rien — les identifiants dans son URL sont repris dans un lien profond et nulle part ailleurs —, et un serveur sans URL publique joignable produit simplement une carte sans bouton plutôt qu’une carte brisée.

Les cartes sont une capacité comme une autre. Un fournisseur qui n’en a pas se comporte exactement comme avant, en texte seul, et un fournisseur qui en refuse une au moment de l’appel garde son flux : la réponse l’emporte toujours sur la carte qui la décrit. Quand le streaming est complètement désactivé, la carte terminée voyage plutôt dans l’unique message publié. Un ticket créé reçoit le même traitement — la confirmation de /cc ticket est une carte terminée dont la raison d’être est le lien vers le ticket qui vient d’être créé.

Les produits de clavardage diffèrent par ce qu’ils peuvent afficher, alors la passerelle est écrite contre la surface la plus riche — une réponse qui grandit en direct dans un fil, un état transitoire « is thinking… », un fil titré, une réponse éphémère à une commande slash — et se dégrade une capacité à la fois. Un fournisseur qui n’en a aucune fonctionne quand même ; les réponses arrivent simplement en un seul message.

Annoncer une capacité n’est pas la promettre. Les réponses en streaming de Slack exigent une fonctionnalité d’un forfait payant, et Slack ne refuse qu’au moment de l’appel. Un refus marqué comme définitif est mémorisé pour toute la durée de vie de la connexion : la passerelle cesse de demander et publie des réponses entières à partir de ce moment, et les paramètres le disent clairement, parce que « mes réponses n’arrivent plus en streaming » devrait pouvoir se répondre sans lire un journal. Les cartes de tâche sont abandonnées de la même façon si Slack en refuse une.

Chaque limite appartient au fournisseur plutôt qu’au relais : le plafond de messages, le plafond de segments de flux et le fait qu’un flux ait besoin ou non d’une ancre de fil sont tous déclarés par l’adaptateur, et une longue réponse est scindée à une frontière de paragraphe, de ligne ou de mot, jamais au milieu d’un mot.

Tout ce qui a la forme d’un produit vit derrière un port. Le noyau générique connaît le markdown, les conversations de clavardage, les fils de clavardage et les membres ; il ne sait rien des enveloppes Slack, de mrkdwn ni des gateway intents. Trois pièces font que cela fonctionne :

  • L’adaptateur normalise un produit : son transport, ses appels d’API, son codec de texte et les particularités de ses enveloppes. Ceux de Slack sont instructifs — une mention de canal arrive deux fois (comme app_mention et comme message) et la plupart des sous-types de message sont du décor plutôt que de nouvelles requêtes. Les deux sont résolus dans l’adaptateur, de sorte que le noyau n’apprend jamais que Slack livre en double.
  • Le descripteur est la forme publique du fournisseur : quels identifiants il exige, quels sont leurs formats, quelles URL aident un utilisateur à les trouver, ce qu’il peut faire et si son application peut être créée ou modifiée d’ici. La carte des paramètres, la boîte de dialogue de connexion et la validation des arguments sont toutes générées à partir de lui — le client renvoie un fournisseur plus une table indexée par identifiant de champ.
  • Le greffon est la composition côté serveur : comment des identifiants deviennent une connexion vérifiée, comment un adaptateur est construit et comment l’application côté fournisseur est gérée.

Ajouter un produit, c’est donc une valeur d’énumération, un adaptateur et un greffon. Aucune nouvelle opération RPC, aucun changement de schéma, aucun changement de client — et le client reste honnête, parce qu’un champ dont il n’a jamais entendu parler s’affiche quand même avec le libellé que le serveur a fourni.

Deux détails empêchent l’abstraction de fuir vers le haut. Les étapes de gestion de l’application pour lesquelles un fournisseur n’a pas d’API (générer un jeton, installer l’application, réinstaller après un changement de permissions) sont renvoyées comme des étapes nommées avec leurs propres liens, de sorte que l’écran de fin de configuration les affiche sans savoir de quel fournisseur il s’agit. Et chaque instruction que la passerelle écrit nomme le fournisseur et la commande que l’application utilise réellement, de sorte qu’une commande renommée ou un deuxième fournisseur se lit correctement sans nouvelle chaîne.

Les jetons de clavardage d’un espace de travail se trouvent dans le répertoire propre de cet espace de travail, à côté de sa base de données — sous forme de JSON simple à l’emplacement <dataDir>/<workspaceId>/chat_credentials/<provider>.json, pas dans la base de données et délibérément pas dans le trousseau du système d’exploitation. Deux conséquences sont faciles à perdre de vue plus tard : supprimer un espace de travail dissocie le répertoire et ses jetons partent avec lui, et exporter ou sauvegarder un espace de travail copie workspace.db, de sorte qu’un espace de travail remis à quelqu’un d’autre ne transporte pas de jetons de robot actifs à l’intérieur. Le fichier hérite de la frontière de confiance du système de fichiers de l’hôte du serveur — tout ce qui a un accès en lecture au répertoire de données peut lire un jeton de robot, ce qui est la même frontière derrière laquelle se trouvent les bases de données.

Un fichier par fournisseur, écrit de façon atomique et réservé au propriétaire : une application révoquée est supprimée en dissociant un seul fichier, et un fichier corrompu coûte un fournisseur plutôt que tous. Les identifiants entrent au moment de l’opération de connexion et ne ressortent jamais — les lectures d’état ne répondent qu’avec les métadonnées de la connexion.

  • Paramètres structurels de l’application. Le transport, les portées et les abonnements aux événements sont imposés, pas exposés. Une application qu’on peut configurer jusqu’au silence depuis un écran de paramètres est un ticket de soutien, pas une fonctionnalité.
  • Conversation et commandes slash. La surface Slack, ce sont les messages, les réponses en fil et les commandes slash.
  • Une deuxième source de vérité. Le clavardage n’est pas un endroit où le travail est stocké. Le fil Slack est un espace, la réponse est une transcription, le ticket est un ticket — tous dans l’espace de travail, joignables depuis chaque client.