Aller au contenu principal
Version: Actuelle

Auth : identité et profils

Aujourd'hui, une room peut être identifiée par une donnée que la personne tape, et cette donnée peut être prouvée par un code à usage unique que le tenant envoie. Il n'y a pas encore de profils.

Identifier une room : identifies: true​

Un champ de formulaire marqué identifies: true identifie la room, sous le nom du champ, dès que la garde accepte le formulaire. Cela vaut pour le formulaire d'une skill (sa garde form, dans select.guards) et pour celui du submit guard (submit_guard.form).

    select:
guards:
- form:
address: {type: text, required: true, label: "Adresse de livraison"}
phone: {type: phone, required: true, label: "Téléphone", identifies: true}

Ensuite :

  • La room ne le redemande pas. Les formulaires suivants de la room — la même carte montrée à nouveau, un autre formulaire avec un champ phone, le submit guard — sont publiés sans ce champ, et Core y répond lui-même. Un formulaire qui n'a plus rien à demander n'est plus montré.
  • Le submit l'envoie. Un champ d'item se lit avec $item.fields.phone ; un champ du submit guard avec $submit.phone (voir Le submit).
  • Une nouvelle saisie remplace l'ancienne. Si la personne tape un autre numéro, c'est lui que la room garde.
  • Le modèle sait que la room a un téléphone, jamais lequel. Le résumé de la room lui donne le nom de la donnée, pour qu'il ne la redemande pas.

C'est une donnée déclarée : tapée, pas prouvée. Elle reste avec cette room et n'en retrouve jamais une autre ; n'importe qui peut taper le numéro d'un autre.

Seul un champ de type text, email ou phone peut identifier une room (les types). Au chargement, Core refuse identifies avec une autre valeur que true, sur un champ d'un autre type, et un même nom déclaré par deux formulaires avec des types différents : la room remplirait l'un avec la réponse de l'autre.

Le déploiement doit fournir CONVOY_IDENTITY_KEY à l'agent et au serveur MCP du tenant, pour que les deux portes connaissent la même donnée. Les scripts de provisionnement la créent une fois par tenant, et le provisioner dérive celle de chaque tenant qu'il crée de la clé du déploiement (une par tenant) ; avec une base de rooms et sans elle, un tenant qui identifie ses rooms refuse de démarrer. Sans base de rooms (en développement), chaque porte garde ses rooms en mémoire, et ce qu'elles savent aussi.

Deux clés différentes ne font échouer aucun démarrage : chaque porte a raison seule, mais la même adresse devient deux personnes. Chaque porte publie donc l'empreinte de sa clé, jamais la clé : l'agent dans /status, MCP dans /health (identity_key). La sonde scripts/readiness/identity_key.py compare les deux et échoue si elles diffèrent.

Prouver une donnée : un code à usage unique​

La station déclare ses fournisseurs d'identité dans station.identity.providers. Un seul mécanisme existe aujourd'hui : otp, un code à usage unique que le tenant envoie et vérifie avec ses propres opérations (via: tenant).

apis:
- name: identite
openapi_url: https://api.example.com/openapi.json
operations:
- {id: sendVerificationCode, access: write}
- {id: checkVerificationCode, access: write}

