DEV Community

Cover image for Apa yang sebenarnya dikirim Claude Code saat mengusulkan app ke Room osFoundry
BBT for BBT Indonesia

Posted on

Apa yang sebenarnya dikirim Claude Code saat mengusulkan app ke Room osFoundry

Ini tool yang dipanggil Claude Code untuk memasukkan app kecil ke Room cloud di osFoundry, persis seperti yang dicantumkan server live di tools/list (diambil tanpa token, dipangkas ke bagian yang penting saja):

{
  "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

Di sebelahnya ada empat tool Room lain: read_room, list_room_apps (limit 1–50), read_room_app_status (butuh room_id dan instance_id) dan propose_room_repo_app (pakai repository_id dan version_id sebagai ganti files). Ketiga tool baca bertipe osfoundry:read dan readOnlyHint: true. Kedua tool usulan bertipe osfoundry:write dan destructiveHint: true, dan itulah yang membuat Claude Code minta izin dulu sebelum memanggilnya.

Kuncinya ada di kata kerjanya. Tool ini tidak membuat apa pun di Room. Yang dibuat adalah usulan, dan manusialah yang mengubahnya menjadi tab Room. Di bawah ini: apa yang dipaksakan schema dan apa yang diserahkan ke server, seberapa besar sebuah usulan, kenapa idempotency key wajib, dan bagaimana client sebaiknya menunggu manusia. Semua diukur dengan client pengganti berbasis Python, karena di Mac ini tidak ada binary claude yang terpasang.

Apa yang ditangkap schema, dan apa yang diserahkan ke server?

room_proto.py memvalidasi panggilan dengan jsonschema terhadap inputSchema live di atas (disimpan sebagai tools-list.json). Dua aturan tambahan berasal dari cara osFoundry menangani usulan snippet. Entry point runtime Room, src/App.tsx, harus ada. Dan setiap path harus berupa path relatif yang aman. Script menerapkan keduanya di atas 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

Separuh panggilan yang salah lolos dari JSON Schema. Key path di dalam objek files tidak bisa dibatasi dengan additionalProperties. Kamu butuh propertyNames dengan pattern, dan pattern yang menolak .. setelah normalisasi cepat jadi berantakan. Jadi schema cuma bilang "map berisi string", dan server yang menilai key-nya. Meski begitu, additionalProperties: false di level teratas benar-benar berguna. Model yang mengarang "visibility": "public" akan dapat error schema, bukan field yang diam-diam diabaikan.

Kalau kamu membuat client, jalankan pengecekan yang sama sebelum memanggil. Usulan yang ditolak memakan satu round trip dan, lebih parah lagi, token output untuk menulis ulang file-nya.

Seberapa besar sebuah usulan?

App di contoh ini adalah timer standup: src/App.tsx (63 baris) dan src/styles.css (14 baris).

== 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

Envelope JSON-RPC menambah 11.9% di atas source mentah, kebanyakan dari newline dan tanda kutip yang di-escape. Tapi bukan itu biaya yang penting. Setiap byte di files adalah output model: Claude Code menulis file-nya, lalu menulisnya lagi sebagai argumen tool. Usulan dua file sekitar 1,100 token argumen. App 21 file, dibuat dengan mengulang komponen yang sama, mencapai 14,143. Token dihitung dengan tiktoken cl100k_base, tokenizer milik OpenAI, jadi anggap saja angka perkiraan.

Schema tidak mencantumkan batas byte untuk files. Dalam praktiknya, batasnya adalah reviewer. Kartu persetujuan menampilkan setiap file secara utuh di bawah "Review all proposed files (N)", masing-masing di kotak scroll sendiri. Tidak ada yang mau membaca 21 file di satu kartu. Untuk ukuran sebesar itu, propose_room_repo_app lebih cocok: tool ini menunjuk ke versi yang di-pin dari repository GitNest di project yang sama, dan kartunya menautkan ke "Review repository code before approving" alih-alih menempelkan kodenya.

Apa yang dijamin oleh tahap review?

Deskripsi tool-nya menyebutkan dengan jelas: snippet "appears in the cloud Room launcher only after its owner reviews and approves the full code". Di Room, launcher adalah tab bar app. Kartunya bertuliskan "Approval makes this code available in the Room app launcher. Review the files before approving." "Approve and create" menambahkan tab-nya. "Decline" membuangnya. Usulan yang masih pending tidak bisa dibuka oleh anggota Room, jadi snippet yang belum direview tidak pernah jalan di sesi siapa pun.

Akibatnya, masalah retry jadi urusan client. MCP lewat HTTP bisa kehilangan respons setelah server sudah bertindak. Kalau client lalu mencoba ulang dengan key baru, owner mendapat dua kartu untuk satu app. room_proto.py memodelkan ini dengan server in-memory yang menyimpan usulan berdasarkan key, 1,000 usulan, dan peluang 5% setiap respons hilang:

== 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

Dengan UUID baru tiap percobaan, 74 dari 1,074 kartu adalah duplikat yang harus dikenali dan ditolak owner. Dengan key yang diturunkan dari konten, "snip-" + sha256(room_id, name, files)[:32], retry membawa key yang sama dan jumlah duplikatnya 0. Perubahan file yang disengaja menghasilkan key baru dan kartu baru, dan memang itu yang diinginkan. Minimum 8 karakter di schema mencegah key seperti "1". Di kedua run, tidak ada yang masuk launcher sebelum disetujui. Bagian itu tugas server, bukan client.

Bagaimana Claude Code sebaiknya menunggu owner?

initialize mengembalikan "capabilities": {"tools": {"listChanged": false}} dan tidak ada yang lain. Tidak ada push semacam "usulanmu sudah disetujui". Client punya tiga pilihan. Ia bisa meminta user memberi tahu setelah menyetujui, bisa polling read_room_app_status (atau list_room_apps sampai instance baru muncul), atau bisa melanjutkan poll_resource_events untuk Room, yang menerima resource_type: "room". Tanpa token, saya tidak memverifikasi event apa yang ditulis sebuah persetujuan di sana, jadi simulasinya polling status.

Inputnya adalah waktu review manusia. Script mengambilnya dari distribusi log-normal dengan median 6 menit dan budget satu jam. Itu asumsi, bukan hasil pengukuran, tapi bentuknya (kebanyakan orang menjawab dalam hitungan menit, beberapa butuh setengah jam) adalah bagian yang realistis:

== 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

Polling setiap 5 detik mendeteksi persetujuan dalam 5 s, dengan 112 panggilan per usulan, dan setiap panggilan adalah satu giliran model yang habis untuk output tool. Interval tetap 30 detik berarti 19 panggilan dengan kasus terburuk 28 detik. Backoff dengan batas 2 menit menurunkannya ke 11 panggilan, dengan lag p95 111 s. 14 dari 2,000 review butuh lebih dari satu jam, dan semua strategi menyerah pada review itu secara sama. Untuk sesi interaktif, jawaban benar yang paling murah adalah nol panggilan: akhiri giliran dengan "approve it under Connected → Needs your approval and tell me" (setujui di Connected → Needs your approval lalu kabari saya), lalu lakukan satu panggilan list_room_apps. Polling baru sepadan kalau Claude Code punya pekerjaan lain di sela-selanya.

Sebagai gambaran skala, ini round trip sungguhan: masing-masing sepuluh panggilan tanpa autentikasi dari Mac ini di Jepang ke endpoint live.

$ 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

Sekitar 0.6 s per panggilan. Jadi loop 5 detik menghabiskan sepersepuluh waktunya di jaringan, dan satu giliran model per panggilan jauh lebih mahal dari itu.

Apa arti "build state" untuk sebuah snippet?

read_room_app_status mengembalikan "the safe launcher and latest build state of one app", tanpa config, file, log, atau secret. Snippet tidak punya build di server. Room meng-compile file-nya di tempat tab dibuka: di iframe sandbox dengan Sucrase di app desktop, dan di sandbox Sandpack di browser. Jalur repository juga di-pin, dan kartu persetujuannya bertuliskan "This path does not build or deploy a bundle." Untuk kedua jenis usulan, yang perlu ditunggu adalah entri di launcher. Build state baru muncul setelah seseorang men-deploy tab-nya, dan itu dilakukan di osFoundry.

Kenapa tidak bisa menjangkau Room lokal?

read_room menyebut "Local Rooms are unavailable", dan initialize membawa aturan umumnya: "Local-only data is unavailable to cloud clients." Room lokal tinggal di database milik app desktop dan tidak pernah disinkronkan. Endpoint MCP adalah layanan cloud yang memegang token OAuth dengan scope workspace, dan tidak punya jalur ke satu laptop tertentu. Untuk menjangkaunya dibutuhkan relay yang dijalankan app desktop: diaktifkan per perangkat dan per project, dengan status online/offline dan allowlist perintah. Tool surface-nya tidak berpura-pura sebaliknya. Batas yang sama berlaku untuk data di dalam snippet. Storage SDK-nya tetap di perangkat orang yang memakai tab tersebut, dan tidak ada tool baca yang mengembalikan isi database.

Kalau kamu merangkainya sendiri

  • Validasi key path di server walaupun kamu sudah memublikasikan schema. JSON Schema bisa mendeskripsikan "map berisi string", tapi tidak bisa "path relatif yang aman", dan di tes ini separuh panggilan yang salah adalah masalah path.
  • Turunkan idempotency key dari konten, bukan dari percobaan. Respons yang hilang jadi tidak memakan biaya apa pun, dan edit sungguhan tetap dapat kartunya sendiri.
  • Biarkan perhatian reviewer yang menentukan batas ukuran. Kalau kartunya menampilkan semua file, batasi snippet ke beberapa file saja dan kirim app yang lebih besar sebagai versi repository yang di-pin.
  • Jangan biarkan agent polling menunggu manusia secara default. Suruh ia mengakhiri gilirannya dan membaca status sekali saat user kembali, dan pakai backoff hanya kalau ia punya pekerjaan lain.
  • Buat pembacaan balik seminimal mungkin. Kembalikan identitas dan state launcher, jangan pernah config atau data app, supaya cek status tidak jadi cara untuk membaca apa yang disimpan orang.
  • Tulis di deskripsi tool apa yang di luar jangkauan ("Local Rooms are unavailable"), supaya model memberi tahu user alih-alih terus mencoba ulang.

Sisi Room, lengkap dengan tab bar dan pengaturannya, ada di halaman Room. Untuk protokolnya secara umum, lihat entri glosarium MCP.

Top comments (0)