Aller au contenu

Multijoueur — identité, appartenance et présence

Control Center fonctionne pour un opérateur seul, mais le modèle qui le sous-tend est multi-acteurs dès le départ. Les humains et les agents échangent des messages dans les mêmes espaces, modifient les mêmes tickets et figurent sur la même liste de participants. Rendre cela sûr et compréhensible se ramène à trois décisions : qui compte comme acteur, ce qui contrôle l’accès et quel état est partagé durablement plutôt que simplement diffusé en direct.

Les principaux : les utilisateurs et les agents sont des acteurs à parts égales

Section intitulée « Les principaux : les utilisateurs et les agents sont des acteurs à parts égales »

Tout ce qui consigne « qui a fait ceci » — messages, tickets, revues, plans, journaux d’exécution, entrées d’audit — se résout par un principal, une union scellée de UserPrincipal et AgentPrincipal dotée d’une forme compacte sur le fil (user:<id> / agent:<id>). L’attribution ne réduit jamais N humains (ou N agents) à un seul compte sentinelle, de sorte qu’une revue laissée par un agent et une revue laissée par une personne se distinguent partout où elles apparaissent.

Parce que le principal est l’unité d’identité, il est aussi l’unité de confinement :

  • Les limites de débit s’appliquent par principal, et non par session. Le garde-fou anti-inondation des appels d’outils à distance établit son budget par utilisateur, et non par connexion : tous les appareils d’un membre puisent dans une même fenêtre glissante partagée (120 appels par minute, dont au plus 30 peuvent modifier l’état), de sorte qu’ouvrir trois appareils ne triple le quota de quiconque et qu’un membre bruyant ne peut pas affamer les autres.
  • L’annulation s’applique par principal. Le journal d’actions de chaque client n’enregistre que les modifications effectuées par son principal. Vous pouvez annuler votre propre modification de ticket avec ⌘Z, mais vous ne pouvez pas annuler celle d’un collègue — au moment où vous la voyez, la voie durable l’a déjà résolue (voir plus bas).

L’appartenance est la frontière d’accès, et non la clé d’appairage

Section intitulée « L’appartenance est la frontière d’accès, et non la clé d’appairage »

Deux identifiants distincts répondent à deux questions distinctes. La clé d’appairage (voir Contrôle à distance et mobile) authentifie un appareil auprès du serveur. L’appartenance autorise l’utilisateur de cet appareil à agir à l’intérieur d’un espace de travail. Un appareil appairé dont l’utilisateur ne détient aucune appartenance dans un espace de travail ne peut ni le lire ni le modifier.

L’appartenance est constituée d’une ligne par couple (espace de travail, utilisateur), stockée dans le fichier de base de données propre à cet espace de travail — ainsi, « qui se trouve dans cet espace de travail ? » est une lecture à portée ordinaire, tandis que la question inverse, « dans quels espaces de travail suis-je ? », s’étend nécessairement sur plusieurs fichiers et constitue l’une des rares diffusions sanctionnées entre espaces de travail. C’est ce que le sélecteur d’espace de travail exécute avant qu’un espace de travail ait été choisi.

Chaque ligne porte un rôle issu d’une échelle de cinq niveaux classés par rang :

Rôle Peut
owner Contrôle total, y compris la suppression de l’espace de travail ; exactement un par espace de travail
admin Gérer les membres, les invitations, les paramètres et les opérations destructrices
member Lecture-écriture au quotidien : messages, tickets, exécutions d’agents
viewer Lecture seule dans tout l’espace de travail ; des motifs d’exclusion de secrets masquent les chemins correspondants sur les surfaces porteuses de code
guest Le même plancher de lecture seule que le rôle viewer, au rang le plus bas de l’échelle

Chaque décision d’accès se ramène à « le rôle de l’appelant est-il au moins X ? ». Le plancher est dérivé du type de l’opération, à moins que celle-ci n’en déclare un explicitement : une lecture exige guest, une modification exige member, une opération destructive exige admin. Les listes de contrôle d’accès par objet, au-delà des autorisations par dépôt décrites ci-dessous, sont délibérément hors périmètre.

Les invitations sont le moyen d’accorder l’appartenance, depuis Paramètres → Espace de travail → Membres. Un administrateur génère un code à usage unique (sous forme de lien ou de QR code) qui expire après sept jours par défaut ; le serveur ne stocke que le hachage SHA-256 du code, et le code lui-même n’est affiché qu’une seule fois. L’utiliser provisionne l’utilisateur à la volée (JIT), enregistre l’appartenance au rôle de l’invitation (jamais owner — la propriété se transfère explicitement, jamais par invitation) et enchaîne avec l’appairage de l’appareil. Les identifiants d’appareil émis après l’appairage sont eux-mêmes limités à 30 jours, de sorte qu’un appairage n’est pas une clé permanente.

