<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BBT Indonesia</title>
    <description>The latest articles on DEV Community by BBT Indonesia (bbt-id).</description>
    <link>https://dev.to/bbt-id</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15902%2Fcef63d33-3268-4b15-a0dd-120e56fe9da2.png</url>
      <title>DEV Community: BBT Indonesia</title>
      <link>https://dev.to/bbt-id</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bbt-id"/>
    <language>en</language>
    <item>
      <title>Cara membiarkan chat Claude mengusulkan perubahan kode GitNest yang kamu setujui di osFoundry</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:55:34 +0000</pubDate>
      <link>https://dev.to/bbt-id/cara-membiarkan-chat-claude-mengusulkan-perubahan-kode-gitnest-yang-kamu-setujui-di-osfoundry-4j2o</link>
      <guid>https://dev.to/bbt-id/cara-membiarkan-chat-claude-mengusulkan-perubahan-kode-gitnest-yang-kamu-setujui-di-osfoundry-4j2o</guid>
      <description>&lt;p&gt;Di akhir panduan ini, percakapan Claude yang terhubung ke &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; bisa membaca repository GitNest, mencari di kodenya, dan menyiapkan perubahan: sebuah task, commit file, change set, retry check, atau pull request. Tidak ada yang terjadi saat Claude memintanya. Setiap aksi masuk sebagai usulan di "Needs your approval" (menunggu persetujuanmu) di osFoundry, lengkap dengan file, branch, dan commit persisnya di kartu, dan baru berjalan saat kamu klik Approve. Claude tidak punya tool yang menulis langsung ke repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Siapa mengerjakan apa
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sisi Claude&lt;/th&gt;
&lt;th&gt;Sisi osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Akun Claude kamu, tidak berubah&lt;/td&gt;
&lt;td&gt;Akun osFoundry kamu sudah menyetujui koneksi sekali&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yang bisa dilihat&lt;/td&gt;
&lt;td&gt;Apa yang dikembalikan tool baca&lt;/td&gt;
&lt;td&gt;Repository GitNest yang ditetapkan ke project tempat koneksi berada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Membaca kode&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;search_gitnest_code&lt;/code&gt;: potongan baris yang dibatasi&lt;/td&gt;
&lt;td&gt;Tanpa file biner, tanpa isi file utuh, tanpa kredensial atau binding lokal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mengubah kode&lt;/td&gt;
&lt;td&gt;Tujuh tool &lt;code&gt;propose_gitnest_…&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Satu usulan per panggilan, menunggu kamu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siapa yang menyetujui&lt;/td&gt;
&lt;td&gt;Tidak ada di sisi Claude&lt;/td&gt;
&lt;td&gt;Kamu, pemilik koneksi, selama masih punya peran di repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Siapa yang menulis&lt;/td&gt;
&lt;td&gt;Tidak ada&lt;/td&gt;
&lt;td&gt;osFoundry menjalankan aksi yang disetujui, tercatat atas nama koneksi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token provider&lt;/td&gt;
&lt;td&gt;Tidak pernah dikirim&lt;/td&gt;
&lt;td&gt;Akun GitHub/GitLab yang kamu tautkan hanya dipakai saat kamu menyetujui PR provider&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Persiapan
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Hubungkan chat sekali saja
&lt;/h3&gt;

&lt;p&gt;Kalau chat Claude belum terhubung ke osFoundry, bagian ini adalah alur custom connector biasa: buat koneksi lewat ikon kunci → Connected, tambahkan alamat servernya di Claude sebagai custom connector, lalu setujui satu workspace. Anthropic menjelaskan sisi Claude di &lt;a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp" rel="noopener noreferrer"&gt;panduan custom connector&lt;/a&gt; miliknya. Sisa panduan ini mengasumsikan koneksi sudah menampilkan "Connected."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Masukkan repository ke sebuah project
&lt;/h3&gt;

&lt;p&gt;Koneksi tidak pernah melihat repository GitNest dengan sendirinya. Repository harus termasuk dalam project tempat koneksi berada.&lt;/p&gt;

&lt;p&gt;Buka repository di GitNest dan cari bagian Project. Teksnya berbunyi "Connected agents in this project can use approved GitNest read tools when their connection owner still has repository access." Hanya pemilik repository yang mendapat pemilih project; orang lain hanya melihat nama project sebagai teks biasa. Pilih project-nya. osFoundry mengonfirmasi dengan "Repository project updated."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Tambahkan koneksi ke project itu
&lt;/h3&gt;

&lt;p&gt;Kembali ke ikon kunci → Connected, pilih koneksinya, dan di bawah Projects tambahkan ke project yang sama. Repository GitNest milik project itu ikut masuk, bersama Notes, knowledge base, dan konten tersinkron lainnya.&lt;/p&gt;

&lt;p&gt;Buka Tools. "All supported tools are on by default", dan daftarnya menampilkan setiap tool dengan spasi sebagai pengganti garis bawah. Yang untuk GitNest adalah:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read gitnest repository        propose gitnest work
search gitnest code            propose gitnest change set
list gitnest work              propose gitnest server composition
read gitnest work              propose gitnest file import
list gitnest saved results     propose gitnest check rerun
list gitnest checks            propose gitnest native publication
                               propose gitnest provider publication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgic1ojuj528iargl9xou.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgic1ojuj528iargl9xou.jpg" alt="Checklist Tools sebuah koneksi Claude, semuanya aktif secara default" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Checklist Tools sebuah koneksi Claude, semuanya aktif secara default&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Kalau kamu ingin Claude berhenti di commit dan tidak pernah menyiapkan pull request, hapus centang dua tool publication lalu klik Save tools. Bagaimanapun, usulan tetap butuh persetujuanmu. Menghapus centang hanya berarti Claude tidak bisa memintanya.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Minta lewat chat
&lt;/h3&gt;

&lt;p&gt;Di percakapan Claude, aktifkan connector osFoundry dari menu + → Connectors lalu minta dengan bahasa biasa. Kalau kamu menambahkan server yang sama ke Claude Code dengan nama &lt;code&gt;osfoundry&lt;/code&gt;, tool-nya muncul di sana sebagai &lt;code&gt;mcp__osfoundry__propose_gitnest_file_import&lt;/code&gt; dan seterusnya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contoh jalannya
&lt;/h2&gt;

&lt;p&gt;Nama tool dan nama argumen di bawah ini adalah yang didaftarkan server osFoundry di &lt;code&gt;tools/list&lt;/code&gt;. Repository, prompt, ID, dan balasannya adalah konten contoh.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contoh

Kamu: Di osFoundry, tambahkan CONTRIBUTING.md ke demo-api yang menjelaskan cara
      menjalankan test (pakai pytest di tests/), dan buka task prioritas rendah
      untuk menautkannya dari README.

Claude → list_granted_resources {}
       ← project "API" · repository GitNest "demo-api"
Claude → read_gitnest_repository {"repository_id": "…"}
       ← repository native, branch default main
Claude → search_gitnest_code {"repository_id": "…", "query": "pytest", "branch": "main"}
       ← pyproject.toml: [tool.pytest.ini_options] · tests/test_limits.py: import pytest
Claude → propose_gitnest_file_import {"repository_id": "…", "branch": "main",
           "message": "Add CONTRIBUTING.md with test instructions",
           "files": [{"path": "CONTRIBUTING.md", "content": "# Contributing\n…"}],
           "idempotency_key": "…"}
       ← usulan menunggu pemilik
Claude → propose_gitnest_work {"repository_id": "…",
           "title": "Link CONTRIBUTING.md from README", "priority": "low",
           "idempotency_key": "…"}
       ← usulan menunggu pemilik

Claude: Saya sudah menyiapkan dua usulan di osFoundry: satu commit yang menambahkan
        CONTRIBUTING.md ke main, dan satu task untuk menautkannya dari README.
        Belum ada yang di-commit. Keduanya menunggu persetujuanmu.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Di osFoundry, header menampilkan "Claude research proposes GitNest file changes", dan panel koneksi mencantumkan keduanya di bawah "Needs your approval". Kartu commit bertuliskan "Commit GitNest files in demo-api", lalu "Commit to branch: main", pesan commit, dan "Review all proposed files (1)" yang terlipat dan bisa dibuka untuk melihat CONTRIBUTING.md lengkap. Di bawahnya: "Expires …", Approve and create, Decline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxr63iutg1ikyj4c9g8cb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxr63iutg1ikyj4c9g8cb.jpg" alt="Usulan publish GitNest di bawah Needs your approval" width="800" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Usulan publish GitNest di bawah Needs your approval&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Prompt kedua, untuk change set yang sudah ada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contoh

Kamu: Change set rate-limit sudah hijau? Kalau integration gagal, ulangi,
      dan begitu lolos, siapkan sebagai pull request.

Claude → list_gitnest_work {"repository_id": "…", "kind": "changes"}
       ← "Rate-limit fix" · versi 3
Claude → list_gitnest_checks {"repository_id": "…", "change_set_id": "…"}
       ← commit tersusun 9c1e47a…: unit lolos · integration gagal (selesai)
Claude → propose_gitnest_check_rerun {"repository_id": "…", "change_set_id": "…",
           "check_name": "integration", "expected_change_set_version": 3,
           "expected_composition_oid": "9c1e47a…", "idempotency_key": "…"}
       ← usulan menunggu pemilik
  … kamu menyetujui retry, hasilnya lolos …
Claude → propose_gitnest_native_publication {"repository_id": "…", "change_set_id": "…",
           "expected_change_set_version": 3,
           "expected_composition_oid": "9c1e47a…", "idempotency_key": "…"}
       ← usulan menunggu pemilik
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perhatikan apa yang harus dikirim Claude. Retry dan publication sama-sama menyebut versi change set dan commit tersusun yang dilihat Claude. Kalau ada yang menyusun ulang change set sebelum kamu klik, versi atau commit yang akan kamu setujui sudah bukan yang ada di kartu, dan pengecekan ulang osFoundry saat persetujuan menolak menjalankannya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang diminta setiap kartu untuk kamu setujui
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Judul kartu&lt;/th&gt;
&lt;th&gt;Tombol&lt;/th&gt;
&lt;th&gt;Yang terjadi saat disetujui&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create GitNest work&lt;/td&gt;
&lt;td&gt;Approve and create&lt;/td&gt;
&lt;td&gt;Membuat Todo dan work item GitNest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_change_set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create GitNest draft change set&lt;/td&gt;
&lt;td&gt;Approve and create&lt;/td&gt;
&lt;td&gt;Membuat change set draft dari hasil run yang tersimpan. Tidak ada yang disusun atau dipublikasikan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_server_composition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compose GitNest change set&lt;/td&gt;
&lt;td&gt;Approve and create&lt;/td&gt;
&lt;td&gt;Menyusun change set di server setelah mengecek ulang versinya&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_file_import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Commit GitNest files&lt;/td&gt;
&lt;td&gt;Approve and create&lt;/td&gt;
&lt;td&gt;Meng-commit hingga 20 file UTF-8 (total 200 KB) ke branch yang tertera di kartu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_check_rerun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Retry GitNest check&lt;/td&gt;
&lt;td&gt;Approve retry&lt;/td&gt;
&lt;td&gt;Memasukkan ulang satu check yang sudah selesai ke antrean untuk versi dan commit itu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_native_publication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Publish GitNest pull request&lt;/td&gt;
&lt;td&gt;Approve and publish&lt;/td&gt;
&lt;td&gt;Membuat branch publication dan membuka pull request di repository native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_provider_publication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Open a draft GitHub or GitLab pull request&lt;/td&gt;
&lt;td&gt;Approve and publish&lt;/td&gt;
&lt;td&gt;Push hanya branch change set tersebut dengan akun provider yang kamu tautkan dan membuka draft PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kartu yang merujuk ke change set memuat "Change set: … · expected version N", "Reviewed commit: …", dan tautan "Review change set before approving" yang membukanya di GitNest. Dua kartu publication masing-masing menambahkan satu baris tentang apa yang akan terjadi, misalnya "Approval creates a publication branch and opens a pull request in this native repository."&lt;/p&gt;

&lt;h2&gt;
  
  
  Di mana batasnya
&lt;/h2&gt;

&lt;p&gt;Koneksi Claude tidak bisa menulis ke GitNest. Route tulis milik GitNest sendiri menolak akses dengan scope agen (&lt;code&gt;denyAgentScope&lt;/code&gt; di route GitNest osFoundry), dan tool Connected tidak memanggilnya. Chat Claude hanya bisa mengajukan usulan, dan osFoundry sendiri yang menjalankan aksinya setelah kamu menyetujui, tercatat dengan koneksi sebagai sumbernya. Dengan begitu hanya ada satu jalur untuk setiap penulisan oleh agen, dan kamu bisa mengauditnya.&lt;/p&gt;

&lt;p&gt;Persetujuan dicek ulang tepat saat kamu klik. osFoundry memeriksa lagi bahwa koneksi masih aktif, repository masih berada di project tempat koneksi bergabung, kamu masih memegang peran di repository, dan usulan belum kedaluwarsa. Untuk composition, retry, atau publication, osFoundry juga mengecek versi change set dan commit-nya, dan untuk publication juga kebijakan risiko repository, check wajib, dan branch tujuan. Usulan yang ditolak, kedaluwarsa, atau usang tidak akan berjalan. Setiap tool propose menerima &lt;code&gt;idempotency_key&lt;/code&gt;, jadi panggilan yang diulang Claude setelah balasan terputus tidak dihitung sebagai permintaan kedua.&lt;/p&gt;

&lt;p&gt;Tidak ada yang di-deploy di sini. Paling jauh, sebuah usulan sampai di pull request: PR native GitNest, atau draft PR di GitHub atau GitLab. Merge, dan apa pun yang dipicu merge, tetap di tanganmu di GitNest atau di provider.&lt;/p&gt;

