# Ce que Claude Code envoie vraiment quand il propose une app à un Room osFoundry

> Source: <https://dev.to/bbt-fr/ce-que-claude-code-envoie-vraiment-quand-il-propose-une-app-a-un-room-osfoundry-5392>
> Published: 2026-11-12 03:13:01+00:00

Voici l'outil que Claude Code appelle pour placer une petite app dans un Room cloud d'[osFoundry](https://osfoundry.io/fr), tel que le serveur en production le liste dans `tools/list` (récupéré sans token, réduit aux parties qui comptent) :

```
{
  "name": "propose_room_snippet_app",
  "description": "Prepare a Room snippet with source files. It appears in the cloud Room launcher only after its owner reviews and approves the full code in osFoundry.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "room_id": {"type": "string"},
      "name": {"type": "string", "maxLength": 200},
      "files": {"type": "object", "additionalProperties": {"type": "string"}},
      "idempotency_key": {"type": "string", "minLength": 8, "maxLength": 128}
    },
    "required": ["room_id", "name", "files", "idempotency_key"],
    "additionalProperties": false
  },
  "annotations": {"readOnlyHint": false, "destructiveHint": true, "openWorldHint": false},
  "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]
}
```

Quatre autres outils Room l'accompagnent : `read_room`, `list_room_apps` (limit 1–50), `read_room_app_status` (qui demande `room_id` et `instance_id`) et `propose_room_repo_app` (un `repository_id` et un `version_id` au lieu de `files`). Les trois outils de lecture sont `osfoundry:read` et `readOnlyHint: true`. Les deux outils de proposition sont `osfoundry:write` et `destructiveHint: true`, et c'est ce qui pousse Claude Code à demander la permission avant de les appeler.

Tout est dans le verbe. L'outil ne crée rien dans le Room. Il crée une proposition, et c'est une personne qui la transforme en onglet du Room. Au programme : ce que le schéma impose et ce qu'il laisse au serveur, ce que pèse une proposition, pourquoi la clé d'idempotence est obligatoire et comment un client devrait attendre un humain. Tout a été mesuré avec un client de substitution en Python, faute de binaire `claude` installé sur ce Mac.

`room_proto.py` valide les appels avec `jsonschema` contre l'`inputSchema` réel ci-dessus (enregistré sous `tools-list.json`). Deux règles supplémentaires découlent de la façon dont osFoundry traite les propositions de snippets. Le point d'entrée du runtime du Room, `src/App.tsx`, doit être présent. Et chaque chemin doit être un chemin relatif sûr. Le script applique les deux en plus du schéma :

``` bash
$ uv run --with jsonschema --with tiktoken python3 room_proto.py
== validation ==
ok               PASS
no App.tsx       REJECT  rule: src/App.tsx missing
absolute path    REJECT  rule: unsafe path '/etc/hosts'
dot-dot path     REJECT  rule: unsafe path 'src/../../x.ts'
backslash path   REJECT  rule: unsafe path 'src\\x.ts'
name 201 chars   REJECT  schema: 'nnnn…' is too long
key 7 chars      REJECT  schema: 'abcdefg' is too short
extra field      REJECT  schema: Additional properties are not allowed ('visibility' was unexpected)
non-string file  REJECT  schema: {'a': 1} is not of type 'string'
caught by published schema: 4, needs server rule: 4
```

La moitié des appels invalides passent le JSON Schema. Les clés de chemin d'un objet `files` ne peuvent pas être contraintes avec `additionalProperties`. Il faudrait `propertyNames` avec un pattern, et un pattern qui rejette `..` après normalisation devient vite illisible. Le schéma dit donc « map de strings » et c'est le serveur qui juge les clés. En revanche, le `additionalProperties: false` au niveau racine fait un vrai travail. Un modèle qui invente `"visibility": "public"` reçoit une erreur de schéma au lieu d'un champ ignoré en silence.

Si vous développez un client, faites la même vérification avant l'appel. Une proposition rejetée coûte un aller-retour et, pire encore, les tokens de sortie nécessaires pour réécrire les fichiers.

L'app de l'exemple est un minuteur de stand-up : `src/App.tsx` (63 lignes) et `src/styles.css` (14 lignes).

```
== size ==
src/App.tsx        2021 B   63 lines    583 tok
src/styles.css     1046 B   14 lines    399 tok
source bytes 3067, envelope bytes 3431, escape+frame overhead 11.9%
argument tokens (cl100k) 1141
   2 files: envelope    4463 B,   1394 tok
   6 files: envelope   12919 B,   4078 tok
  21 files: envelope   44639 B,  14143 tok
tools/list 45368 B for 59 tools; the 5 room tools 3482 B (939 tok)
```

L'enveloppe JSON-RPC ajoute 11.9% au code source brut, surtout à cause des retours à la ligne et des guillemets échappés. Ce n'est pas le coût qui compte. Chaque octet de `files` est de la sortie du modèle : Claude Code écrit le fichier, puis le réécrit en argument de l'outil. Une proposition à deux fichiers représente environ 1,100 tokens d'arguments. Une app à 21 fichiers, obtenue en répétant le même composant, en représente 14,143. Les tokens sont comptés avec tiktoken `cl100k_base`, le tokenizer d'OpenAI, donc à prendre comme des ordres de grandeur.

Le schéma ne publie aucune limite en octets pour `files`. En pratique, la limite, c'est la personne qui relit. La carte d'approbation affiche chaque fichier en entier sous « Review all proposed files (N) », chacun dans sa propre zone de défilement. Personne ne lit 21 fichiers sur une carte. Pour une app de cette taille, `propose_room_repo_app` convient mieux : il pointe vers une version épinglée d'un dépôt GitNest du même projet, et la carte renvoie vers « Review repository code before approving » au lieu de coller le code.

La description de l'outil le dit clairement : le snippet « appears in the cloud Room launcher only after its owner reviews and approves the full code ». Dans le Room, le launcher, c'est la barre d'onglets des apps. La carte indique « Approval makes this code available in the Room app launcher. Review the files before approving. » « Approve and create » insère l'onglet. « Decline » l'écarte. Les membres du Room ne peuvent pas ouvrir une proposition en attente, donc un snippet non relu ne s'exécute jamais dans la session de qui que ce soit.

Le problème des retries retombe donc sur le client. Avec MCP sur HTTP, une réponse peut se perdre alors que le serveur a déjà agi. Si le client relance alors avec une nouvelle clé, le propriétaire reçoit deux cartes pour une seule app. `room_proto.py` simule ce cas avec un serveur en mémoire qui stocke les propositions par clé, 1,000 propositions et 5% de chances que chaque réponse se perde :

```
== review gate, 1000 proposals, 5% lost responses ==
random   calls 1074  owner cards 1074  duplicates 74  in launcher before approval 0
content  calls 1057  owner cards 1000  duplicates 0  in launcher before approval 0
```

Avec un nouvel UUID à chaque tentative, 74 des 1,074 cartes sont des doublons que le propriétaire doit repérer et refuser. Avec une clé dérivée du contenu, `"snip-" + sha256(room_id, name, files)[:32]`, un retry porte la même clé et le nombre de doublons tombe à 0. Une modification volontaire des fichiers produit une nouvelle clé et une nouvelle carte, ce qui est exactement ce qu'on veut. Le minimum de 8 caractères du schéma écarte les clés du genre `"1"`. Dans aucun des deux runs, rien n'a atteint le launcher avant l'approbation. Ça, c'est le travail du serveur, pas du client.

`initialize` renvoie `"capabilities": {"tools": {"listChanged": false}}` et rien d'autre. Il n'y a aucun push du type « votre proposition a été approuvée ». Le client a trois options. Il peut demander à l'utilisateur de le prévenir une fois l'approbation faite, il peut faire du polling sur `read_room_app_status` (ou sur `list_room_apps` jusqu'à ce que la nouvelle instance apparaisse), ou il peut reprendre `poll_resource_events` pour le Room, qui accepte `resource_type: "room"`. Sans token, je n'ai pas vérifié quels événements une approbation y écrit, donc la simulation interroge le statut.

Le temps de revue humain est la donnée d'entrée. Le script le tire d'une loi log-normale de médiane 6 minutes, avec un budget d'une heure. C'est une hypothèse, pas une mesure, mais la forme (la plupart répondent en quelques minutes, quelques-uns mettent une demi-heure) est la partie réaliste :

```
== polling while the owner reviews (n=2000, median review 6 min, 1 h budget) ==
review delay p50 357s  p95 1816s
fixed 5s                   calls mean  111.8  lag p50   2.4s  p95    4.7s  gave up 14
fixed 30s                  calls mean   19.0  lag p50  14.5s  p95   28.4s  gave up 14
backoff 5s x1.5 cap 120s   calls mean   10.8  lag p50  47.0s  p95  111.0s  gave up 14
backoff 10s x2 cap 300s    calls mean    6.1  lag p50 105.2s  p95  273.8s  gave up 14
```

Un polling toutes les 5 secondes repère l'approbation en moins de 5 s, pour 112 appels par proposition, et chaque appel est un tour du modèle dépensé en sortie d'outil. Un intervalle fixe de 30 secondes, c'est 19 appels avec 28 secondes dans le pire cas. Un backoff plafonné à 2 minutes descend à 11 appels, avec une latence p95 de 111 s. 14 des 2,000 revues ont pris plus d'une heure, et toutes les stratégies ont abandonné de la même façon. Pour une session interactive, la réponse correcte la moins chère, c'est zéro appel : terminer le tour avec « approve it under Connected → Needs your approval and tell me » (approuvez-la dans Connected → Needs your approval et prévenez-moi), puis faire un seul appel à `list_room_apps`. Le polling n'en vaut la peine que si Claude Code a autre chose à faire entre-temps.

Des allers-retours réels, pour fixer l'ordre de grandeur : dix appels non authentifiés de chaque type depuis ce Mac au Japon vers l'endpoint en production.

``` bash
$ python3 probe.py
initialize                       HTTP 200     308 B  median    680 ms  min   358  max   968
tools/list                       HTTP 200   45368 B  median    687 ms  min   429  max  1253
tools/call read_room (no token)  HTTP 401     337 B  median    559 ms  min   427  max   847
  www-authenticate: Bearer resource_metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp", error="invalid_token", error_description="Connect osFoundry to continue"
```

Environ 0.6 s par appel. Une boucle de 5 secondes passe donc un dixième de son temps sur le réseau, et un tour de modèle par appel coûte bien plus que ça.

`read_room_app_status` renvoie « the safe launcher and latest build state of one app », sans config, fichiers, logs ni secrets. Un snippet n'a pas de build côté serveur. Le Room compile ses fichiers là où l'onglet est ouvert : dans une iframe sandboxée avec Sucrase dans l'app desktop, dans une sandbox Sandpack dans le navigateur. La voie du dépôt est elle aussi épinglée, et sa carte d'approbation indique « This path does not build or deploy a bundle. » Pour les deux types de proposition, c'est l'entrée dans le launcher qu'il faut attendre. Un build state n'apparaît qu'une fois que quelqu'un déploie l'onglet, et ça se fait dans osFoundry.

`read_room` répond « Local Rooms are unavailable », et `initialize` porte la règle générale : « Local-only data is unavailable to cloud clients. » Un Room local vit dans la base de données propre de l'app desktop et n'est jamais synchronisé. L'endpoint MCP est un service cloud qui détient un token OAuth limité au workspace, et il n'a aucun chemin vers un laptop en particulier. Pour l'atteindre, il faudrait un relais géré par l'app desktop : activé par appareil et par projet, avec un état en ligne/hors ligne et une allowlist de commandes. La surface d'outils ne prétend pas le contraire. La même frontière s'applique aux données d'un snippet. Son stockage SDK reste sur l'appareil de la personne qui utilise l'onglet, et aucun outil de lecture ne renvoie le contenu de la base.

Le côté Room, avec la barre d'onglets et les réglages, est sur la [page Room](https://osfoundry.io/fr/platform/rooms-detail). Pour le protocole en général, voir l'[entrée du glossaire sur MCP](https://osfoundry.io/fr/glossary/mcp).