Les autorisations par dépôt se situent sous l’appartenance. Comme le serveur détient des copies de travail complètes, l’appartenance à un espace de travail ne doit jamais, à elle seule, accorder plus de privilèges que la forge : chaque surface porteuse de code — fichiers, diffs, revue de PR, recherche de code — vérifie l’autorisation du membre sur ce dépôt. Les autorisations sont définies par triplet (espace de travail, utilisateur, dépôt) et classées none → read → review → write, où write signifie diriger des exécutions d’agents qui écrivent dans le dépôt. L’absence de ligne signifie none ; les propriétaires et les administrateurs détiennent implicitement write sur chaque dépôt lié et ne portent aucune ligne.

Control Center répartit l’état partagé en deux voies aux règles de durabilité opposées, parce que les deux types d’état échouent de manières opposées.

La présence — qui est en ligne, où chacun se trouve, ce que chacun est en train d’écrire, ce qu’un agent fait à l’instant — ne vaut rien dès qu’elle est périmée. Si elle était persistée, chaque reconnexion devrait réconcilier « la base de données dit que Sam modifie ce ticket » avec la réalité. C’est pourquoi la présence n’est jamais écrite dans la base de données. Elle réside dans un concentrateur de présence en mémoire sur le serveur : les clients publient avec presence.update, les pairs s’abonnent avec presence.watch, et les entrées expirent automatiquement après trois battements de cœur manqués (les battements ont lieu toutes les 10 secondes ; le concentrateur balaie toutes les 5 secondes). Il n’y a rien à réconcilier — seulement à laisser expirer.

Deux propriétés rendent cette voie fiable :

  • L’identité provient de la session, jamais des arguments du client. Le serveur associe l’appareil authentifié de la connexion à un identifiant d’utilisateur ; un client ne peut jamais mettre à jour que sa propre présence, de sorte que personne ne peut usurper le statut ou le locus d’un autre participant. Une mise à jour offline explicite (déconnexion ou bascule d’invisibilité) supprime immédiatement l’entrée.
  • La liste des participants passe par le concentrateur du serveur. Les pairs n’échangent jamais leur présence directement, de sorte que chaque participant voit la même liste, quelle que soit la topologie. Les agents n’ont pas de client ; le serveur synthétise donc leurs entrées à partir de l’état des exécutions en direct — réflexion, en cours, bloqué sur une barrière d’approbation, terminé — une seule exécution représentative décidant de l’état et du locus d’un agent, pour que des exécutions simultanées ne fassent pas osciller la liste.

La diffusion est étagée par consommateur : le poste de bureau et le Web empruntent un palier complet (au plus une émission de la liste toutes les 100 ms), le téléphone un palier de synthèse (toutes les 500 ms), pour que des mises à jour à la cadence du curseur ne fassent pas fondre la PWA.

Le locus d’un participant — l’endroit où il se trouve actuellement, jusqu’à l’artefact près — est une union scellée : Space, File (dépôt + chemin + ligne facultative), Pr, Ticket, PlanNode. Tous les locus n’ont pas de destination vers laquelle sauter : il n’existe pas de route de visionneuse de fichiers, de sorte qu’un locus File s’affiche comme une étiquette plutôt que comme une destination. La voie transporte aussi des revendications souples (« Sam modifie ce ticket », « un agent détient cet arbre de travail ») : la visibilité des conflits, pas des verrous. Les types de locus inconnus sont interprétés comme null, de sorte qu’un ancien client n’affiche tout simplement pas ce qu’il ne comprend pas.

Les enregistrements qui doivent survivre — tickets, document Notes partagé de l’espace, messages — empruntent le chemin opposé : faisant autorité, ordonnés et persistés. Les modifications simultanées se résolvent par le dernier écrivain l’emporte (LWW) selon l’ordre de réception par le serveur, jamais selon les horloges des clients. La résolution est délibérément à grain fin : les modifications de ticket se font par colonne, de sorte que deux personnes modifiant des champs différents du même ticket aboutissent toutes les deux et qu’aucune n’écrase l’autre (les transitions de flux de travail conservent plutôt un verrouillage optimiste) ; le document Notes de l’espace ne forme qu’une seule colonne, de sorte que tout le document suit le LWW, les revendications souples de la voie de présence rendant la modification simultanée visible plutôt que verrouillée. Ce n’est délibérément pas un CRDT — la charge de la preuve incombe au CRDT, et le LWW selon l’ordre de réception, conjugué à des revendications visibles, couvre les schémas de collaboration réels.