&lt;p&gt;Token provider Git kamu tidak pernah sampai ke Claude. PR provider memakai akun yang kamu tautkan di osFoundry, pada saat persetujuan, dan hanya push branch khusus milik change set itu, tidak pernah ref kamu yang lain.&lt;/p&gt;

&lt;p&gt;Pembacaan sengaja dibatasi. &lt;code&gt;search_gitnest_code&lt;/code&gt; mengembalikan potongan baris yang dibatasi, tidak pernah file biner atau file utuh. Pembacaan work, run, dan hasil tersimpan tidak menyertakan binding lokal dan kredensial. Untuk mengubah file yang sudah ada, tempel isi terbarunya ke chat supaya Claude bisa mengusulkan versi baru secara utuh. Menambah file baru bisa langsung dari potongan barisnya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selanjutnya
&lt;/h2&gt;

&lt;p&gt;Coba penulisan terkecil dulu: minta Claude mengusulkan satu file baru saja, misalnya &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, lalu buka kartunya dan bentangkan "Review all proposed files" sebelum kamu menyetujui. Cara GitNest sendiri menangani clone, push, dan access token ada di &lt;a href="https://osfoundry.io/id/features/gitnest" rel="noopener noreferrer"&gt;halaman fitur GitNest&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Kenapa osFoundry membuat Claude mengusulkan perubahan GitNest alih-alih langsung push?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:46:32 +0000</pubDate>
      <link>https://dev.to/bbt-id/kenapa-osfoundry-membuat-claude-mengusulkan-perubahan-gitnest-alih-alih-langsung-push-1k5b</link>
      <guid>https://dev.to/bbt-id/kenapa-osfoundry-membuat-claude-mengusulkan-perubahan-gitnest-alih-alih-langsung-push-1k5b</guid>
      <description>&lt;p&gt;Ini adalah tool yang dipakai chat Claude saat ingin memasukkan file ke repository GitNest lewat MCP connector &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt;. Tool ini diambil dari &lt;code&gt;tools/list&lt;/code&gt; publik milik endpoint live, di-fetch dengan curl pada 2026-11-06 (katalognya bisa dibaca tanpa token):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -X POST https://api.osfoundry.io/api/v1/external-mcp \
    -H 'content-type: application/json' -H 'accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"propose_gitnest_file_import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Prepare up to 20 UTF-8 files (200 KB total) for a native GitNest repository branch. The owner reviews every file and approves the commit in osFoundry. Repository GitNest must be enabled."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"repository_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"branch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"minItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;}}}},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;}}},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"openWorldHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perhatikan kata kerjanya. Tool ini "prepares", sedangkan owner yang "approves the commit". Call-nya sudah return sebelum ada apa pun yang menyentuh Git, dan tidak ada tool lain di connector ini yang menyentuh Git. &lt;code&gt;idempotency_key&lt;/code&gt; wajib diisi, bukan opsional, dan punya panjang minimum, jadi client tidak bisa mengirim key kosong atau satu karakter lalu menyebutnya idempotent.&lt;/p&gt;

&lt;p&gt;Sisa tulisan ini membahas kenapa bentuk seperti itu tepat untuk agent, dan apa yang harus dilakukan server supaya bentuk itu tetap jujur. Setelah itu, versi paling kecilnya dibangun ulang di atas repository Git sungguhan, lalu diukur apa yang rusak kalau beberapa bagiannya dihilangkan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang dikunci oleh tujuh tool propose?
&lt;/h2&gt;

&lt;p&gt;GitNest punya 13 tool di connector. Enam di antaranya hanya membaca: &lt;code&gt;read_gitnest_repository&lt;/code&gt;, &lt;code&gt;search_gitnest_code&lt;/code&gt;, &lt;code&gt;list_gitnest_work&lt;/code&gt;, &lt;code&gt;read_gitnest_work&lt;/code&gt;, &lt;code&gt;list_gitnest_saved_results&lt;/code&gt; dan &lt;code&gt;list_gitnest_checks&lt;/code&gt;. Tujuh sisanya diawali &lt;code&gt;propose_&lt;/code&gt;. Field wajibnya menunjukkan apa yang dikunci masing-masing tool pada saat model memanggilnya:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;&lt;code&gt;idempotency_key&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;expected_change_set_version&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;expected_composition_oid&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;openWorldHint&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_change_set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_server_composition&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_file_import&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;tidak&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_check_rerun&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_native_publication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;propose_gitnest_provider_publication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;wajib&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Makin besar dampaknya, makin banyak yang dikunci. Sebuah task hanya mengunci key-nya. Composing mengunci versi change set yang dibaca model. Menjalankan ulang check atau memublikasikan pull request mengunci versi itu sekaligus commit hasil compose yang persis. Kedua tool publication ditandai &lt;code&gt;openWorldHint: true&lt;/code&gt;, karena keduanya menghasilkan sesuatu di luar proposal store: sebuah pull request, dan untuk tool provider, sebuah branch yang di-push ke GitHub atau GitLab.&lt;/p&gt;

&lt;p&gt;Di review card milik owner, kunci-kunci itu muncul sebagai teks: "Change set: … · expected version 3", "Reviewed commit: 9c1e47a…". Yang di-approve owner adalah versi dan commit itu, bukan apa pun yang kebetulan terbaru saat klik-nya sampai.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kenapa tidak memberi agent token push saja?
&lt;/h2&gt;

&lt;p&gt;Karena push diputuskan saat model mengeluarkannya, sedangkan proposal diputuskan saat seseorang dengan hak akses yang masih berlaku melihatnya.&lt;/p&gt;

&lt;p&gt;Kode osFoundry menarik garis itu secara eksplisit. Route write GitNest dijaga oleh &lt;code&gt;denyAgentScope&lt;/code&gt;, dan bridge Maestro di dalam aplikasi ke GitNest hanya mengizinkan read. Connected tools tidak mengakali itu dengan token browser pinjaman. Setiap proposal disimpan, ditampilkan ke owner koneksi, lalu dieksekusi oleh writer milik osFoundry sendiri setelah di-approve, dengan atribusi ke koneksi tersebut. Percakapan Claude tidak pernah memegang credential yang bisa menulis ke repository, dan tidak pernah melihat token GitHub atau GitLab milik owner. Publication ke provider hanya mem-push branch khusus milik change set, dengan lease absent-ref, dan tidak pernah me-mirror ref lain yang tidak terkait.&lt;/p&gt;

&lt;p&gt;Harga dari pilihan itu adalah waktu. Jeda antara "proposed" dan "approved" adalah waktu manusia: beberapa menit, kadang sehari. Dalam jeda itu branch bergerak, seseorang meng-compose ulang change set, sebuah check selesai, owner kehilangan role di repository, koneksinya dicabut. Approval di osFoundry mengecek ulang koneksi, keanggotaan project, role repository owner saat ini, masa berlaku, dan untuk aksi change set juga versi dan commit yang persis, serta untuk publication juga risk policy dan check yang diwajibkan. "A declined, expired, or stale proposal must not execute" adalah satu baris di plan-nya.&lt;/p&gt;

&lt;p&gt;Tinggal dua mode kegagalan, dan keduanya jadi bahan rebuild:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Dua approval terhadap base yang sama. Masing-masing terlihat baik-baik saja.&lt;/li&gt;
&lt;li&gt;Retry setelah kegagalan parsial. Git sudah punya commit-nya, database belum tahu, lalu owner klik lagi.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Untuk yang kedua, plan osFoundry menjelaskan perbaikan yang sudah dirilis di Git service: file import mengirim proposal ID yang immutable sebagai idempotency key, dan service tersebut "updates the branch and replay marker atomically with compare-and-swap, so a database failure after the Git commit can be retried without another commit."&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild-nya
&lt;/h2&gt;

&lt;p&gt;Dua file, standard library ditambah git CLI (Apple Git 2.39.5), dijalankan di Mac Apple M1 yang sedang merender video untuk job lain juga:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;server.py&lt;/code&gt;, 225 baris: endpoint JSON-RPC di atas bare Git repository dan sebuah file sqlite.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;client.py&lt;/code&gt;, 203 baris: "Claude" yang di-script dan owner yang di-script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tidak ada binary Claude atau akun Claude yang terlibat. Client mengirim request &lt;code&gt;tools/call&lt;/code&gt; yang sejenis dengan yang dikirim chat yang terhubung. Tool di rebuild ini adalah &lt;code&gt;read_repo&lt;/code&gt;, &lt;code&gt;propose_file_import&lt;/code&gt; dan &lt;code&gt;read_proposal&lt;/code&gt;. Nama-nama itu buatan rebuild sendiri, dan tidak satu pun menulis ke Git. Owner memutuskan lewat route terpisah, &lt;code&gt;/owner/decide&lt;/code&gt;, yang menjawab token agent dengan &lt;code&gt;403 agent_scope_denied&lt;/code&gt;. Itulah ide &lt;code&gt;denyAgentScope&lt;/code&gt; dalam satu &lt;code&gt;if&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Satu perbedaan yang disengaja: rebuild ini juga mengunci base commit pada file import (&lt;code&gt;expected_base_oid&lt;/code&gt;, default-nya tip branch saat proposal dibuat). Schema &lt;code&gt;propose_gitnest_file_import&lt;/code&gt; milik osFoundry tidak punya field seperti itu. Anggap bagian itu sebagai pilihan desain rebuild, bukan deskripsi osFoundry.&lt;/p&gt;

&lt;p&gt;Intake proposal bersifat idempotent pada &lt;code&gt;(connection, idempotency_key)&lt;/code&gt;, dengan hash dari body, jadi key yang dipakai ulang dengan body berbeda menjadi error, bukan replay diam-diam:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO proposals (id,conn,idem_key,body_hash,branch,base_oid,files,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message,status,created_at,expires_at) VALUES (?,?,?,?,?,?,?,?,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pending&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(...))&lt;/span&gt;
&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM proposals WHERE conn=? AND idem_key=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Conflict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency_key_reused_with_different_body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approval membangun commit hanya dengan plumbing (&lt;code&gt;read-tree&lt;/code&gt; ke index sementara, &lt;code&gt;hash-object -w&lt;/code&gt;, &lt;code&gt;update-index --cacheinfo&lt;/code&gt;, &lt;code&gt;write-tree&lt;/code&gt;, &lt;code&gt;commit-tree -p &amp;lt;base&amp;gt;&lt;/code&gt;). Tidak ada satu pun dari itu yang menggeser ref. Lalu satu transaksi menggeser branch hanya jika branch itu masih menunjuk ke base yang di-review, sekaligus membuat marker ref yang dinamai sesuai proposal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;update refs/heads/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_oid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;create refs/proposals/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;proposal_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;prepare&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;commit&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;update-ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--stdin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;update&lt;/code&gt; dengan old value adalah compare-and-swap. &lt;code&gt;create&lt;/code&gt; gagal kalau ref-nya sudah ada. Git menerapkan keduanya atau tidak sama sekali. Sebelum membangun apa pun, approval mencari &lt;code&gt;refs/proposals/&amp;lt;id&amp;gt;&lt;/code&gt;. Kalau ada, berarti percobaan sebelumnya sudah sampai ke Git, jadi approval mencatat commit itu dan mengembalikannya sebagai replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang dilihat Claude yang di-script
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;python3 client.py scenario&lt;/code&gt;, dengan UUID proposal dipersingkat. Baris expiry berasal dari server kedua yang dijalankan dengan TTL 0.3 s:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read_repo                          -&amp;gt; head 29a940e8  (1 commit)
propose A  README.md  key=readme-fix-0001 -&amp;gt; {"proposal_id": "96dcf05f-…", "status": "pending", "base_oid": "29a940e8"}
propose A  (retry, same key+body)  -&amp;gt; {"proposal_id": "96dcf05f-…", "status": "pending", "base_oid": "29a940e8", "replayed": true}
propose A' (same key, other body)  -&amp;gt; idempotency_key_reused_with_different_body
propose B  app.py     key=app-bump-0001   -&amp;gt; {"proposal_id": "c91824e3-…", "status": "pending", "base_oid": "29a940e8"}
decide A with the agent token      -&amp;gt; HTTP 403 {'error': 'agent_scope_denied'}
owner approves A                   -&amp;gt; HTTP 200 {"status": "executed", "commit": "f07b9fda"}  (2 commits)
owner approves B                   -&amp;gt; HTTP 200 {"status": "stale", "refused": true, "base_oid": "29a940e8", "branch_head": "f07b9fda"}  (2 commits)
propose B2 on new head f07b9fda    -&amp;gt; {"proposal_id": "bc3cedfc-…", "status": "pending", "base_oid": "f07b9fda"}
owner approves B2, crash before DB -&amp;gt; HTTP 500 {'error': 'injected: crash after git, before DB'}  (3 commits)
owner retries approval of B2       -&amp;gt; HTTP 200 {"status": "executed", "commit": "fb736eda", "replayed": true}  (3 commits)
owner clicks approve on A again    -&amp;gt; HTTP 200 {"status": "executed", "commit": "f07b9fda", "replayed": true}  (3 commits)
approve after decline              -&amp;gt; HTTP 200 {'status': 'declined', 'refused': True}  (3 commits)
approve after expiry (ttl 0.3 s)   -&amp;gt; HTTP 200 {'status': 'expired', 'refused': True}  (1 commit)

