Aller au contenu
Bêta

L’autorisation

Un jeton qui ne porte pas ses droits

Un client tiers agit sous le mandat d’un compte, jamais de son propre chef. Le jeton qu’il présente ne porte aucun droit : ses scopes et son audience sont ceux de la délégation dont il dérive, lus à deux sauts. Un jeton plus large que sa délégation n’est donc pas interdit — il n’y a pas de ligne à élargir.

Un jeton, un code ou un vérificateur publié serait le secret lui-même. Ces trois listes sont vides et le resteront : aucune des trois faces n’en lira jamais un.

Les sept droits, dérivés du modèle

« SCOPES » de scripts/lib/identite.mjs, relevé dans le source et non recopié. Le contrôle porte dans les deux sens : un scope qui existe sans être annoncé échoue, et un scope annoncé qui n'existe pas échoue.

six délivrables sur sept.
DroitCe qu’il permetDélivré ici
lire lire le corpus public, les kits, le radar et les propositions oui
calculer demander un calcul sur ce qui est déjà lisible — un calcul est une lecture oui
produire_brouillon produire un brouillon qui ne publie rien oui
suivre suivre et cesser de suivre, pour le compte qui a délégué oui
contribuer préparer et confirmer une contribution : proposition, faille, amendement, argument, synthèse oui
voter transmettre le vote du compte — jamais en créer un second, le cap borne la voix à une par tour oui
moderer prononcer une décision de modération ou trancher un appel non

docs/PHASE-2-AGENT-FIRST.md esquisse quinze noms en forme OAuth — « corpus:read », « votes:prepare », « action:confirm »… Le modèle livré en porte sept. Annoncer quinze noms qu'aucune délégation ne peut porter serait annoncer un droit que la base refuse : « accorderDelegation » lève « Scope inconnu » sur tout nom absent de SCOPES. La mesure gagne, et le découpage de l'esquisse reste souhaitable pour plus tard.

« moderer » existe, et ce connecteur ne le délivre pas

  • Il existe : dans le modèle : « moderer » est l’un des sept scopes de SCOPES, et une délégation peut le porter
  • Il est refusé : par ce connecteur : « L’administration, les décisions de modération, les comptes, l’envoi de lettres et l’approbation de la veille ne sont pas exposés par ce connecteur. »
  • Il reste faisable : par le site, à qui tient un rang de modération — une délégation accordée depuis la page humaine peut le porter, et c’est la même ligne de la même table

Un client qui le demande reçoit « scope_insuffisant », et ce document l’annonçait.

Le défi : S256 seul, et la base le tient

« plain » n'est pas refusé par une branche qu'une refonte pourrait retirer : « CHECK (methode_de_defi = 'S256') » fait refuser la ligne par la base. Et le document de découverte n'annonce que S256, parce qu'annoncer « plain » en le refusant serait promettre puis dédire.

base64url(sha256(verificateur)) === defi

C'est la longueur d'un condensé de 256 bits en base64url, donc le plancher sous lequel un vérificateur porte moins d'entropie que le défi qu'il produit.

Un défi calculé en « base64 » ordinaire au lieu de « base64url » : les deux ne diffèrent que sur « + », « / » et le remplissage, soit rien du tout sur un condensé donné. Le contrôle joue donc plusieurs vérificateurs jusqu'à en trouver un qui les sépare, au lieu d'un seul témoin qui peut ne pas voir la différence.

Deux durées courtes, et pourquoi celles-là

  • Un code : 60 secondes : Il traverse un navigateur, une fois, en quelques secondes. Une minute laisse place à un réseau lent sans laisser un code traîner.
  • Un jeton : 600 secondes au plus : « jetons courts et révocables » de docs/PHASE-2-AGENT-FIRST.md. Dix minutes bornent la fenêtre d'un jeton volé sans forcer un échange à chaque requête.

Ce que la base tient elle-même : expire_le > cree_le sur un code ; expire_le > emis_le sur un jeton.

Ce qu’un jeton ne porte pas

  • Impossible : Qu’un jeton porte plus de scopes que la délégation dont il dérive. Ce n’est pas une interdiction : les cinq colonnes qui porteraient un droit n’existent sur aucun des deux moteurs. Il n’y a pas de ligne à élargir. Les droits se lisent à deux sauts, par « jetons_d_acces → delegations ».
  • Refusé : Qu’un jeton vive plus longtemps que sa délégation : son expiration est le plus tôt des deux, et c’est la commande d’échange qui le tranche, éprouvée dans les deux sens.
  • Faisable encore : Repointer un jeton vers une autre délégation, à qui tient un accès direct à la base. Mais il ne gagne alors aucun droit « en trop » : il porte exactement ceux de sa nouvelle délégation, puisqu’il n’en porte aucun de son propre fait. Le dire autrement serait promettre au schéma ce qu’il ne tient pas.

Parce que « impossible », « refusé » et « faisable encore à qui… » ne sont pas la même promesse, et qu’une seule phrase qui les mêle finit par dire le plus fort des trois. C’est le défaut « J1 » du registre.

Un code s’échange une fois, sous deux gardes

  • la colonne « echange_le » passe de NULL à un instant
  • « jetons_d_acces.code_id » est UNIQUE

Un second échange ne peut pas créer un second jeton même si la branche qui lit « echange_le » disparaissait. Et chacune est éprouvée séparément : c'est le piège B3, où une épreuve qu'un second invariant attrape passe pour la bonne raison sans rien prouver.

