DEV Community

Cover image for Cosa invia davvero Claude Code quando propone un'app a un Room di osFoundry
BBT for BBT Italiano

Posted on

Cosa invia davvero Claude Code quando propone un'app a un Room di osFoundry

Questo è il tool che Claude Code chiama per mettere una piccola app in un Room cloud di osFoundry, così come il server in produzione lo elenca in tools/list (recuperato senza token e ridotto alle parti che contano):

{
  "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"]}]
}
Enter fullscreen mode Exit fullscreen mode

Accanto ci sono altri quattro tool per i Room: read_room, list_room_apps (limit 1–50), read_room_app_status (richiede room_id e instance_id) e propose_room_repo_app (un repository_id e una version_id al posto di files). Tutti e tre i tool di lettura sono osfoundry:read e readOnlyHint: true. Entrambi i tool di proposta sono osfoundry:write e destructiveHint: true, ed è questo che fa chiedere conferma a Claude Code prima di chiamarli.

Il punto è il verbo. Il tool non crea niente nel Room. Crea una proposta, ed è una persona a trasformarla in una tab del Room. Di seguito: cosa impone lo schema e cosa lascia al server, quanto pesa una proposta, perché la chiave di idempotenza è obbligatoria e come un client dovrebbe aspettare un essere umano. Tutto è stato misurato con un sostituto del client scritto in Python, dato che su questo Mac non è installato nessun binario claude.

Cosa blocca lo schema, e cosa lascia al server?

room_proto.py valida le chiamate con jsonschema contro l'inputSchema reale qui sopra (salvato come tools-list.json). Altre due regole derivano da come osFoundry gestisce le proposte di snippet. L'entry point del runtime del Room, src/App.tsx, deve essere presente. E ogni percorso deve essere un percorso relativo sicuro. Lo script applica entrambe sopra lo schema:

$ 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
Enter fullscreen mode Exit fullscreen mode

Metà delle chiamate sbagliate supera il JSON Schema. Le chiavi di percorso in un oggetto files non si possono vincolare con additionalProperties. Servirebbe propertyNames con un pattern, e un pattern che rifiuti .. dopo la normalizzazione diventa presto brutto. Quindi lo schema dice "mappa di stringhe" e le chiavi le giudica il server. Il additionalProperties: false al livello più alto, invece, fa un lavoro vero. Un modello che si inventa "visibility": "public" riceve un errore di schema invece di un campo ignorato in silenzio.

Se costruisci un client, fai lo stesso controllo prima della chiamata. Una proposta rifiutata costa un round trip e, peggio ancora, i token di output per riscrivere i file.

Quanto pesa una proposta?

L'app dell'esempio è un timer per lo standup: src/App.tsx (63 righe) e src/styles.css (14 righe).

== 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)
Enter fullscreen mode Exit fullscreen mode

L'envelope JSON-RPC aggiunge l'11.9% rispetto al sorgente grezzo, soprattutto per a capo e virgolette con escape. Non è questo il costo che conta. Ogni byte in files è output del modello: Claude Code scrive il file e poi lo riscrive come argomento del tool. Una proposta con due file sono circa 1,100 token di argomenti. Un'app da 21 file, ottenuta ripetendo lo stesso componente, ne fa 14,143. I token sono contati con tiktoken cl100k_base, il tokenizer di OpenAI, quindi vanno presi come indicativi.

Lo schema non pubblica nessun limite in byte per files. In pratica il limite è chi revisiona. La card di approvazione mostra ogni file per intero sotto "Review all proposed files (N)", ognuno nel proprio riquadro scorrevole. Nessuno legge 21 file su una card. Per qualcosa di quelle dimensioni è più adatto propose_room_repo_app: punta a una versione fissata di un repository GitNest nello stesso progetto, e la card rimanda a "Review repository code before approving" invece di incollare il codice.

Cosa garantisce il passaggio di revisione?

La descrizione del tool lo dice chiaramente: lo snippet "appears in the cloud Room launcher only after its owner reviews and approves the full code". Nel Room, il launcher è la barra delle tab delle app. La card riporta "Approval makes this code available in the Room app launcher. Review the files before approving." "Approve and create" inserisce la tab. "Decline" la scarta. I membri del Room non possono aprire una proposta in sospeso, quindi uno snippet non revisionato non gira mai nella sessione di nessuno.

Così il problema dei retry passa al client. Con MCP su HTTP una risposta può andare persa dopo che il server ha già agito. Se il client allora riprova con una chiave nuova, l'owner si ritrova due card per una sola app. room_proto.py lo simula con un server in memoria che salva le proposte per chiave, 1,000 proposte e una probabilità del 5% che ogni risposta vada persa:

== 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
Enter fullscreen mode Exit fullscreen mode

Con un UUID nuovo a ogni tentativo, 74 delle 1,074 card sono duplicati che l'owner deve riconoscere e rifiutare. Con una chiave derivata dal contenuto, "snip-" + sha256(room_id, name, files)[:32], un retry porta la stessa chiave e i duplicati sono 0. Una modifica voluta ai file produce una chiave nuova e una card nuova, che è proprio quello che vuoi. Il minimo di 8 caratteri dello schema tiene fuori chiavi come "1". In nessuna delle due esecuzioni qualcosa è arrivato nel launcher prima dell'approvazione. Quella parte è compito del server, non del client.