fb736ed Claude (conn-claude-research) | repo-owner | Bump app
f07b9fd Claude (conn-claude-research) | repo-owner | Pin install version
29a940e owner | owner | seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;B mengubah &lt;code&gt;app.py&lt;/code&gt; dan A mengubah &lt;code&gt;README.md&lt;/code&gt;, tapi B tetap ditolak. Itulah tafsiran ketat dari "owner me-review ini terhadap 29a940e8". Perbaikannya murah: client membaca head yang baru lalu mengusulkan lagi, dan itulah B2. Baris &lt;code&gt;git log&lt;/code&gt; menunjukkan provenance yang ditulis store. Author-nya adalah koneksi, committer-nya adalah orang yang meng-approve, dan message-nya membawa trailer &lt;code&gt;Proposal: &amp;lt;id&amp;gt;&lt;/code&gt; dan &lt;code&gt;Approved-by:&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang rusak tanpa compare-and-swap?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--mode naive&lt;/code&gt; mempertahankan semuanya kecuali transaksi. Mode ini mengecek tip di Python, membangun commit, lalu menjalankan &lt;code&gt;git update-ref refs/heads/main &amp;lt;new&amp;gt;&lt;/code&gt; tanpa old value dan tanpa marker. Server sama, client sama, layout repository sama.&lt;/p&gt;

&lt;p&gt;Uji race: dua proposal berbeda di atas base yang sama, di-approve pada saat yang persis sama dari dua thread (&lt;code&gt;threading.Barrier&lt;/code&gt;), 100 percobaan per run, dua run. Setelah setiap percobaan, client bertanya ke &lt;code&gt;git merge-base --is-ancestor&lt;/code&gt; apakah setiap commit yang dilaporkan executed benar-benar ada di &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Uji crash: approve dengan kegagalan yang disuntikkan setelah langkah Git dan sebelum update database, lalu approve lagi. 50 percobaan per run, dua run. Client menghitung commit di &lt;code&gt;main&lt;/code&gt; sebelum dan sesudahnya.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Race: keduanya dilaporkan "executed"&lt;/th&gt;
&lt;th&gt;Race: commit yang di-approve tidak ada di main&lt;/th&gt;
&lt;th&gt;Crash, lalu retry&lt;/th&gt;
&lt;th&gt;Commit tambahan setelah retry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CAS + marker, run 1&lt;/td&gt;
&lt;td&gt;0 / 100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;50 × executed (replayed)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CAS + marker, run 2&lt;/td&gt;
&lt;td&gt;0 / 100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;50 × executed (replayed)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive, run 1&lt;/td&gt;
&lt;td&gt;100 / 100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;50 × stale&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive, run 2&lt;/td&gt;
&lt;td&gt;100 / 100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;50 × stale&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Race di mode naive gagal di setiap percobaan, bukan sesekali. Jendela antara "tip sama dengan base" dan "tulis ref" panjangnya empat proses git, puluhan milidetik, dan dua thread yang dilepas oleh barrier selalu sama-sama mendarat di dalamnya. Kedua approval mengembalikan &lt;code&gt;executed&lt;/code&gt; beserta commit ID. Owner melihat dua hasil hijau, padahal salah satu dari dua commit itu tidak ada di history branch. Tidak ada error sama sekali. Dengan compare-and-swap, setiap percobaan berakhir dengan tepat satu proposal executed dan satu penolakan &lt;code&gt;stale&lt;/code&gt; yang menyebutkan head baru.&lt;/p&gt;

&lt;p&gt;Retry setelah crash di mode naive tidak menghasilkan commit ganda. Pengecekan base menangkapnya, karena branch sudah bergerak. Tapi tertangkapnya sebagai &lt;code&gt;stale&lt;/code&gt;. Commit milik proposal itu sendiri ada di &lt;code&gt;main&lt;/code&gt;, sementara store sekarang melaporkan proposal itu sebagai ditolak. Kalau pengecekan base dibuang supaya masalah itu hilang, retry akan menulis commit itu untuk kedua kalinya. Replay marker-lah yang membuat retry bisa bilang "ini sudah terjadi, ini commit-nya".&lt;/p&gt;

&lt;h2&gt;
  
  
  Berapa biayanya?
&lt;/h2&gt;

&lt;p&gt;Waktu wall-clock per request dari client lewat HTTP loopback (urllib, koneksi baru setiap kali), n = 200 per operasi per run, dua run. Rentangnya dari run 1 ke run 2.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operasi&lt;/th&gt;
&lt;th&gt;CAS median&lt;/th&gt;
&lt;th&gt;CAS p95&lt;/th&gt;
&lt;th&gt;naive median&lt;/th&gt;
&lt;th&gt;naive p95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;propose (key baru)&lt;/td&gt;
&lt;td&gt;4.42–4.44 ms&lt;/td&gt;
&lt;td&gt;7.1–12.3 ms&lt;/td&gt;
&lt;td&gt;4.08–4.66 ms&lt;/td&gt;
&lt;td&gt;6.4–10.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;propose (key sama, replay)&lt;/td&gt;
&lt;td&gt;1.42–1.50 ms&lt;/td&gt;
&lt;td&gt;3.3–4.5 ms&lt;/td&gt;
&lt;td&gt;1.27–1.49 ms&lt;/td&gt;
&lt;td&gt;2.4–4.1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;approve, dieksekusi&lt;/td&gt;
&lt;td&gt;115–119 ms&lt;/td&gt;
&lt;td&gt;199–257 ms&lt;/td&gt;
&lt;td&gt;92–105 ms&lt;/td&gt;
&lt;td&gt;130–193 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;approve, ditolak sebagai stale&lt;/td&gt;
&lt;td&gt;28–31 ms&lt;/td&gt;
&lt;td&gt;52–70 ms&lt;/td&gt;
&lt;td&gt;14–17 ms&lt;/td&gt;
&lt;td&gt;24–34 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;approve lagi, sudah dieksekusi&lt;/td&gt;
&lt;td&gt;1.46–1.60 ms&lt;/td&gt;
&lt;td&gt;3.4–4.7 ms&lt;/td&gt;
&lt;td&gt;1.38–1.54 ms&lt;/td&gt;
&lt;td&gt;2.6–4.2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hampir semuanya adalah spawn proses. Approval CAS yang dieksekusi menjalankan delapan proses git (lookup marker, lookup tip, &lt;code&gt;read-tree&lt;/code&gt;, &lt;code&gt;hash-object&lt;/code&gt;, &lt;code&gt;update-index&lt;/code&gt;, &lt;code&gt;write-tree&lt;/code&gt;, &lt;code&gt;commit-tree&lt;/code&gt;, &lt;code&gt;update-ref --stdin&lt;/code&gt;), yang naive enam. Penolakan stale berarti dua lookup dibanding satu. Dengan kira-kira 14 ms per proses git di mesin yang sedang sibuk ini, jalur aman ini menambah sekitar dua spawn, sekitar 15–25 ms, pada request yang toh menunggu klik manusia. Server yang me-link libgit2 alih-alih memanggil shell akan memangkas semua kolom ini. Selisih di antara keduanya akan tetap dua lookup itu.&lt;/p&gt;

&lt;p&gt;Replay adalah jalur termurah di kedua tabel, cukup satu read sqlite. Client yang melakukan retry justru lebih murah bagi server dibanding request baru.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kalau kamu mau merakitnya sendiri
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Jangan beri agent tool write apa pun untuk repository, hanya &lt;code&gt;propose_*&lt;/code&gt;, dan cek di route decide bahwa pemanggilnya bukan token agent. Jangan mengandalkan agent tidak menemukan route itu.&lt;/li&gt;
&lt;li&gt;Kunci apa yang dilihat reviewer, versi dan commit, di dalam proposal, lalu bandingkan saat approval. Tolak dengan menyertakan head terbaru di response, supaya client bisa membaca ulang dan mengusulkan lagi.&lt;/li&gt;
&lt;li&gt;Jadikan write ke Git sebagai satu transaksi ref: update branch dengan old value, ditambah marker create-only yang dinamai sesuai proposal. Cari marker itu sebelum membangun apa pun.&lt;/li&gt;
&lt;li&gt;Wajibkan idempotency key dengan panjang minimum, simpan bersama hash dari body, dan tolak key yang dipakai ulang dengan body berbeda.&lt;/li&gt;
&lt;li&gt;Tulis provenance ke dalam commit itu sendiri. Author-nya koneksi, committer-nya orang yang meng-approve, dan sebuah trailer membawa proposal ID. &lt;code&gt;git log&lt;/code&gt; akan tetap menyimpannya meskipun database kamu sudah hilang.&lt;/li&gt;
&lt;li&gt;Berhenti di pull request. Merge dan deploy adalah urusan pemilik repository, di layarnya sendiri.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sisi GitNest dari semua ini, yaitu clone, push, dan access token, dijelaskan di &lt;a href="https://osfoundry.io/id/features/gitnest" rel="noopener noreferrer"&gt;halaman GitNest&lt;/a&gt; osFoundry. Connector-nya berbicara &lt;a href="https://osfoundry.io/id/glossary/mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; biasa, jadi tool proposal yang sama juga bisa dipakai dari Claude Code atau host MCP lainnya.&lt;/p&gt;

</description>
      <category>git</category>
      <category>mcp</category>
      <category>claude</category>
      <category>agen</category>
    </item>
    <item>
      <title>Menjalankan Claude Code sebagai worker osFoundry dengan langganan Claude sendiri</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 16:29:04 +0000</pubDate>
      <link>https://dev.to/bbt-id/menjalankan-claude-code-sebagai-worker-osfoundry-dengan-langganan-claude-sendiri-2c5</link>
      <guid>https://dev.to/bbt-id/menjalankan-claude-code-sebagai-worker-osfoundry-dengan-langganan-claude-sendiri-2c5</guid>
      <description>&lt;p&gt;Setelah setup ini selesai, role Worker di &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; dipegang oleh Claude Code. Saat sebuah langkah dalam plan sampai ke Worker, aplikasi desktop menjalankan &lt;code&gt;claude&lt;/code&gt; di folder proyek kamu, dengan login langganan Claude milikmu sendiri. Claude Code mengedit file dengan tool-nya sendiri dan memanggil tool osFoundry, misalnya pencarian di catatan kamu, sebagai &lt;code&gt;mcp__osfoundry__memory_search&lt;/code&gt;. Setiap panggilan itu muncul di chat osFoundry sebagai baris "Using …".&lt;/p&gt;

&lt;h2&gt;
  
  
  Siapa mengerjakan apa
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Aplikasi desktop osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Langganan Claude kamu, disimpan oleh CLI setelah &lt;code&gt;/login&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tidak pernah melihatnya. Menghapus &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; dan variabel sejenis dari environment proses anak supaya run tidak beralih ke tagihan API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biaya&lt;/td&gt;
&lt;td&gt;Diambil dari langganan kamu&lt;/td&gt;
&lt;td&gt;Mencatat run dengan biaya 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File&lt;/td&gt;
&lt;td&gt;Read, Edit, dan Bash miliknya sendiri, di dalam satu folder (&lt;code&gt;--add-dir&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Memilih folder itu: yang paling baru di Local Folders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool osFoundry&lt;/td&gt;
&lt;td&gt;Memanggilnya sebagai &lt;code&gt;mcp__osfoundry__&amp;lt;tool&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Menyajikannya di port loopback khusus untuk run ini, dan mencocokkan setiap panggilan dengan allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persetujuan&lt;/td&gt;
&lt;td&gt;Berjalan dengan &lt;code&gt;--permission-mode bypassPermissions&lt;/code&gt; di foldernya&lt;/td&gt;
&lt;td&gt;Setiap panggilan lewat bridge melewati gerbang persetujuan dan policy yang sama dengan panggilan tool dari model lain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Teks akhir&lt;/td&gt;
&lt;td&gt;Menjadi output role. Aktivitas tool ditampilkan sebagai baris progres&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Instal Claude Code dan login sekali
&lt;/h3&gt;

&lt;p&gt;Instal CLI sesuai dokumentasi Anthropic. Lalu login dari terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ claude
&amp;gt; /login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;osFoundry mencari binary-nya di &lt;code&gt;PATH&lt;/code&gt;, di &lt;code&gt;~/.claude/local&lt;/code&gt;, dan di dalam bundle ekstensi VS Code (&lt;code&gt;~/.vscode/extensions/anthropic.claude-code-*/resources/native-binary/claude&lt;/code&gt;). Jadi Claude Code yang hanya terpasang lewat ekstensi VS Code juga tetap ketemu.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Aktifkan di External AI
&lt;/h3&gt;

&lt;p&gt;Di aplikasi desktop, klik ikon kunci di header (API Keys (BYOK)), lalu buka tab External AI (AI eksternal). Baris Claude Code seharusnya terlihat seperti ini:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Claude Code   Installed                                  [Enabled]
/Users/you/.claude/local/claude
Model id: cli:claude (chat) · cli:claude:agent (autonomous)
[Verify sign-in]  [Sign in]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqzi2cklp0hde8t8elvk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqzi2cklp0hde8t8elvk.jpg" alt="Claude Code dan Codex di tab External AI" width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Claude Code dan Codex di tab External AI&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Klik Verify sign-in (cek login). Aplikasi menjalankan completion kecil lewat CLI dan menampilkan ✓ Signed in atau Needs sign-in. Sign in membuka Claude Code di jendela terminal dengan petunjuk "Type /login to sign in, then close this window."&lt;/p&gt;

&lt;p&gt;Mungkin kamu melihat badge API key in env. Artinya ada variabel seperti &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; yang terpasang di environment tempat osFoundry dijalankan. Aplikasi menghapusnya sebelum menjalankan &lt;code&gt;claude&lt;/code&gt;, jadi run tetap memakai langganan kamu. Badge itu hanya pemberitahuan.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Berikan folder
&lt;/h3&gt;

&lt;p&gt;Agent mode bekerja di direktori sungguhan. Tambahkan proyek kamu di Settings → Local Folders. Kalau ada beberapa, yang terakhir ditambahkan yang dipakai. Run yang dimulai dari surface Code memakai repository yang didaftarkan untuk run itu.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pasang ke role
&lt;/h3&gt;

&lt;p&gt;Buka tab Pipeline → Pipeline Roles. Di bagian Agents, pilih Worker lalu pilih &lt;code&gt;cli:claude:agent&lt;/code&gt; dari grup External AI (subscription).&lt;/p&gt;

&lt;p&gt;Kedua id itu adalah dua mode yang berbeda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cli:claude:agent&lt;/code&gt; bersifat otonom. Claude Code punya tool-nya sendiri di folder kamu, ditambah tool osFoundry lewat bridge. Pakai untuk Worker, Researcher, Document Agent, dan role sejenis.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cli:claude&lt;/code&gt; adalah mode chat. CLI berjalan dengan &lt;code&gt;--disallowedTools '*'&lt;/code&gt; dan tidak bisa memanggil apa pun. Cocok untuk role yang hanya teks seperti Titling atau Compaction. Kalau role yang butuh tool dipasang ke id ini, run gagal dengan pesan yang menyuruh kamu memakai &lt;code&gt;cli:claude:agent&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent yang kamu buat di layar Agents juga bisa memakainya. Pemilih modelnya mencantumkannya di bawah CLI agents, dengan label "autonomous — runs in your local folder".&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Apa yang sebenarnya dijalankan aplikasi
&lt;/h3&gt;

&lt;p&gt;Untuk setiap run, osFoundry menyalakan MCP bridge baru, lalu menjalankan CLI kurang lebih seperti ini. Flag-nya diambil dari &lt;code&gt;build_claude_command&lt;/code&gt; di &lt;code&gt;src-tauri/src/providers/agent_cli.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;claude -p "&amp;lt;conversation&amp;gt;" \
  --output-format stream-json --verbose --include-partial-messages \
  --permission-mode bypassPermissions \
  --add-dir /Users/you/work/site \
  --mcp-config '{"mcpServers":{"osfoundry":{"type":"http",
      "url":"http://127.0.0.1:&amp;lt;port&amp;gt;/mcp",
      "headers":{"Authorization":"Bearer &amp;lt;per-run token&amp;gt;"},
      "timeout":120000}}}' \
  --strict-mcp-config \
  --append-system-prompt "&amp;lt;role prompt&amp;gt;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--strict-mcp-config&lt;/code&gt; menjaga server MCP global atau proyek milikmu sendiri tetap di luar run ini. Hanya server &lt;code&gt;osfoundry&lt;/code&gt; yang terpasang. Port dan token selalu baru di setiap run, dan listener ditutup begitu prosesnya selesai.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contoh run sungguhan