station:
identity:
providers:
email:
mechanism: otp
verifies: customer_email # le nom de la donnée prouvée
via: tenant
send: sendVerificationCode # envoie un code à la valeur
check: checkVerificationCode # dit si le code tapé est le bon
  • Ce que reçoivent les opérations. send reçoit la valeur sous le nom de la donnée (customer_email) ; check reçoit la valeur et code. Core lit dans le schéma de chaque opération où elle les prend, en paramètre ou dans son corps JSON ; mieux vaut le corps, car une adresse et un code n'ont rien à faire dans une URL que les journaux gardent. Une opération qui ne prend pas ces noms est refusée au chargement.
  • Seul un oui explicite compte. check répond {"verified": true} (ou true). Toute autre réponse — verified: false, pas de verdict, une erreur — veut dire non.
  • Le modèle ne mène jamais la preuve. Il n'a que sign_in. Dans le widget et la PWA, un cadre demande la donnée et le code ; dans Claude et ChatGPT, c'est la fenêtre de connexion de l'hôte. Sans valeur tapée, le code part à celle que la room a déjà déclarée. Une valeur tapée pour recevoir un code est retrouvée par la vérification.
  • verifies nomme un champ qui identifie la room (identifies: true) : la room tient ainsi la valeur, et le code part sans que le modèle la répète.
  • Une seule écriture par type. Avant d'être déclarée, envoyée au tenant dans le submit, ou prouvée, une valeur est écrite d'une seule façon, selon le type du champ : email en minuscules, phone au format international (+33…). Un numéro national (06…) n'est pas deviné : le tenant le reçoit tel quel, et aucun code ne le prouve.
  • Un code juste rend la donnée vérifiée, et la room appartient à la personne. Son propriétaire est un identifiant opaque tiré de la valeur, jamais la valeur. La même personne peut prouver une autre donnée dans sa room (le téléphone après l'e-mail) ; une autre valeur pour une donnée déjà prouvée, ou une room réclamée sans rien de prouvé dedans, appartient à quelqu'un d'autre. Une preuve connecte toujours la personne qui la fait. Un appel sans clé de navigateur (un script, un ancien bundle) est refusé avant tout envoi de code : il ne connecterait personne.
  • Le modèle apprend quelle donnée est prouvée, jamais sa valeur. Le résumé de la room dit « Proved in this room: customer_email ».
  • Convoy ne voit jamais le code envoyé. Le tenant le garde, en limite la durée et le nombre d'essais ; Core lui passe ce que la personne a tapé et enregistre la réponse.
  • Core compte aussi les faux codes, quoi que fasse le tenant. Après cinq faux codes pour une même valeur, il n'interroge plus le tenant pendant un quart d'heure ; un nouveau code envoyé remet le compte à zéro. Il n'envoie pas plus de cinq codes par heure à une même valeur, et ce compte-là ne se remet pas à zéro. Les comptes vivent dans la base des rooms : l'agent et MCP le partagent, et un redémarrage ne l'efface pas. Il est tenu sous l'empreinte de la valeur, jamais la valeur.

Au chargement, Core refuse un autre mécanisme qu'otp, un autre via que tenant, un send ou un check absent, qu'aucune API ne déclare ou qui ne prend pas ces noms, un verifies qu'aucun champ identifies: true ne porte, et une donnée qu'un formulaire déclare comme un nombre. Une station qui prouve une donnée a besoin de CONVOY_IDENTITY_KEY, comme celle qui en déclare une.

Qui est connecté : le titulaire​

Une preuve connecte la personne qui l'a faite, sur cette surface, pas la room. Dans le widget et la PWA, le navigateur garde une clé à lui (convoy-holder), que Core ne stocke que hachée. Un code juste lie ce navigateur à la personne :

  • le code d'une room ne donne aucun droit. Quelqu'un qui ouvre avec son code une room qui n'est à personne voit la sélection, mais il n'est pas connecté pour autant. Une room qui a un propriétaire ne s'ouvre qu'à lui, connecté (Contexte) ;
  • la personne reste connectée d'une visite à l'autre, sur ce navigateur. Une room qu'elle ouvre et qui n'est à personne devient la sienne ; celle de quelqu'un d'autre le reste ;
  • ses rooms la suivent : connectée, une personne qui arrive sans code de room (un nouvel appareil, Claude, ChatGPT) retrouve la dernière room qu'elle a utilisée, où que ce soit. Le menu ⋮ et l'outil room_list listent ses autres rooms, par ce dont elles parlent, jamais par ce qui y a été dit. Lire une room, ou l'ouvrir avec son code sans en être le propriétaire, ne change pas celle où elle revient ;
  • une connexion inutilisée prend fin : après station.identity.idle_minutes sans usage (une semaine par défaut, comptée depuis le dernier usage), la personne se reconnecte. Se déconnecter (POST /api/identity/sign-out) y met fin tout de suite, sur le serveur. C'est ce qui protège un ordinateur partagé ;
  • le modèle sait si la personne est connectée, par des noms seulement : « Signed in on this surface » ou « Not signed in » ;
  • la conversation s'ouvre sur une invitation à se connecter, dans le chat (le widget, la PWA) ou dans le cadre de l'ouverture de la room (Claude, ChatGPT) ;
  • dans le widget, les étapes s'affichent dans un cadre : la donnée, le code, puis « Vous êtes connecté·e », dans la conversation : depuis l'invitation, ou quand la personne le demande et que le modèle appelle sign_in. Une fois connectée, « ✓ Connecté·e » s'affiche sous la zone de saisie ;
  • le menu ⋮ de l'en-tête, dans le widget et la PWA, propose Se connecter (les étapes s'ouvrent dans la conversation) ou Se déconnecter, puis Commencer une nouvelle conversation, la seule façon de créer une room. Une déconnexion qui n'a pas abouti est dite, jamais montrée comme faite ;
  • submit_guard.submitted_by: [{who: owner}] réserve le submit au propriétaire de la room, connecté : le code de la room ne suffit plus. Le panneau de la sélection fait se connecter la personne avant le bouton ; sur Claude et ChatGPT, le submit demande la connexion de l'hôte. L'adresse et le code vont du cadre à Core : le modèle ne les voit jamais. Une donnée que la room tient déjà est proposée sans être affichée (« Utiliser celle déjà donnée »). Un refus du tenant s'affiche avec ses propres mots. Recommandé pour toute station qui passe des commandes : sans connexion, rien ne distingue la cliente sur un autre appareil d'un inconnu qui tient le code. Si la station sait connecter sans l'exiger, la sélection invite à se connecter avant de finaliser, sans y obliger ;