Come dovrebbe Claude Code aspettare l'owner?

initialize restituisce "capabilities": {"tools": {"listChanged": false}} e nient'altro. Non c'è nessun push del tipo "la tua proposta è stata approvata". Il client ha tre opzioni. Può chiedere all'utente di avvisare quando ha approvato, può fare polling su read_room_app_status (oppure su list_room_apps finché non compare la nuova istanza), oppure può riprendere poll_resource_events per il Room, che accetta resource_type: "room". Senza token non ho verificato quali eventi scriva lì un'approvazione, quindi la simulazione interroga lo stato.

L'input è il tempo di revisione umano. Lo script lo estrae da una distribuzione log-normale con mediana di 6 minuti e un budget di un'ora. È un'ipotesi, non una misura, ma la forma (quasi tutti rispondono in pochi minuti, qualcuno ci mette mezz'ora) è la parte realistica:

== 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
Enter fullscreen mode Exit fullscreen mode

Un polling ogni 5 secondi si accorge dell'approvazione entro 5 s, al prezzo di 112 chiamate per proposta, e ogni chiamata è un turno del modello speso sull'output del tool. Un intervallo fisso di 30 secondi fa 19 chiamate con 28 secondi nel caso peggiore. Il backoff fino a un tetto di 2 minuti scende a 11 chiamate, con un ritardo p95 di 111 s. 14 delle 2,000 revisioni hanno richiesto più di un'ora, e tutte le strategie ci hanno rinunciato allo stesso modo. In una sessione interattiva, la risposta corretta più economica è zero chiamate: chiudere il turno con "approve it under Connected → Needs your approval and tell me" (approvala in Connected → Needs your approval e fammi sapere) e poi fare una sola chiamata a list_room_apps. Il polling conviene solo quando Claude Code ha altro lavoro da fare nel frattempo.

Round trip reali, per avere un'idea delle grandezze: dieci chiamate non autenticate per tipo da questo Mac in Giappone verso l'endpoint in produzione.

$ 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"
Enter fullscreen mode Exit fullscreen mode

Circa 0.6 s a chiamata. Quindi un ciclo da 5 secondi passa un decimo del suo tempo in rete, e un turno del modello per ogni chiamata costa molto di più.

Cosa significa "build state" per uno snippet?

read_room_app_status restituisce "the safe launcher and latest build state of one app", senza config, file, log o secret. Uno snippet non ha una build lato server. Il Room compila i suoi file dove viene aperta la tab: in un iframe sandboxed con Sucrase nell'app desktop, in una sandbox Sandpack nel browser. Anche la strada del repository è fissata a una versione, e la sua card di approvazione dice "This path does not build or deploy a bundle." Per entrambi i tipi di proposta, quello da aspettare è la voce nel launcher. Un build state compare quando qualcuno fa il deploy della tab, e questo si fa in osFoundry.

Perché non può raggiungere un Room locale?

read_room dice "Local Rooms are unavailable", e initialize riporta la regola generale: "Local-only data is unavailable to cloud clients." Un Room locale vive nel database dell'app desktop e non viene mai sincronizzato. L'endpoint MCP è un servizio cloud con un token OAuth limitato al workspace, e non ha nessuna strada verso un laptop specifico. Per raggiungerlo servirebbe un relay gestito dall'app desktop: attivato per dispositivo e per progetto, con uno stato online/offline e una allowlist di comandi. La superficie dei tool non finge il contrario. Lo stesso confine vale per i dati dentro uno snippet. Il suo storage SDK resta sul dispositivo di chi usa la tab, e nessuno dei tool di lettura restituisce contenuti del database.

Se lo colleghi tu

  • Valida le chiavi di percorso sul server anche se pubblichi uno schema. JSON Schema sa descrivere "mappa di stringhe", ma non "percorso relativo sicuro", e in questo test metà delle chiamate sbagliate erano problemi di percorso.
  • Deriva la chiave di idempotenza dal contenuto, non dal tentativo. Così le risposte perse non costano nulla, e una modifica reale ottiene comunque la sua card.
  • Lascia che sia l'attenzione di chi revisiona a fissare il limite di dimensione. Se la card mostra ogni file, tieni gli snippet a pochi file e manda le app più grandi come versione fissata di un repository.
  • Non far fare polling all'agente per aspettare un umano come comportamento predefinito. Fagli chiudere il turno e leggere lo stato una volta quando l'utente torna, e usa il backoff solo quando ha altro lavoro.
  • Tieni la lettura di ritorno essenziale. Restituisci identità e stato del launcher, mai config o dati dell'app, così i controlli di stato non diventano un modo per leggere ciò che le persone hanno salvato.
  • Scrivi nella descrizione del tool cosa è fuori portata ("Local Rooms are unavailable"), così il modello lo dice all'utente invece di riprovare.

Il lato Room, con la barra delle tab e le impostazioni, è nella pagina Room. Per il protocollo in generale, vedi la voce del glossario su MCP.

Top comments (0)