&lt;/h2&gt;

&lt;p&gt;Contoh. Catatan dan repository-nya adalah konten demo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kamu (chat Maestro, Worker = cli:claude:agent)
  Cari di catatanku kapan review anggaran Q4, lalu tambahkan ke TODO.md.

Panggilan tool Claude Code
  mcp__osfoundry__memory_search  {"query": "review anggaran Q4"}
    → "Review anggaran Q4: Kam 14:00."
  Read   TODO.md
  Edit   TODO.md   (+ "- [ ] Siapkan angka untuk review anggaran Q4 (Kam 14:00)")

Baris progres osFoundry
  Using memory_search — {"query":"review anggaran Q4"}
  Using Read — {"file_path":"TODO.md"}
  Using Edit — {"file_path":"TODO.md", …}

Jawaban
  "Siapkan angka untuk review anggaran Q4 (Kam 14:00)" sudah ditambahkan ke TODO.md.
  Waktunya diambil dari catatan kamu "Review anggaran Q4: Kam 14:00."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefiks &lt;code&gt;mcp__osfoundry__&lt;/code&gt; adalah cara Claude Code menamai tool dari server bernama &lt;code&gt;osfoundry&lt;/code&gt;. osFoundry membuangnya di baris progres, jadi &lt;code&gt;memory_search&lt;/code&gt; terbaca sama seperti saat memakai model API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Di mana batasnya
&lt;/h2&gt;

&lt;p&gt;Bridge menawarkan daftar yang pendek dan tetap. Isinya tool read-only seperti &lt;code&gt;memory_search&lt;/code&gt;, &lt;code&gt;web_search&lt;/code&gt;, &lt;code&gt;extract_content&lt;/code&gt;, &lt;code&gt;code_search&lt;/code&gt;, &lt;code&gt;todo_list&lt;/code&gt;, &lt;code&gt;transcribe&lt;/code&gt; dan tool baca &lt;code&gt;cloud_*&lt;/code&gt;, ditambah orkestrasi (&lt;code&gt;spawn_fleet&lt;/code&gt;, &lt;code&gt;graph_run&lt;/code&gt;, &lt;code&gt;loop_run&lt;/code&gt;), layar live, dan tool Canvas. &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;, dan &lt;code&gt;execute_command&lt;/code&gt; sengaja tidak dimasukkan. Claude Code sudah punya versi tool itu yang terbatas pada foldernya, dan executor kedua tanpa batas lewat MCP bisa menjangkau di luar &lt;code&gt;--add-dir&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Allowlist ditegakkan oleh bridge, bukan oleh flag CLI. Run dalam mode &lt;code&gt;bypassPermissions&lt;/code&gt; tidak menghormati &lt;code&gt;--allowedTools&lt;/code&gt;, jadi osFoundry tidak mengirimkannya. Panggilan ke tool apa pun di luar daftar run mendapat error JSON-RPC: "tool '…' is not available to this agent".&lt;/p&gt;

&lt;p&gt;Workspace dan channel dikunci saat bridge dimulai. Panggilan tool tidak bisa mengalihkan dirinya ke workspace lain lewat argumennya.&lt;/p&gt;

&lt;p&gt;Penulisan ke layar atau ke editor Canvas (&lt;code&gt;ui_agent_act&lt;/code&gt;, tool tulis Canvas) selalu meminta izin kamu dulu, sama seperti dengan model API. Run yang tidak sedang diawasi siapa pun, misalnya agent yang menjawab mention di channel, menolak tool yang butuh persetujuan alih-alih menunggu klik yang tidak akan datang.&lt;/p&gt;

&lt;p&gt;File kamu tidak pernah melewati osFoundry. Claude Code membaca dan mengeditnya langsung, dan trafiknya menuju Anthropic dengan login kamu sendiri, sama seperti di terminal.&lt;/p&gt;

&lt;p&gt;Langkah berikutnya adalah memasang &lt;code&gt;cli:claude:agent&lt;/code&gt; juga ke role Researcher, di samping Worker, lalu menjalankan satu task yang diorkestrasi. &lt;a href="https://osfoundry.io/id/platform/pipeline-tab" rel="noopener noreferrer"&gt;Halaman Pipeline roles&lt;/a&gt; mencantumkan semua role yang bisa kamu serahkan kepadanya, dan &lt;a href="https://osfoundry.io/id/platform/local-folders" rel="noopener noreferrer"&gt;Local Folders&lt;/a&gt; menjelaskan cara folder kerja dipilih.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cara osFoundry menemukan sesi Claude Code dan Codex kamu di disk, dibuat ulang dengan Python</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:43:01 +0000</pubDate>
      <link>https://dev.to/bbt-id/cara-osfoundry-menemukan-sesi-claude-code-dan-codex-kamu-di-disk-dibuat-ulang-dengan-python-1fj</link>
      <guid>https://dev.to/bbt-id/cara-osfoundry-menemukan-sesi-claude-code-dan-codex-kamu-di-disk-dibuat-ulang-dengan-python-1fj</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58.jsonl
~/.claude/projects/-Users-alex-code-shop-api/c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58/subagents/agent-&amp;lt;id&amp;gt;.jsonl
~/.codex/state_5.sqlite                      -&amp;gt; table threads (id, rollout_path, cwd, git_branch, title, …)
~/.codex/sessions/2026/11/04/rollout-2026-11-04T10-00-00-&amp;lt;thread id&amp;gt;.jsonl

$ claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
$ codex resume 019a2f1c-7d0e-7b31-9c4e-5a1f0e2d8b63
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hanya itu kontrak antara CLI dan sebuah session picker. Claude Code dan Codex masing-masing menyimpan sesinya sebagai file biasa di folder home kamu, dan masing-masing menerima sebuah id untuk melanjutkan satu sesi. Layar External agents di aplikasi desktop &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; adalah pembaca yang berjalan di atas file-file itu (Rust, &lt;code&gt;runtime/session_inventory.rs&lt;/code&gt; dan &lt;code&gt;runtime/external_sessions.rs&lt;/code&gt;). Layar ini menampilkan sesi dari dua belas aplikasi, mencocokkan pencarian dengan judul, folder, branch, aplikasi, dan id, lalu menjalankan perintah resume yang tepat di terminal.&lt;/p&gt;

&lt;p&gt;Di bawah ini bagian Claude Code dan Codex dari fitur itu, dibuat ulang sebagai skrip Python 150 baris dan dijalankan pada folder sesi asli di Mac tempat tulisan ini dibuat. Tidak ada isi transkrip yang ditampilkan di mana pun dalam tulisan ini. Setiap angka adalah jumlah, ukuran, atau waktu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seperti apa satu sesi Claude Code di disk?
&lt;/h2&gt;

&lt;p&gt;Satu file JSONL per sesi, dinamai dengan id sesi, di dalam folder yang dinamai sesuai direktori kerja. Nama foldernya adalah path absolut dengan setiap karakter yang bukan huruf atau angka diganti &lt;code&gt;-&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^A-Za-z0-9]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# /Users/alex/code/shop-api -&amp;gt; -Users-alex-code-shop-api
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dicek terhadap 40 transkrip di mesin ini: 40 dari 40 folder sesuai aturan itu. Aturannya lossy (&lt;code&gt;my.app&lt;/code&gt; dan &lt;code&gt;my-app&lt;/code&gt; bentrok, begitu juga &lt;code&gt;/&lt;/code&gt; dan &lt;code&gt;.&lt;/code&gt;), jadi jangan pernah men-decode nama folder. Baca &lt;code&gt;cwd&lt;/code&gt; dari record-nya.&lt;/p&gt;

&lt;p&gt;Sub-agent (Explore dan kawan-kawan dari tool Task) menulis transkripnya sendiri di &lt;code&gt;&amp;lt;session id&amp;gt;/subagents/agent-&amp;lt;id&amp;gt;.jsonl&lt;/code&gt;. Di sini ada 129. Itu bukan sesi yang akan di-resume orang, dan osFoundry melewati setiap file yang stem-nya diawali &lt;code&gt;agent-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Setiap baris adalah satu record dengan sebuah &lt;code&gt;type&lt;/code&gt;. Menghitung type di 400 baris pertama dari masing-masing 40 file:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;type&lt;/th&gt;
&lt;th&gt;record&lt;/th&gt;
&lt;th&gt;key yang penting untuk index&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;assistant&lt;/td&gt;
&lt;td&gt;4,060&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;requestedModel&lt;/code&gt;, &lt;code&gt;effort&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;message.model&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;attachment&lt;/td&gt;
&lt;td&gt;3,197&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;user&lt;/td&gt;
&lt;td&gt;2,469&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;permissionMode&lt;/code&gt;, &lt;code&gt;isSidechain&lt;/code&gt;, &lt;code&gt;message.content&lt;/code&gt;, &lt;code&gt;entrypoint&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;custom-title&lt;/td&gt;
&lt;td&gt;922&lt;/td&gt;
&lt;td&gt;&lt;code&gt;customTitle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;agent-name&lt;/td&gt;
&lt;td&gt;827&lt;/td&gt;
&lt;td&gt;&lt;code&gt;agentName&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;system, mode, queue-operation, file-history-&lt;em&gt;, api-request&lt;/em&gt;, …&lt;/td&gt;
&lt;td&gt;2,000+&lt;/td&gt;
&lt;td&gt;tidak diperlukan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Record percakapan (&lt;code&gt;user&lt;/code&gt;, &lt;code&gt;assistant&lt;/code&gt;, &lt;code&gt;attachment&lt;/code&gt;, &lt;code&gt;system&lt;/code&gt;) semuanya membawa &lt;code&gt;sessionId&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt;, dan &lt;code&gt;version&lt;/code&gt;. &lt;code&gt;message.content&lt;/code&gt; bisa berupa string atau list of blocks; di baris-baris itu tipe block-nya adalah &lt;code&gt;tool_result&lt;/code&gt; 2,261, &lt;code&gt;tool_use&lt;/code&gt; 2,223, &lt;code&gt;thinking&lt;/code&gt; 1,224, dan &lt;code&gt;text&lt;/code&gt; 754. Judul berasal dari record &lt;code&gt;custom-title&lt;/code&gt; atau, pada build yang menulisnya, record &lt;code&gt;ai-title&lt;/code&gt; dengan &lt;code&gt;aiTitle&lt;/code&gt;. Di sini semua file punya &lt;code&gt;custom-title&lt;/code&gt; dan tidak ada yang punya &lt;code&gt;ai-title&lt;/code&gt;, jadi baca keduanya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kenapa tidak parse semua file saja?
&lt;/h2&gt;

&lt;p&gt;Karena ukurannya besar. Ke-40 transkrip level atas di sini totalnya 19.4 GB. Median file-nya 168 MB, yang terbesar 2.07 GB, dan persentil ke-90 1.94 GB.&lt;/p&gt;

&lt;p&gt;Men-stream file median lewat &lt;code&gt;json.loads&lt;/code&gt; baris per baris butuh 0.61 s (11,558 baris, sekitar 279 MB/s di Mac Apple Silicon, dengan page cache yang sudah hangat). Dengan kecepatan itu, parse penuh untuk semua 40 file akan makan sekitar 70 s, bahkan sebelum ada yang ditampilkan.&lt;/p&gt;

