|
| 1 | +# Bağlı sitelerde ajan eylemleri — kontrat |
| 2 | + |
| 3 | +Durum: **tasarım kesinleşti, uygulama sürüyor.** Bu belge Orbit ile MCP köprüsü |
| 4 | +arasındaki sözleşmedir. MCP tarafını Selene kuruyor; buradaki hiçbir şey |
| 5 | +`orbit-remote-mcp` deposuna dokunmadan değişmemeli. |
| 6 | + |
| 7 | +## Neyi çözüyor |
| 8 | + |
| 9 | +İnsan, Orbit hesabıyla girdiği bir siteyi panelden ajanına açıyor. Ajan o |
| 10 | +sitede **insanın adına** iş yapıyor — ayrı ajan hesabı, ayrı ajan listesi |
| 11 | +oluşmuyor. Orbit'in kendisi istisna: orada ajanlar kendi adına davranır ve o |
| 12 | +yol `mcp_authorization_grants` üzerinden yürür, buraya karışmaz. |
| 13 | + |
| 14 | +Tasarımın taşıyıcı kısıtı: **Orbit sitelerin işlerini bilmez.** "Listeye anime |
| 15 | +ekle" Orbit'te tanımlı değildir. Orbit yetkiyi tutar, imzalar ve taşır; işlemin |
| 16 | +ne olduğunu site söyler. Beşinci site eklendiğinde Orbit'e kod girmemesinin |
| 17 | +sebebi bu. |
| 18 | + |
| 19 | +İkinci kısıt: **ajan hiçbir şey saklamaz.** ChatGPT Web gibi istemcilerde |
| 20 | +konuşmalar arasında kalıcı depo yok. Ajanın elinde site anahtarı tutulmaz; |
| 21 | +Orbit'e bağlı olması yeter, site anahtarını Orbit üretip kendisi kullanır. |
| 22 | + |
| 23 | +## 1. Katalog — ajan ne yapabileceğini nereden öğrenir |
| 24 | + |
| 25 | +``` |
| 26 | +GET /v1/me/connected-sites/actions |
| 27 | +Authorization: Bearer <ajan credential> |
| 28 | +``` |
| 29 | + |
| 30 | +Ajan kimliğiyle çağrılır. Yanıt, ajanın insanının **ajan erişimi açık** olan |
| 31 | +bağlı sitelerini ve o sitelerin bildirdiği işlemleri taşır. Erişimi kapalı |
| 32 | +siteler listede hiç görünmez — kapalı bir kapıyı katalogda göstermek, ajanı |
| 33 | +kesin reddedilecek bir isteğe davet etmek olurdu. |
| 34 | + |
| 35 | +```json |
| 36 | +{ |
| 37 | + "sites": [ |
| 38 | + { |
| 39 | + "grantId": "grant_01H…", |
| 40 | + "clientId": "orbit-equinox-rota", |
| 41 | + "label": "Equinox Rota", |
| 42 | + "siteUrl": "https://anime.sametbasbug.dev", |
| 43 | + "catalogFetchedAt": 1756000000000, |
| 44 | + "operations": [ |
| 45 | + { |
| 46 | + "operationId": "rota.listeyeEkle", |
| 47 | + "summary": "İnsanın listesine anime ekler veya durumunu günceller.", |
| 48 | + "idempotent": true, |
| 49 | + "input": { |
| 50 | + "type": "object", |
| 51 | + "required": ["animeId", "durum"], |
| 52 | + "additionalProperties": false, |
| 53 | + "properties": { |
| 54 | + "animeId": { "type": "string", "maxLength": 300 }, |
| 55 | + "durum": { "enum": ["IZLIYOR", "BITTI", "PLANLI", "BIRAKTI"] }, |
| 56 | + "ilerleme": { "type": "integer", "minimum": 0, "maximum": 100000 }, |
| 57 | + "puan": { "type": "integer", "minimum": 1, "maximum": 10 } |
| 58 | + } |
| 59 | + }, |
| 60 | + "output": { |
| 61 | + "type": "object", |
| 62 | + "properties": { "animeId": { "type": "string" }, "durum": { "type": "string" } } |
| 63 | + } |
| 64 | + } |
| 65 | + ] |
| 66 | + } |
| 67 | + ] |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +### `describe` neden ayrı bir uç değil |
| 72 | + |
| 73 | +Şema kataloğun içinde geliyor. Ayrı bir `describe` ucu her işlem için ek bir |
| 74 | +gidiş-dönüş demekti ve ajanın elinde saklayacak yer olmadığı için o gidiş-dönüş |
| 75 | +her seferinde tekrarlanırdı. Katalog küçük ve zaten site başına filtreli. |
| 76 | + |
| 77 | +`input` ve `output` **JSON Schema draft 2020-12** alt kümesidir: `type`, |
| 78 | +`required`, `properties`, `enum`, `additionalProperties`, `minimum`, `maximum`, |
| 79 | +`maxLength`, `items`. Bu listeyi genişletmek MCP tarafını kırabilir; genişlerse |
| 80 | +burada ilan edilir. |
| 81 | + |
| 82 | +## 2. Çağrı — ajan işi nasıl yaptırır |
| 83 | + |
| 84 | +``` |
| 85 | +POST /v1/me/connected-sites/{grantId}/actions |
| 86 | +Authorization: Bearer <ajan credential> |
| 87 | +Content-Type: application/json |
| 88 | +
|
| 89 | +{ |
| 90 | + "operationId": "rota.listeyeEkle", |
| 91 | + "input": { "animeId": "kitsu:1376", "durum": "IZLIYOR" }, |
| 92 | + "idempotencyKey": "<yazdıran tarafın ürettiği, ≤128 basılabilir karakter>" |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +`grantId` yolda, çünkü yetki kontrolünün baktığı satır o. Gövdede `clientId` |
| 97 | +taşımak, aynı bilgiyi iki yerden alıp uyuşmazlığını kontrol etmek olurdu. |
| 98 | + |
| 99 | +`idempotencyKey` **zorunlu**. Bu bir yazma işlemi ve ajan tarafı yeniden |
| 100 | +denemeye yatkın; anahtarsız bir tekrar, listeye ikinci kez yazmak demek. |
| 101 | +Biçim Orbit'in başka yerlerindekiyle aynı: 1–128 basılabilir ASCII. |
| 102 | + |
| 103 | +### Yanıt |
| 104 | + |
| 105 | +```json |
| 106 | +{ |
| 107 | + "action": { |
| 108 | + "operationId": "rota.listeyeEkle", |
| 109 | + "status": "applied", |
| 110 | + "output": { "animeId": "kitsu:1376", "durum": "IZLIYOR" } |
| 111 | + } |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +`status`: `applied` (uygulandı) veya `replayed` (aynı `idempotencyKey` ile daha |
| 116 | +önce uygulanmıştı; `output` ilk çalışmanınkidir). |
| 117 | + |
| 118 | +### Hatalar |
| 119 | + |
| 120 | +| kod | durum | anlamı | |
| 121 | +|---|---|---| |
| 122 | +| `connected_site_not_found` | 404 | İzin yok ya da başkasına ait. | |
| 123 | +| `agent_access_closed` | 403 | İnsan bu site için ajan erişimini açmamış ya da kapatmış. | |
| 124 | +| `unknown_site_operation` | 400 | `operationId` sitenin kataloğunda yok. | |
| 125 | +| `invalid_site_operation_input` | 400 | `input`, işlemin şemasına uymuyor. | |
| 126 | +| `site_catalog_unavailable` | 503 | Sitenin katalog dosyası okunamadı. | |
| 127 | +| `site_action_failed` | 502 | Site isteği reddetti ya da erişilemedi; `details.siteStatus` taşınır. | |
| 128 | +| `idempotency_conflict` | 409 | Aynı anahtar farklı bir gövdeyle kullanıldı. | |
| 129 | + |
| 130 | +Girdi doğrulaması **Orbit'te** yapılır, siteye geçmeden. Site kendi kontrolünü |
| 131 | +ayrıca yapar — iki kat, çünkü Orbit şemayı siteden okur ve okuduğu şey eskimiş |
| 132 | +olabilir. |
| 133 | + |
| 134 | +## 3. Site kendini nasıl bildirir |
| 135 | + |
| 136 | +`oauth_clients` satırına `actions_url` alanı eklenir. Site oraya statik bir |
| 137 | +JSON koyar; sunucu gerekmez, GitHub Pages'te duran bir dosya yeter. |
| 138 | + |
| 139 | +```json |
| 140 | +{ |
| 141 | + "version": 1, |
| 142 | + "actionsEndpoint": "https://rslltclzdfzwaxqtozmq.supabase.co/functions/v1/orbit-eylem", |
| 143 | + "operations": [ /* katalogdaki `operations` ile aynı biçim */ ] |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +Orbit bu dosyayı okur ve **10 dakika** önbellekte tutar; `catalogFetchedAt` o |
| 148 | +anı verir. Yeni bir işlem eklendiğinde Orbit'e kod girmez, en geç 10 dakika |
| 149 | +sonra katalogda görünür. |
| 150 | + |
| 151 | +`actionsEndpoint` `https` olmak ve `oauth_clients.site_url` ile aynı kayıtlı |
| 152 | +siteye ait olmak zorundadır; katalog dosyası Orbit'i keyfi bir adrese istek |
| 153 | +atmaya ikna edemez. |
| 154 | + |
| 155 | +## 4. Orbit → site: devretme belgesi |
| 156 | + |
| 157 | +Orbit siteye şunu gönderir: |
| 158 | + |
| 159 | +``` |
| 160 | +POST <actionsEndpoint> |
| 161 | +Authorization: Bearer <eylem belgesi> |
| 162 | +Content-Type: application/json |
| 163 | +Idempotency-Key: <ajanın verdiği anahtar> |
| 164 | +
|
| 165 | +{ "operationId": "rota.listeyeEkle", "input": { … } } |
| 166 | +``` |
| 167 | + |
| 168 | +Eylem belgesi **ES256 imzalı JWT**'dir ve Orbit'in mevcut site anahtarlarıyla |
| 169 | +imzalanır; site onu `/.well-known/jwks.json` üzerinden doğrular. Siteyle |
| 170 | +paylaşılan kalıcı bir sır **yoktur** — sızacak bir şey olmasın diye. |
| 171 | + |
| 172 | +```json |
| 173 | +{ |
| 174 | + "iss": "https://orbit.sametbasbug.dev", |
| 175 | + "aud": "orbit-equinox-rota", |
| 176 | + "sub": "<pairwise subject — sitenin o insan için tanıdığı kimlik>", |
| 177 | + "act": { "sub": "agent:<agentId>", "handle": "<ajan handle>" }, |
| 178 | + "scope": "site.actions", |
| 179 | + "operation": "rota.listeyeEkle", |
| 180 | + "jti": "<tek kullanımlık>", |
| 181 | + "iat": 1756000000, |
| 182 | + "exp": 1756000060 |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +- `sub` sitenin giriş sırasında tanıdığı **aynı** pairwise kimliktir. Site |
| 187 | + kullanıcıyı kendi kaydında bununla bulur; yeni bir eşleme kurmasına gerek yok. |
| 188 | +- `act` (RFC 8693 "actor") işi yapanın ajan olduğunu söyler. İş insanın |
| 189 | + adınadır ama **kimin yaptığı kaybolmaz**; site isterse kaydeder. |
| 190 | +- `operation` belgeye gömülüdür: bir işlem için alınmış belge, gövdesi |
| 191 | + değiştirilerek başka bir işlem için kullanılamaz. |
| 192 | +- Ömür **60 saniye**. Ajan saklamaz, Orbit her çağrıda yeniden üretir. |
| 193 | + |
| 194 | +Site şunları doğrulamak zorundadır: imza, `iss`, `aud` (kendi clientId'si), |
| 195 | +`exp`, ve `operation` ile gövdedeki `operationId`'nin eşleştiği. |
| 196 | + |
| 197 | +## 5. Rota'nın ilk işlemleri |
| 198 | + |
| 199 | +| operationId | ne yapar | |
| 200 | +|---|---| |
| 201 | +| `rota.listeyeEkle` | Listeye ekler ya da var olan kaydın durumunu/ilerlemesini/puanını günceller. | |
| 202 | +| `rota.listeyiOku` | İnsanın listesini döndürür. | |
| 203 | + |
| 204 | +İkisi de `personal_list_entries` üzerinde ve **insanın satırlarında** çalışır. |
| 205 | +Ajanın kendi listesi yoktur. |
| 206 | + |
| 207 | +## Kararlar ve gerekçeleri |
| 208 | + |
| 209 | +**Neden ajan siteye doğrudan konuşmuyor.** Konuşsaydı elinde bir site anahtarı |
| 210 | +tutması gerekirdi; saklama yeri olmayan istemcilerde bu çalışmaz. Ayrıca iptal |
| 211 | +tek yerde kalmaz — insan Orbit'te kapatır ama ajanın elindeki anahtar yaşamaya |
| 212 | +devam ederdi. |
| 213 | + |
| 214 | +**Neden izin `oauth_client_grants` satırının içinde.** Site bağlantısı |
| 215 | +kesildiğinde ajan erişimi de düşmeli ve bunun ayrı bir temizlik adımı |
| 216 | +gerektirmemesi lazım; unutulan temizlik açık kalan kapıdır. |
| 217 | + |
| 218 | +**Neden katalog siteden geliyor.** Alternatifi, her site özelliği için Orbit'e |
| 219 | +kod eklemekti. Beş sitede Orbit her şeyi bilmek zorunda olan bir yere dönerdi. |
0 commit comments