quest'n'click 4.5.0 · r1

Construite pour quest'n'click 4.5.0, révision r1. Si elle est publiée, et laquelle est la courante, est dit par publication.json.

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 : jobs reality, reality-xcheck ; plan de CI reality ; 2 fichiers de tests · passes humaines : aucune · limites connues : bridge-dedupe-retention, bridge-no-queue-for-connectors, bridge-tenant-isolation-scope, bridge-proof-scope

Le 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 : jobs bridge-postgres, deployment-smoke ; plan de CI reality ; 1 fichier de tests · passes humaines : bridge-postgres-https pas faite (#77) · limites connues : bridge-dedupe-retention, bridge-no-queue-for-connectors, bridge-tenant-isolation-scope, bridge-proof-scope

Les 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 CI reality ; 3 fichiers de tests · passes humaines : connectors-real-security pas 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

QuestionRéponseOù ça s’arrête
Un connecteur envoie deux fois le même signalAccepté une fois : 202 la première fois, 200 avec "duplicate": true ensuiteLa 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 ordreAucun 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 foisLa livraison est au moins une fois, jamais « exactement une fois »
Le Bridge est occupé ou absent503 busy avec Retry-After: 1 (rien d’écrit), 429 au-delà d’un quota ; le jeu continueC’est au connecteur de renvoyer ; le Bridge ne met rien en file pour lui
Deux tenants sur un même déploiementChaque requête est rattachée à un tenant avant toute lecture ; les jetons et signaux de l’un sont refusés par l’autrePas 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 adversaireLa 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 sur aff6caab7514 · portée vérifiée par son auteur (IA), pas relue Un signal proposé de nouveau avec une dedupeKey dé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 sur aff6caab7514 · 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-retention Le Bridge reconnaît une clé tant qu’il garde le signal. Sur le journal JSON-lines, compact retire 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ë.

Garantie · bridge-sequences-contiguous · référencée · exécutée sur aff6caab7514 · 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 une dedupeKey n’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 sur aff6caab7514 · 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 sur aff6caab7514 · 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 sur aff6caab7514 · 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 arriveCe que répond le BridgeQuoi faire
Un autre processus garde le verrou d’écriture de SQLite plus de 5 s503 {"error": "busy"}, Retry-After: 1 ; rien d’écritRenvoyer (les connecteurs le font, avec la même clé)
Le stockage ne répond pas/readyz et /healthz en 503 {"status": "unavailable"} ; /livez reste à 200Regarder 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 bailLire 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 pendingLe 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ées429 rate, Retry-After: 60Attendre

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 sur aff6caab7514 · portée vérifiée par son auteur (IA), pas relue Un stockage SQLite occupé plus longtemps que son attente répond 503 avec Retry-After: 1 en 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-connectors Le 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 :

Garantie · bridge-tenant-token-bound · référencée · exécutée sur aff6caab7514 · 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 sur aff6caab7514 · portée vérifiée par son auteur (IA), pas relue Un serveur qui tient plusieurs tenants route chaque requête par Host, 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-scope L’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 :

Garantie · bridge-applied-once · référencée · exécutée sur aff6caab7514 · 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 sur aff6caab7514 · 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 :

  1. fermé : aucun signal ; le jeu doit pouvoir se finir par ses solutions de repli ;
  2. 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 signaux required, au moins un scénario les envoie tous ;
  3. 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 sur aff6caab7514 · 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 sur aff6caab7514 · 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 sur aff6caab7514 · portée vérifiée par son auteur (IA), pas relue Un run dont la preuve Reality manque reçoit le code signal-missing, une preuve modifiée signal-signature, et une preuve signée par une autre clé le verdict missing-reality-proof. Sans trousseau, le verdict est inconclusive (no-keyring).

Limite · bridge-proof-scope La 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


Modifier la source de cette page · Signaler une erreur