Quand une révocation prend effet

pour toute transaction qui commence après que la révocation est enregistrée

« eprouverDelegation » est rejouée à l'écriture, dans la transaction, et pas seulement à la préparation — c'est écrit dans son propre commentaire depuis P2-01 : « c'est la seule façon qu'une révocation survenue entre les deux soit vue ». Une préparation déjà confirmée n'est donc pas défaite, et une révocation entre la préparation et la confirmation est vue à la confirmation.

« revoquerDelegation » est la commande unique. Aucune face ne joue d'« UPDATE delegations » de sa main, et un contrôle le relève.

Les quatre familles qu’un client ne lit pas

  • administration : Le cap et la spécification excluent l’administration de ce connecteur, et aucun scope ne la nomme.
  • veille non revue : Un signal non revu n’est pas une information : le publier par une face agent lui donnerait le crédit que la revue n’a pas encore accordé.
  • retours prives : Un retour est donné au portail, pas au public : son auteur ne l’a pas écrit pour être lu par un agent.
  • votes d autrui : Un vote individuel est privé par défaut, et un reçu ne se relit que par celui qui l’a reçu — c’est déjà ce que « vote.mjs » refuse avec « lecture_croisee_refusee ».

Un échantillon d'une famille ne dit rien d'une autre : c'est le défaut G2. Chacune est éprouvée, avec un témoin de forme d'abord — une lecture légitime du même principal doit réussir, sinon le refus pourrait venir d'ailleurs.

Aucune clé étrangère directe ne relie un jeton ou un code à l'une de ces tables, et aucun chemin servi ne les expose. Les chemins qui existent à un et deux sauts sont relevés et rendus, pas niés : « principaux » est la table des comptes, donc « aucun chemin n'existe » serait faux par construction. C'est la leçon de J1.

Ce que la carte de l’agent ne fait pas

  • aucun rappel sortant : le suivi se fait par interrogation, et rien ne part du serveur vers un client
  • aucun accès réseau décidé par une pièce : une source lue n’ouvre aucune requête
  • aucune orchestration de modèles, et aucun appel de modèle — Polideo ne paie le modèle de personne
  • aucune profondeur de délégation non bornée : le déclencheur de la migration 009 refuse qu’une délégation fille sorte des droits de sa parente, et une sous-tâche ne prolonge ni durée ni périmètre

Le cap, ligne 216 : « Ces références décrivent les mécanismes d'interopérabilité. Elles ne prouvent pas qu'un client réel ». Aucune face servie ne porte « compatible ChatGPT », « compatible Claude » ni « compatible A2A », et le contrôle le relève sur toutes les faces, pas seulement sur la carte.

Les trois écritures déclarées, et aucune montée

  • POST /autorisation/demandes : émettre un code d’autorisation
  • POST /autorisation/jetons : échanger un code contre un jeton
  • DELETE /autorisation/jetons/{jeton} : révoquer un jeton

Monter une écriture publique est une décision d'Adam, pas un geste d'exécution : c'est irréversible pour les données qui arriveront. Les trois sont nommées au 404 et déclarées au contrat ; le service public rend 405. Construire la face d'autorisation ne la monte pas.

Ce document se lit à « /agents/v1/autorisation ». Le service statique du portail décide le type de contenu par l'extension — « MIME[extname(cible)] || 'application/octet-stream' » — et servirait ce document, qui n'en a pas, en « application/octet-stream ». Annoncer un document de découverte à son adresse standard avec le mauvais type serait pire que ne pas l'y annoncer. La prémisse est exécutable : si la table des types gagne une entrée pour l'extension vide, la limite tombe et le contrôle échoue.

Ce que ce lot n’établit pas

  • aucun client réel n’a parcouru ce chemin : Le point 5 du brief 40 — « exécuter le parcours avec ChatGPT ou le premier client réel » — n’appartient pas à ce lot : Adam a posé qu’aucun coût ni contact extérieur ne s’engage. Ce qui est éprouvé est le modèle et ses refus, dans un magasin jetable, par un client écrit dans ce dépôt.
  • aucun point de terminaison d’écriture n’est monté : Les trois écritures de cette face sont nommées au 404 et déclarées au contrat ; le service public rend 405. Monter une écriture publique est une décision d’Adam, et c’est irréversible pour les données qui arriveront.
  • le document de découverte n’est pas à son adresse standard : Le service statique décide le type de contenu par l’extension, et servirait ce document, qui n’en a pas, en « application/octet-stream ». L’annoncer à son adresse standard avec le mauvais type serait pire que ne pas l’y annoncer.
  • la migration 020 ne part pas en production : Aucune de ses trois tables n’est exigée par le portail servi, qui reste à 11 migrations. Les lectures montées publiquement n’exigent que les 19 tables de communauté.
  • aucune bibliothèque de cryptographie n’est adoptée : La spécification préfère « une bibliothèque maintenue et figée » à une réimplémentation improvisée. Ici, rien n’est réimplémenté : « node:crypto » fait sha256 et base64url, tous deux dans la bibliothèque standard, et la comparaison est à temps constant par « timingSafeEqual ». Le dépôt porte deux dépendances de production — « pg » et « fast-xml-parser » — et aucune des deux n’est de la cryptographie : ma première prémisse disait « aucune dépendance » et elle était fausse, mesurée par ce contrôle même.

Chaque limite porte une prémisse qui s’exécute, et le contrôle échoue si une limite reste écrite alors que sa raison est tombée.