&lt;p&gt;Data yang dibutuhkan sebuah daftar ada di kedua ujung file. Diukur pada semua 40 file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;record pertama yang punya &lt;code&gt;cwd&lt;/code&gt; paling lambat ada di baris 2 (median 2)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gitBranch&lt;/code&gt; pertama dan record &lt;code&gt;user&lt;/code&gt; pertama: paling lambat baris 2&lt;/li&gt;
&lt;li&gt;record judul ada di dalam 512 KiB terakhir pada 40 dari 40 file&lt;/li&gt;
&lt;li&gt;tidak ada baris di 80 baris pertama yang lebih dari 256 KiB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Jadi index membaca 80 baris pertama dan 512 KiB terakhir. Itu juga yang dilakukan osFoundry: &lt;code&gt;HEAD_LINES = 80&lt;/code&gt; dengan batas 256 KiB per baris di &lt;code&gt;session_inventory.rs&lt;/code&gt;, dan &lt;code&gt;SETTINGS_TAIL = 512 * 1024&lt;/code&gt; di &lt;code&gt;external_sessions.rs&lt;/code&gt;, yang dibaca untuk model, effort, mode approval, dan branch terbaru. Membaca head dan tail dari semua 40 file butuh 86 ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;HEAD_LINES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LINE_CAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TAIL_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEAD_LINES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LINE_CAP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LINE_CAP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# a pasted file or image: skip the rest of it
&lt;/span&gt;                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TAIL_BYTES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nbytes&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;   &lt;span class="c1"&gt;# first piece is a cut line
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;readline&lt;/code&gt; yang dibatasi itu penting. Satu giliran user yang berisi gambar yang di-paste adalah satu baris berukuran beberapa megabyte, dan &lt;code&gt;for line in f&lt;/code&gt; biasa akan menarik semuanya ke memori hanya untuk dibuang.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field apa saja yang benar-benar dibutuhkan picker?
&lt;/h2&gt;

&lt;p&gt;Lima, ditambah waktu: aplikasi, id, folder, branch, judul. Head memberikan &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;gitBranch&lt;/code&gt; pertama, dan judul cadangan dari pesan user asli yang pertama (bukan sidechain, dan bukan yang diawali &lt;code&gt;&amp;lt;&lt;/code&gt;, karena begitulah wrapper command dan hook dimulai). Tail menimpanya dengan judul dan branch terbaru, karena sesi yang panjang sering bertahan lebih lama dari branch tempat ia dimulai.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;claude_sessions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;CLAUDE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;proj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;           &lt;span class="c1"&gt;# sub-agent transcript, not resumable
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
                &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gitBranch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isSidechain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;first_text&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;                 &lt;span class="c1"&gt;# newest wins: titles, branch moves
&lt;/span&gt;                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aiTitle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                        &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gitBranch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;records()&lt;/code&gt; adalah &lt;code&gt;json.loads&lt;/code&gt; per baris dengan baris rusak dilewati (baris pertama dari pembacaan tail terpotong di tengah). &lt;code&gt;first_text()&lt;/code&gt; mengembalikan isi yang berupa string, atau block &lt;code&gt;text&lt;/code&gt; pertama. Waktu terakhir dipakai diambil dari mtime file, yang juga dipakai osFoundry untuk pengurutan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Di mana Codex menyimpan data yang sama?
&lt;/h2&gt;

&lt;p&gt;Codex menulis satu rollout per sesi di bawah pohon folder berdasarkan tanggal, &lt;code&gt;~/.codex/sessions/YYYY/MM/DD/rollout-&amp;lt;timestamp&amp;gt;-&amp;lt;thread id&amp;gt;.jsonl&lt;/code&gt;, dan menyimpan yang diarsipkan di &lt;code&gt;~/.codex/archived_sessions/&lt;/code&gt;. Di sini: 10 rollout (28.5 MB) dan 2 yang diarsipkan. Baris pertama setiap rollout adalah record &lt;code&gt;session_meta&lt;/code&gt; yang &lt;code&gt;payload&lt;/code&gt;-nya berisi &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;originator&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;cli_version&lt;/code&gt;, &lt;code&gt;model_provider&lt;/code&gt;, &lt;code&gt;timestamp&lt;/code&gt; dan, di beberapa build, &lt;code&gt;git.branch&lt;/code&gt;. Sisanya adalah record &lt;code&gt;event_msg&lt;/code&gt;, &lt;code&gt;response_item&lt;/code&gt; (dengan tipe payload &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;function_call&lt;/code&gt;, &lt;code&gt;function_call_output&lt;/code&gt;, &lt;code&gt;reasoning&lt;/code&gt;, &lt;code&gt;custom_tool_call&lt;/code&gt;, …), &lt;code&gt;turn_context&lt;/code&gt;, dan &lt;code&gt;compacted&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Codex juga menyimpan index, jadi kamu sama sekali tidak perlu membuka rollout. &lt;code&gt;~/.codex/state_&amp;lt;n&amp;gt;.sqlite&lt;/code&gt; punya tabel &lt;code&gt;threads&lt;/code&gt;; di sini file-nya &lt;code&gt;state_5.sqlite&lt;/code&gt; dengan 43 kolom dan 12 baris, termasuk &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;rollout_path&lt;/code&gt;, &lt;code&gt;cwd&lt;/code&gt;, &lt;code&gt;git_branch&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;reasoning_effort&lt;/code&gt;, &lt;code&gt;approval_mode&lt;/code&gt;, &lt;code&gt;sandbox_policy&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;archived&lt;/code&gt;, dan &lt;code&gt;updated_at_ms&lt;/code&gt;. Bisa ada lebih dari satu file &lt;code&gt;state_&amp;lt;n&amp;gt;&lt;/code&gt;; &lt;code&gt;codex_state_db&lt;/code&gt; di osFoundry mengambil &lt;code&gt;n&lt;/code&gt; tertinggi, dan skripnya juga begitu. Baris yang &lt;code&gt;source&lt;/code&gt;-nya berupa objek JSON adalah thread sub-agent; lewati saja.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;codex_sessions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;codex_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?mode=ro&amp;amp;immutable=1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT id, cwd, git_branch, title, COALESCE(updated_at_ms, updated_at*1000), source &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
             &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FROM threads ORDER BY 5 DESC LIMIT 300&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;id_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;        &lt;span class="c1"&gt;# sub-agent thread
&lt;/span&gt;                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ms&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CODEX&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/*/*/rollout-*.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;records&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_meta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;pl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
               &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Satu hal yang butuh percobaan kedua: membuka database hanya dengan &lt;code&gt;?mode=ro&lt;/code&gt; gagal di sini dengan &lt;code&gt;unable to open database file&lt;/code&gt;. Menambahkan &lt;code&gt;immutable=1&lt;/code&gt; berhasil. Immutable memberi tahu SQLite bahwa file tidak akan berubah selama dibuka, jadi SQLite melewati lock dan file &lt;code&gt;-shm&lt;/code&gt;; itu aman untuk index berupa snapshot, dan artinya kamu bisa membaca database yang sedang dibuka oleh aplikasinya. osFoundry membuka index Codex dengan &lt;code&gt;SQLITE_OPEN_READ_ONLY&lt;/code&gt;, dan memakai open dengan &lt;code&gt;immutable=1&lt;/code&gt; untuk database yang mungkin sedang dibuka aplikasi lain untuk menulis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seberapa cepat di folder sungguhan?
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 sessidx.py build          # four runs, same Mac, real ~/.claude and ~/.codex
52 sessions  claude 154 ms  codex 3 ms
52 sessions  claude 80 ms  codex 2 ms
52 sessions  claude 83 ms  codex 2 ms
52 sessions  claude 107 ms  codex 4 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;52 sesi (40 Claude Code, 12 Codex), semuanya punya folder dan judul, dan ke-40 sesi Claude Code punya branch. Run pertama adalah yang paling dingin; sisanya sudah hangat. File index-nya 11,480 byte. Dibandingkan parse penuh 70 s, 80 sampai 150 ms adalah beda antara daftar yang langsung terbuka dan daftar yang menampilkan spinner.&lt;/p&gt;

&lt;p&gt;Pencarian lalu cukup berupa tes substring pada lima field, sama dengan yang dipakai &lt;code&gt;matchesQuery&lt;/code&gt; milik osFoundry di &lt;code&gt;lib/external-sessions.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cwd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;branch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pada home palsu yang dibuat dengan struktur yang sama (data contoh, bukan sesi asli):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ SESSIDX_HOME=./fx python3 sessidx.py find checkout
codex       019a2f1c  test/checkout-e2e    -
claude-code c5e1a7b2  fix/checkout-retry   Fix double charge on checkout retry
$ SESSIDX_HOME=./fx python3 sessidx.py resume c5e1
cd /Users/alex/code/shop-api &amp;amp;&amp;amp; claude --resume c5e1a7b2-9d40-4f3e-8b16-2a7c0e9d4f58
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mencari di dalam teks pesan berarti membaca 19.4 GB itu, atau membangun full-text index dari semua yang pernah kamu paste ke agent. Mencari berdasarkan folder dan branch menemukan sesi lewat nama proyek, dan memang itu yang biasanya kamu ingat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang ditambahkan oleh resume yang aman?
&lt;/h2&gt;

