Garanties
English · Type : explication + référence · Public : auteur, administrateur · Contexte : Reality Bridge · Statut : prête · S'applique depuis : 4.2.5
Le Reality Bridge, profil
local(SQLite) · contrat : stable · vérifié par la CI (matrice de support de la 4.4.0, 2026-10-23) : Node 22.13+ avec node:sqlite, sous Ubuntu ; Chromium et WebKit de bout en bout · vérifications automatiques : jobsreality,reality-xcheck; plan de CIreality; 2 fichiers de tests · passes humaines : aucune · limites connues :bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scopeLe Reality Bridge, profil
distributed(Postgres) · contrat : expérimental · vérifié par la CI (matrice de support de la 4.4.0, 2026-10-23) : Postgres 16 en CI ; Le profil de qualification : HTTPS, trois instances (ops/qualify/) · vérifications automatiques : jobsbridge-postgres,deployment-smoke; plan de CIreality; 1 fichier de tests · passes humaines :bridge-postgres-httpspas faite (#77) · limites connues :bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scopeLes connecteurs (email, Telnet, SSH, Open Badges) · contrat : expérimental · vérifié par la CI (matrice de support de la 4.4.0, 2026-10-23) : Node 22 sous Ubuntu (Telnet et SSH pas sous Windows) · vérifications automatiques : jobs
connectors,connector-archive-e2e; plan de CIreality; 3 fichiers de tests · passes humaines :connectors-real-securitypas faite (#79) · limites connues :bridge-dedupe-retention,bridge-no-queue-for-connectors,bridge-tenant-isolation-scope,bridge-proof-scope
Ce que le Reality Bridge et le joueur promettent à propos d’un signal : un doublon, l’ordre, une livraison répétée ou rejouée, un Bridge absent ou occupé, deux tenants côte à côte, et ce qu’un signal fait à la progression d’un jeu et à sa preuve. Chaque réponse est lue dans le code actuel et, quand un test le montre, rattachée à ce test. Chacune dit où elle s’arrête.
En bref
| Question | Réponse | Où ça s’arrête |
|---|---|---|
| Un connecteur envoie deux fois le même signal | Accepté une fois : 202 la première fois, 200 avec "duplicate": true ensuite | La clé est connue tant que le stockage garde le signal |
| Dans quel ordre un joueur reçoit-il les signaux ? | Numérotés 1, 2, 3… par joueur, dans l’ordre où le Bridge les a acceptés, appliqués dans cet ordre | Aucun ordre entre joueurs, et l’heure d’un fait extérieur ne réordonne rien |
| La réponse s’est perdue, la page a planté | Relivré depuis la dernière séquence acquittée, reconnu, appliqué une fois | La livraison est au moins une fois, jamais « exactement une fois » |
| Le Bridge est occupé ou absent | 503 busy avec Retry-After: 1 (rien d’écrit), 429 au-delà d’un quota ; le jeu continue | C’est au connecteur de renvoyer ; le Bridge ne met rien en file pour lui |
| Deux tenants sur un même déploiement | Chaque requête est rattachée à un tenant avant toute lecture ; les jetons et signaux de l’un sont refusés par l’autre | Pas de sécurité par ligne dans Postgres ; un processus détient les clés de tous les tenants |
| L’extérieur peut-il bloquer le jeu ? | Non : un signal required a une solution de repli, et le jeu est prouvé fermé, par scénario et en adversaire | La preuve couvre les signaux déclarés, pas le comportement des connecteurs |
Les doublons
Un connecteur donne à chaque fait une clé de déduplication (dedupeKey) : le connecteur e-mail
sha256('email:' + Message-ID), Telnet sha256('telnet:' + session + ':' + ligne), le webhook de démonstration
<source>:<id>. Sur POST /v1/signals, le Bridge répond 202 la première fois qu’il voit une clé pour un joueur,
et 200 avec le signal gardé et "duplicate": true pour une clé déjà acceptée : le même id, la même sequence,
rien d’écrit.
La recherche, la séquence et l’écriture forment une seule étape. Dans un processus, les propositions pour un joueur
passent une à une (un verrou par joueur) ; entre processus, une transaction de la base s’en charge, adossée à des
contraintes d’unicité sur (tenant, player, dedupe_key) et (tenant, player, sequence)
(bridge/migrations/0001.up.sql). Deux connecteurs, ou deux instances, qui proposent la même clé au même instant :
une seule acceptation.
Garantie ·
bridge-duplicate-200· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Un signal proposé de nouveau avec unededupeKeydéjà acceptée pour ce joueur reçoit 200, avec la séquence du premier signal et"duplicate": true; le joueur ne le reçoit qu’une fois.
Garantie ·
bridge-dedupe-concurrent· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Cinquante propositions d’une même clé en même temps donnent une acceptation et un identifiant ; cent clés distinctes en même temps donnent les séquences 1 à 100, sans partage.
Limite ·
bridge-dedupe-retentionLe Bridge reconnaît une clé tant qu’il garde le signal. Sur le journal JSON-lines,compactretire les signaux acquittés plus vieux que sa rétention (90 jours par défaut ; le dernier signal d’un joueur reste toujours) : une clé renvoyée après cela est un nouveau signal. Un stockage SQL garde tous les signaux (il n’a pas encore de rétention). La clé est propre à un joueur : la même clé pour deux joueurs fait deux signaux.
L’ordre
Chaque joueur a sa propre séquence, contiguë depuis 1, tirée quand le Bridge accepte un signal. Le joueur lit les signaux par séquence (un flux ou une lecture par curseur, les deux donnent la séquence de chaque signal) et le moteur les applique dans cet ordre : un signal arrivé avant un trou attend que le trou soit comblé, puis le curseur avance sur la suite contiguë.
- L’ordre est celui de l’acceptation par le Bridge. Le
occurredAtd’un connecteur voyage dans le signal à titre d’information ; il ne réordonne jamais rien. - Il n’y a pas d’ordre entre deux joueurs, ni entre deux tenants.
- Avec plusieurs instances, un signal accepté par l’une atteint le flux que tient une autre, dans l’ordre, une fois chacun.
Garantie ·
bridge-sequences-contiguous· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Quel que soit l’entrelacement des propositions concurrentes et de leurs répétitions, les séquences d’un joueur sont 1..n sans trou ni doublon, et unededupeKeyn’est acceptée qu’une fois.
Livraison, accusé de réception et rejeu
La livraison est au moins une fois, appliquée une fois. Le transport du joueur tient trois curseurs : reçu (la
dernière séquence envoyée par le Bridge), livré (la dernière remise au jeu) et durable (la dernière acquittée,
après la sauvegarde). Le client vérifie un signal, le remet au moteur, attend que la sauvegarde soit durable, et
seulement alors envoie POST /v1/ack { through }. Chaque requête au Bridge, une lecture périodique ou une reconnexion,
part du curseur durable : un signal remis et non acquitté (un refus qui peut passer plus tard, une sauvegarde ratée, un
plantage avant l’accusé) est relivré, avec une attente qui grandit jusqu’à une minute tant que rien ne se règle. Avant
la 4.1.8, le transport partait du curseur livré.
La sauvegarde garde la dernière séquence reçue sans trou et les identifiants appliqués au-dessus
(GameState.reality, au plus 1 024 au-dessus du curseur), si bien qu’un signal relivré est reconnu et n’est pas
appliqué deux fois. Un signal est aussi une entrée de la session,
{ external: { id, sequence, signal, source, receivedAt, playerId } } : npm run replay l’applique hors ligne, sans
Bridge.
Garantie ·
bridge-ack-after-durable-save· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Le client n’acquitte un signal qu’après que la sauvegarde qui le contient est durable, et seulement jusqu’au curseur contigu.
Garantie ·
bridge-resume-from-durable· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue En polling, un signal remis au jeu et non acquitté est redemandé depuis le curseur acquitté.
Garantie ·
bridge-session-replays-offline· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Une session qui contient des signaux se rejoue hors ligne, sans Bridge, jusqu’au même état.
Quand le Bridge est occupé ou absent
| Ce qui arrive | Ce que répond le Bridge | Quoi faire |
|---|---|---|
| Un autre processus garde le verrou d’écriture de SQLite plus de 5 s | 503 {"error": "busy"}, Retry-After: 1 ; rien d’écrit | Renvoyer (les connecteurs le font, avec la même clé) |
| Le stockage ne répond pas | /readyz et /healthz en 503 {"status": "unavailable"} ; /livez reste à 200 | Regarder la base |
Un serve a perdu son bail de maintenance (4.1.20) | Ses écritures répondent 503 et /readyz échoue jusqu’à ce qu’il reprenne un bail | Lire sa ligne de journal maintenance.lease.lost |
| Au-delà du quota d’un connecteur (120 par minute) ou de l’arriéré d’un joueur (1 000 non acquittés) | 429 quota ou pending | Le connecteur le voit ; rien n’est perdu en silence |
| Une adresse au-delà de 60 requêtes par minute sur les routes ouvertes à tous, ou de 60 authentifications ratées | 429 rate, Retry-After: 60 | Attendre |
Un connecteur renvoie avec la même clé une proposition dont la réponse s’est perdue : trois essais en tout par défaut,
puis un refus qu’il compte (timeout, unreachable). Le connecteur e-mail laisse un message non lu quand le Bridge l’a
refusé pour une raison passagère, et le relit au relevé suivant. Côté joueur, un jeu hors ligne ou sans signal
continue, et ce qui arrive plus tard s’applique à son arrivée. Une instance tuée ou plantée ne perd rien : une
transaction est validée ou absente (tests/bridge-fanout.test.ts tue une instance sur trois pendant 1 000
propositions).
Garantie ·
bridge-busy-503· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Un stockage SQLite occupé plus longtemps que son attente répond 503 avecRetry-After: 1en moins de deux secondes. Une fois le verrou libre, la proposition suivante est acceptée avec la séquence 1.
Limite ·
bridge-no-queue-for-connectorsLe Bridge ne garde pas une proposition refusée pour plus tard : après un 503 ou un 429, c’est au connecteur de renvoyer. Le quota d’un connecteur est compté par instance : avec N instances, un connecteur peut proposer N fois son quota.
L’isolation des tenants
Un tenant est le jeu d’un opérateur dans un environnement, avec sa propre racine Biscuit, sa clé d’événements, son jeton d’opérateur, ses quotas et ses révocations (Plusieurs tenants). Sur un serveur qui en tient plusieurs :
- Une requête est rattachée à son tenant par son
Host(leshostsde chaque tenant), ou parX-Web-Scumm-Tenantquand--tenant-headerl’autorise, avant toute lecture ; aucun tenant ne correspond : une 404tenant. - Chaque ligne du stockage porte son tenant, et chaque requête SQL filtre dessus.
- Le jeton d’un connecteur est lié à son tenant (
grantl’ajoute) : un jeton du tenant A est refusé par le tenant B, même quand les deux ont reçu (à tort) la même racine Biscuit. - Un tenant d’un déploiement partagé signe des
WorldSignalV2, qui nomment leur tenant : un signal de A présenté à un joueur de B est refuséaudience-mismatch, même sous une clé d’événements partagée.
Garantie ·
bridge-tenant-token-bound· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Un jeton de connecteur lié à son tenant est refusé par un autre tenant (403), même quand les deux tenants partagent une racine Biscuit.
Garantie ·
bridge-tenant-routing· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Un serveur qui tient plusieurs tenants route chaque requête parHost, par l’en-tête de tenant seulement quand il est autorisé, et répond 404 à une requête qui ne nomme aucun de ses tenants.
Limite ·
bridge-tenant-isolation-scopeL’isolation est dans le code du Bridge, pas dans la base : il n’y a pas de sécurité par ligne et par tenant dans Postgres, et un processus qui sert plusieurs tenants détient les clés privées de chacun (une instance compromise, ce sont tous ses tenants compromis : Sécurité).
L’effet sur la progression du jeu
Un signal arrive dans le jeu comme l’événement du même nom, comme une entrée du joueur :
once(par défauttrue) : son effet s’exécute une fois par partie, quel que soit le nombre de ses arrivées. Aveconce: false, chaque signal distinct venu du Bridge le relance ; une livraison répétée du même ne le relance jamais.- Un signal qui arrive pendant que le jeu est occupé (un dialogue, une cinématique, un mini-jeu) l’attend.
- Le premier signal lie la sauvegarde à son joueur pseudonyme (
GameState.reality.playerId, lep-…donné par le Bridge à l’appairage ; jamais la capacité). Une sauvegarde qui arrive sur un appareil lié à quelqu’un d’autre (importée d’un autre appareil, ou faite avant un « Unlink » suivi d’un nouvel appairage) est en désaccord (mismatch) : rien n’en est appliqué ni acquitté, le jeu continue, et la ligne World link du menu pause propose d’utiliser le lien de cet appareil avec cette sauvegarde ; son état de lien repart alors de zéro pour ce joueur (curseur 0), et les signaux dont la sauvegarde a déjà l’effet ne sont pas réappliqués. Une livraison sans joueur ne lie rien (4.4.0). - La sauvegarde et la session gardent l’identifiant du joueur, le curseur et les identifiants appliqués : jamais un jeton, un e-mail, un message ni une charge.
Garantie ·
bridge-applied-once· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Le moteur applique une fois un signal déclaré, l’inscrit dans la session et le sauvegarde avec l’état.
Garantie ·
bridge-save-bound-to-player· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue La première livraison lie la sauvegarde à son joueur ; une livraison pour un autre joueur est un désaccord et ne change rien.
L’effet sur la preuve
Une preuve ne suppose jamais que l’extérieur coopère. npm run solve:reality (inclus dans npm run prove:game et dans
quest-n-click release) prouve le jeu dans trois sortes de mondes :
- fermé : aucun signal ; le jeu doit pouvoir se finir par ses solutions de repli ;
- chaque scénario de
games/<id>/reality/scenarios/*.json({ "signals": ["mail.answer.wrong", "mail.answer.correct"] }), puis chaque rejeu enregistré par les connecteurs (games/<id>/replays/*.json) : ces signaux dans cet ordre, chacun pouvant arriver à n’importe quel moment après le précédent ; quand le jeu a des signauxrequired, au moins un scénario les envoie tous ; - adverse : n’importe quel signal déclaré, à n’importe quel moment, encore et encore.
Chacun doit être résolu sans blocage (softlock) et jamais tronqué. Le solveur remet lui-même le signal au moteur :
aucun service n’est contacté. Dans le MCP du Studio, solve prend reality pour essayer un monde. npm run validate
vérifie que la fallback d’un signal requis nomme une règle ; le lint du contenu (npm run lint, dans verify:game)
vérifie que le témoin du monde fermé la joue, et avertit fallback-unplayed quand le jeu se finit sans l’extérieur par
un autre chemin (la solution de repli est déclarée sur une règle dont personne n’a besoin : déclarez l’action que joue
le témoin, ou faites de celle déclarée le passage obligé). Comment lire un résultat du solveur en général :
Ce que prouve le solveur.
Garantie ·
bridge-proof-closed· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Dans le monde fermé, le solveur finit le jeu par la solution de repli, jamais par un signal.
Garantie ·
bridge-proof-adversarial· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Dans le monde adverse, les doublons et tous les ordres des signaux déclarés ne font jamais perdre la partie.
La catégorie d’un speedrun dit ce qu’elle accepte de l’extérieur : forbidden (un signal dans le run est
reality-forbidden), recorded (chaque signal appliqué est gardé avec son JWS signé, son identifiant de clé et son
empreinte, et le vérificateur contrôle la signature avec les clés publiques du Bridge, --keys ou GET /v1/keys ;
sans preuve : missing-reality-proof) ou live (la même preuve, dans une catégorie à part, car la latence et la
disponibilité d’un signal en direct ne se reproduisent pas ; sans les clés, le vérificateur ne peut pas conclure,
no-keyring). Le détail : Speedrun pour les auteurs.
Garantie ·
bridge-speedrun-reality-proof· référencée · exécutée suraff6caab7514· portée vérifiée par son auteur (IA), pas relue Un run dont la preuve Reality manque reçoit le codesignal-missing, une preuve modifiéesignal-signature, et une preuve signée par une autre clé le verdictmissing-reality-proof. Sans trousseau, le verdict estinconclusive(no-keyring).
Limite ·
bridge-proof-scopeLa preuve couvre les signaux que le jeu déclare, dans n’importe quel ordre et nombre. Elle ne dit rien de la question de savoir si un connecteur en enverra un, ni de ce qu’un connecteur fait de son entrée.
Ce que le Bridge garde d’un joueur
Par joueur : le playerId, le SHA-256 de sa capacité, son expiration, l’identifiant de session, l’origine depuis
laquelle il s’est appairé. Par signal : son identifiant, sa séquence, son nom, sa source, la clé de déduplication du
connecteur, l’empreinte de ce que le connecteur a vu (evidenceHash), les heures, l’enveloppe signée et, depuis la
4.1.2, la charge telle qu’acceptée (pour la re-signer après une rotation) : jamais un jeton, un e-mail, un message ni
un badge. Les codes d’appairage attendent dix minutes : en mémoire sur le journal (un redémarrage les oublie), dans la
table pairings d’un stockage SQL. Le journal d’exécution (sortie standard) nomme des événements et des identifiants
pseudonymes, jamais un secret. Conservation, export et suppression :
Installer et lancer le Bridge ; tout ce qu’un jeu, un Bridge et ses connecteurs gardent d’un joueur :
Vie privée.
Ce qui n’est pas garanti
- « Exactement une fois » : jamais promis. Chaque étape peut répéter ; les garanties ci-dessus rendent une répétition sans effet.
- Les connecteurs et le profil
distributedsont expérimentaux : personne n’a encore fait tourner un connecteur contre un vrai fournisseur d’e-mail, un vrai badge ou un terminal exposé sur un réseau, ni le profildistributedderrière un vrai domaine pendant 24 à 48 heures (Stabilité et support). - Pas de sécurité par ligne et par tenant dans Postgres, pas de rétention sur un stockage SQL, pas de quota de connecteur partagé entre instances.
- Le Bridge de référence est fait pour un jeu, un développeur, un événement ou les jeux d’un petit studio ; c’est le protocole qui reste.