DEV Community

Cover image for Mengapa osFoundry memberi Codex satu tool untuk mengusulkan handoff dan tool lain untuk memulai run?
BBT for BBT Indonesia

Posted on

Mengapa osFoundry memberi Codex satu tool untuk mengusulkan handoff dan tool lain untuk memulai run?

Inilah yang diterima Codex CLI saat pertama kali memanggil tool sesi di osFoundry sebelum login:

POST /api/v1/external-mcp  {"method":"tools/call","params":{"name":"propose_session_handoff","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"}], …}}
Enter fullscreen mode Exit fullscreen mode

initialize dan tools/list menjawab tanpa token, jadi client bisa melihat ke-59 tool sebelum ada yang login. Dua di antaranya terlihat seperti melakukan hal yang sama:

{"name": "propose_session_handoff",
 "description": "Send a scoped task and context summary to the owner of an invited session. The owner must accept before a run is queued.",
 "required": ["session_type","session_id","goal","context_summary","requested_output","idempotency_key"],
 "annotations": {"readOnlyHint": false, "openWorldHint": true, "destructiveHint": true},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}

{"name": "request_session_run",
 "description": "Queue a bounded goal for an agent, team, or loop session where this assistant is an invited runner. The osFoundry runtime executes the task when available.",
 "required": ["session_type","session_id","goal","idempotency_key"],
 "annotations": {"readOnlyHint": false, "openWorldHint": false, "destructiveHint": true},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:run"]}]}
Enter fullscreen mode Exit fullscreen mode

Keduanya berujung pada sebuah sesi yang mengerjakan sesuatu. Sisa tulisan ini membahas kenapa keduanya dipisah, batas apa saja yang berlaku di masing-masing, dan sebuah rekonstruksi sisi client dengan Python yang mengecek batas-batas itu terhadap schema live.

Apa yang membedakan kedua panggilan itu?

Siapa yang bilang ya, dan kapan.

Handoff meminta persetujuan orang, saat itu juga. Owner menerima notifikasi ("Codex CLI requests a handoff"). Tab Handoffs di sesi menampilkannya di bawah "Outside assistant requests" (permintaan dari asisten luar) beserta tujuannya, bagian lipat "Context and expected output", dan baris "Up to N rounds on your agent runtime". Tidak ada yang masuk antrean sampai seseorang mengklik Accept and run. Setelah itu web client mengirim {decision: "accept"} ke /agent-sessions/{id}/external-handoffs/{handoff_id}/decision, dan toast-nya berbunyi "Handoff accepted and run queued". Baris handoff berpindah dari pending ke accepted, declined, atau cancelled.

Run sudah disetujui sebelumnya. Owner memberi koneksi itu kursi Runner di tab Connected, dan itulah persetujuannya. Teks bantuan di sana: "Invite it to a session as Contributor to post notes and propose handoffs, or Runner to also request runs. A handoff still needs the session owner's approval." Di halaman login, scope run tertulis "Run sessions where you grant a runner role".

Kalau keduanya digabung jadi satu tool dengan flag needs_approval, pemanggil yang menentukan apakah manusia perlu ditanya. Dengan dua tool, keputusan itu ada di sisi owner, di dua tempat yang tidak bisa disentuh pemanggil: scope OAuth (osfoundry:write dibanding osfoundry:run) dan kursi (Contributor dibanding Runner). Konfigurasi Codex yang tidak mencantumkan osfoundry:run di scopes tidak bisa memulai run, bahkan di kursi Runner.

Annotations-nya mengikuti pemisahan yang sama. propose_session_handoff bernilai openWorldHint: true karena output-nya sampai ke orang di luar panggilan. request_session_run bernilai openWorldHint: false, karena hanya menyentuh antrean sesi yang sudah setuju sejak awal. Keduanya destructiveHint: true, jadi client yang memakai default_tools_approval_mode = "writes" milik Codex akan bertanya dulu sebelum menjalankan salah satunya.

Mengapa handoff mewajibkan ringkasan konteks?

Karena ada orang yang membacanya sebelum apa pun dijalankan. context_summary dan requested_output wajib di handoff dan tidak ada di run. Owner sedang memutuskan apakah runtime sesinya mau dipakai untuk ini, dan "fix the export" saja tidak cukup untuk memutuskan. Runner sudah punya kepercayaan itu, jadi run hanya butuh goal.

Batasnya juga berbeda. goal handoff berhenti di 5,000 karakter, ditambah 5,000 untuk konteks dan 1,000 untuk output yang diminta. goal run bisa sampai 10,000. Dalam praktiknya, goal run-lah yang membawa konteks, karena tidak ada field lain untuk itu.

Apa yang bisa dibaca kembali oleh Codex?

Lebih sedikit dari isi sesinya:

  • read_session_task_result: "the status and attributed runtime result for one task started by this connection". Task id milik koneksi lain tidak ada gunanya.
  • read_session_mail: "notes addressed to this connection and notes it posted", 1 sampai 20 per halaman dengan before_id untuk yang lebih lama. "Internal agent analysis is excluded."
  • read_session_state: status terkini dan task terbaru dari sesi yang sudah diikuti koneksi itu.
  • post_session_note: catatan beratribusi yang bisa dibalas agen melalui "the returned external sender slug". Catatan ini "does not trigger an automatic run", jadi tidak bisa dipakai untuk melewati klik owner.

Di sesi tim, catatan-catatan itu muncul di Team mail dengan nama tampilan koneksi dan label "outside assistant".

Rekonstruksinya

Tidak ada binary codex yang terpasang di Mac ini, jadi handoff_sim.py (sekitar 220 baris) berperan sebagai client. Script ini melakukan tiga hal:

  1. Memanggil server live tanpa token: initialize, tools/list, dan satu tools/call, masing-masing 15 kali, lalu menyimpan daftar tool.
  2. Memvalidasi dua belas kasus argumen terhadap inputSchema live dengan jsonschema (Draft 2020-12), jadi tidak ada batas yang ditebak-tebak.
  3. Menjalankan model in-memory kecil berisi kursi, scope, keputusan owner, dan idempotensi. Kode server osFoundry tidak publik, jadi bagian ini direkonstruksi dari deskripsi tool dan web client yang menampilkan handoff. Responsnya berasal dari model, bukan dari server.
$ uv run --with tiktoken --with jsonschema python3 handoff_sim.py
Enter fullscreen mode Exit fullscreen mode

Jaringan

initialize             HTTP 200  median  566.3  p90  799.0
tools/list             HTTP 200  median  559.7  p90  937.7
tools/call (no token)  HTTP 401  median  482.6  p90  677.0
Enter fullscreen mode Exit fullscreen mode

Satuannya milidetik, diukur dari Mac ini di Jepang lewat edge KIX milik CloudFront, jadi angkanya lebih banyak bercerita tentang rute daripada server. Yang penting adalah polanya. Discovery gratis dan cukup cepat untuk dijalankan setiap kali Codex start. Panggilan sungguhan pertama memakan satu round trip 401, dan 401 itu membawa resource_metadata, yang membuat Codex bisa menemukan server OAuth tanpa diberi tahu.

Tujuh tool sesi itu memakan 1,152 token dari 10,197 token daftar lengkap (11.3%, cl100k_base, jadi anggap saja perkiraan untuk tokenizer Codex sendiri). Mencantumkan hanya tool-tool itu di enabled_tools membuat prompt tetap kecil.

Batasnya

Tool Scope Wajib Batas
propose_session_handoff write 6 goal 5,000, context 5,000, output 1,000, max_rounds 1–10
cancel_session_handoff write 1 hanya handoff_id
request_session_run run 4 goal 10,000, max_rounds 1–10
read_session_state read 2 tidak ada
read_session_task_result read 3 tidak ada
read_session_mail read 2 limit 1–20
post_session_note write 5 subject 200, body 10,000

Dua belas kasus terhadap schema live:

propose_session_handoff   6,000-char goal        REJECT maxLength: goal
request_session_run       6,000-char goal        ok
propose_session_handoff   no context_summary     REJECT required
propose_session_handoff   max_rounds omitted     ok
request_session_run       max_rounds 0           REJECT minimum: max_rounds
request_session_run       max_rounds 11          REJECT maximum: max_rounds
request_session_run       no idempotency_key     REJECT required
request_session_run       session_type 'room'    REJECT enum: session_type
request_session_run       extra field 'model'    REJECT additionalProperties
read_session_mail         limit 21               REJECT maximum: limit
post_session_note         201-char subject       REJECT maxLength: subject
cancel_session_handoff    only handoff_id        ok
Enter fullscreen mode Exit fullscreen mode

Dua di antaranya layak dilihat lagi. goal yang sama sepanjang 6,000 karakter lolos sebagai run tapi terlalu panjang sebagai handoff. Client yang menyusun satu prompt panjang lalu mengirimnya ke tool mana pun yang tersedia akan kena masalah ini. Lalu additionalProperties: false menolak field model: pemanggil tidak bisa memilih model sesi. Sesi berjalan dengan pengaturannya sendiri.

max_rounds opsional di keduanya. Schema tidak mencantumkan nilai default, jadi kalau angkanya penting buatmu, kirimkan.

Kursi, cancel, dan accept (model)

tool                      observer contributor runner
propose_session_handoff   -        yes         yes
cancel_session_handoff    -        yes         yes
request_session_run       -        -           yes
read_session_state        yes      yes         yes
read_session_task_result  yes      yes         yes
read_session_mail         yes      yes         yes
post_session_note         -        yes         yes
runner seat, token without osfoundry:run -> {'error': 'token lacks osfoundry:run'}

cancel while pending -> {'status': 'cancelled'}
owner clicks Accept after cancel -> cancelled
owner accepts -> accepted | cancel after -> {'error': 'already accepted'}
another connection reads that task -> {'error': 'not a task started by this connection'}
Enter fullscreen mode Exit fullscreen mode

Matriks ini adalah teks bantuan tab Connected yang diubah jadi tabel. Aturan yang menarik adalah jendela cancel. cancel_session_handoff dideskripsikan bekerja "before its owner accepts it", jadi siapa yang lebih dulu, dia yang menang. Begitu run sudah masuk antrean, menariknya kembali bukan lagi wewenang pemanggil. Itu juga sebabnya cancel hanya menerima handoff_id: tidak ada state yang perlu dikirim, hanya baris pending yang perlu ditutup.

Respons yang hilang

Setiap write mewajibkan idempotency_key. Untuk melihat manfaatnya, model mengirim 1,000 panggilan request_session_run, membuang 20% respons (seed 7), dan membiarkan client mencoba ulang sampai tiga kali dengan key yang sama:

server honours idempotency_key=True  calls sent 1276  tasks queued 1000  duplicates 0
server honours idempotency_key=False calls sent 1276  tasks queued 1276  duplicates 276
Enter fullscreen mode Exit fullscreen mode

276 run tambahan di runtime milik orang lain, itulah kasus yang ingin dicegah oleh key wajib ini. Client mana pun yang retry setelah timeout akan mengalaminya, dan run yang sudah jalan bukan sesuatu yang ingin terjadi dua kali. Karena key-nya wajib, bukan opsional, tidak ada client yang bisa lupa.

Seluruh script memakan 26.4 s waktu nyata di Apple M1. Hampir semuanya untuk 45 panggilan jaringan, dan waktu CPU user-nya 0.37 s.

Kalau kamu merangkainya sendiri

  • Pisahkan "bertanya ke orang" dan "bertindak dengan izin yang sudah ada" menjadi dua tool dengan dua scope, supaya pilihan owner ada di token dan kursi, bukan di argumen yang diatur pemanggil.
  • Wajibkan field yang dibutuhkan orang untuk memutuskan (context_summary, requested_output) hanya di proposal, dan beri batas yang lebih ketat daripada goal run.
  • Izinkan pemanggil membatalkan hanya selama permintaan masih pending. Setelah diterima, menghentikan pekerjaan adalah urusan owner.
  • Batasi setiap pembacaan kembali pada apa yang dimulai atau dikirim ke koneksi ini, dan jangan sertakan analisis internal sesi.
  • Wajibkan idempotency_key di setiap write, dan kembalikan hasil pertama saat ada pengulangan.
  • Tutup schema dengan additionalProperties: false, supaya pemanggil tidak bisa menyelipkan model atau daftar tool yang tidak dipilih sesi.

Cara agen-agen osFoundry sendiri saling mengoper pekerjaan antar langkah ada di halaman handoffs. Untuk protokol di baliknya, lihat entri glosarium MCP.

Top comments (0)