« Dernier écrivain » désigne l’ordre de réception par le serveur, jamais une horloge de client — un ordinateur portable dont l’horloge est décalée ne peut pas gagner une modification qu’il a perdue. Derrière la voie durable se trouve le flux de synchronisation : des déclencheurs SQLite ajoutent chaque modification à un journal des modifications propre à l’espace de travail à l’intérieur de la transaction d’écriture, en allouant un numéro de séquence monotone propre à l’espace de travail dans la même transaction que la modification elle-même. Un service du serveur suit ce journal et le transforme en trames delta versionnées, dans l’ordre de la séquence. Les clients passent des instantanés complets aux deltas ; une lacune dans la séquence fait retomber le magasin concerné en mode instantané plutôt que d’appliquer des trames de façon erronée.

Suivre, guider, prendre le contrôle, rendre la main

Section intitulée « Suivre, guider, prendre le contrôle, rendre la main »

La voie de présence n’est pas qu’une liste de participants — elle pilote les verbes de collaboration :

  • Mode suivi. Cliquez sur un participant et votre client suit son locus, en naviguant à mesure que lui navigue. Le suivi dure « jusqu’à ce que vous agissiez » : dès que vous naviguez de votre propre chef, le suivi se détache. Une bannière que l’on peut fermer annonce quand un autre participant met en avant (présente) un espace où vous vous trouvez.
  • Guider. Un message adressé à un agent en cours d’exécution est mis en file comme consigne de pilotage ; l’agent le relit avant de reprendre son tour suivant, de sorte que vous pouvez corriger le cap d’une exécution en plein vol sans l’interrompre.
  • Prendre le contrôle. Prendre le contrôle d’une conversation met en pause chaque exécution active à sa prochaine frontière de tour nette (une exécution Claude Code, qui n’a pas de frontière sûre, est arrêtée) derrière un instantané git par tour, de sorte que la prise de contrôle est réversible. Un marqueur durable bloque les nouveaux lancements dans l’espace et survit à un redémarrage du serveur — le serveur ne reprend jamais automatiquement par-dessus la modification à moitié terminée d’un humain. Un seul éditeur actif par arbre de travail constitue une revendication souple : une deuxième prise de contrôle est visible et refusée, et non fusionnée en silence.
  • Rendre la main. Rendre le contrôle publie dans l’espace un résumé structuré du diff de l’arbre de travail, le met en file comme consigne de pilotage pour les exécutions en pause, les reprend et efface le marqueur.

Le guidage et la prise de contrôle sont des interventions a posteriori. Le cadran d’autonomie est le réglage permanent : défini par espace et par agent, depuis l’en-tête de l’espace, il décide de ce qui se produit quand un agent atteint une action soumise à autorisation.

Niveau Valeur transmise Effet sur un appel d’outil soumis à autorisation
Proposer uniquement proposeOnly Refusé d’emblée, avec une raison que l’agent est chargé de relayer — il peut décrire l’action, mais pas l’exécuter
Agir avec approbation actWithApproval La barrière d’approbation à refus par défaut : une confirmation est présentée à un opérateur connecté, et l’absence d’approbateur vaut refus
Agir librement actFreely Préapprouvé, sauf lorsqu’une règle de garde-fou deny stricte s’applique — une règle de refus l’emporte toujours
(non défini) null Identique à « agir avec approbation » : la valeur par défaut du serveur

Deux éléments échappent au cadran, dans les deux sens. Le verbe de sortie propre à un mode est épinglé et ne peut jamais être refusé, quoi que dise le cadran — le mode orchestrate a déjà été structurellement incapable d’émettre son seul livrable parce qu’un préréglage refusait la classe d’effets que ce verbe déclare. Et un deny strict provenant des garde-fous d’action ne peut pas être annulé par actFreely.

Tout cela est additif. Le flux de travail d’un opérateur en solo ne change pas : avec un seul humain et aucun agent, le concentrateur de présence n’a rien à diffuser, de sorte que la voie émet son instantané initial puis reste inactive sans aucun trafic ; la mécanique LWW et de synchronisation demeure le même chemin à écrivain unique qu’elle a toujours été ; et la clé d’appairage reste l’identifiant d’appareil qu’elle a toujours été. L’appartenance constitue déjà la frontière d’accès — avec un seul humain, il y a une seule ligne de propriétaire et aucun élément d’interface propre à la liste des participants.