Sur Claude et ChatGPT, la connexion passe par l'hôte. Dès que la station déclare un fournisseur, le serveur MCP du tenant fait aussi serveur d'autorisation (OAuth 2.1, la méthode de l'autorisation MCP que suivent les deux hôtes). Le connecteur s'installe sans connexion. Quand la personne demande à se connecter, le modèle appelle sign_in : Claude ou ChatGPT ouvre sa fenêtre de connexion, qui mène à la page du tenant sur ce serveur, avec les mêmes étapes (la donnée, le code). L'hôte garde ensuite le jeton : la personne reste connectée d'une conversation à l'autre. Sur Claude, ouvrir sans connexion la room de quelqu'un, avec n'importe quel outil, ouvre aussi cette fenêtre : une room possédée ne s'ouvre qu'à sa propriétaire.

Sur Grok, la connexion se fait à l'ajout du connecteur. Grok ne lance la connexion qu'à ce moment-là : un outil qui la demande plus tard lui paraît en panne. Sur une station qui connecte les personnes, Grok demande donc la connexion dès l'ajout, et ne s'y utilise pas sans connexion. Un connecteur ajouté avant ce réglage se supprime et se rajoute.

Seuls les hôtes connus s'inscrivent. Un hôte s'inscrit auprès du serveur d'autorisation avant de connecter quelqu'un, en donnant l'adresse où renvoyer la personne. Elle doit être en https sur un hôte que la station sert, exactement (claude.ai.evil.com n'est pas claude.ai), ou sur la machine de la personne pour une application installée. Par défaut : claude.ai, claude.com, chatgpt.com, grok.com. Une station peut donner sa propre liste dans station.identity.hosts, qui remplace celle-ci ; un nom avec un schéma, un chemin ou un port y est refusé au chargement.

  • Le jeton ne vaut que pour les outils de ce serveur ; il porte l'identifiant opaque de la personne, jamais l'e-mail ; Core ne garde que son empreinte, et il ne part jamais vers l'API du tenant.
  • Le modèle n'a que sign_in ; il ne voit ni la donnée ni le code.
  • Le serveur se décrit à l'adresse par laquelle on l'appelle ; MCP_PUBLIC_URL la fixe quand il est derrière un tunnel ou un proxy qui ne la transmet pas.

