Aller au contenu

Le relais de la passerelle

L'interface HTTP sur laquelle l'éditeur s'engage : adressage, transformations, flux, refus et bornes de transport.

Cette page décrit ce qu’un programme tiers peut écrire contre la passerelle. C’est la partie contractuelle de son interface : ce qui y figure ne change pas sans que le changement soit annoncé.

Ce qui relève de l’exploitation d’un déploiement (variables d’environnement, rôles, tables) vit sur trois autres pages, marquées « interne ».

Trois surfaces la composent : le relais, par lequel on appelle un modèle, la documentation hors ligne, que la passerelle sert quand l’exploitant l’a installée, et le service de questions par lequel un agent l’interroge.

ALL /:endpoint/*

La route par laquelle un programme tiers appelle un modèle. Le premier segment du chemin nomme un endpoint déclaré par l’exploitant ; tout ce qui suit (chemin et chaîne de requête) est concaténé derrière l’adresse de base du fournisseur. Toutes les méthodes HTTP y passent. C’est un attrape-tout : il prend tout premier segment que les routes montées avant lui n’ont pas réservé.

La passerelle concatène : l’adresse de base du fournisseur, telle que l’exploitant l’a déclarée, suivie de tout ce qui reste du chemin après le premier segment, chaîne de requête comprise.

appel https://passerelle.interne/sonnet/v1/messages?beta=1
endpoint sonnet
relayé <adresse de base du fournisseur>/v1/messages?beta=1

Trois conséquences que l’adressage impose :

  • le premier segment est retiré par sa longueur, donc lui seul. Un chemin qui répète le nom de l’endpoint plus loin le conserve ;
  • le chemin part tel que l’appelant l’a écrit, sans être remis en forme ;
  • / et // ne sont pas des adresses de la passerelle. Elles ne portent aucun premier segment, la route ne les reconnaît pas, et la réponse est celle du cadre HTTP sous-jacent : du texte, pas l’enveloppe JSON décrite plus bas.

Deux en-têtes, et aucun autre :

Authorization: Bearer <jeton>
x-api-key: <clé>

Les deux sont retirés de la requête avant qu’elle parte chez le fournisseur : la preuve présentée à la passerelle ne voyage pas au-delà d’elle.

Ces en-têtes sont retirés :

  • host
  • accept-encoding
  • authorization
  • x-api-key
  • content-length

À la place de la preuve retirée, la passerelle pose la clé du fournisseur, dans l’en-tête que l’exploitant a déclaré pour lui.

Le corps est relayé tel quel, à une exception près : s’il s’analyse comme du JSON, son champ model est écrasé par le modèle de l’endpoint visé. Un corps qui ne s’analyse pas comme du JSON part inchangé.

La passerelle ne valide ni le schéma du corps, ni son type de contenu, ni sa taille.

Ces en-têtes sont retirés :

  • content-encoding
  • transfer-encoding

Le statut, les autres en-têtes et le corps du fournisseur sont relayés tels quels.

La réponse est relayée au fil de l’eau dès que l’une des deux conditions est remplie :

  • l’appelant a demandé "stream": true dans un corps JSON ;
  • le fournisseur répond avec un type de contenu text/event-stream.

L’une suffit : un fournisseur qui omet le type de contenu ne doit pas ramener l’échange à une réponse mise en tampon.

En flux, ces en-têtes disparaissent en plus :

  • content-length

Ils portent tous la même enveloppe, { "error": "<phrase>" }, sauf le 404 d’endpoint inconnu, qui porte un second champ.

Un refus prononcé par le fournisseur n’est pas dans cette table : il est relayé tel quel, avec son propre corps. Ceux de la documentation hors ligne ont leur propre table, plus bas.

CodeQuandCorps
401Aucune preuve d’identité utilisable n’accompagne la requête : ni Authorization: Bearer, ni x-api-key, ou bien l’un des deux est vide.{ "error": "Missing credentials. Set apiKey in your Lemniscate config." }
401Une preuve est présentée et refusée. La passerelle ne dit pas pourquoi : le motif part au journal des décisions, parce que dire à l’appelant ce qui n’a pas convenu l’aide à deviner ce qui aurait convenu.{ "error": "Unauthorized." }
403Le sujet figure sur la liste de révocation de l’instance, et sa révocation n’a pas été levée. Le motif et l’auteur de la révocation restent au journal.{ "error": "Your access to this instance has been revoked. Contact the operator of this deployment." }
403L’appel nomme, par l’en-tête x-lemniscate-session, une session d’agent que l’administration a révoquée, ou un sous-agent d’une telle session. Seule cette session est refusée : la même personne, sous une autre session ou sans en nommer, est servie. La réponse porte l’instant de la révocation et l’en-tête x-lemniscate-session-standing: revoked ; l’auteur et le motif restent au journal.{ "error": "This agent session was revoked by your organization's administration.", "code": "agent-session-revoked", "revokedAt": "<instant ISO>" }
404Le premier segment du chemin ne nomme aucun endpoint déclaré. C’est le seul refus dont le corps porte un second champ : available liste les noms d’endpoints existants.{ "error": "Unknown endpoint: <nom>", "available": ["<nom>", "…"] }
403L’endpoint existe mais l’exploitant l’a désactivé.{ "error": "Endpoint \"<nom>\" is disabled." }
403La licence du déploiement ne couvre pas cette requête. Trois motifs la conduisent ici, et le corps rendu dit lequel : le nombre de sièges licenciés est atteint pour le mois en cours, la capacité demandée n’est pas incluse dans la licence, ou la licence a expiré et sa période de grâce est écoulée. Le contrôle est placé après la résolution de l’endpoint (il faut son nom pour juger de la capacité) et avant l’autorisation : un déploiement sans droit de licence n’a pas à consulter la politique de rôles. Ce refus n’existe que sur le profil on-premise : en serverless, les limites commerciales passent par la facturation.{ "error": "The licensed seat limit for this deployment has been reached for the current month. Contact the operator to increase the seat count." } — ou { "error": "The capability \"<nom>\" is not included in this deployment's license." } — ou { "error": "The deployment license has expired and the grace period has elapsed. Contact the operator to renew the license." }
403L’identité est établie, et aucun rôle porté par le sujet ne donne le droit d’appeler cet endpoint. C’est aussi l’état de tout le monde sur une instance neuve : sans rôle attribué, l’ensemble des permissions est vide.{ "error": "Your identity is recognised, but no role you hold carries the right to call this endpoint." }
403Aucun plan n’est affecté au compte. Distinct du plafond atteint : aucun paiement ne s’impute à un plan qui n’existe pas, et le mois suivant n’y change rien. Sans objet en déploiement client, où il n’y a rien à revendre.{ "error": "No plan assigned. Ask your administrator to assign one." }
402Le plafond mensuel du plan est atteint. Sans objet en déploiement client.{ "error": "Plan limit reached" }
503La passerelle n’a pas pu lire ce dont elle a besoin pour décider : la liste de révocation des sujets, celle des sessions d’agent, ou les rôles et la politique en vigueur. Elle refuse plutôt que de supposer : une politique installée pour restreindre, silencieusement remplacée par la plus large, serait une ouverture déguisée en tolérance de panne.{ "error": "Authorization decision could not be made, so the request was refused." }
503L’accord était acquis, et le journal des décisions n’a pas pu l’enregistrer. Un accord que le journal n’a pas pris n’est pas un accord.{ "error": "Authorization decision could not be recorded, so the request was refused." }
500L’endpoint désigne une variable d’environnement pour la clé du fournisseur, et cette variable est absente de l’environnement de la passerelle. C’est une panne de configuration côté exploitant, pas un défaut de droit.{ "error": "Missing env variable: <nom de la variable>" }
502La passerelle n’a pas réussi à joindre le fournisseur : proxy d’entreprise injoignable, autorité de certification interne absente, certificat client refusé. 502 et non 500, parce que la panne est en aval.{ "error": "<le message de la couche de sortie réseau, qui nomme la variable à poser>" }
503La base de facturation n’a pas répondu au moment de vérifier le plan et le plafond du compte. La passerelle refuse plutôt que de servir un appel qu’elle sait ne pas pouvoir compter, et elle refuse avant d’appeler le fournisseur, donc rien n’est payé. Ce refus n’existe que sur le profil serverless : en on-premise il n’y a rien à facturer.{ "error": "Usage cannot be metered right now, so the request was refused rather than served unbilled. Nothing was sent upstream." }
503Le fournisseur a répondu, et la ligne de facturation de cette réponse n’a pas pu être écrite. La réponse est retenue plutôt que servie gratuitement : l’appel au fournisseur, lui, est déjà payé. Ne concerne que les réponses non diffusées : sur un flux, l’appelant a déjà tout reçu quand l’écriture échoue.{ "error": "This answer could not be metered, so it was not served. Nothing was billed for it either." }

Ce que l’appelant doit prévoir, et surtout ce sur quoi personne ne le protège :

BorneÉtat
Plafond de durée d’une requêteIl existe. 1800 secondes par défaut, réglable par l’exploitant.
Limite de débitAucune.
Limite de taille du corpsAucune.
Requête préalable CORSNon traitée.
Identifiant de corrélation renvoyéAucun. La passerelle lit x-request-id pour son journal, elle ne le renvoie pas.

Ces absences sont des informations contractuelles au même titre qu’un code d’erreur : qui construit sur cette passerelle doit savoir que rien ne le protège d’un appel trop gros ou trop fréquent.

La passerelle sert la documentation du produit quand l’exploitant l’a installée. C’est ce qui la rend lisible sur un réseau fermé, où le site public est injoignable par construction.

Deux adresses, et un seul segment de chemin réservé :

RouteCe qu’elle fait
ALL /vLa racine du segment que la documentation réserve. Elle redirige vers la version installée, ce qui donne une adresse d’entrée qui ne change pas quand le déploiement est mis à jour. Lecture seule et sans authentification, comme tout ce qui vit sous ce segment.
ALL /v/*Les pages, feuilles de style, index de recherche et fichiers pour agents de l’archive de documentation que l’exploitant a dépliée (D-33). Une adresse qui porte le numéro de la version installée sert le fichier ; une adresse qui n’en porte pas redirige vers la version installée ; un autre numéro est refusé. Lecture seule, sans authentification, et jamais de liste de répertoire.

Trois propriétés à connaître avant d’écrire contre ces adresses :

  • Aucune authentification n’est demandée, et une preuve présentée est sans effet. Les mêmes pages sont publiées sans compte sur le site public, et un navigateur ne peut pas présenter les en-têtes de preuve décrits plus haut.
  • Le premier segment est réservé. Un endpoint de relais qui porterait ce nom serait masqué par ces deux routes, qui sont montées avant le relais.
  • L’adresse sans numéro de version est stable. Une adresse qui ne porte pas de numéro redirige vers la version installée : elle reste juste après une mise à jour du déploiement, ce qui en fait la valeur à configurer plutôt que l’adresse portant le numéro.

Ce que la passerelle refuse sous ces adresses :

CodeQuandCorps
501Aucune archive de documentation n’est déclarée sur ce déploiement. Le segment reste réservé quand même : un segment qui n’apparaîtrait qu’une fois l’archive dépliée laisserait marcher un point d’appel qui porte ce nom, jusqu’au jour où l’exploitant l’installe.{ "error": "This gateway serves no documentation: no directory is configured (LEMNISCATE_DOCS_DIR). Ask the operator of this deployment to unpack the documentation archive shipped with the server release and point the gateway at it." }
405Une méthode autre que la lecture est employée sous le segment de documentation.{ "error": "Documentation is read-only: only GET and HEAD are served under this path." }
404L’adresse vise un numéro de version qui n’est pas celui installé. Elle n’est pas redirigée vers celui qui l’est : rendre une autre version que celle demandée serait un mensonge invisible, puisque les pages se ressemblent.{ "error": "This gateway serves documentation version <numéro installé>, at <base>. Version <numéro demandé> is not installed here." }
400Le chemin cherche à sortir de l’archive : un segment qui remonte, une barre encodée, un encodage invalide.{ "error": "Not a documentation path." }
404Aucun fichier ne correspond à l’adresse sous la version installée, ou le répertoire visé n’a pas de page d’accueil : un répertoire ne rend jamais la liste de son contenu.{ "error": "No such documentation page." }

Rien n’est servi tant que l’exploitant n’a pas posé LEMNISCATE_DOCS_DIR. Ce réglage est décrit sur la page des variables d’environnement.

Les deux fichiers pour machines de l’archive (llms.txt et llms-full.txt) se lisent directement, sans numéro de version : /v/llms.txt et /v/llms-full.txt. Ils rendent le corpus entier.

Pour une question ciblée, deux adresses de plus, sous le même segment réservé :

RouteCe qu’elle fait
ALL /v/_searchUne question, et les passages de la documentation installée qui la couvrent, avec l’adresse de chaque page sur cette passerelle. Aucun modèle de langage n’est appelé : le service rend du texte qu’il a, jamais du texte qu’il fabrique, et quand rien ne couvre la question il rend une liste vide en le disant. Lecture seule et sans authentification, comme tout ce qui vit sous ce segment.
ALL /v/_mcpLe protocole par lequel un agent interroge la documentation installée : JSON-RPC 2.0, sans état, sans flux d’événements, un seul outil de recherche. Il ne fait que traduire la route de recherche ci-dessous, et rend la même donnée. Aucun modèle de langage n’est appelé. POST seulement ; une lecture reçoit un refus, ce serveur n’ouvrant aucun canal d’événements.

Aucun modèle de langage n’est appelé, et c’est la propriété à connaître avant d’écrire contre ces adresses. Le service rend des passages de la documentation, jamais une réponse rédigée. Trois conséquences :

  • ce qu’il rend est du texte publié, vérifiable en suivant l’adresse jointe à chaque passage ;
  • il ne coûte rien par question, et rien ne sort de la machine qui l’héberge ;
  • quand rien ne couvre la question, il rend une liste vide et le dit. Le champ answered vaut alors false et un champ note porte la phrase. Une liste vide est un fait sur la documentation, pas une panne du service.
GET /v/_search?q=comment+plafonner+la+depense+d+un+client&limit=3
{ "version": "1.0.0", "base": "/v/1.0.0/",
"query": "comment plafonner la depense d un client",
"terms": ["plafonner", "depense", "client"],
"answered": true,
"results": [ { "page": "Plafonner la dépense d'un client", "section": null,
"url": "/v/1.0.0/guides/plafonner-la-depense/",
"excerpt": "…", "score": 11.4, "coverage": 1 } ] }

Le serveur MCP expose le même service sous le protocole que les clients d’agents savent parler. Il est sans état : chaque message est indépendant, aucune session n’est ouverte, et une lecture est refusée puisqu’il n’a aucun événement à pousser. Il implémente initialize, ping, tools/list et tools/call, et offre un seul outil, search_documentation, dont la donnée structurée est exactement la réponse ci-dessus.

Ce que ces deux adresses refusent :

CodeQuandCorps
501Aucune archive de documentation n’est déclarée sur ce déploiement, donc il n’y a rien à interroger. Le chemin reste monté quand même, pour la même raison que le reste du segment : une adresse qui n’apparaîtrait qu’une fois l’archive dépliée cacherait la réservation jusqu’à ce jour-là.{ "error": "This gateway serves no documentation, so it cannot answer questions about it (LEMNISCATE_DOCS_DIR is not set). Ask the operator of this deployment to unpack the documentation archive shipped with the server release and point the gateway at it." }
501L’archive dépliée ne porte pas llms-full.txt, le corpus pour machines dont l’index est tiré. Les pages continuent d’être servies : seule la recherche est indisponible. La passerelle ne refuse pas de démarrer pour autant : l’éteindre entière, donc le relais vers les modèles, pour un fichier de commodité serait hors de proportion.{ "error": "The documentation installed here carries no llms-full.txt, which is the machine-readable corpus this search reads. Pages are still served; only search is unavailable. That file sits at the root of every documentation archive, so the most likely cause is a partly unpacked archive." }
405Une méthode autre que la lecture est employée sur la recherche.{ "error": "Documentation search is read-only: only GET and HEAD are served at this path." }
400Aucune question n’accompagne l’appel, ou elle est vide. Le refus nomme le paramètre et en donne un exemple complet, plutôt que de rendre une liste vide qu’un appelant prendrait pour « la documentation ne sait pas ».{ "error": "Ask a question with the \"q\" query parameter, for example /v/_search?q=how+do+I+disable+telemetry. Add \"limit\" to ask for fewer or more passages." }
405Une lecture est tentée sur le serveur MCP. Le protocole ouvre son canal d’événements par une lecture ; ce serveur n’a rien à pousser et le refuse, plutôt que d’ouvrir un canal muet qu’un client attendrait indéfiniment.{ "error": "This MCP endpoint answers POST only. It is stateless and opens no event stream. To read the same answers without MCP, use /v/_search?q=…" }
400Le corps de la requête ne s’analyse pas comme du JSON. Une erreur de protocole sur un message bien formé, elle, se rend en JSON-RPC avec un statut 200 : ce n’est pas le transport qui a échoué.{ "error": "This MCP endpoint expects a single JSON-RPC 2.0 message as the request body." }

Il n’existe pas de description OpenAPI, et c’est délibéré

Section intitulée « Il n’existe pas de description OpenAPI, et c’est délibéré »

Le relais ne définit pas d’interface à lui : il concatène l’adresse de l’appelant derrière celle du fournisseur. Un fichier OpenAPI ne pourrait donc décrire que la route fourre-tout ci-dessus (dont aucun client utile ne se génère) ou bien recopier l’interface du fournisseur, qui n’est pas la nôtre et se périmerait ici à chaque évolution chez lui.

Il annoncerait surtout une forme de requête que la passerelle ne vérifie pas : elle ne valide ni schéma, ni type de contenu, ni taille. Ce qu’il faut réellement savoir pour écrire contre elle est la règle de concaténation et la table des refus, et c’est ce que cette page donne.