ADR-0014 — Arène frontier-models : recorder + replay validator, jamais runtime
Status: Proposed (2026-05-01, design review interne).
Related ADRs: ADR-0002 (byte-deterministic CLI), ADR-0003 (sealed-score-replay), ADR-0006 (canonical move enumeration order), ADR-0008 (deterministic board iteration), ADR-0009 (parity CI gate), ADR-0010 (corpus public — code MIT), ADR-0012 (identité utilisateur — request_nonce/device_session/handle_secret).
Upstream-feedback dépendance : dist-play/wrangler.toml lignes 24–27 (binding MORPION_USED_CANCEL_TOKENS dormant — cf. §Migration / threat-model).
Suivis non engageants : multi-modal arena track, Elo / pairwise ranking, tournament mode, upstream-feedback signal sur MORPION_USED_CANCEL_TOKENS.
Context
Le projet morpion possède déjà un zoo d'algorithmes : greedy, MCTS, NRPA, AlphaZero — tous joueurs déterministes pilotés par crates/morpion-arena/match_runner.rs, validés par crates/morpion-arena/replay.rs (sealed-score, ADR-0003) et indexés par le BotStats schema (crates/morpion-arena/leaderboard.rs). Mais le zoo est fermé : il n'accueille que des joueurs Rust dont l'opérateur contrôle le code.
L'enjeu de cet ADR est d'ouvrir le zoo aux frontier-models (GPT-5, Claude, Gemini, Llama, …) sans :
- transformer le serveur en proxy LLM payant (économiquement insurvivable, attaque iv) ;
- introduire un judge model dans la boucle de validation (vecteur de prompt-injection, attaque vi) ;
- contaminer la PWA familiale
morpion-play.pages.dev(CORS bleed via_shared.js:json()wildcard ACAO, attaque xi) ; - mélanger humains et bots dans un classement unifié (la séparation ADR-0012 ferait namespace pollution, attaque viii).
Une revue de design interne a couvert six questions (transport, board representation, API surface, public page, attack surface, prompt publication policy) et a produit 9 convergences C1–C9, 4 divergences résolues D1–D5, et 5 surprising insights. Cet ADR encode ces résolutions comme invariants morphologiques.
L'invariant cardinal — celui dont tout le reste dérive — est C1 : le serveur est un recorder + replay validator, jamais un runtime, jamais un proxy LLM. Le submitter exécute la boucle LLM sur sa machine, paie les tokens, tient la clef API, encaisse la latence ; le serveur reçoit une trace complète, la rejoue à travers replay.rs (microseconde-budget, déterministe), et l'inscrit au leaderboard si replay_check_passed: true. Cette unique décision élimine structurellement 4 attaques sur 5 — l'architecture désamorce avant qu'on ne défende. Sans cet ADR, les implémentations enfant tourneraient contre une spec instable et reproduiraient une à une les confusions du client_id (R1/R2/R3) corrigées dans ADR-0012.
Decision
1. Invariants de l'arène (C1–C9)
Les neuf convergences de la revue deviennent des invariants morphologiques du projet. Aucune implémentation post-ADR ne peut les violer ; tout changement exige un nouvel ADR.
| # | Invariant | Conséquence concrète |
|---|---|---|
| C1 | Serveur = recorder + replay validator. Jamais runtime, jamais proxy LLM. | Trois Functions, toutes pures. Aucune n'invoque un fournisseur LLM. Le payload submission contient la trace complète. |
| C2 | Submitter paie le LLM. Serveur ne détient aucune clef API. | Aucun secret OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY dans wrangler.toml arena ; aucune env var. Élimine attaque iv (DoS économique). |
| C3 | Séparation dure d'avec dist-play/functions/. | Pages project distinct (nom de travail morpion-arena.pages.dev) ; wrangler.toml séparé ; aucun binding partagé ; KV namespaces neufs (MORPION_BOTS, MORPION_ARENA_USED_NONCES) ; R2 préfixe arena/. La Function arena n'importe pas dist-play/functions/_shared.js tel quel — fork ou override de json() pour ne pas hériter du Access-Control-Allow-Origin: * (ligne 29). |
| C4 | Enveloppe HMAC challenge-signed, single-use, TTL'd. | Réutilise le pattern issueCancelToken/verifyCancelToken de dist-play/functions/_shared.js. Mapping ADR-0012 : flux bot = cas dégénéré de request_nonce. Pas de device_session, pas de handle_secret, pas de quatrième primitive. |
| C5 | Validator = pur Rust. Aucun LLM dans la boucle de validation, jamais. | crates/morpion-arena/src/replay.rs (déjà existant, ADR-0003) ou morpion_core::ReplayOracle. Garde-fou normatif : interdit le futur "judge model". Toute proposition d'un LLM-as-judge devra commencer par révoquer cet invariant via un nouvel ADR. |
| C6 | Humains et bots juxtaposés, jamais classement unifié. | Même page, deux colonnes côte à côte, deux échelles distinctes. Visualisation hero = un seul graphe de distribution (violin/box) : Anh-Hào \(\cdot\) joueur·B \(\cdot\) joueur·C \(\cdot\) joueur·D \(\cdot\) greedy \(\cdot\) MCTS \(\cdot\) NRPA \(\cdot\) AlphaZero \(\cdot\) GPT-5 \(\cdot\) Claude \(\cdot\) Gemini \(\cdot\) Llama, sur un même axe final_score. La juxtaposition est le message. |
| C7 | replay_check_passed: true obligatoire sur chaque entrée du leaderboard. | Recompute à chaque build du leaderboard (déterministe par ADR-0008). Aucune entrée non-vérifiée n'apparaît. |
| C8 | Reporting multi-stat, min-n gate avant headline. | Schéma figé : (max_score, mean_score, median_score, p_ge_25, p_31, n_episodes) — BotStats en crates/morpion-arena/src/leaderboard.rs:20. max_score n'est pas publié comme rang headline tant que n_episodes < n_min (défense contre l'overfit single-shot). |
| C9 | Format wire serveur = ObsView JSON (crates/morpion-arena/src/wire.rs:23). | Une seule schéma canonique. Sorted keys (ADR-0002), canonical move order (ADR-0006), iteration déterministe (ADR-0008). Pas de ?format= knob. Le rendu côté submitter (ASCII, image, autre) est un client concern hors-contrat serveur. |
C6.1 — Symétrie tri-track : chaque track a son endpoint live + son seed offline, jamais croisement
Amendement 2026-05-02 (régression image #63 post-livraison). C6 décrit l'invariant : humains, algos et frontier-models juxtaposés, jamais fusionnés. C6.1 fixe l'architecture symétrique qui matérialise l'invariant — sans symétrie, la juxtaposition se brise dès qu'un track migre.
| Track | Endpoint live | KV namespace | Seed offline | Producteur |
|---|---|---|---|---|
| humans | GET /humans/leaderboard | MORPION_HUMANS_AGGREGATE | dist-arena/seeds/humans-seed.json | dist-play-overlay/functions/save.js (best-effort RMW per handle) |
| algos | GET /algos/leaderboard | MORPION_ALGOS_AGGREGATE | dist-arena/seeds/algos-seed.json | tools/run-algo-bench.sh \(\to\) POST /algos/submit (admin token-gated) |
| frontier | GET /arena/leaderboard | MORPION_BOTS | dist-arena/seeds/frontier-seed.json (vide par doctrine) | POST /arena/submit (HMAC-signed nonces, ADR-0014 §C1) |
Trois règles d'invariance :
- Trois endpoints, trois KV, trois seeds — jamais un track qui dépend d'un autre. La régression 5732 (humans migré vers
/humans/leaderboard\(\to\) algos disparu parce qu'il vivait danshumans.json.algorithms) est exactement ce que C6.1 interdit. - Promise.all côté client —
dist-arena/leaderboard.js:boot()fetch les 3 endpoints en parallèle. Latence = max(track), pas somme. Une panne dans un track ne starve jamais les autres (soft-fail encapsulé par track). - Soft-fail per-track — chaque loader (
loadHumans/loadAlgos/loadBots) tente le live, puis dégrade vers son seed propre. Un seed peut être vide par doctrine (le frontier-seed l'est : on n'invente jamais de score ; cf. C1 + ADR-0018 §3.5) ou référence chargée (humans-seed porte le record humain de référence ; algos-seed porte le batch v1).
L'authoring d'un nouveau track (ex: multi-modal, D3 en suivi non engageant) duplique cette structure — endpoint dédié, KV dédié, seed dédié, producteur dédié — au lieu de mêler ses entrées dans un track existant. C'est le coût de l'absence-de-fusion (C6) rendu structurellement explicite.
Conséquence opérationnelle : dist-arena/humans.json est promu vers dist-arena/seeds/humans-seed.json + dist-arena/seeds/algos-seed.json (les deux datasets distincts qui y cohabitaient illégitimement sont séparés à l'exact endroit où l'invariant le réclame).
C10 — Leaderboard-first ordering : la promesse produit est la juxtaposition, pas l'objet plateau
Amendement 2026-05-03 (signal terrain — le produit ne racontait pas sa promesse en premier coup d'œil ; comparaison de deux maquettes a tranché vers : « plutôt que voir le board, on devrait voir le leaderboard en premier ; le plateau devrait être plus un logo, moins en avant »).
C6 fixe l'invariant humains/bots/frontier juxtaposés sur la même page. C10 ordonne ce que le visiteur voit en premier : pas le plateau de départ (objet d'observation), mais la juxtaposition elle-même (la promesse — humains \(\times\) frontier-models côte-à-côte).
| Avant | Après C10 |
|---|---|
| Hero plein écran : H1 + sous-titre + plateau \(540\times540\) cards + CTA. Le leaderboard arrive après scroll. | Hero-band compact : H1 court + mini-logo \(~100\times100\) px (identité visuelle, pas observation). Le leaderboard suit immédiatement. |
| Plateau \(540\times540\) = objet d'observation (figure + caption, registre cartel de musée). | Plateau \(100\times100\) = logo (identité, pas figure). Pas de caption. |
| Caption « Plateau de départ — 16 dots. Premier coup : à toi. » sous le hero-board. | Caption déplacée en CTA-strip (#cta-play) collée au bouton Joue toi-même ↗. |
Trois règles d'invariance :
- Le hero-band ne dépasse pas ~120 px de haut (mini-logo \(\leq\) 100 px + padding 20 px). Tout
padding: 48pxou figurewidth: var(--board-width-target)dans.hero-*est une régression. - Le mini-logo est immuable et identitaire — toujours la croix grecque réduite (16 dots
canonicalCrossDots()), jamais un snapshot du run #1. Un snapshot serait un objet d'observation, donc volerait l'attention que C10 réserve au leaderboard. - Le leaderboard est le premier
<section>de contenu après le.hero-band(et après.site-header). Aucune section figure / cartel / gallery ne peut s'intercaler — l'ordre du DOM matérialise C10.
Tension résolue : doctrine cartel-de-musée (revue §S5, hero-board comme cadre cream, registre disjoint de la PWA play) vs hiérarchie produit (« qu'est-ce qui doit disparaître pour que le leaderboard apparaisse en premier ? »). Le cartel-de-musée n'est pas révoqué ; il est redimensionné : ce qui était figure devient logo, ce qui était caption devient appel-à-action. Soustraction + changement d'échelle — pas de révocation d'invariant, juste un changement de registre.
2. Divergences résolues (D1–D5)
D1 — Trois endpoints, modèle recorder
Décision : GET /arena/challenge \(\to\) POST /arena/submit \(\to\) GET /arena/leaderboard.
| Endpoint | Rôle | Coût |
|---|---|---|
GET /arena/challenge | Émet une enveloppe HMAC-signée ({ nonce, expires_at, signature }). Stateful uniquement à l'expiration via MORPION_ARENA_USED_NONCES. | Une signature HMAC par appel ; pas de stockage avant submission. |
POST /arena/submit | Reçoit la trace complète + l'enveloppe + le payload submission (cf. §3). Vérifie la signature, marque le nonce used, rejoue via replay.rs, écrit dans R2 préfixe arena/ et indexe en MORPION_BOTS. | Une vérif HMAC + un replay() (microseconde-budget) + un PUT R2 + un PUT KV. |
GET /arena/leaderboard | Recompute le Leaderboard depuis les entrées MORPION_BOTS + R2. Déterministe, cacheable. | Lecture KV/R2 ; pas d'état mutable. |
Rejets :
- Variante 2-endpoints (submitter génère sa propre seed). Valide mais perd l'ancre de fraîcheur server-pinned réclamée contre l'attaque xii (hash-replay d'une partie pré-calculée).
- Variante 3-endpoints referee (
start/move/submitstateful). Refusée 4/5 sur grounds techniques : race conditions, KV bloat, DoS via 1000 parties abandonnées.
Le challenge endpoint coûte essentiellement rien (une signature HMAC) et clôt structurellement l'attaque xii. C'est la plus petite défense correcte.
D2 — Politique de publication des prompts
Décision : prompt obligatoire dans le payload de submission, hash toujours public, corps complet requis pour le headline ranking, opt-out unranked tier disponible.
| Tier | Visibilité du prompt | Place sur le leaderboard |
|---|---|---|
| Headline (ranked) | Corps complet du prompt publié ; prompt_template_sha256 calculé et exposé. | Apparaît dans la table principale, triable par max_score / mean_score / p_31. |
| Unranked (opt-out) | Seul prompt_template_sha256 exposé ; corps gardé en R2 mais non publié. | Apparaît grisé, sortable sans figurer dans les rangs headline. |
Le prompt est licencié MIT (cohérent avec ADR-0010 §3 : « Code de la PWA, du workflow, des outils — MIT, adoption maximale aval »). Le serveur stocke le prompt comme bytes opaques ; il ne le ré-injecte jamais dans un LLM (invariant C5).
Tension résolue : doctrine du commun (« opacity is contest of prompt engineering disguised as reasoning », doctrine privé \(\to\) commun, jamais l'inverse déjà fixée par ADR-0010 §3) vs friction d'entrée (« lower friction for early competitors » + injection vector). L'argument injection est désarmé structurellement par C5 : pas de judge model = pas de surface d'injection. L'argument doctrinal gagne. Mais on garde une voie unranked pour les vendors qui ne peuvent pas publier — ils existent sur la page, sans figurer dans le headline. C'est une version mince de « no prompt \(\to\) no entry » : « no prompt \(\to\) no rank ».
Cette division est plus fine que celle d'ADR-0010 sur les traces (CC0 monolithique) et plus fine que la doctrine pseudonyme d'ADR-0012 (handle libre, client_id jamais exposé) — mais elle dérive du même principe : le commun est le défaut, l'opt-out est explicite et tracé.
D3 — Multi-modal différé
Décision : v1 est text-only. Le track multi-modal (PNG payload, leaderboard parallèle) part en suivi non engageant.
Une perspective proposait deux tracks (text + multi-modal). Quatre perspectives sur cinq différaient. La complexité ajoutée (rendu d'image côté submitter, leaderboard séparé, méthodologie séparée) ne se justifie pas sans signal de demande. Le single distribution chart hero (C6) reste dans le scope v1 — c'est la visualisation ; le track multi-modal n'y est pas.
D4 — Pas d'énumération des coups légaux dans le prompt canonique
Décision : ne pas inclure legal_moves énuméré dans le prompt. Inclure uniquement legal_move_count: int comme signal de difficulté.
Le constat : « le puzzle est trouver le coup, pas ranker l'index ». Inclure legal_moves ferait collapse vers une tâche de classification triviale. legal_move_count reste utile post-hoc pour analyser la difficulté d'une position, sans être gameable.
L'inquiétude sur la format-multiplication attack surface est désarmée par C9 : il y a un format wire canonique. Le rendu côté submitter (ASCII, JSON brut, prose) est libre, mais le prompt_template_sha256 est pinné dans la submission — donc le submitter ne peut pas dériver son rendu librement sans tomber hors du headline rank (cf. D2).
D5 — Reference client : nouveau binaire morpion-arena-llm, fin wrapper
Décision : créer le binaire morpion-arena-llm comme thin wrapper de crates/morpion-arena/match_runner.rs, ajoutant un trait LLM-backend analogue au Bot trait (crates/morpion-arena/src/bot.rs).
Le binaire Rust est la référence reproductible canonique. Le script Python de référence — livré séparément — est un artefact pédagogique plus petit, destiné aux chercheurs qui ne veulent pas builder Rust. morpion-cli (ADR-0002) reste la référence du format wire, sans changement.
Le travail est léger : le Bot trait existant couvre l'essentiel du game-loop ; le delta est une trait LlmBackend avec une méthode propose_move(&ObsView, &Prompt) -> Result<MoveProposal>.
3. Schéma de submission (canonical payload)
Champs alphabétiquement ordonnés (ADR-0002), figés dès v1 :
{
"bot_run_token": "<base64url HMAC envelope from /arena/challenge>",
"episodes": [
{
"final_score": 24,
"history": [/* MoveWire[] — canonical move order, ADR-0006 */],
"model_id": "anthropic/claude-opus-4-7",
"prompt_template_sha256": "<hex sha256 of prompt body>",
"seed": 17,
"started_at": "<RFC3339 day-granularity>"
}
],
"prompt_body": "<MIT-licensed prompt template, opaque bytes server-side>",
"publish_prompt": true,
"submitter_label": "<vendor>:<model>:<rev>"
}
prompt_bodyest obligatoire (toujours présent dans le payload).publish_prompt: falseroute l'entrée vers le unranked tier (D2).prompt_template_sha256est calculé serveur-side et comparé au client pour intégrité.bot_run_tokenest validé puis brûlé (MORPION_ARENA_USED_NONCES).- Le serveur ne lit jamais
prompt_bodydans une boucle LLM (C5).
4. Identity & namespace contract
Reprend l'invariant ADR-0012 §1 (trois primitives normatives) et l'étend à l'arène. Aucun namespace ne contient les clefs d'un autre.
| Namespace | Owner | Schéma de clef | Doit ne jamais contenir |
|---|---|---|---|
MORPION_HANDLES | humains (PWA familiale) | handle:<name> | un model_id de bot |
MORPION_BOTS | bots (arena) | bot:<vendor>:<model>:<rev> | un handle humain |
MORPION_ARENA_USED_NONCES | bots (arena) | nonce:<sha256> | quoi que ce soit hors du TTL nonce |
R2 : un seul bucket physique, deux préfixes lisibles :
| Préfixe | Owner | Écrit par |
|---|---|---|
traces/ | humains | dist-play/functions/save.js (ADR-0010) |
arena/ | bots | arena/functions/submit.js (livraison sibling B) |
Les écritures arena ne touchent jamais traces/. Le _shared.js:json() de dist-play/functions/ ne peut pas être importé tel quel par l'arène : il poserait Access-Control-Allow-Origin: * (ligne 29) et créerait le CORS bleed (attaque xi). L'arène fork ou override un json_arena() avec un allowlist explicite (au minimum : son propre origin, optionnellement les origins documentés des reference clients).
Architecture
submitter machine (LLM lives here)
┌─────────────────────────────────┐
│ morpion-arena-llm binary │
│ ─ match_runner.rs game loop │
│ ─ LlmBackend trait calls │
│ ─ holds vendor API key │
└────────────────┬────────────────┘
│
① GET /arena/challenge
◀──── { nonce, expires_at, sig }
│
│ run N episodes locally,
│ pay LLM tokens
│
② POST /arena/submit
────▶ { bot_run_token, episodes,
prompt_body, publish_prompt,
submitter_label }
│
▼
┌─────────────────────────────────────────────────────────┐
│ morpion-arena.pages.dev (separate Pages project) │
│ │
│ GET /arena/challenge → HMAC envelope, single-use, │
│ TTL'd in MORPION_ARENA_… │
│ │
│ POST /arena/submit → verify HMAC ▸ burn nonce ▸ │
│ replay() in pure Rust ▸ │
│ R2 PUT arena/<entry>.ndjson ▸ │
│ KV PUT MORPION_BOTS:<key> │
│ │
│ GET /arena/leaderboard → recompute Leaderboard │
│ (BotStats schema) on each │
│ build, deterministic order │
└────────────────┬────────────────────────────────────────┘
│
│ HTML page consumes JSON
▼
┌─────────────────────────────────┐
│ arena public page │
│ ─ hero distribution chart │
│ ─ humans + bots juxtaposed │
│ ─ replay viewer (wow) │
│ ─ methodology + prompt links │
└─────────────────────────────────┘
Out-of-scope
Cet ADR refuse explicitement, ou diffère, les options suivantes. Chaque ligne différée est un suivi non engageant.
| Item | Statut |
|---|---|
| Multi-modal arena track (PNG payload, leaderboard parallèle) | Suivi non engageant post-ship |
| Elo / pairwise ranking entre modèles | Suivi non engageant |
| Tournament mode (épisodes head-to-head) | Suivi non engageant |
| Server-side LLM proxy | Refusé (C1, C2). Économiquement insurvivable, vecteur d'attaque iv. |
| Judge model in validation loop | Refusé (C5). Garde-fou normatif. |
| Per-move HMAC chain | Refusé (qualifié de « semver bear-trap » par la revue). |
Stateful referee endpoints (start/move/submit) | Refusé (D1, race conditions, KV bloat). |
Format ?format= knob (alternate wire formats) | Refusé (C9, attack-surface multiplication). |
legal_moves enumerated in canonical prompt | Refusé (D4). |
Migration / threat-model
Reprend la table de 12 attaques issue de l'analyse adversariale, avec verdict post-ADR :
| # | Attaque | Sévérité | Verdict ADR |
|---|---|---|---|
| i | Server-side rule access (bot demande au serveur les coups) | NON-ATTAQUE | Le puzzle est raisonner, pas cacher la règle. ADR ne prend rien. |
| ii | Overfitting via \(N\to\infty\) submissions | MED | C8 (min-n gate) + C4 (challenge nonce single-use, rate-limit) atténuent. Résiduel accepté. |
| iii | Mechanical-Turk smuggling (humain joue sous étiquette bot) | RÉSIDUEL ACCEPTÉ | Indistinguible côté serveur d'un modèle lent. La doctrine pseudonyme d'ADR-0012 ne se contre pas par identification — par invariant. |
| iv | Economic DoS (bot DDoS la clef API serveur) | RÉSOLU PAR C2 | Pas de clef API serveur — surface inexistante. |
| v | Trace forgery (faux trace gagnant) | RÉSOLU PAR C5+C7 | replay() pur Rust déterministe ; replay_check_passed: true obligatoire. Les traces forgées échouent au replay. |
| vi | Prompt-injection of validator (LLM judge prompt-jacké) | RÉSOLU PAR C5 | Pas de judge model — pas de surface d'injection. Garde-fou normatif. |
| vii | Off-by-one segment validation (forgery via used_segments mal validé) | RÉSOLU PAR C5+ADR-0007 | replay() valide les used_segments canonicalement (ADR-0007). |
| viii | KV pollution into MORPION_HANDLES | RÉSOLU PAR C3 | Namespace dédié MORPION_BOTS. Aucune écriture arena ne peut atteindre MORPION_HANDLES. |
| ix | R2 cost amplification (storage flood) | Suivi non engageant | Cap dur sur episodes.length par submission, cap sur prompt_body size, rate-limit Cloudflare-edge. À détailler dans la livraison Functions. |
| x | Submission size DoS (multi-MB payloads) | Suivi non engageant | Cap explicite Content-Length \(\leq\) 1 MB par submission. À détailler dans la livraison Functions. |
| xi | CORS bleed via shared _shared.js:json() wildcard ACAO | RÉSOLU PAR C3 | L'arena fork/override json() avec allowlist explicite. Ne pas importer dist-play/functions/_shared.js tel quel. |
| xii | Hash-replay d'un trace gagnant antérieur | RÉSOLU PAR C4 + upstream-feedback fix | Le challenge nonce est single-use, marqué dans MORPION_ARENA_USED_NONCES après submission. MAIS : nécessite que le pattern MORPION_USED_CANCEL_TOKENS soit câblé d'abord — voir upstream-feedback signal ci-dessous. |
Upstream-feedback signal — MORPION_USED_CANCEL_TOKENS est commenté
dist-play/wrangler.toml lignes 24–27 laissent le binding MORPION_USED_CANCEL_TOKENS commenté, dormant. Le pattern issueCancelToken/verifyCancelToken que C4 réutilise (et qui sous-tend la défense contre l'attaque xii) dépend de ce KV pour la marque single-use. Tant qu'il n'est pas câblé :
- l'arène ne peut pas garantir l'invariant single-use du nonce ;
- l'attaque xii (hash-replay d'un trace gagnant) reste triviale.
Conséquence opérationnelle :
- L'arène ne ship pas avant que le binding
MORPION_USED_CANCEL_TOKENS(côtédist-play) soit câblé, ou avant qu'un binding équivalent ne soit câblé directement côté arena (MORPION_ARENA_USED_NONCES, indépendant). - La revue recommande la seconde voie (séparation C3) : l'arène n'hérite pas du KV de
dist-play; elle câble son propreMORPION_ARENA_USED_NONCESdans sonwrangler.tomlséparé. Le câblage deMORPION_USED_CANCEL_TOKENScôtédist-playreste un travail indépendant (dette technique de la PWA familiale) et est sorti du scope de l'arène.
Open questions
Cinq questions restent ouvertes et seront tranchées par les livraisons enfant ou par un follow-up ADR. Aucune ne bloque l'invariant.
- Min-
ngate exact (C8). Hypothèse de travail :n_min = 30épisodes pour publiermax_scorecomme rang headline. À calibrer sur les données simulées. - Cap
episodes.lengthpar submission (attaque ix/x). Hypothèse de travail : 100 épisodes max par POST. - Forme du
submitter_label(<vendor>:<model>:<rev>proposé). Le format exact (séparateurs, casse, longueur max) est à fixer. - TTL du nonce (
expires_atdans l'enveloppe HMAC). Hypothèse de travail : 1 heure. À calibrer en fonction de la durée typique de N épisodes côté submitter. - Allowlist CORS arena — origins explicitement autorisés en cross-origin. Hypothèse de travail :
https://morpion-arena.pages.devonly ; les reference clients (Rust, Python) appellent en server-to-server, pas de CORS.
Invariant
Le serveur de l'arène frontier-models est un recorder + replay validator, jamais un runtime, jamais un proxy LLM. Aucune clef API LLM n'est détenue serveur-side ; aucun judge model n'intervient dans la boucle de validation. Le validateur est
crates/morpion-arena/src/replay.rsen Rust pur. Les humains et les bots sont juxtaposés mais jamais classés ensemble —MORPION_HANDLESn'accueille aucunmodel_id,MORPION_BOTSn'accueille aucun handle humain. Le format wire serveur estObsViewJSON (crates/morpion-arena/src/wire.rs), unique et sans variantes. Tout changement de cette répartition est un breaking change qui exige un nouvel ADR.
Derivation — Issue d'une revue de design interne convergeant sur cinq angles : (1) architecture recorder + replay validator avec séparation dure d'avec la PWA familiale dist-play, hard split humains/bots, best-of-N + distribution ; (2) format wire canonique JSON+ASCII, refus d'énumérer legal_moves, distribution chart en hero, track multi-modal différé en suivi non engageant ; (3) modèle 3 endpoints avec enveloppe HMAC smallest-correct, mapping sur ADR-0012 sans nouvelle primitive, namespace dédié MORPION_BOTS, ObsView canonique ; (4) doctrine one-product/one-URL, replay viewer comme effet wow, prompts MIT obligatoires (cohérent avec la doctrine privé \(\to\) commun, jamais l'inverse déjà fixée par ADR-0010) ; (5) table de 12 attaques avec 8 ship-blockers + 4 résiduels, attaques vi/vii/viii/ix/xi/xii cataloguées, et le signal upstream-feedback sur MORPION_USED_CANCEL_TOKENS.