Voir le parcours d'authentification de la cible.

Les skills réservées, et la clé de la personne​

Une skill peut être réservée à une personne connectée : authenticated: true. Core ne la lance pas pour quelqu'un qui ne l'est pas, et n'appelle même pas le tenant. Chaque surface demande alors sa connexion : les étapes dans la conversation (widget, PWA), la fenêtre de l'hôte (Claude, ChatGPT). Une fois la personne connectée, la skill est relancée. Le modèle l'apprend dans la description de la skill ; il ne demande jamais de code lui-même.

Une fois connectée, la personne est servie en son nom. Si la vérification du tenant répond avec une clé pour cette personne, Core la garde et l'envoie sur les appels faits pour elle : l'API du tenant répond alors pour elle, avec son périmètre et ses refus. C'est le tenant qui décide ce que chacun voit, pas Convoy.

apis:
- name: lecture
openapi_url: https://api.example.com/openapi.json
operations:
- {id: listTeams, access: read}
- {id: getTimesheets, access: read}
- {id: sendVerificationCode, access: write}
- {id: checkVerificationCode, access: write}
auth:
type: api_key # la clé de la station : ce que tout le monde peut demander
key_name: X-Api-Key
key_location: header
key_env: EXAMPLE_API_KEY
person: {provider: email} # + la clé de la personne connectée (Authorization: Bearer …)

skills:
- name: feuilles
invoke: {method: getTimesheets}
authenticated: true # seulement pour une personne connectée

station:
identity:
providers:
email:
mechanism: otp
verifies: email
via: tenant
send: sendVerificationCode
check: checkVerificationCode
credential: access_token # le champ où `check` rend la clé de la personne
  • Où elle vit. Avec la connexion de la personne (le titulaire), chiffrée en base. Jamais dans une room, un événement, un journal, ce que lit le modèle, ni une réponse à la surface.
  • Quand elle part. Seulement sur les appels que Core fait pour cette personne : ses skills, son submit. person.header et person.scheme changent l'en-tête (Authorization, Bearer).
  • Quand le tenant ne l'accepte plus. Une réponse 401 à un appel qui la portait déconnecte la personne, et la surface lui redemande de se connecter. Un 401 à un appel qui ne la portait pas vient de la clé de la station, et ne déconnecte personne.
  • Une donnée qu'aucun formulaire ne demande (un tenant qui ne vend rien) sert seulement à se connecter : la personne la tape dans les étapes de connexion. Le type du fournisseur (text, email ou phone) dit alors comment la demander et l'écrire ; un formulaire qui la déclare doit avoir le même.
  • Au chargement, Core refuse un auth.person qui nomme un fournisseur sans credential, et une skill réservée sur une station sans fournisseur.

La démo Shiftly le montre : camille@example.com voit ses deux équipes, anta@example.com, la manager, les quatre ; les codes s'affichent sur /demo/inbox.

Ce qui n'existe pas encore​

  • Pas de SSO, pas de login du tenant, pas de code envoyé par Convoy lui-même (via: convoy).
  • Pas de connexion silencieuse depuis le SSO d'une entreprise (« Enterprise Managed Auth » de Claude), ni de Client ID Metadata Documents : les hôtes s'inscrivent dynamiquement.
  • Chaque porte propose tous les fournisseurs de la station : un portail ne choisit pas encore le sien.
  • Les autres rooms d'un propriétaire ne lui sont pas encore rendues. Sa connexion n'expire pas d'elle-même, et il n'y a pas encore de « Se déconnecter ».
  • Sans credential, la clé d'une API est celle de la station : pour l'outil du tenant, tout le monde est la même personne (Sources).
  • Aucune règle ne distingue un client d'un autre : pas de profils.

La cible ajoute les fournisseurs d'identité, une room réclamée par son propriétaire vérifié, et les profils : Identité, Profils.