&lt;p&gt;Mencetak &lt;code&gt;claude --resume &amp;lt;id&amp;gt;&lt;/code&gt; itu bagian yang gampang. Yang ditambahkan osFoundry di sekitarnya (&lt;code&gt;plan_command&lt;/code&gt; dan launch plan di &lt;code&gt;external_sessions.rs&lt;/code&gt;, plus pengecekan yang dilakukan layar sebelum membuka terminal) layak ditiru:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jalankan di folder yang tercatat, dan tolak kalau folder itu sudah tidak ada, alih-alih melanjutkan di tempat lain.&lt;/li&gt;
&lt;li&gt;Bandingkan branch yang tercatat dengan &lt;code&gt;git -C &amp;lt;cwd&amp;gt; rev-parse --abbrev-ref HEAD&lt;/code&gt;. Kalau berbeda, beri tahu dan tanyakan; jangan pernah checkout.&lt;/li&gt;
&lt;li&gt;Bawa model (&lt;code&gt;--model&lt;/code&gt;, di Codex &lt;code&gt;-m&lt;/code&gt;) dan effort (&lt;code&gt;--effort&lt;/code&gt;, di Codex &lt;code&gt;-c model_reasoning_effort="…"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Bawa mode approval yang tercatat hanya kalau setidaknya seketat default CLI. Untuk Claude Code itu &lt;code&gt;default&lt;/code&gt; atau &lt;code&gt;plan&lt;/code&gt;; untuk Codex, approval &lt;code&gt;untrusted&lt;/code&gt; atau &lt;code&gt;on-request&lt;/code&gt;, sandbox &lt;code&gt;read-only&lt;/code&gt; atau &lt;code&gt;workspace-write&lt;/code&gt;. Yang lebih longgar ditahan, kecuali orangnya memilih untuk mengaktifkannya pada peluncuran itu saja.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;sessidx.py&lt;/code&gt; lengkapnya terdiri dari tiga bagian di atas plus sekitar 30 baris kode perekat (override env untuk folder home, output JSON, subcommand &lt;code&gt;find&lt;/code&gt; dan &lt;code&gt;resume&lt;/code&gt;). Tidak ada binary claude atau codex di Mac yang dipakai di sini, jadi baris resume-nya dicetak, bukan dijalankan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kalau kamu mau membuatnya sendiri
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Perlakukan file-file itu sebagai API: hanya baca, jangan pernah menulis di sebelahnya, dan jangan pernah membuka file auth milik CLI untuk mengambil nama akun.&lt;/li&gt;
&lt;li&gt;Baca head dan tail, bukan seluruh transkrip. Batasi panjang baris supaya satu gambar yang di-paste tidak memakan memori sebesar ukurannya.&lt;/li&gt;
&lt;li&gt;Ambil &lt;code&gt;cwd&lt;/code&gt; dari record, bukan dari nama folder yang di-escape.&lt;/li&gt;
&lt;li&gt;Utamakan &lt;code&gt;state_&amp;lt;n&amp;gt;.sqlite&lt;/code&gt; milik Codex (&lt;code&gt;n&lt;/code&gt; tertinggi) daripada menelusuri pohon tanggal, dan buka dalam mode immutable kalau aplikasinya mungkin sedang memegangnya.&lt;/li&gt;
&lt;li&gt;Jangan masukkan transkrip sub-agent (&lt;code&gt;agent-*.jsonl&lt;/code&gt;, thread dengan &lt;code&gt;source&lt;/code&gt; JSON) ke daftar; tidak ada yang me-resume-nya secara langsung.&lt;/li&gt;
&lt;li&gt;Tentukan di awal apa saja yang boleh diubah oleh resume. Default yang aman: folder ya, model ya, approval yang lebih longgar hanya kalau diminta, branch tidak pernah.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tampilan Sessions untuk agent milik osFoundry sendiri bekerja dari sumber yang berbeda; penjelasannya ada di &lt;a href="https://osfoundry.io/id/platform/agent-sessions" rel="noopener noreferrer"&gt;halaman sesi agent osFoundry&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>codex</category>
      <category>python</category>
      <category>sesi</category>
    </item>
    <item>
      <title>Cara membiarkan chat Claude mengedit Notes osFoundry tanpa menimpa editan rekan kerja</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 15:21:00 +0000</pubDate>
      <link>https://dev.to/bbt-id/cara-membiarkan-chat-claude-mengedit-notes-osfoundry-tanpa-menimpa-editan-rekan-kerja-i4j</link>
      <guid>https://dev.to/bbt-id/cara-membiarkan-chat-claude-mengedit-notes-osfoundry-tanpa-menimpa-editan-rekan-kerja-i4j</guid>
      <description>&lt;p&gt;Di akhir panduan ini, percakapan Claude membaca rentang sel dari tab Sheet di salah satu Notes &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; Anda, menulis ulang tab teks di Note yang sama, dan menambahkan tab Slides baru, sementara seseorang terus mengetik di Note itu. Setiap penulisan menyebutkan ID tab yang tepat dan versi Note yang terakhir dibaca Claude. Jika orang tersebut menyimpan sesuatu di sela-selanya, osFoundry menolak penulisan Claude, dan Claude membaca ulang sebelum mencoba lagi. Tidak ada editan siapa pun yang hilang.&lt;/p&gt;

&lt;h2&gt;
  
  
  Siapa mengerjakan apa
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sisi Claude&lt;/th&gt;
&lt;th&gt;Sisi osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Akun Claude Anda, tidak berubah&lt;/td&gt;
&lt;td&gt;Koneksi yang Anda setujui untuk satu workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Note mana&lt;/td&gt;
&lt;td&gt;Claude memilihnya dari daftar yang diberikan server&lt;/td&gt;
&lt;td&gt;Hanya Notes di project yang sudah ditambahi koneksi tersebut&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab mana&lt;/td&gt;
&lt;td&gt;Claude harus mengirim &lt;code&gt;tab_id&lt;/code&gt; yang tepat&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list_note_tabs&lt;/code&gt; mengembalikan setiap instance tab, termasuk dua Sheet dengan tipe yang sama&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Konflik&lt;/td&gt;
&lt;td&gt;Claude membaca ulang dan mengulang perubahannya&lt;/td&gt;
&lt;td&gt;Penulisan membawa &lt;code&gt;expected_version&lt;/code&gt;; versi usang ditolak, bukan digabung&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percobaan ulang&lt;/td&gt;
&lt;td&gt;Claude boleh mengirim ulang panggilan yang sama&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;idempotency_key&lt;/code&gt; yang sama mengembalikan hasil pertama alih-alih menulis dua kali&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persetujuan&lt;/td&gt;
&lt;td&gt;Prompt izin tool milik Claude sendiri sebelum setiap penulisan&lt;/td&gt;
&lt;td&gt;Hak edit Anda saat ini pada Note itu, dicek di setiap panggilan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Persiapan
&lt;/h2&gt;

&lt;p&gt;Konektornya sama dengan yang dari pengaturan custom connector Claude (ikon kunci → Connected → Add assistant, lalu tambahkan &lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp&lt;/code&gt; di Claude). Langkah-langkah di bawah mengasumsikan koneksi itu sudah ada dan sudah ditambahkan ke sebuah project.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Masukkan Note ke project yang terhubung
&lt;/h3&gt;

&lt;p&gt;Buka Note, buka panel kanannya, lalu masuk ke Config → Settings. Kolom Project menampilkan "Add to project" (tambahkan ke project) kalau Note belum punya project. Pilih project tempat koneksi Claude berada.&lt;/p&gt;

&lt;p&gt;Note yang hanya ada di perangkat ini tidak punya salinan di cloud, jadi koneksi tidak bisa melihatnya. Note harus sudah tersinkron ke workspace Anda.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Pastikan tool Note aktif
&lt;/h3&gt;

&lt;p&gt;Di ikon kunci → Connected, pilih koneksi Claude lalu lihat Tools. Semua tool yang didukung aktif secara default. Untuk panduan ini, tool berikut harus tetap dicentang:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read   list_note_tabs   read_note_tab   read_sheet_range   list_note_attachments
write  edit_note_tab    create_note_tab add_note_source
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcewoaeir7gsvmacia3bg.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcewoaeir7gsvmacia3bg.jpg" alt="Daftar centang Tools pada koneksi Claude, semuanya aktif secara default" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Daftar centang Tools pada koneksi Claude, semuanya aktif secara default&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Untuk membuat chat hanya-baca sementara waktu, hapus centang tiga tool write lalu klik Save tools. Membaca tetap berfungsi.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cek sebentar hak edit Anda
&lt;/h3&gt;

&lt;p&gt;Penulisan Claude berjalan dengan hak orang yang menyetujui koneksi, dan dicek ulang di setiap panggilan. Jika Anda hanya bisa melihat Note, &lt;code&gt;edit_note_tab&lt;/code&gt; ditolak meskipun tool-nya aktif. Buka tab Access di Note kalau ragu.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Minta dengan menyebut nama tab
&lt;/h3&gt;

&lt;p&gt;Di chat Claude yang konektor osFoundry-nya aktif, sebutkan nama Note dan tab-nya. Claude sendiri yang mengubah nama menjadi ID tab, tetapi "sheet Owners" lebih tidak ambigu daripada "tabelnya".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Di osFoundry, buka Note "Q3 launch". Baca A1:D12 di sheet Owners,
tulis ulang tab Brief supaya setiap item terbuka punya penanggung jawab dari sheet itu,
lalu tambahkan tab Slides "Launch status" dengan satu slide per penanggung jawab.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Contoh eksekusi nyata
&lt;/h2&gt;

&lt;p&gt;Isi Note dan jawaban Claude adalah data contoh. Nama tool dan nama argumen sama persis dengan yang didaftarkan server produksi di &lt;code&gt;tools/list&lt;/code&gt;. Di Claude Code, tool yang sama muncul sebagai &lt;code&gt;mcp__osfoundry__list_note_tabs&lt;/code&gt; dan seterusnya.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contoh

Claude → list_note_tabs {"note_id": "…"}
       ← version "…41.118Z"
         tab: Brief (note), Owners (sheet), Owners archive (sheet)
Claude → read_sheet_range {"note_id": "…", "tab_id": "&amp;lt;Owners&amp;gt;",
                           "start_row": 0, "start_col": 0,
                           "row_count": 12, "col_count": 4}
       ← 12 × 4 nilai
Claude → read_note_tab {"note_id": "…", "tab_id": "&amp;lt;Brief&amp;gt;"}
       ← teks, version "…41.118Z"
Claude → edit_note_tab {"note_id": "…", "tab_id": "&amp;lt;Brief&amp;gt;",
                        "operation": "replace_text", "text": "…",
                        "expected_version": "…41.118Z",
                        "idempotency_key": "brief-owners-1"}
       [Claude meminta Anda mengizinkan panggilan tool ini → Allow]
       ← ditolak: Note berubah setelah versi itu

         (sementara itu, Mika menambahkan "press kit" ke tab Brief di osFoundry)

Claude → read_note_tab {"note_id": "…", "tab_id": "&amp;lt;Brief&amp;gt;"}
       ← teks kini memuat "press kit", version "…07.502Z"
Claude → edit_note_tab {... "text": "… press kit — owner: Mika …",
                        "expected_version": "…07.502Z",
                        "idempotency_key": "brief-owners-2"}
       ← tersimpan, version "…19.044Z"
Claude → create_note_tab {"note_id": "…", "tab_type": "slides",
                          "label": "Launch status",
                          "slides": [{"title": "Aiko"}, {"title": "Mika"}, …],
                          "expected_version": "…19.044Z",
                          "idempotency_key": "launch-status-deck"}
       ← ID tab baru "&amp;lt;Launch status&amp;gt;"

Claude: Brief sekarang mencantumkan penanggung jawab untuk semua 9 item terbuka,
        termasuk "press kit", yang ditambahkan Mika saat saya mengedit. Tab
        Launch status yang baru punya satu slide per penanggung jawab.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ada tiga hal yang terjadi yang tidak terlihat di chat biasa. Claude membaca rentang Sheet, bukan seluruh workbook. &lt;code&gt;read_sheet_range&lt;/code&gt; mengembalikan paling banyak 100 kali 100 sel, jadi Sheet yang besar tidak membanjiri percakapan. Editan pertama memang sengaja gagal: versi Claude lebih lama daripada simpanan Mika. Claude tidak mengirim ulang teks lamanya dengan versi baru. Claude membaca ulang tab itu dan menulis Brief yang berisi perubahannya sekaligus baris dari Mika. Panggilan create membawa key-nya sendiri, jadi kalau balasannya hilang dan Claude memanggil lagi, osFoundry akan mengembalikan tab Launch status yang sama alih-alih membuat tab kedua.&lt;/p&gt;

&lt;p&gt;Tab Owners archive adalah alasan ID tab itu penting. Satu Note bisa berisi beberapa tab Sheet atau Slides, dan nama atau tipe saja tidak menunjuk satu tab tertentu. Setiap panggilan &lt;code&gt;edit_note_tab&lt;/code&gt; membutuhkan ID yang dikembalikan oleh &lt;code&gt;list_note_tabs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Di mana batasnya
&lt;/h2&gt;

&lt;p&gt;Penulisan usang ditolak, tidak pernah digabung. osFoundry tidak mencoba menggabungkan teks Claude dengan teks orang tersebut di server, karena &lt;code&gt;replace_text&lt;/code&gt; yang berisi teks lengkap tidak punya cara yang andal untuk tahu versi paragraf mana yang Anda maksud. Yang tahu maksudnya adalah agen, jadi agenlah yang mengulang perubahan di atas isi yang ada sekarang. Simpanan orang itu sendiri dari aplikasi desktop juga dijaga dengan cara yang sama. Sebuah simpanan membawa revisi tempat ia mulai, dan jika Note berubah di bawahnya, aplikasi menyimpan editan orang itu sebagai draf lokal dan menandai konflik sinkronisasi alih-alih menimpa perubahan Claude.&lt;/p&gt;

&lt;p&gt;Versi itu milik Note. Anda mengambilnya dari &lt;code&gt;list_note_tabs&lt;/code&gt; sebelum membuat tab dan dari &lt;code&gt;read_note_tab&lt;/code&gt; sebelum mengedit tab. Ketika orang tersebut mengedit tab mana pun di Note, penulisan Claude berikutnya harus membaca ulang, meskipun menyasar tab lain. Pembacaan tambahan itu murah, dan memastikan Claude tidak pernah menulis berdasarkan gambaran Note yang sudah usang.&lt;/p&gt;

&lt;p&gt;Membuat tab hanya menambah. &lt;code&gt;create_note_tab&lt;/code&gt; membuat tab Slides dan Sheet, dan tidak bisa mengganti isi tab yang sudah ada. Setiap operasi edit cocok untuk satu jenis tab: &lt;code&gt;replace_text&lt;/code&gt; untuk tab Note dan Markdown, &lt;code&gt;slide_add&lt;/code&gt; dan &lt;code&gt;slides_add_outline&lt;/code&gt; untuk Slides, &lt;code&gt;sheet_set_range&lt;/code&gt; untuk sel Sheet yang tepat, &lt;code&gt;replace_tab_data&lt;/code&gt; untuk tab konten lain yang didukung.&lt;/p&gt;

&lt;p&gt;File tetap berupa metadata. &lt;code&gt;list_note_attachments&lt;/code&gt; menampilkan Files dan Sources dari sebuah Note, tetapi tidak mengembalikan isi file maupun link unduhan. Link yang ditempel di chat bisa diteruskan ke mana saja, dan tetap berfungsi setelah koneksi dicabut. Claude bisa menambahkan sitasi dengan &lt;code&gt;add_note_source&lt;/code&gt;, yang menerima URL HTTP atau HTTPS, versi Note, dan sebuah key, sama seperti penulisan lainnya.&lt;/p&gt;

&lt;p&gt;Memindahkan Note memutus akses di panggilan berikutnya. Pindahkan ke project yang tidak ada di koneksi, hapus project dari koneksi, atau klik Revoke connection, dan pembacaan Claude berikutnya ditolak. Token yang diterbitkan sebelumnya tidak membuat akses tetap terbuka.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selanjutnya
&lt;/h2&gt;

&lt;p&gt;Minta Claude mengubah rentang Owners menjadi briefing lima slide di tab Slides baru, lalu edit sendiri satu judul slide saat Claude bekerja. Anda akan melihat penulisan yang ditolak dan pembacaan ulangnya di panggilan tool. Bagaimana tab Slides berdampingan dengan bagian lain dari Note ada di &lt;a href="https://osfoundry.io/id/platform/notes-slides" rel="noopener noreferrer"&gt;halaman Note Slides&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cara menghubungkan chat Claude ke proyek osFoundry kamu dengan custom connector</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:47:56 +0000</pubDate>
      <link>https://dev.to/bbt-id/cara-menghubungkan-chat-claude-ke-proyek-osfoundry-kamu-dengan-custom-connector-5d1b</link>
      <guid>https://dev.to/bbt-id/cara-menghubungkan-chat-claude-ke-proyek-osfoundry-kamu-dengan-custom-connector-5d1b</guid>
      <description>&lt;p&gt;Setelah mengikuti langkah-langkah ini, percakapan Claude biasa di claude.ai bisa membuka Notes, knowledge base, dan Records di salah satu proyek &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; kamu, menjawab berdasarkan isinya, dan memposting di channel diskusi proyek itu. Dengan persetujuanmu, chat itu juga bisa mengeditnya. Kamu tetap login ke Claude seperti biasa. Di setiap panggilan tool, osFoundry yang memutuskan apa saja yang boleh disentuh chat itu, dan setiap panggilan tercatat di osFoundry pada Recent activity milik koneksi tersebut. ChatGPT terhubung dengan cara yang sama, memakai alamat server yang sama.&lt;/p&gt;

&lt;h2&gt;
  
  
  Siapa mengerjakan apa
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sisi Claude&lt;/th&gt;
&lt;th&gt;Sisi osFoundry&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;Akun Claude kamu, tidak berubah&lt;/td&gt;
&lt;td&gt;Akun osFoundry kamu menyetujui koneksi sekali, untuk satu workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yang menjalankan model&lt;/td&gt;
&lt;td&gt;Claude, di chat yang sudah kamu pakai&lt;/td&gt;
&lt;td&gt;Tidak ada. Di jalur ini tidak ada model dan tidak ada provider key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tempat tool dijalankan&lt;/td&gt;
&lt;td&gt;Claude mengirim panggilan tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api.osfoundry.io&lt;/code&gt; menjalankannya dan mengecek akses lebih dulu&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yang bisa dijangkau&lt;/td&gt;
&lt;td&gt;Apa pun yang dikembalikan server&lt;/td&gt;
&lt;td&gt;Proyek yang kamu tambahkan koneksinya, plus channel tempat kamu mengundangnya&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persetujuan&lt;/td&gt;
&lt;td&gt;Prompt izin tool milik Claude sendiri&lt;/td&gt;
&lt;td&gt;Perubahan GitNest dan Room menunggu di "Needs your approval"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tempat data tinggal&lt;/td&gt;
&lt;td&gt;Jawaban ada di chat Claude kamu&lt;/td&gt;
&lt;td&gt;Notes, record, dan artikel tetap di osFoundry. Item yang hanya lokal tidak pernah keluar dari perangkat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Buat koneksi di osFoundry
&lt;/h3&gt;

&lt;p&gt;Di osFoundry, klik ikon kunci di header dan buka tab Connected. Panelnya berjudul "Connected agents" dan menampilkan workspace yang sedang kamu buka. Di bawah "Connect in three steps":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assistant: pilih Claude&lt;/li&gt;
&lt;li&gt;Display name: nama apa saja yang nanti mudah kamu kenali, misalnya &lt;code&gt;Claude research&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Klik Add assistant (tambah asisten)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Koneksi baru menampilkan "Setup pending. This assistant cannot read or change anything yet." Salin Server address dengan tombol Copy di sebelahnya:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.osfoundry.io/api/v1/external-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhi61cfxig1v7yltczuxr.jpg" alt="Tab Connected setelah Add assistant, dengan alamat server dan koneksi yang masih pending" width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Tab Connected setelah Add assistant, dengan alamat server dan koneksi yang masih pending&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Tambahkan ke Claude sebagai custom connector
&lt;/h3&gt;

&lt;p&gt;Di Claude, buka Customize → Connectors lalu tambahkan custom connector dengan URL tersebut. Di paket Team atau Enterprise, owner organisasi menambahkannya lebih dulu di Organization settings → Connectors, lalu setiap anggota menghubungkan sendiri. Langkah-langkah dari Anthropic untuk remote MCP connector ada di &lt;a href="https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp" rel="noopener noreferrer"&gt;panduan custom connector Claude&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Saat kamu klik Connect, Claude menemukan halaman login osFoundry dengan sendirinya. Kamu tidak perlu menempelkan client ID atau key.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Setujui satu workspace
&lt;/h3&gt;

&lt;p&gt;Browser kamu akan membuka &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;, halaman berjudul "Connect an agent". Halaman ini menampilkan client asisten dan host tujuan kembalinya, lalu meminta:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workspace to connect: satu workspace. Workspace kedua butuh koneksi sendiri.&lt;/li&gt;
&lt;li&gt;Requested capabilities, dalam bahasa sederhana: "Read supported resources in projects you add", "Change supported resources where you have edit access", "Run sessions where you grant a runner role".&lt;/li&gt;
&lt;li&gt;Connection: pilih "Use pending: Claude research" supaya tersambung ke koneksi dari langkah 1, atau biarkan "Create a new connection".&lt;/li&gt;
&lt;li&gt;Centang "I approve connecting this assistant to ", lalu Connect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kamu akan dikembalikan ke Claude. Koneksinya sudah aktif tapi belum bisa melihat apa pun. Koneksi baru dimulai dengan nol proyek.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Tambahkan ke proyek
&lt;/h3&gt;

&lt;p&gt;Kembali ke ikon kunci → Connected, pilih koneksinya. Sekarang tertulis "Connected." Di Projects, pilih proyek lalu klik Add to project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9myc1s54srpz8aum8m06.jpg" alt="Koneksi yang sudah ditambahkan ke proyek, dengan chat yang tertaut di bawahnya" width="800" height="1110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Koneksi yang sudah ditambahkan ke proyek, dengan chat yang tertaut di bawahnya&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Ini memberi akses ke konten tersinkron yang didukung di proyek itu: Notes, knowledge base, channel Record, Room cloud, repositori GitNest yang ditugaskan ke proyek, dan channel diskusi proyek. Konten yang kamu tambahkan ke proyek nanti juga ikut. Channel Message biasa terpisah. Gunakan Message channels → Choose a Message channel → Invite untuk setiap channel yang perlu dibaca dan dibalas oleh chat.&lt;/p&gt;

&lt;p&gt;Bagian Tools menulis "All supported tools are on by default." Hapus centang tool yang tidak ingin kamu berikan ke koneksi ini lalu klik Save tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6tyzev2to95pu562is99.jpg" alt="Checklist Tools pada koneksi Claude, semuanya aktif secara default" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Contoh: Checklist Tools pada koneksi Claude, semuanya aktif secara default&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Aktifkan di chat
&lt;/h3&gt;

&lt;p&gt;Di percakapan Claude, buka menu + → Connectors lalu aktifkan connector osFoundry. Setelah itu, minta saja dengan bahasa biasa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contoh jalannya
&lt;/h2&gt;

&lt;p&gt;Prompt dan jawaban di bawah adalah konten contoh dari proyek demo. Nama tool dan nama argumennya persis seperti yang diumumkan server live di &lt;code&gt;tools/list&lt;/code&gt;. Di claude.ai, panggilan-panggilan ini muncul di bawah nama connector. Kalau kamu menambahkan URL yang sama ke Claude Code dengan nama &lt;code&gt;osfoundry&lt;/code&gt;, tool yang sama muncul sebagai &lt;code&gt;mcp__osfoundry__list_project_resources&lt;/code&gt; dan seterusnya.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Contoh

Kamu: Pakai osFoundry, apa saja yang masih terbuka di checklist peluncuran Q3?
      Cek KB support kalau ada yang bertentangan, lalu tandai
      record "pricing page" sebagai Ready.

Claude → list_granted_resources {}
       ← 1 proyek terhubung: "Launch Q3" (workspace)
Claude → list_project_resources {"project_id": "…", "resource_type": "note", "search": "checklist"}
       ← Note "Q3 launch checklist"
Claude → list_note_tabs {"note_id": "…"}
       ← tab: Note (teks), Sheet "Owners"
Claude → read_note_tab {"note_id": "…", "tab_id": "…"}
       ← 9 item, 3 belum dicentang
Claude → search_knowledge_base {"knowledge_base_id": "…", "query": "refund window"}
       ← artikel "Refund policy" (diperbarui 2 hari lalu)
Claude → read_record_item {"record_channel_id": "…", "item_id": "…"}
       ← "pricing page", status Draft, version …
Claude → edit_record_item {"record_channel_id": "…", "item_id": "…",
                           "values": {"status": "Ready"},
                           "expected_version": "…", "idempotency_key": "…"}
       [Claude meminta izinmu untuk panggilan tool ini → Allow]
       ← tersimpan, version baru …

Claude: Ada tiga item yang masih terbuka: tanggal embargo pers,
        terjemahan FAQ, dan baris refund di pricing page.
        Checklist menyebut 14 hari; artikel KB "Refund policy"
        menyebut 30. Record pricing page sudah saya tandai Ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dua detail di transkrip itu memang disengaja. &lt;code&gt;edit_record_item&lt;/code&gt; butuh version yang dikembalikan &lt;code&gt;read_record_item&lt;/code&gt;, jadi record yang berubah di antara keduanya akan ditolak, bukan ditimpa. &lt;code&gt;idempotency_key&lt;/code&gt; yang diulang mengembalikan hasil pertama, jadi panggilan yang di-retry tidak menulis dua kali.&lt;/p&gt;

&lt;h2&gt;
  
  
  Di mana batasnya
&lt;/h2&gt;

&lt;p&gt;osFoundry tidak bisa membuka chat Claude kamu. Koneksi MCP membuat Claude bisa memanggil tool osFoundry saat Claude memutuskan perlu. osFoundry tidak punya API ke arah sebaliknya untuk menampilkan daftar, membaca, atau memulai percakapan pribadi. Tool yang menautkan chat ke proyek (&lt;code&gt;link_current_chat&lt;/code&gt;) menerima label yang dilaporkan chat itu tentang dirinya sendiri, dan osFoundry memperlakukannya sebagai petunjuk routing, bukan identitas yang terverifikasi. Karena alasan yang sama, Claude tidak menerima notifikasi latar belakang dari osFoundry. Claude baru bertindak ketika kamu membuka percakapan dan meminta.&lt;/p&gt;

&lt;p&gt;Data yang hanya lokal tetap lokal. Server sendiri menyatakannya di balasan &lt;code&gt;initialize&lt;/code&gt;: "Local-only data is unavailable to cloud clients." Room lokal, sesi Graph lokal, dan konten yang tidak pernah tersinkron tidak terlihat oleh koneksi. Berbagi sesi Graph desktop adalah switch terpisah di tab yang sama, dan aplikasi desktop harus terbuka dan dalam keadaan login.&lt;/p&gt;

&lt;p&gt;Akses dicek di setiap panggilan, bukan di setiap login. Setiap panggilan dicocokkan dengan status koneksi, workspace-nya, keanggotaan proyek saat ini, proyek tempat item itu berada saat itu, dan peranmu sendiri pada item tersebut. Pindahkan Note ke proyek lain, keluarkan proyeknya, atau klik Revoke connection, dan panggilan berikutnya akan ditolak. Edit juga mensyaratkan kamu, orang yang menyetujui koneksi, masih punya hak edit.&lt;/p&gt;

&lt;p&gt;Sebagian penulisan berubah menjadi usulan. Pekerjaan GitNest, change set, impor file, pull request, dan instalasi aplikasi di Room melewati tool &lt;code&gt;propose_…&lt;/code&gt;. Semuanya menunggu di tab Connected pada "Needs your approval", tempat kamu melihat repositori atau Room tujuan dan file-filenya sebelum memilih Approve atau Decline. Claude tidak pernah menerima token Git provider kamu atau nilai rahasia apa pun.&lt;/p&gt;

&lt;p&gt;Channel Message perlu undangan. Menjadi bagian dari proyek hanya membuka channel diskusi proyek itu. Channel lain butuh Invite secara eksplisit, dan channel privat harus diundang oleh owner-nya.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selanjutnya
&lt;/h2&gt;

&lt;p&gt;Tambahkan knowledge base ke proyek yang sama, lalu ajukan ke Claude pertanyaan yang biasanya kamu jawab dari sana, misalnya "Apa kata KB kita soal refund, sertakan nama artikelnya?". &lt;code&gt;search_knowledge_base&lt;/code&gt; mengembalikan ID artikel dan kapan terakhir diperbarui, jadi kamu bisa mengecek sumbernya. Cara knowledge base di osFoundry disusun ada di &lt;a href="https://osfoundry.io/id/features/knowledge-bases" rel="noopener noreferrer"&gt;halaman knowledge base&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>mcp</category>
      <category>osfoundry</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Bagaimana custom connector Claude menemukan server OAuth osFoundry, dan kenapa akses dicek di setiap panggilan?</title>
      <dc:creator>BBT</dc:creator>
      <pubDate>Fri, 06 Nov 2026 10:45:59 +0000</pubDate>
      <link>https://dev.to/bbt-id/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-akses-dicek-di-4084</link>
      <guid>https://dev.to/bbt-id/bagaimana-custom-connector-claude-menemukan-server-oauth-osfoundry-dan-kenapa-akses-dicek-di-4084</guid>
      <description>&lt;p&gt;Ini yang diterima client MCP remote saat memanggil tool &lt;a href="https://osfoundry.io/id" rel="noopener noreferrer"&gt;osFoundry&lt;/a&gt; tanpa token. Request-nya dikirim dengan curl dari sebuah Mac di Tokyo pada 2026-11-06:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -s -D - -X POST https://api.osfoundry.io/api/v1/external-mcp \
    -H 'content-type: application/json' \
    -H 'accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
         "params":{"name":"list_project_resources","arguments":{}}}'

