Yapay zekâ ajanları
Her yapay zekâ ajanına API anahtarınız yerine kendi kimliğini verin. Ajan bir isim değil, bir anahtardır: yalnızca açık anahtarını kaydedersiniz, ajan private_key_jwt iddiasıyla giriş yapar ve o anahtara DPoP ile bağlı 10 dakikalık bir jeton alır; bir logdan ya da istemden kopyalanan jeton tek başına işe yaramaz. Her ajanın sorumlu bir sahibi, kullanıcılarınızdan onay isteyebileceği işlem türleri ve bir gözden geçirme tarihi vardır.
tq_ anahtarı yetkilerinin izin verdiği her şeyi yapabilir; bir ajan jetonu yalnızca ajanın işini. Bir betiğe anahtar vermeniz gerekiyorsa dar yetkili bir anahtar oluşturun (örneğin login:*): geri kalan her şey 403 insufficient_scope döner.| Parça | Kural |
|---|---|
client_assertion | Ajanın anahtarlarından biriyle imzalı JWT (RFC 7523; ES256 ya da EdDSA, başlıkta kid): iss = sub = ajan kimliği, aud = …/agents/token, en çok 5 dakika, bir kez kullanılan jti. |
DPoP | Jeton isteğinde ve her çağrıda bir kanıt JWT (RFC 9449): htm, htu, taze bir iat, benzersiz bir jti; çağrılarda ath (jetonun özeti). Jeton yalnızca bağlandığı anahtarla çalışır. |
access_token | Opak, 10 dakika, yetkiler agent:read approvals:request approvals:read. Yenileme jetonu yok: ajan yeniden ister (SDK'lar bunu yapar). |
| Durum | Anlamı |
|---|---|
blocked: suspended | Siz askıya aldınız (API ya da dashboard); devam ettirince tekrar çalışır. |
statusReason: owner_left | Sahibi organizasyondan ayrıldı: yeni bir sahip verin (PATCH ownerEmail) ya da iptal edin. |
blocked: review_overdue | Gözden geçirme tarihi geçti: hâlâ gerekli olduğunu kontrol edip yenileyin (PATCH reviewInDays). |
blocked: workspace_paused | Kill switch açık: çalışma alanının tüm ajanları durdu. |
blocked: revoked | Kalıcı olarak iptal edildi; kayıt denetim izi için kalır. |
Her ajan çağrısı ajanını yeniden denetler; askıya alma, iptal, anahtar kaldırma ya da kill switch (PUT /agents/kill-switch) önceden verilmiş jetonları da bitirir. Dashboard'da (Developers → AI agents) durdurmak anında olur; ajanın yeniden çalışmasına yöneticinin telefonu onay verir. Her giriş ve değişiklik bir log satırıdır (AGENT_*), üzerine işlem yapabileceğiniz olanlar ayrıca webhook olarak gelir. Bir SIEM hepsini GET /agents/events?after=… ile okur (en eskiden başlar; next'i saklayın; her SDK'da tq.agents.listEvents); agent.approval tetikleyicisindeki bir Action telefona bir şey gitmeden reddedebilir, adım yükseltebilir ya da passkey isteyebilir.
Ajan POST /agents/approvals ile serbest metin özet değil, tipli authorizationDetails gönderir. Touchque her alanı denetler ve telefondaki satırları kendi sözcükleriyle çizer: önce ajanın adı, sonra ayrıntılar, en sonda doğrulanmamış diye işaretli ajan gerekçesi. Görünmez, kontrol ya da yön değiştiren karakter içeren metin reddedilir. İstek bir girişle aynı denetimlerden geçer (dondurma, itme sınırı, risk motoru, Actions, Vishing Shield, vaka kodu); telefon Face ID ya da donanım anahtarı ister ve imzası tam olarak bu satırları kapsar. presence: interactive (varsayılan) ile kişi konuşmanızda görünen sayıyı yazar; background'da sayı yoktur ve kritik işlem türlerinde asla izin verilmez.
| type | Alanlar |
|---|---|
payment_initiation | instructedAmount { currency, amount }, creditorName, creditorAccount { iban | accountNumber }, remittanceInformationUnstructured IBAN'lar kontrol basamaklarını geçmelidir. |
purchase | amount { currency, amount }, merchant, item |
data_access | resource, actions [read, write, delete, export, share], records, destination |
account_change | setting, from, to |
message_send | channel [email, sms, chat, social, other], recipients[], subject |
generic | fields [{ label, value }] (1-4) Kendi etiketleriniz; mümkünse tipli bir kayıt kullanın. |
| status | Anlamı |
|---|---|
pending | Telefon bekleniyor; challengeCode'u konuşmada gösterin. |
approved | Yanıtta approvalToken var (5 dakika; kullanılabilir oldukça yenisini isteyin). |
provisional | Bir telefon görüşmesi sırasında onaylandı (Vishing Shield): telefon görüşmeden sonra doğrulayana kadar jeton yok. |
rejected · blocked · revoked | Kişi reddetti, risk motoru reddetti ya da onay geri alındı. |
awaiting_second | Dört göz: kişi onayladı, ikinci onaylayan henüz onaylamadı (secondApprover, secondChallengeCode). İkisi de onaylamadan jeton yok. |
expired · stale · redeemed | Zamanında yanıtlanmadı, 15 dakikadan önce onaylandı ya da zaten kullanıldı. |
typ: touchque-approval+jwt, anahtarlar GET /agents/jwks.json'da: sub (kişi), act.sub (ajan), authorization_details, cnf.jkt (ajanın anahtarı), details ve proof (telefonun bunlar üzerindeki donanım imzası), 5 dakika. Yürüten sunucunuz işlemden hemen önce onu bir kez kullanır (POST /agents/approvals/redeem) ve ayrıntıları yapacağı işle karşılaştırır; SDK'lar ikisini redeemApproval(token, expect) ile yapar ve uyuşmazlıkta hiçbir şey kullanılmadan reddeder.Her ajanın bir politikası vardır (PUT /agents/{agentId}/policy ya da dashboard'daki sayfası); bir telefona bir şey ulaşmadan önce denetlenir, ardından kişinin kendi itme sınırı, dondurma ve risk motoru yine uygulanır. Dashboard'da daha sıkı bir politika hemen kaydedilir, daha gevşek olan yöneticinin telefonunu bekler.
| Alan | Kural |
|---|---|
requestsPer10Min · requestsPerUserPer10Min | 60 · 5. Ajanın tüm istekleri ve tek bir kişiye istekler (bildirim yorgunluğu). |
amountPerRequest · amountPerDay | { "TRY": "1000.00" }: bir onayın ve 24 saatte bekleyen ile onaylananların taşıyabileceği en çok tutar. |
background | non_critical (varsayılan) ya da never. Kritik bir işlem türü asla arka planda sorulmaz. |
newRecipients · recipientAllowlist | Bu kişinin bu ajan için hiç onaylamadığı hesap, satıcı ya da adresi ask (varsayılan) ile sor ya da deny ile reddet; IBAN ya da hesap listesi diğerlerini reddeder. |
fourEyes | { approvers, above?, types? }: kişiden sonra ikinci bir kişi (asla aynı kişi, asla ajanın sahibi değil) kendi telefonunda onaylar; bu işlem türlerinde ya da above üstündeki tutarlarda. |
requirePasskeyAbove | { "TRY": "10000.00" }: üstünde kişi sizin sayfanızda passkey ile onaylar (onay requiresPasskey der; sayfanız @touchque/web'in passkeys.approveLogin({ requestId: approvalId }) çağrısını yapar, çağrı relay'inizin /passkey/login rotalarından geçer), asla telefonda ya da sürekli izinle değil; arka plan isteği 409 interactive_required alır. |
PUT /agents/workspace-policy | Aynı alanlar tüm çalışma alanı için: her ajanın tabanı. Ajanın kendi politikası her alanı yalnızca sıkılaştırabilir; çalışma alanının boş bıraktığı alan taban koymaz. |
Bir sürekli izin, kişinin düşünmeden onaylayacağı itmeleri tek bir bilinçli karara çevirir: ajan bir kez sorar (POST /agents/mandates): tek bir işlem türü, en çok N istek, tek para biriminde toplam ve isteğe bağlı istek başı üst sınır, isteğe bağlı alıcılar, bir bitiş zamanı (5 dakikadan 7 güne). Telefon her sınırı gösterir ve kişi sayıyla onaylar. İçindeki istekler (POST /agents/approvals üzerinde mandateId) itme olmadan onaylanır ve izinden atomik olarak düşülür; döngüye giren ya da ele geçirilen bir ajan kişinin okuduğunun ötesine asla geçemez. Sürekli izinler yalnızca ödeme ve satın almaları kapsar ve dört gözün yerine asla geçmez. Kişi, ajan, siz ya da bir yönetici istediği an bitirebilir.
| error | Anlamı |
|---|---|
blocked_by_person | Kişi "bu ben değilim" dedi, telefondaki birinin yönlendirdiğini söyledi ya da 30 dakikada üç kez reddetti. Yalnızca bir yönetici kendi telefonuyla kaldırır. |
amount_over_limit · daily_amount_over_limit | amountPerRequest ya da amountPerDay aşıldı. |
recipient_not_allowed · new_recipient | Listede yok ya da yeni ve newRecipients deny. |
loop_detected (429) | 10 dakikada üç kez aynı istek; AGENT_LOOP_DETECTED olarak loglanır. |
no_second_approver · four_eyes_required | Dört göz geçerli ve bu kişi ya da sahip dışında kimse yok; bir sürekli izin dört gözün yerine geçemez. |
mandate_mismatch · mandate_exceeded · mandate_revoked … | Sürekli iznin dışında (tür, para birimi, alıcı, istek başı), yer kalmadı ya da etkin değil. |
suspicious_content (400) | Ajanın metninde modele bir talimat ya da veri taşımak için kurulmuş bir bağlantı var (markers: instruction, data_url). Motora da telefona da bir şey gitmez. |
interactive_required (409) | Denetimler sayı eşleştirme istedi ve ajan arka planda sordu; sayıyı okuyacak kimse yok. presence interactive ile yeniden sorun. |
Tek bir onay politikası bir araç çağrısını kimin, hangi işlem türü olarak onaylayacağını ve telefonun ne göstereceğini (aracın argümanlarından çıkarılan tipli ayrıntılar) söyler. Bir aracı withTouchqueApproval ile sarın (Python: @with_touchque_approval): yalnızca kişi telefonda onayladıktan sonra çalışır; sarılmış araç imzasını korur, böylece LangChain, LlamaIndex, OpenAI Agents SDK ve Vercel AI SDK aynı argümanları okur; currentApproval() yürütücünüz için tek kullanımlık jetonu tutar. Kendi onay adımı olan çatıların her biri için bir yardımcı var: approveInterruptionsWithTouchque (OpenAI Agents SDK kesintileri), touchqueCanUseTool ve touchquePreToolUseHook (Claude Agent SDK), guardMcpClient (bir MCP istemcisi; jeton sunucuya _meta içinde gider). Go ve PHP'de RequireApproval / requireApproval var: sor, sayıyı göster, bekle. Bir ajan kendi Touchque kimliğini yukarıdaki gibi SDK'lardan alır: TQ_AGENT_ID ve TQ_AGENT_KEY ile TouchqueAgent ve çatısındaki bu yardımcılar. Bunu kodunuza bir yapay zekâ kod asistanına kurdurmak için Touchque MCP'yi kullanın; o bir ajan olarak değil, sizin adınıza giriş yapar.
Yukarıdaki her ret bir caseCode döner: dashboard'da (Logs → Look up a case) açın; ne girdiğini, hangi kuralın reddettiğini ve neyin geçeceğini görürsünüz. Risk motoru her isteği ajanın kendi geçmişiyle de karşılaştırır; her sinyal bir adım yükseltmedir (kişi sayıyı yazar), tek başına asla engel değildir; adım yükselttiği arka plan isteği interactive_required ile reddedilir. Sayısı bir insanın okuyup yazabileceğinden önce dönen onay (telefonu bir şey sürüyor) asla kullanılamaz: durum blocked, neden inhuman_approval. Adı, çalışma alanındaki başka bir ajanınkine benzeyen (benzer harfler, 0/O, 1/l, büyük-küçük harf, boşluk) yeni ya da yeniden adlandırılan ajan name_too_similar ile reddedilir; telefon önce ajanın adını gösterir. Webhook'lar: diğerlerinin yanında AGENT_INJECTION_SUSPECTED, AGENT_INHUMAN_APPROVAL, AGENT_LOOKALIKE_NAME; dashboard'da yapılan değişiklikler de gönderir.
| Sinyal | Ne zaman |
|---|---|
agent_volume_spike | Bu saatte, geçen haftanın en yoğun saatinin iki katından fazla istek (en az 10). |
agent_amount_outlier | Bu para biriminde olağan tutarının beş katından ve şimdiye kadarkinin hepsinden fazla. |
agent_new_network | Ajanın daha önceki bir günde hiç kullanmadığı bir ağ (ASN). |
agent_off_hours | İsteklerinin %1'inden azını tutan bir saat. |
agent_new_destination | Bu kişinin bu ajan için hiç onaylamadığı bir alıcı. |
Şirketinizin kendi ajanları ve otomasyonları için onları dashboard'da ekleyin (Organization → AI agents): her biri için sorumlu bir üye (ayrılınca ajan durur), belirteç alabileceği OpenID Connect uygulamaları, kapsamlar, bir gözden geçirme tarihi ve kimliğini nasıl kanıtlayacağı: kendi açık anahtarı ya da platformunun zaten verdiği belirteç (GitHub Actions, GitLab, Google Cloud, Azure, Kubernetes; güven politikası yayıncıyı, konuyu ve kitleyi belirtir). Paylaşılan gizli anahtar yok. /sso/token'dan tek bir uygulama için 5 dakikalık, DPoP anahtarına bağlı (cnf.jkt) at+jwt belirteçleri alır; uygulama bunları SSO'nun JWKS'i ve her çağrıdaki kanıtla doğrular ya da /sso/introspect'e sorar: ajan askıya alınınca, kill switch açılınca ya da sorumlusu ayrılınca hemen etkin değil der. Bir ajana daha fazla yetki vermek yöneticinin telefonuyla onaylanır. Üyeler adına çalışmasına izin verilmiş bir ajan, bir üyenin kendi belirtecini ikisini birden adlandıran bir belirteçle değiştirebilir (jeton değişimi, RFC 8693: sub üye, act.sub ajan; üyenin açabileceğinden fazlası asla) ya da üyeye telefonda sorabilir (CIBA, /sso/bc-authorize, yoklama): telefon ajanı, uygulamayı ve tipli authorization_details'i gösterir, Face ID ve ajanın gösterdiği sayıyı ister; belirteç tam olarak onaylananı taşır.
Organization → AI agents → Accounts agents may use altında bir hesap bağlantısı ekleyin (Google, Microsoft 365, GitHub, Slack, Salesforce ya da herhangi bir OAuth 2.0 hizmeti; telefonunuz onaylar) ve onu kullanabilecek ajanları seçin. Her üye kendi hesabını portalda (/connections) bir kez bağlar; Touchque jetonları şifreli saklar ve yeniler. Ajan sonra /sso/token'a jeton değişimiyle, üyenin belirteci, requested_token_type=urn:touchque:params:oauth:token-type:connection-access-token ve connection ile sorar; o hizmet için bir Bearer jetonu alır, yenileme jetonunu asla almaz; scope yalnızca üyenin verdiğini daraltabilir. Üye bağlantıyı keserse, ayrılırsa ya da askıya alınırsa (jetonları silinir), ajan durursa ya da bağlantı kapatılır veya ajanı artık listelemezse hiçbir şey dönmez.
Organization → AI agents her ajanı neler yapabildiği, son 30 günde ne yaptığı ve risk düzeyiyle (üyeler adına çalışma ile birlikte üyelerin hesapları ya da ortak hesaplar, çok sayıda ret, kullanılmama, gecikmiş gözden geçirme) ve üyelerin MCP sunucularınıza bağladığı ya da bağlamaya çalıştığı her yapay zekâ aracını geldiği alan adıyla gösterir. Bir aracı telefonunuzla onaylayın ya da engelleyin: tüm bağlantıları anında biter ve kimse yeniden bağlayamaz. "Only approved AI tools" açıkken üyeler onaylamadığınız araçları bağlayamaz. Erişim gözden geçirmeleri yapay zekâ ajanlarını da kapsayabilir: seçtiğiniz sahip ve yöneticiler her birini gözden geçirir (hiçbir zaman sorumlusu değil); tutmak 90 gün onaylar, iptal ajanı iptal eder, kanıt imzalıdır. Yapay zekâ ajanı kanıt paketi (imzalı JSON + CSV, herkese açık doğrulama sayfasında denetlenir) bir dönemdeki ajanları, araçları ve her jetonu, üye onayını, hesabı, ortak hesabı ve araç çağrısını listeler. Log akışları ajan ve araç olaylarını agent kategorisiyle gönderir.
Saldırganlar da tarayıcıyı yazılımla sürer: Playwright ya da Selenium, başsız bir Chrome, oturum açma sayfasını dolduran bir yapay zekâ tarayıcı ajanı ya da gizli bir istemle e-posta kodunu okuyup sızdıran kullanıcının kendi ajanı. Her Touchque oturum açma sayfası (SSO, Touchque Login, onaylar, dashboard) kendi sayfa denetimini gönderir; kendi sayfalarınızda @touchque/web startClientSignals() aynısını yapar, siz clientSignals olarak iletirsiniz. Yalnızca evet/hayır, sayı ve süre bildirir: navigator.webdriver, otomasyon izleri, başsız tarayıcı, betiğin ürettiği giriş, yazmadan doldurulan alanlar, makine düzeninde yazma, tarayıcıya uymayan istek başlıkları. Formun nasıl doldurulduğuna dair işaretler birlikte bir kez sayılır; formu dolduran ve gönderen bir parola yöneticisi tek başına yetmez. Risk motoru ağı (barındırma adresi, tarayıcının kendisine dair bir işaretle) ekler ve automated_client der: sessiz giriş yok, tek başına e-posta kodu ya da bağlantısı yok; passkey ya da sayı eşleştirmeli telefon onayı. Her çalışma alanı, organizasyon ve Touchque Login kiracısı monitor, step_up (varsayılan) ya da block seçer: block açık bir işarette (webdriver, otomasyon izleri, başsız tarayıcı, tarayıcı yerine bir araç) reddeder, daha zayıf işaretlerde step_up gibi sorar. Ajan tuzağına notunun istediği kelimeyi yazan yazılım monitor dışında reddedilir. Her retin bir vaka kodu vardır.
Kim olduğunu söyleyen ajanlar insan sayfalarında değil, ajan yolunda karşılanır: Web Bot Auth ile imzalanmış bir istek (HTTP Message Signatures, RFC 9421; anahtarları ajanın /.well-known/http-message-signatures-directory dizininde, yalnızca HTTPS ile getirilip ana makine başına sabitlenir) o ajan olarak tanınır ve agent_identity_required ile ajan kimliklerine yönlendirilerek reddedilir; doğrulanmayan imza (değiştirilmiş, yeniden kullanılmış, bilinmeyen anahtar) da aynı şekilde reddedilir. Telefonda emülatör ya da simülatör, hata ayıklayıcı, geliştirici seçenekleri ya da dokunabilen bir erişilebilirlik hizmeti (ekran okuyucular ve parola yöneticileri dışında) kritik bir işlemi asla onaylayamaz; bir çalışma alanı donanımı kanıtlanmış telefon isteyebilir (requireAttestedPhone).
Organizasyonunuzun her MCP sunucusunu dashboard'da kaydedin (Organization → MCP servers): adresi, araçlarının gerektirdiği kapsamlar ve her birinin riski, kimlerin bağlanabileceği. Üyeler Claude, Cursor, VS Code ya da herhangi bir MCP istemcisini standart yolla bağlar: sunucunun korunan kaynak meta verisi (RFC 9728) https://sso.touchque.com/mcp'yi gösterir, istemci kendini bir istemci kimlik meta veri belgesiyle tanıtır (https adresi, kayıt yok) ve üyeyi PKCE (S256) ve zorunlu resource (RFC 8707) ile oturum açtırır. Üye istemcinin adresini ve kapsamları gösteren bir onay ekranı görür; hassas bir kapsam telefonunu ister (Face ID ve bir sayı). Belirteçler yalnızca o sunucu için (aud = adresi) 10 dakikalık at+jwt'dir, isteğe bağlı DPoP'ye bağlıdır; yenileme belirteçleri döner, eskisinin yeniden kullanılması bağlantıyı bitirir. Yönetici her bağlantıyı görür ve bitirebilir; bir sunucuyu daraltmak ya da kapatmak anında olur, genişletmek telefonla onaylanır. Sunucuda her SDK'daki McpAuth meta veriyi sunar, her belirteci denetler (Touchque'nun JWKS'i, yayıncı, kitle, süre, DPoP kanıtı) ve MCP istemcilerinin izlediği WWW-Authenticate başlığıyla 401 ya da 403 insufficient_scope döner.
Bir sunucu için Touchque ağ geçidini açın (dashboard'daki sayfası): yapay zekâ araçlarıyla sunucunun arasına bir politika noktası girer. Araçlar artık yalnızca https://sso.touchque.com/sso/mcp/gw/<id> adresine bağlanır; sunucunun kendisi için jeton istenirse reddedilir, sunucu her çağrıda Touchque'nun bastığı 2 dakikalık bir jeton alır (act.sub touchque-mcp-gateway), aracın kendi jetonunu asla. Araçlar tanımlarının özetiyle sabitlenir: yeni bir araç siz onaylayana dek gizlidir, tanımı değişen araç yeniden gizlenir (rug pull); modeli yönlendiren, anahtar dosyalarını anan, başka bir aracı anan ya da başka bir aracın adına benzeyen açıklamalar işaretlenir. Her araç izinli, üyenin telefonuna soran ya da yasak olabilir; talimat ya da gizli anahtar taşıyan argümanlar telefona sorar, kişisel veri ya da izin listeniz dışındaki bir adres dışarı yazan bir araçla çıkamaz; güvenilmeyen girdi ve özel veri okumuş bir oturum böyle bir araçtan önce telefona sorar (ikinin kuralı). Dakika bütçesi, döngü kesici ve kill switch kontrolden çıkan ajanları durdurur; her çağrının vaka kodu vardır ve SIEM'inize gider.
Organizasyonunuzun OpenID Connect uygulamalarından birini (istemci sırrı olan) dashboard'da bir hizmete bağlayın (Organization → AI agents → Cross App Access): MCP sunucularınıza ya da yetkilendirme sunucusu Touchque'dan ID-JAG kabul eden başka bir hizmete (yayıncısı, uygulamanın oradaki client ID'si, kaynaklar, kapsamlar). Telefonunuz onaylar. Uygulama sonra bir üyenin ID token'ını /sso/token'da bir Identity Assertion JWT Authorization Grant ile (ID-JAG, açık bir IETF taslağı; MCP Enterprise-Managed Authorization uzantısı bunun üstüne kurulu), ID-JAG'i de o hizmette bir erişim belirteciyle değiştirir; üye başına rıza ekranı yoktur. Her ID-JAG 5 dakika yaşar (typ oauth-id-jag+jwt), üyeyi, hizmeti ve uygulamanın oradaki client ID'sini adlandırır ve her seferinde yeniden denetlenir: üye hâlâ organizasyonda ve uygulamaya izinli mi, bağlantı açık mı, yapay zekâ aracı kuralı. Kendi MCP sunucularınız onu /sso/mcp/token'da alır (jwt-bearer, bir kez, yenileme belirteci yok). Bir yetkilendirme sunucusu işletiyorsanız her SDK ID-JAG'i sizin için denetler (yayıncı, tek kitle, istemciniz, süre, bir kez kullanım). Ajanınız başka bir ajanın A2A kartına güvenmeden önce SDK'lar imzalarını denetler (RFC 8785 kanonik kart üzerinde JWS); jku'nun gösterdiği anahtarlar yalnızca kartın kendi alan adından ya da güvendiğiniz alan adlarından alınır.
import { Touchque, TouchqueAgent, generateAgentKey } from '@touchque/node';
const tq = new Touchque(); // TQ_API_KEY / TQ_API_SECRET ortamdan
// Bir kez, sunucunuzdan (agents:write yetkili bir API anahtarıyla)
const { privateJwk, publicJwk } = generateAgentKey(); // ES256; 'EdDSA' too
const agent = await tq.agents.create({
name: 'Invoice bot',
allowedTypes: ['PAY_INVOICE'], // isteyebileceği tek onaylar
publicKey: publicJwk, // özel anahtar asla gönderilmez
ownerEmail: 'ada@acme.com', // sorumlu kişi; ayrılınca ajan durur
platform: 'openai-agents',
});
// agent.id ve privateJwk ajanın gizli deposuna: TQ_AGENT_ID, TQ_AGENT_KEY
// Ajanın içinde: kendi anahtarıyla giriş, DPoP
const me = await new TouchqueAgent().me();
// Bir şey ters giderse
await tq.agents.suspend(agent.id, 'looping');
await tq.agents.setKillSwitch(true, 'incident'); // tüm ajanlar, hemenfrom touchque import TouchqueAgent, generate_agent_key
keys = generate_agent_key()
agent = client.agents.create('Invoice bot', ['PAY_INVOICE'], public_key=keys['public_jwk'],
owner_email='ada@acme.com', platform='langchain')
me = TouchqueAgent().me() # TQ_AGENT_ID, TQ_AGENT_KEY (the touchque-authenticator[agent] extra)
client.agents.suspend(agent['id'], 'looping')
client.agents.set_kill_switch(True, 'incident')use Touchque\AgentKeys;
use Touchque\TouchqueAgent;
$keys = AgentKeys::generate();
$agent = $tq->agents->create('Invoice bot', ['PAY_INVOICE'], ['publicKey' => $keys['publicJwk'], 'ownerEmail' => 'ada@acme.com']);
$me = (new TouchqueAgent())->me(); // TQ_AGENT_ID, TQ_AGENT_KEY
$tq->agents->suspend($agent['id'], 'looping');
$tq->agents->setKillSwitch(true, 'incident');keys, _ := touchque.GenerateAgentKey("ES256")
agent, err := client.Agents.Create(touchque.CreateAgentParams{
Name: "Invoice bot", AllowedTypes: []string{"PAY_INVOICE"}, PublicKey: keys.PublicJWK, OwnerEmail: "ada@acme.com",
})
a, _ := touchque.NewAgent(touchque.AgentConfig{}) // TQ_AGENT_ID, TQ_AGENT_KEY
me, err := a.Me()
client.Agents.Suspend(agent.ID, "looping")
client.Agents.SetKillSwitch(true, "incident")var keys = AgentKeys.Generate(); // ES256, or AgentKeys.Generate("EdDSA")
var agent = await tq.Agents.CreateAsync(new CreateAgentOptions
{
Name = "Invoice bot", AllowedTypes = ["PAY_INVOICE"], PublicKey = keys.PublicJwk, OwnerEmail = "ada@acme.com",
});
using var bot = new TouchqueAgent(); // TQ_AGENT_ID, TQ_AGENT_KEY
var me = await bot.MeAsync();
await tq.Agents.SuspendAsync(agent.Id, "looping");
await tq.Agents.SetKillSwitchAsync(true, "incident");AgentKeys.Pair keys = AgentKeys.generate(); // ES256, or AgentKeys.generate("EdDSA")
JsonNode agent = tq.agents().create(new Agents.CreateOptions("Invoice bot", List.of("PAY_INVOICE"))
.publicKey(keys.publicJwk()).ownerEmail("ada@acme.com"));
var bot = new TouchqueAgent(); // TQ_AGENT_ID, TQ_AGENT_KEY
JsonNode me = bot.me();
tq.agents().suspend(agent.get("id").asText(), "looping");
tq.agents().setKillSwitch(true, "incident");keys = Touchque::AgentKeys.generate
agent = tq.agents.create("Invoice bot", ["PAY_INVOICE"], public_key: keys["publicJwk"], owner_email: "ada@acme.com")
me = Touchque::Agent.new.me # TQ_AGENT_ID, TQ_AGENT_KEY
tq.agents.suspend(agent["id"], reason: "looping")
tq.agents.set_kill_switch(true, reason: "incident")# Ortak: imzalı istek (HMAC), bkz. Kimlik Doğrulama
POST /agents
{ "name": "Invoice bot", "allowedTypes": ["PAY_INVOICE"],
"publicKey": { "kty": "EC", "crv": "P-256", "x": "…", "y": "…" } }
# Ajan: kendi anahtarı, API anahtarı yok
POST /agents/token
DPoP: <proof JWT>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<JWT signed by the agent key>
GET /agents/me
Authorization: DPoP tqat_…
DPoP: <proof JWT with ath>// Ajanda: serbest metin değil, tipli ayrıntılar
const ask = await agent.requestApproval({
user: 'jane@example.com', type: 'PAY_INVOICE', reason: 'October rent',
authorizationDetails: [{ type: 'payment_initiation',
instructedAmount: { currency: 'TRY', amount: '500.00' },
creditorName: 'Jane Doe', creditorAccount: { iban: 'TR33 0006 1005 1978 6457 8413 26' } }],
});
reply(`Type ${ask.challengeCode} in the Touchque app to approve.`);
const { approvalToken } = await agent.waitForApproval(ask.approvalId);
// Yürüten sunucuda, ödemeden hemen önce: bir kez, yalnızca bu iş için
const approval = await tq.agents.redeemApproval(approvalToken, {
user: 'jane@example.com', type: 'PAY_INVOICE',
authorizationDetails: { type: 'payment_initiation', instructedAmount: { currency: 'TRY', amount: '500.00' } },
});ask = agent.request_approval('jane@example.com', 'PAY_INVOICE', [payment], reason='October rent')
token = agent.wait_for_approval(ask['approvalId'])['approvalToken']
approval = client.agents.redeem_approval(token, user='jane@example.com', authorization_details=payment)$ask = $agent->requestApproval('jane@example.com', 'PAY_INVOICE', [$payment], ['reason' => 'October rent']);
$token = $agent->waitForApproval($ask['approvalId'])['approvalToken'];
$approval = $tq->agents->redeemApproval($token, ['user' => 'jane@example.com', 'authorizationDetails' => $payment]);ask, _ := a.RequestApproval(touchque.AgentApprovalParams{User: "jane@example.com", Type: "PAY_INVOICE",
AuthorizationDetails: []map[string]interface{}{payment}, Reason: "October rent"})
done, _ := a.WaitForApproval(ctx, ask.ApprovalID, 0, 0)
approval, err := client.Agents.RedeemApproval(done.ApprovalToken, touchque.ApprovalExpectation{User: "jane@example.com", AuthorizationDetails: payment})var ask = await bot.RequestApprovalAsync("jane@example.com", "PAY_INVOICE", new[] { payment }, new AgentApprovalOptions { Reason = "October rent" });
var done = await bot.WaitForApprovalAsync(ask.ApprovalId);
var approval = await tq.Agents.RedeemApprovalAsync(done.ApprovalToken!, new ApprovalExpectation { User = "jane@example.com", AuthorizationDetails = payment });var ask = bot.requestApproval("jane@example.com", "PAY_INVOICE", List.of(payment), new TouchqueAgent.ApprovalOptions().reason("October rent"));
var done = bot.waitForApproval(ask.approvalId(), null, null);
JsonNode approval = tq.agents().redeemApproval(done.approvalToken(), new ApprovalToken.Expectation("jane@example.com", null, payment));ask = agent.request_approval("jane@example.com", "PAY_INVOICE", [payment], reason: "October rent")
token = agent.wait_for_approval(ask["approvalId"])["approvalToken"]
approval = tq.agents.redeem_approval(token, user: "jane@example.com", authorization_details: payment)POST /agents/approvals # ajan jetonu + DPoP
{ "externalUsername": "jane@example.com", "type": "PAY_INVOICE", "reason": "October rent",
"authorizationDetails": [{ "type": "payment_initiation",
"instructedAmount": { "currency": "TRY", "amount": "500.00" },
"creditorName": "Jane Doe", "creditorAccount": { "iban": "TR330006100519786457841326" } }] }
GET /agents/approvals/{approvalId} # → approved + approvalToken
POST /agents/approvals/redeem # sizin API anahtarınız (HMAC)
{ "token": "<approvalToken>" }import { withTouchqueApproval, currentApproval, touchqueCanUseTool, guardMcpClient } from '@touchque/node';
const policy = { agent, user: 'jane@example.com', type: 'PAY_INVOICE',
details: ({ iban, amount }) => [{ type: 'payment_initiation',
instructedAmount: { currency: 'TRY', amount }, creditorAccount: { iban } }],
onChallenge: (s) => reply(`Type ${s.challengeCode} in the Touchque app.`) };
// LangChain tool(), OpenAI Agents tool(), Vercel AI SDK tool(): aynı imza
const pay = withTouchqueApproval(async ({ iban, amount }) =>
bank.pay(iban, amount, currentApproval().approvalToken), policy);
// Claude Agent SDK
query({ prompt, options: { canUseTool: touchqueCanUseTool({ pay: policy }, { tokenField: 'approvalToken' }) } });
// MCP istemcisi: jeton _meta["touchque/approvalToken"] içinde gider
const guarded = guardMcpClient(client, { pay: policy });from touchque import ApprovalPolicy, with_touchque_approval, current_approval
policy = ApprovalPolicy(agent, user='jane@example.com', type='PAY_INVOICE',
details=lambda a, tool: [{'type': 'payment_initiation',
'instructedAmount': {'currency': 'TRY', 'amount': a['amount']},
'creditorAccount': {'iban': a['iban']}}])
@tool # LangChain, LlamaIndex, OpenAI Agents (@function_tool): imza korunur
@with_touchque_approval(policy)
def pay(iban: str, amount: str) -> str:
return bank.pay(iban, amount, current_approval()['approvalToken'])
# approve_interruptions_with_touchque(result, {'pay': policy}) OpenAI Agents SDK
# touchque_can_use_tool({'pay': policy}) Claude Agent SDK
# guard_mcp_session(session, {'pay': policy}) MCP ClientSession$done = $agent->requireApproval('jane@example.com', 'PAY_INVOICE', [$payment], [],
fn ($s) => $reply("Type {$s['challengeCode']} in the Touchque app."));
$bank->pay($iban, $amount, $done['approvalToken']);done, err := a.RequireApproval(ctx, touchque.AgentApprovalParams{User: "jane@example.com", Type: "PAY_INVOICE",
AuthorizationDetails: []map[string]interface{}{payment}},
func(s *touchque.AgentApprovalState) { reply("Type " + s.ChallengeCode + " in the Touchque app.") }, 5*time.Minute)
if err != nil { return err } // reddedildi, engellendi, süresi doldu
bank.Pay(iban, amount, done.ApprovalToken)var done = await bot.RequireApprovalAsync("jane@example.com", "PAY_INVOICE", new[] { payment },
onChallenge: s => ReplyAsync($"Type {s.ChallengeCode} in the Touchque app.")); // reddedilirse TouchqueApprovalException
await bank.PayAsync(iban, amount, done.ApprovalToken!);var done = bot.requireApproval("jane@example.com", "PAY_INVOICE", List.of(payment), null,
s -> reply("Type " + s.challengeCode() + " in the Touchque app."), null); // reddedilirse TouchqueApprovalException
bank.pay(iban, amount, done.approvalToken());done = agent.require_approval("jane@example.com", "PAY_INVOICE", [payment]) do |s|
reply "Type #{s["challengeCode"]} in the Touchque app."
end # reddedilirse Touchque::ApprovalError
bank.pay(iban, amount, done["approvalToken"])import { TouchqueOrgAgent, githubActionsToken } from '@touchque/node';
// Kendi anahtarıyla (TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY)
const agent = new TouchqueOrgAgent();
const res = await agent.fetch('https://payments.acme.com/invoices', { resource: 'tqapp_…', scope: 'invoices:read' });
// GitHub Actions içinde, saklanacak anahtar yok (permissions: id-token: write)
const ci = new TouchqueOrgAgent({ agentId: 'oagt_…', assertion: githubActionsToken('touchque') });
// Bir üye adına: kendi belirteciyle (RFC 8693) ya da telefonundan onay isteyerek (CIBA)
const forAda = await agent.exchangeToken({ subjectToken: adasAccessToken, resource: 'tqapp_…' });
const ask = await agent.askMember({ loginHint: 'ada@acme.com', bindingMessage: 'Pay invoice 42', authorizationDetails: [payment] });
reply(`Type ${ask.bindingCode} in the Touchque app.`);
const approved = await agent.waitForMember(ask); // approved.authorizationDetails
// Jeton kasası: üyenin bağladığı kendi Google hesabı, yenileme anahtarı olmadan
const google = await agent.getConnectionToken({ subjectToken: adasAccessToken, connection: 'google' });from touchque import TouchqueOrgAgent, github_actions_token
agent = TouchqueOrgAgent() # TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
r = agent.request('GET', 'https://payments.acme.com/invoices', resource='tqapp_...', scope='invoices:read')
ci = TouchqueOrgAgent(agent_id='oagt_...', assertion=github_actions_token('touchque'))
google = agent.get_connection_token(adas_access_token, 'google') # jeton kasasıuse Touchque\TouchqueOrgAgent;
$agent = new TouchqueOrgAgent(); // TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
$headers = $agent->authorization('GET', 'https://payments.acme.com/invoices', ['resource' => 'tqapp_...']);
$ci = new TouchqueOrgAgent(['agentId' => 'oagt_...', 'assertion' => TouchqueOrgAgent::githubActionsToken('touchque')]);
$google = $agent->getConnectionToken($adasAccessToken, 'google'); // jeton kasasıagent, _ := touchque.NewOrgAgent(touchque.OrgAgentConfig{}) // TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
req, _ := http.NewRequest("GET", "https://payments.acme.com/invoices", nil)
_ = agent.Authorize(req, "tqapp_…", "invoices:read")
ci, _ := touchque.NewOrgAgent(touchque.OrgAgentConfig{AgentID: "oagt_…", Assertion: touchque.GitHubActionsToken("touchque")})
google, _ := agent.GetConnectionToken(adasAccessToken, "google", "") // jeton kasasıusing var agent = new TouchqueOrgAgent(); // TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
var request = new HttpRequestMessage(HttpMethod.Get, "https://payments.acme.com/invoices");
await agent.AuthorizeAsync(request, "tqapp_…", "invoices:read");
using var ci = new TouchqueOrgAgent(new TouchqueOrgAgentOptions { AgentId = "oagt_…", Assertion = TouchqueOrgAgent.GitHubActionsToken("touchque") });
var google = await agent.GetConnectionTokenAsync(adasAccessToken, "google"); // jeton kasasıvar agent = new TouchqueOrgAgent(); // TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
var request = agent.authorize(HttpRequest.newBuilder(URI.create("https://payments.acme.com/invoices")),
"GET", "https://payments.acme.com/invoices", "tqapp_…", "invoices:read");
var ci = new TouchqueOrgAgent(new TouchqueOrgAgent.Options().agentId("oagt_…").assertion(TouchqueOrgAgent.githubActionsToken("touchque")));
var google = agent.getConnectionToken(adasAccessToken, "google", null); // jeton kasasıagent = Touchque::OrgAgent.new # TQ_ORG_AGENT_ID, TQ_ORG_AGENT_KEY
headers = agent.authorization("GET", "https://payments.acme.com/invoices", resource: "tqapp_…", scope: "invoices:read")
ci = Touchque::OrgAgent.new(agent_id: "oagt_…", assertion: Touchque::OrgAgent.github_actions_token("touchque"))
google = agent.get_connection_token(adas_access_token, "google") # jeton kasasıPOST https://sso.touchque.com/sso/token
DPoP: <proof>
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=oagt_…
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=<kendi anahtarıyla imzalı JWT ya da platformun belirteci>
&resource=tqapp_…&scope=invoices:read
# → { "access_token": "<at+jwt>", "token_type": "DPoP", "expires_in": 300 }// Tarayıcıda (oturum açma sayfanız)
import { startClientSignals, addAgentTrap } from '@touchque/web';
const probe = startClientSignals(); // sayfa açılınca
addAgentTrap(form); // ekran dışında bir alan: insan görmez; notunun istediği kelimeyi yazan yazılımın girişi reddedilir
form.onsubmit = () => { form.clientSignals.value = probe.encode(); };
// Sunucunuzda
const risk = await tq.risk.check({ externalUsername, clientIp, browserProof, clientSignals: req.body.clientSignals });
await tq.login.request({ externalUsername, type: 'LOGIN', clientIp, clientSignals: req.body.clientSignals });
// yazılımla sürülen tarayıcı: sessiz giriş yok, sayı eşleştirme; ya da 403 automated_clientrisk = tq.risk.check(user, client_ip=ip, browser_proof=proof, client_signals=form['clientSignals'])
tq.login.request(user, type='LOGIN', client_ip=ip, client_signals=form['clientSignals'])$risk = $tq->risk->check(['externalUsername' => $user, 'clientIp' => $ip, 'clientSignals' => $_POST['clientSignals']]);
$tq->login->request($user, 'LOGIN', null, $ip, null, false, false, null, null, null, null, null, $_POST['clientSignals']);risk, _ := tq.Risk.Check(ctx, touchque.RiskCheckOptions{ExternalUsername: user, ClientIP: ip, ClientSignals: r.FormValue("clientSignals")})
_, err := tq.Login.RequestWithOptions(touchque.LoginRequestOptions{ExternalUsername: user, ClientIP: ip, ClientSignals: r.FormValue("clientSignals")})var risk = await tq.Risk.CheckAsync(new RiskCheckOptions { ExternalUsername = user, ClientIp = ip, ClientSignals = form["clientSignals"] });
await tq.Login.RequestAsync(new LoginRequestOptions { ExternalUsername = user, ClientIp = ip, ClientSignals = form["clientSignals"] });var risk = tq.risk().check(new Risk.CheckOptions(user).clientIp(ip).clientSignals(request.getParameter("clientSignals")));
tq.login().request(new LoginRequestOptions(user).clientIp(ip).clientSignals(request.getParameter("clientSignals")));risk = tq.risk.check(user, client_ip: ip, browser_proof: proof, client_signals: params[:clientSignals])
tq.login.request(user, type: "LOGIN", client_ip: ip, client_signals: params[:clientSignals])# Web Bot Auth: imzalı bir ajanın isteği
Signature-Agent: "https://agent.example"
Signature-Input: sig1=("@authority" "signature-agent");created=…;expires=…;keyid="<JWK thumbprint>";alg="ed25519";nonce="…";tag="web-bot-auth"
Signature: sig1=:…:
# → 403 { "error": "agent_identity_required", "agent": "agent.example", "caseCode": "…" }import { McpAuth } from '@touchque/node';
const auth = new McpAuth({
issuer: 'https://sso.touchque.com/mcp', // Organization → MCP servers
resource: 'https://mcp.acme.com/mcp', // kayıtlı olduğu gibi
tools: { search_tickets: 'tickets:read', delete_ticket: 'tickets:delete' },
});
app.use(auth.express()); // meta veriyi sunar; belirteç yoksa 401 + WWW-Authenticate
// bir aracı çalıştırmadan önce
auth.requireTool(req.touchqueMcp, 'delete_ticket'); // 403 insufficient_scope: istemci üyeden fazlasını isterfrom touchque import McpAuth, McpAuthError
auth = McpAuth(issuer='https://sso.touchque.com/mcp', resource='https://mcp.acme.com/mcp',
tools={'search_tickets': 'tickets:read', 'delete_ticket': 'tickets:delete'})
claims = auth.verify(request.method, str(request.url), dict(request.headers))
auth.require_tool(claims, 'delete_ticket')
# FastMCP TokenVerifier: auth.verify_token(token)use Touchque\McpAuth;
$auth = new McpAuth(['issuer' => 'https://sso.touchque.com/mcp', 'resource' => 'https://mcp.acme.com/mcp',
'tools' => ['search_tickets' => 'tickets:read', 'delete_ticket' => 'tickets:delete']]);
$claims = $auth->verify($_SERVER['REQUEST_METHOD'], $url, getallheaders());
$auth->requireTool($claims, 'delete_ticket'); // TouchqueMcpAuthException: $e->status, $e->wwwAuthenticateauth, _ := touchque.NewMcpAuth(touchque.McpAuthConfig{
Issuer: "https://sso.touchque.com/mcp", Resource: "https://mcp.acme.com/mcp",
Tools: map[string]string{"search_tickets": "tickets:read", "delete_ticket": "tickets:delete"},
})
http.Handle("/", auth.Middleware(mcpHandler))
claims := touchque.McpClaimsFrom(r.Context())
if err := auth.RequireTool(claims, "delete_ticket"); err != nil {
touchque.WriteMcpAuthError(w, err)
}var auth = new McpAuth(new McpAuthOptions
{
Issuer = "https://sso.touchque.com/mcp", Resource = "https://mcp.acme.com/mcp",
Tools = new Dictionary<string, string> { ["search_tickets"] = "tickets:read", ["delete_ticket"] = "tickets:delete" },
});
app.MapTouchqueMcpMetadata(auth); // /.well-known/oauth-protected-resource/mcp
app.MapPost("/mcp", HandleMcp).RequireTouchqueMcp(auth); // 401 + WWW-Authenticate without a valid token
auth.RequireTool(http.GetTouchqueMcpClaims()!, "delete_ticket"); // TouchqueMcpAuthException: Status, WwwAuthenticate# application.properties (Spring Boot starter)
touchque.mcp.issuer=https://sso.touchque.com/mcp
touchque.mcp.resource=https://mcp.acme.com/mcp
touchque.mcp.tools.search_tickets=tickets:read
touchque.mcp.tools.delete_ticket=tickets:delete
# → /.well-known/oauth-protected-resource/mcp, and 401 + WWW-Authenticate on /mcp without a valid token
// İşleyicide:
mcpAuth.requireTool(TouchqueSpring.mcpClaims(request), "delete_ticket"); // TouchqueMcpAuthException: status(), wwwAuthenticate()auth = Touchque::McpAuth.new(issuer: "https://sso.touchque.com/mcp", resource: "https://mcp.acme.com/mcp",
tools: { "search_tickets" => "tickets:read", "delete_ticket" => "tickets:delete" })
claims = auth.verify(request.request_method, request.url, request.headers)
auth.require_tool(claims, "delete_ticket") # Touchque::McpAuthError: status, www_authenticateGET https://mcp.acme.com/.well-known/oauth-protected-resource/mcp
# → { "resource": "https://mcp.acme.com/mcp", "authorization_servers": ["https://sso.touchque.com/mcp"] }
GET https://sso.touchque.com/sso/mcp/authorize?response_type=code
&client_id=https://tool.example/oauth/client.json # istemci kimlik meta veri belgesi
&redirect_uri=…&code_challenge=…&code_challenge_method=S256
&resource=https://mcp.acme.com/mcp&scope=tickets:read&state=…
POST https://sso.touchque.com/sso/mcp/token
grant_type=authorization_code&code=…&code_verifier=…&resource=https://mcp.acme.com/mcp
# → { "access_token": "<at+jwt>", "token_type": "Bearer", "expires_in": 600, "refresh_token": "…" }import { requestIdJag, redeemIdJag, IdJagVerifier, verifyAgentCard } from '@touchque/node';
// Uygulamanız (gizli anahtarlı OIDC uygulaması): üyenin ID token'ı → ID-JAG → erişim belirteci
const { assertion } = await requestIdJag({ issuer: 'https://sso.touchque.com', clientId, clientSecret, idToken, audience: 'https://auth.files.example' });
const tokens = await redeemIdJag({ tokenEndpoint: 'https://auth.files.example/token', clientId: 'acme', clientSecret: filesSecret, assertion });
// Kendi yetkilendirme sunucunuz (jwt-bearer): bir kez kullanılır
const claims = await new IdJagVerifier({ issuer: 'https://sso.touchque.com', audience: 'https://auth.files.example' })
.verify(form.assertion, { clientId: authenticatedClientId });
// Başka bir ajanın A2A kartı
await verifyAgentCard(card, { cardUrl: 'https://agent.example/.well-known/agent-card.json' });jag = request_id_jag(issuer='https://sso.touchque.com', client_id=client_id, client_secret=secret,
id_token=id_token, audience='https://auth.files.example')
claims = IdJagVerifier(issuer='https://sso.touchque.com', audience='https://auth.files.example') \
.verify(form['assertion'], client_id=authenticated_client_id)
verify_agent_card(card, card_url='https://agent.example/.well-known/agent-card.json')$jag = CrossAppAccess::requestIdJag(['issuer' => 'https://sso.touchque.com', 'clientId' => $id, 'clientSecret' => $secret,
'idToken' => $idToken, 'audience' => 'https://auth.files.example']);
$claims = (new IdJagVerifier(['issuer' => 'https://sso.touchque.com', 'audience' => 'https://auth.files.example']))
->verify($_POST['assertion'], $authenticatedClientId);
AgentCard::verify($cardJson, ['cardUrl' => 'https://agent.example/.well-known/agent-card.json']);jag, err := touchque.RequestIDJAG(ctx, touchque.RequestIDJAGParams{Issuer: "https://sso.touchque.com",
ClientID: id, ClientSecret: secret, IDToken: idToken, Audience: "https://auth.files.example"})
v, _ := touchque.NewIDJAGVerifier(touchque.IDJAGVerifierConfig{Issuer: "https://sso.touchque.com", Audience: "https://auth.files.example"})
claims, err := v.Verify(ctx, r.PostFormValue("assertion"), authenticatedClientID)
res, err := touchque.VerifyAgentCard(ctx, cardJSON, touchque.VerifyAgentCardOptions{CardURL: "https://agent.example/.well-known/agent-card.json"})var jag = await CrossAppAccess.RequestIdJagAsync(new() { Issuer = "https://sso.touchque.com", ClientId = id,
ClientSecret = secret, IdToken = idToken, Audience = "https://auth.files.example" });
var claims = await new IdJagVerifier(new() { Issuer = "https://sso.touchque.com", Audience = "https://auth.files.example" })
.VerifyAsync(form["assertion"]!, authenticatedClientId);
await AgentCard.VerifyAsync(cardJson, new() { CardUrl = "https://agent.example/.well-known/agent-card.json" });var jag = CrossAppAccess.requestIdJag(new CrossAppAccess.RequestOptions(id, secret, idToken, "https://auth.files.example")
.issuer("https://sso.touchque.com"));
var claims = new IdJagVerifier(new IdJagVerifier.Options("https://sso.touchque.com", "https://auth.files.example"))
.verify(form.get("assertion"), authenticatedClientId);
AgentCard.verify(cardJson, new AgentCard.VerifyOptions().cardUrl("https://agent.example/.well-known/agent-card.json"));jag = Touchque::CrossAppAccess.request_id_jag(issuer: "https://sso.touchque.com", client_id: id, client_secret: secret,
id_token: id_token, audience: "https://auth.files.example")
claims = Touchque::IdJagVerifier.new(issuer: "https://sso.touchque.com", audience: "https://auth.files.example")
.verify(params[:assertion], client_id: authenticated_client_id)
Touchque::AgentCard.verify(card_json, card_url: "https://agent.example/.well-known/agent-card.json")POST https://sso.touchque.com/sso/token # uygulamanın client_id + client_secret'ı
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&requested_token_type=urn:ietf:params:oauth:token-type:id-jag
&subject_token=<ID token>&subject_token_type=urn:ietf:params:oauth:token-type:id_token
&audience=https://auth.files.example&scope=files:read
# → { "access_token": "<ID-JAG>", "issued_token_type": "urn:ietf:params:oauth:token-type:id-jag", "token_type": "N_A", "expires_in": 300 }
POST https://sso.touchque.com/sso/mcp/token # kendi MCP sunucularınız
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=<ID-JAG>&resource=https://mcp.acme.com/mcp// Bir kez sor: en çok 5 fatura, toplam 10.000 TRY, bu IBAN, 24 saat
const m = await agent.requestMandate({
user: 'jane@example.com', type: 'PAY_INVOICE', maxCount: 5,
maxTotal: { currency: 'TRY', amount: '10000.00' }, perRequest: '2500.00',
recipients: ['TR33 0006 1005 1978 6457 8413 26'], validFor: 86400,
});
reply(`Type ${m.challengeCode} in the Touchque app.`);
// Etkinleşince: her ödeme itme olmadan onaylanır, atomik olarak sayılır
const ask = await agent.requestApproval({ user: 'jane@example.com', type: 'PAY_INVOICE',
authorizationDetails: [payment], mandateId: m.mandateId });
// ask.status === "approved", token yanıtta; redeemApproval aynı
await tq.agents.setPolicy(agentId, { fourEyes: { approvers: ['cfo@acme.com'], above: { TRY: '5000' } } });m = agent.request_mandate('jane@example.com', 'PAY_INVOICE', 5, {'currency': 'TRY', 'amount': '10000.00'},
per_request='2500.00', recipients=[iban], valid_for=86400)
ask = agent.request_approval('jane@example.com', 'PAY_INVOICE', [payment], mandate_id=m['mandateId'])
client.agents.set_policy(agent_id, {'fourEyes': {'approvers': ['cfo@acme.com'], 'above': {'TRY': '5000'}}})$m = $agent->requestMandate('jane@example.com', 'PAY_INVOICE', 5, ['currency' => 'TRY', 'amount' => '10000.00'],
['perRequest' => '2500.00', 'recipients' => [$iban], 'validFor' => 86400]);
$ask = $agent->requestApproval('jane@example.com', 'PAY_INVOICE', [$payment], ['mandateId' => $m['mandateId']]);
$tq->agents->setPolicy($agentId, ['fourEyes' => ['approvers' => ['cfo@acme.com'], 'above' => ['TRY' => '5000']]]);m, _ := a.RequestMandate(touchque.MandateParams{User: "jane@example.com", Type: "PAY_INVOICE", MaxCount: 5,
MaxTotal: map[string]string{"currency": "TRY", "amount": "10000.00"}, PerRequest: "2500.00", Recipients: []string{iban}})
ask, _ := a.RequestApproval(touchque.AgentApprovalParams{User: "jane@example.com", Type: "PAY_INVOICE",
AuthorizationDetails: []map[string]interface{}{payment}, MandateID: m.MandateID})
client.Agents.SetPolicy(agentID, touchque.AgentPolicy{FourEyes: &touchque.FourEyes{Approvers: []string{"cfo@acme.com"},
Above: map[string]string{"TRY": "5000"}}})var m = await bot.RequestMandateAsync("jane@example.com", "PAY_INVOICE", 5, new MandateAmount("TRY", "10000.00"),
new MandateOptions { PerRequest = new MandateAmount("TRY", "2500.00"), Recipients = [iban], ValidFor = 86400 });
var ask = await bot.RequestApprovalAsync("jane@example.com", "PAY_INVOICE", new[] { payment }, new AgentApprovalOptions { MandateId = m.MandateId });
await tq.Agents.SetPolicyAsync(agentId, new AgentPolicy
{
FourEyes = new FourEyes { Approvers = ["cfo@acme.com"], Above = new Dictionary<string, string> { ["TRY"] = "5000" } },
});JsonNode m = bot.requestMandate("jane@example.com", "PAY_INVOICE", 5, new TouchqueAgent.MandateAmount("TRY", "10000.00"),
new TouchqueAgent.MandateOptions().perRequest(new TouchqueAgent.MandateAmount("TRY", "2500.00")).recipients(List.of(iban)).validFor(86400));
var ask = bot.requestApproval("jane@example.com", "PAY_INVOICE", List.of(payment),
new TouchqueAgent.ApprovalOptions().mandateId(m.get("mandateId").asText()));
tq.agents().setPolicy(agentId, Map.of("fourEyes", Map.of("approvers", List.of("cfo@acme.com"), "above", Map.of("TRY", "5000"))));m = agent.request_mandate("jane@example.com", "PAY_INVOICE", 5, { currency: "TRY", amount: "10000.00" },
per_request: "2500.00", recipients: [iban], valid_for: 86400)
ask = agent.request_approval("jane@example.com", "PAY_INVOICE", [payment], mandate_id: m["mandateId"])
tq.agents.set_policy(agent_id, { fourEyes: { approvers: ["cfo@acme.com"], above: { TRY: "5000" } } })POST /agents/mandates # ajan jetonu + DPoP
{ "externalUsername": "jane@example.com", "type": "PAY_INVOICE", "maxCount": 5,
"maxTotal": { "currency": "TRY", "amount": "10000.00" }, "perRequest": "2500.00",
"recipients": ["TR330006100519786457841326"], "validFor": 86400 }
POST /agents/approvals # "mandateId": "…" ile: itme yok
POST /agents/mandates/{id}/revoke # ajan vazgeçer
POST /agents/{agentId}/mandates/{id}/revoke # siz (HMAC)
PUT /agents/{agentId}/policy # { "fourEyes": { "approvers": […], "above": { "TRY": "5000" } } }{
"access_token": "tqat_…",
"token_type": "DPoP",
"expires_in": 600,
"scope": "agent:read approvals:request approvals:read"
}