HTTP/2 401
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"

{"jsonrpc":"2.0","id":3,"result":{"isError":true,
  "content":[{"type":"text","text":"Authentication required"}],
  "_meta":{"mcp/www_authenticate":["Bearer resource_metadata=\"https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp\", ..."]}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Satu header itu sudah cukup bagi custom connector Claude, atau koneksi MCP di developer mode ChatGPT, untuk menemukan halaman login. Pengguna cukup menempelkan URL-nya. Semua yang ada di bawah berasal dari request tanpa autentikasi ke endpoint live, ditambah rebuild kecil dalam Python yang dijalankan secara lokal untuk mengukur bagian yang tidak bisa diperlihatkan curl: apa yang terjadi pada akses setelah login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa yang dilihat client sebelum punya token?
&lt;/h2&gt;

&lt;p&gt;Tiga panggilan, tiga jawaban berbeda:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Yang dikembalikan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;protokol &lt;code&gt;2025-06-18&lt;/code&gt;, server &lt;code&gt;osfoundry 0.1.0&lt;/code&gt;, plus instruksi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;59 tool, 45,368 byte&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;WWW-Authenticate&lt;/code&gt; yang menunjuk ke resource metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GET&lt;/code&gt; ke endpoint&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;tidak ada stream SSE lewat GET&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Handshake dan katalog tool bersifat publik. Hanya pemanggilan tool yang butuh token. Dengan begitu host bisa menunjukkan ke pengguna tool apa saja yang ada sebelum siapa pun login. Tidak ada data pengguna di katalog, dan string &lt;code&gt;instructions&lt;/code&gt; di &lt;code&gt;initialize&lt;/code&gt; bersifat umum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use list_granted_resources first. Every Note edit must use the exact tab ID.
Local-only data is unavailable to cloud clients.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setiap tool di &lt;code&gt;tools/list&lt;/code&gt; mendeklarasikan scope OAuth yang dibutuhkannya dan apakah tool itu hanya membaca:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edit_record_item"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"readOnlyHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"destructiveHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"openWorldHint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"securitySchemes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oauth2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;]}]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dari 59 tool: 30 butuh &lt;code&gt;osfoundry:read&lt;/code&gt; dan ditandai &lt;code&gt;readOnlyHint: true&lt;/code&gt;, 26 butuh &lt;code&gt;osfoundry:write&lt;/code&gt;, dan 3 butuh &lt;code&gt;osfoundry:run&lt;/code&gt; (&lt;code&gt;request_session_run&lt;/code&gt;, &lt;code&gt;run_graph_session&lt;/code&gt;, &lt;code&gt;cancel_graph_run&lt;/code&gt;). Host bisa memakai &lt;code&gt;readOnlyHint&lt;/code&gt; untuk menentukan panggilan mana yang perlu prompt persetujuan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ke mana 401 itu mengarahkannya?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;resource_metadata&lt;/code&gt; mengikuti RFC 9728 (OAuth Protected Resource Metadata). Suffix well-known diletakkan tepat setelah host, lalu path resource ditambahkan di belakangnya. Suffix itu tidak ditempel di ujung URL resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /.well-known/oauth-protected-resource/api/v1/external-mcp   200
GET /.well-known/oauth-protected-resource                       200 (same document)
GET /api/v1/external-mcp/.well-known/oauth-protected-resource   404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"osfoundry:models"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kalau kamu membuat client sendiri, 404 itu adalah kesalahan pertama yang akan kamu buat. Ambil URL dari header, jangan menyusunnya sendiri.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apa kata authorization server tentang dirinya?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt; (RFC 8414):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/authorize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"registration_endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.osfoundry.io/api/v1/external-oauth/register"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"grant_types_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"authorization_code"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refresh_token"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"token_endpoint_auth_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"none"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"code_challenge_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S256"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"authorization_response_iss_parameter_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Baca ini sebagai rangkaian keputusan. Ada &lt;code&gt;registration_endpoint&lt;/code&gt;, jadi host bisa mendaftarkan dirinya sendiri (dynamic client registration) dan pengguna tidak pernah berurusan dengan client ID. Autentikasi token-nya &lt;code&gt;none&lt;/code&gt;, jadi host adalah public client tanpa client secret, dan PKCE dengan S256 yang mengikat code ke client yang memintanya. &lt;code&gt;iss&lt;/code&gt; di authorization response mencegah salah tukar saat sebuah host berbicara dengan beberapa authorization server.&lt;/p&gt;

&lt;p&gt;Langkah authorize mengarahkan browser ke halaman consent osFoundry, &lt;code&gt;https://osfoundry.io/external-assistant-authorize&lt;/code&gt;. Di sana pengguna memilih tepat satu workspace dan mencentang "I approve connecting this assistant to …" (saya menyetujui asisten ini dihubungkan ke …). Persetujuan itu hanya menyebut satu workspace, tidak lebih. Koneksi baru belum punya project. Owner menambahkannya belakangan di aplikasi (key icon → Connected → Add to project).&lt;/p&gt;

&lt;h2&gt;
  
  
  Kenapa scope saja tidak cukup?
&lt;/h2&gt;

&lt;p&gt;Scope ditetapkan saat token diterbitkan. &lt;code&gt;osfoundry:write&lt;/code&gt; menyatakan bahwa koneksi boleh melakukan aksi tulis. Scope itu tidak menyebut Note yang mana, dan tidak bisa tahu bahwa Note tersebut dipindahkan ke project lain sepuluh menit lalu. Catatan desain osFoundry mendaftar apa saja yang dibaca ulang di setiap panggilan: status koneksi, workspace, keanggotaan project, project dan ACL resource saat ini, keanggotaan channel atau role di session, tool-nya, dan scope OAuth. Orang yang menyetujui koneksi juga harus masih punya hak edit atas item tersebut. Penghapusan atau pemindahan "langsung berlaku".&lt;/p&gt;

&lt;p&gt;Alternatifnya adalah memasukkan daftar project ke dalam token, atau ke cache yang di-key oleh token, lalu memercayainya sampai kedaluwarsa. Itu lebih murah untuk ditulis. Rebuild di bawah mengimplementasikan keduanya supaya perbedaan perilakunya bisa diukur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild-nya
&lt;/h2&gt;

&lt;p&gt;Tidak ada binary Claude yang terlibat. &lt;code&gt;client.py&lt;/code&gt; berperan sebagai host dan &lt;code&gt;server.py&lt;/code&gt; sebagai server MCP. Keduanya hanya memakai standard library, sekitar 280 baris jika digabung, dan dijalankan di Python 3.9.6 pada Apple M1. Server-nya mengimplementasikan &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, dan &lt;code&gt;tools/call&lt;/code&gt; lewat JSON-RPC di atas HTTP biasa, dua dokumen well-known, &lt;code&gt;/authorize&lt;/code&gt; (halaman consent otomatis disetujui untuk satu workspace), dan &lt;code&gt;/token&lt;/code&gt; dengan PKCE S256. Beberapa route &lt;code&gt;/admin/*&lt;/code&gt; menggantikan klik owner di tab Connected.&lt;/p&gt;

&lt;p&gt;Pengecekan yang berjalan sebelum setiap tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;access&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TOOL_SCOPE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scopes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insufficient_scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                     &lt;span class="c1"&gt;# design B: trust issue-time state
&lt;/span&gt;        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects_at_issue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection_revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;note_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;note_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_info&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PROJECT_MEMBERS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_in_project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client mulai tanpa token. Ia memanggil sebuah tool, menerima 401, mem-parse &lt;code&gt;resource_metadata&lt;/code&gt; dari &lt;code&gt;WWW-Authenticate&lt;/code&gt;, mengambil kedua dokumen metadata, menjalankan authorize dan token dengan PKCE verifier baru, lalu menjalankan skenarionya:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ CHECK=per_call python3 server.py &amp;amp;  python3 client.py 8767
discovery: first call -&amp;gt; 401, metadata /.well-known/oauth-protected-resource/mcp, 5 round trips, 11.3 ms
1 fresh connection, no project yet           200 OK   []
2 after owner adds project A                 200 OK   ["note-a1"]
3 read note in A                             200 OK   "Q3 launch checklist"
4 read note in B (never added)               200 DENY not_in_project
5 write with a read-only token               403 DENY insufficient_scope
6 write with read+write token                200 OK   "saved"
7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 DENY not_in_project
9 same read after revoke                     200 DENY connection_revoked
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client yang sama terhadap &lt;code&gt;CHECK=token_only&lt;/code&gt;. Di sini token membawa daftar project dari refresh terakhirnya:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7 same read after note moved to B            200 DENY not_in_project
8 same read after owner removes project A    200 OK   "Q3 launch checklist v2"
9 same read after revoke                     200 OK   "Q3 launch checklist v2"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Langkah 1–7 identik. Langkah 7 tetap ditolak di desain token-only karena project milik note dicek secara live. Desain token-only hanya jadi basi untuk fakta yang disimpan di dalam token. Langkah 8 dan 9 adalah yang penting bagi owner: mereka sudah menghapus project, lalu mencabut koneksi, tapi chat tetap bisa membaca. Di desain token-only, kondisi itu bertahan sampai token kedaluwarsa, yaitu &lt;code&gt;expires_in: 3600&lt;/code&gt; di rebuild ini.&lt;/p&gt;

&lt;p&gt;Penolakan terbagi di dua lapisan. Scope yang kurang adalah kegagalan di level protokol: 403 dengan &lt;code&gt;error="insufficient_scope"&lt;/code&gt; dan scope yang dibutuhkan di &lt;code&gt;WWW-Authenticate&lt;/code&gt;, sehingga host bisa meminta lebih. Semua yang berkaitan dengan item mana yang terlibat adalah hasil tool biasa dengan &lt;code&gt;isError: true&lt;/code&gt;. Otorisasi ulang tidak akan memperbaikinya, jadi tidak ada gunanya menyuruh host mencoba lagi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Berapa biaya pengecekan di setiap panggilan?
&lt;/h2&gt;

&lt;p&gt;Diukur dengan &lt;code&gt;time.perf_counter_ns()&lt;/code&gt; di sekitar &lt;code&gt;access()&lt;/code&gt; di dalam server, 1,000 panggilan per run (500 diizinkan, 500 ditolak), di Mac yang juga sedang menjalankan job lain:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;median access()&lt;/th&gt;
&lt;th&gt;p99 access()&lt;/th&gt;
&lt;th&gt;median tool call loopback&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cek per panggilan, 6 run&lt;/td&gt;
&lt;td&gt;1.3–1.6 µs&lt;/td&gt;
&lt;td&gt;6.1–7.0 µs&lt;/td&gt;
&lt;td&gt;0.43–0.78 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;token-only, 3 run&lt;/td&gt;
&lt;td&gt;0.7–1.6 µs&lt;/td&gt;
&lt;td&gt;3.5–7.7 µs&lt;/td&gt;
&lt;td&gt;0.35–1.36 ms (2 run)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Di memori, pengecekan ulang tenggelam di noise panggilan HTTP loopback. Server sungguhan melakukan query database, bukan sekadar cek keanggotaan set, jadi biayanya lebih tinggi. Rebuild ini tidak bisa mengukurnya, jadi tidak ada angka yang diberikan. Sebagai gambaran, perjalanan jaringan ke endpoint live dari Tokyo sekitar 500 ms sampai 800 ms per request dengan urllib dan koneksi TLS baru setiap kali (masing-masing n=10: median &lt;code&gt;initialize&lt;/code&gt; 790 ms, &lt;code&gt;tools/list&lt;/code&gt; 816 ms, &lt;code&gt;tools/call&lt;/code&gt; yang 401 565 ms, tiap dokumen metadata sekitar 520–530 ms). Waktu sebuah connector tidak habis di pengecekan akses.&lt;/p&gt;

&lt;p&gt;Discovery-nya sendiri butuh 5 round trip HTTP secara lokal: 401, resource metadata, server metadata, authorize, token. Itu memakan 7.5–13.4 ms dalam tujuh run dan 34 ms sekali saat Mac sedang sibuk. Terhadap server live, tiga leg GET/POST yang bisa dilakukan tanpa kredensial totalnya kira-kira 1.6 s dari cold start. Leg authorize toh menunggu manusia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kenapa Claude bisa memanggil osFoundry, tapi osFoundry tidak bisa membuka chat Claude kamu?
&lt;/h2&gt;

&lt;p&gt;Di setup ini, host-lah yang memanggil. Host membuka koneksi, mengambil daftar tool, dan memutuskan kapan memanggil salah satunya. Server menjawab. Tidak ada apa pun di setup ini yang memberi server akses ke percakapan milik host. Rencana osFoundry menuliskannya di tabel kapabilitasnya: membuka atau mendaftar chat pribadi ChatGPT/Claude sembarang adalah "No", dengan keterangan "No general host API is provided for this".&lt;/p&gt;

&lt;p&gt;Hal itu membentuk tool-nya. Dengan &lt;code&gt;link_current_chat&lt;/code&gt;, chat itu sendiri yang membuat link dari dalam chat. Label-nya dilaporkan sendiri oleh chat, dan osFoundry memakainya untuk routing, bukan sebagai identitas. Pekerjaan yang ditugaskan ke chat Claude yang ter-link menunggu sampai pengguna membuka chat itu dan memintanya untuk mengecek. ChatGPT punya MCP Events untuk chat yang berlangganan, dan rencana osFoundry memperlakukannya sebagai fitur khusus host tersebut.&lt;/p&gt;

&lt;p&gt;Sisi server memegang semua keputusan soal data, karena hanya sisi itu yang tahu kondisi terkini sebuah project. Sisi host memegang semua keputusan soal kapan bertindak dan apakah perlu bertanya ke pengguna. Pengecekan per panggilan inilah yang membuat server tetap benar ketika kedua sisi itu tidak lagi sinkron.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kalau kamu memasangnya sendiri
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Taruh &lt;code&gt;resource_metadata&lt;/code&gt; di &lt;code&gt;WWW-Authenticate&lt;/code&gt; milik respons 401 dan sajikan RFC 9728 di &lt;code&gt;/.well-known/oauth-protected-resource/&amp;lt;resource path&amp;gt;&lt;/code&gt;. Client yang menyusun URL sendiri bisa salah. Header itu menyelamatkan setiap client dari kesalahan tersebut.&lt;/li&gt;
&lt;li&gt;Tentukan apa yang publik sebelum login. osFoundry membuka &lt;code&gt;initialize&lt;/code&gt; dan &lt;code&gt;tools/list&lt;/code&gt; dan menjaga &lt;code&gt;tools/call&lt;/code&gt;. Kalau deskripsi tool kamu membocorkan sesuatu tentang tenant, jaga juga &lt;code&gt;tools/list&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Deklarasikan scope dan &lt;code&gt;readOnlyHint&lt;/code&gt; per tool. Host bisa membangun prompt persetujuan dari situ, dan 403 &lt;code&gt;insufficient_scope&lt;/code&gt; memberi host sesuatu yang konkret untuk diminta ulang.&lt;/li&gt;
&lt;li&gt;Buat token tetap kecil: siapa, koneksi mana, scope apa. Baca ulang workspace, keanggotaan project, project milik resource, dan role terkini orang yang menyetujui di setiap panggilan, supaya pencabutan dan "pindah ke project lain" langsung berlaku di panggilan berikutnya.&lt;/li&gt;
&lt;li&gt;Kembalikan penolakan resource sebagai hasil tool, bukan sebagai 401/403. Token yang berbeda tidak akan memperbaiki "Note ini ada di project B".&lt;/li&gt;
&lt;li&gt;Buat operasi tulis membawa &lt;code&gt;expected_version&lt;/code&gt; dan idempotency key. Host melakukan retry, dan model mengulang panggilan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sisi MCP dari spesifikasinya ada di &lt;a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization" rel="noopener noreferrer"&gt;halaman otorisasi MCP&lt;/a&gt;. Definisi singkat versi osFoundry ada di &lt;a href="https://osfoundry.io/id/glossary/mcp" rel="noopener noreferrer"&gt;entri glosariumnya tentang MCP&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>oauth</category>
      <category>claude</category>
      <category>keamanan</category>
    </item>
  </channel>
</rss>
