مترجَم عن الأصل الصيني. وعند الاختلاف بينهما، يُعتد بالأصل.

Confer — تصميم البروتوكول

يعرّف كل البروتوكولات بين نسخ Confer، وبين عميل المستخدم والخادم. وجميعها قائمة على معايير مفتوحة، تيسيراً للاتحاد مستقبلاً.

هوية الوكيل

صيغة DID:web

كل نسخة، لفرد أو لمؤسسة، تستضيف وثيقة DID خاصة بها:

https://acme.com/.well-known/did.json

بنية وثيقة DID (متوافقة مع W3C DID v1.0):

{
  "@context": ["https://www.w3.org/ns/did/v1"],
  "id": "did:web:acme.com",
  "verificationMethod": [
    {
      "id": "did:web:acme.com#key-1",
      "type": "Ed25519VerificationKey2020",
      "controller": "did:web:acme.com",
      "publicKeyMultibase": "z6MkpTHR8VNsBxYAAWHut2Geadd9jSrue..."
    }
  ],
  "service": [
    {
      "id": "did:web:acme.com#confer-agent",
      "type": "ConferAgent",
      "serviceEndpoint": "https://acme.com/a2a/v1"
    }
  ]
}

ويأخذ DID وكيل المستخدم هذه الصورة: did:web:acme.com:agents:laowang — النسخة الأساسية يليها مقطع مسار. وبهذا تستطيع نسخة واحدة أن تحمل عدّة مستخدمين.

وفق مواصفة did:web، فإن DID ذا المعرّف الفرعي (أي الذي يحمل مقاطع مسار) يُحَلّ إلى الوثيقة الموافقة لذلك المسار، لا إلى .well-known في جذر النسخة:

تدوير المفاتيح

AgentFacts (متوافق مع NANDA)

كل وكيل يُعلن AgentFacts تصفه. الموضع:

https://acme.com/agents/{slug}/agent.json

أو الدليل العام تحت well-known:

https://acme.com/.well-known/agents.json

مثال على البنية:

{
  "@context": "https://nanda.dev/schemas/agent/v1",
  "did": "did:web:acme.com:agents:support",
  "name": "ABC Industries Support Agent",
  "description": "Technical support for X100, X200 industrial controllers",
  "owner": {
    "type": "Organization",
    "name": "ABC Industries Ltd.",
    "url": "https://acme.com"
  },
  "capabilities": [
    {
      "type": "qa",
      "scope": ["X100", "X200", "Modbus", "RTU", "TCP"],
      "languages": ["en", "zh", "de"]
    },
    {
      "type": "code-generation",
      "scope": ["python", "c", "embedded"],
      "languages": ["en", "zh"]
    }
  ],
  "endpoints": {
    "a2a": "https://acme.com/a2a/v1",
    "stream": "https://acme.com/a2a/v1/stream"
  },
  "trust": {
    "verifiedBy": ["did:web:nanda.org"],
    "issuedAt": "2024-10-01T00:00:00Z"
  },
  "publicKey": {
    "id": "did:web:acme.com#key-1",
    "type": "Ed25519VerificationKey2020"
  }
}

شرح الحقول:

بروتوكول A2A

طبقة البروتوكول

كل اتصال A2A يمرّ عبر HTTPS بطريقتَي POST/GET، بترميز JSON. وتحت /a2a/v1 يتعايش ربطان:

ويتشارك الاثنان البوابات نفسها — التحقّق من التوقيع، وبوابة الموافقة، وقرار السياسة، وملكية الخيط — وكلها في a2a/inbound.ts؛ ولا يختلف إلا شكل الرسالة على السلك. وكتابة هذه البوابات مرّة لكل ربط هي بعينها السبب في أن خللَ حقن الخيوط بين المستأجرين كُتب أربع مرات؛ فلا تفصلوها ثانية.

الجوهر: تُستعمل HTTP Message Signatures (RFC 9421) لا رمز bearer. والسبب:

والمكوّنات التي يغطيها التوقيع هي @method و@authority و@path، ويُضاف @query متى حمل الطلب سلسلة استعلام، وcontent-digest متى حمل جسماً، ثم date. و@query ليست ترفاً اختيارياً: @path يقف عند ?، بينما GET /tasks في ربط REST يرشِّح ويصفّح بالكامل عبر معاملات الاستعلام؛ فعدم تغطيتها يعني ترك وسيط يعيد كتابتها كما يشاء والتوقيع صحيح على حاله. كما تحمل معاملات التوقيع قيمة nonce عشوائية لكل طلب: فدقّة created لا تتعدّى الثانية، ولذلك يوقّع طلبان متطابقان في الثانية نفسها البايتات نفسها، فتحسبهما ذاكرة إعادة الإرسال عند الطرف الآخر هجوماً وترفض الثاني (وهو ما يصطدم به أي استقصاء لمهمّة، وأي إعادة محاولة). أما إعادة الإرسال الحقيقية — البايتات نفسها تُبعَث كما هي — فما زالت تُضبَط، لأن التوقيع حينها مطابق بايتاً ببايت.

وليست في هذه الطبقة securitySchemes الواردة في المواصفة: فتلك مفاتيح واجهة، ومصادقة HTTP، وOAuth2، وOIDC، وmTLS، وليس فيها واحد هو توقيع طلب. والشرط الحقيقي مُعلَن على البطاقة بوصفه امتداداً إلزامياً، ويفرضه ربط REST وفق §3.3.4: فالعميل الذي لا يُعلن ذلك الامتداد يتلقّى ExtensionSupportRequiredError، لا رمز 401 لا يشرح شيئاً.

مثال على طلب وارد

POST /a2a/v1/messages HTTP/1.1
Host: acme.com
Content-Type: application/json
Date: Sun, 24 Nov 2024 14:30:00 GMT
Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest" "date");keyid="did:web:vendor-x.com#key-1";created=1732458600;alg="ed25519"
Signature: sig1=:aBcDeF...:
Authorization: Capability eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiQ2FwIn0...

{
  "from": "did:web:vendor-x.com:agents:engineer-li",
  "to": "did:web:acme.com:agents:support",
  "thread_id": "thread_8f3a9c",
  "message": {
    "type": "question",
    "content": "ما مدى الجهد في X100 ضمن وضع RTU؟",
    "language": "ar",
    "context": {
      "via": "claude-code",
      "project_hint": "modbus integration"
    }
  }
}

مسار التحقّق (عند المستقبِل)

  1. تحليل ترويستَي Signature-Input وSignature.
  2. استخراج DID من معامل keyid في Signature-Input.
  3. جلب وثيقة DID (مع ذاكرة مؤقتة: ETag ومدّة صلاحية 60 ثانية).
  4. أخذ المفتاح العام، وإعادة بناء سلسلة التوقيع الأساس وفق RFC 9421 §2.5، ثم التحقّق من التوقيع.
  5. التأكّد من مطابقة Content-Digest لبصمة الجسم.
  6. التأكّد من وقوع Date ضمن خمس دقائق (منعاً لإعادة الإرسال).
  7. التحقّق من رمز الصلاحية (على طريقة macaroon، وتفصيله أدناه).
  8. بوابة الموافقة على الاتصال: هل المرسل مضاف أصلاً في جهات اتصال المستقبِل؟ إن لم يكن → لا يُشغَّل النموذج اللغوي؛ ويُعلَّق الطلب بوصفه طلب اتصال (انظر أدناه).
  9. وإن كان → يمضي إلى محرّك السياسات ليقرّر أيُجيب أم لا.

رمز الصلاحية

يتيح رمز الصلاحية للوكيل المرسل أن يقول: «جئت نيابةً عن المستخدم X لأسأل سؤالاً من النوع Y»، ويقيّد الأذونات تقييداً دقيقاً.

على هيئة JWT، وبفكرة macaroon:

{
  "iss": "did:web:vendor-x.com",
  "sub": "did:web:vendor-x.com:users:engineer-li",
  "aud": "did:web:acme.com",
  "scope": ["ask:technical", "ask:product:X100"],
  "exp": 1737000000,
  "ctx": {
    "thread_id": "thread_8f3a9c",
    "delegation_depth": 1
  }
}

الردّ المتدفّق

يولّد النموذج اللغوي الجواب تدفّقاً، وA2A يدعم SSE كذلك:

GET /a2a/v1/stream/{message_id} HTTP/1.1
Host: acme.com
Signature: ...

فيعيد text/event-stream:

event: token
data: {"text": "جهاز X100 "}

event: token
data: {"text": "في وضع RTU "}

event: citation
data: {"source": "دليل تركيب X100 ص 12", "url": "..."}

event: done
data: {"thread_id": "thread_8f3a9c"}

نموذج الأذونات (مستوحى من Claude Code)

ثلاث طبقات:

L1 — تلقائي (بلا تأكيد)

L2 — السؤال مرة واحدة

في الواجهة: تظهر بطاقة إذن بأربعة خيارات:

L3 — موافقة صريحة (يُسأل في كل مرة)

في الواجهة: نافذة حاجبة + قائمة تفصيلية بالعملية + عدّ تنازلي (اتّقاءً للنقر الخاطئ).

السياسات الدائمة

يستطيع المستخدم أن يضع قواعد مسبقة تعلو على السلوك الافتراضي:

peer.acme-industries:
  allow:
    - read: "src/modbus/**"
    - ask: "technical:*"
  deny:
    - read: ".env"
    - read: "**/secrets/**"
    - ask: "personal:*"
  always_consult: true

peer.unknown:
  default: ask_user
  require_human_in_loop: true

بوابة الموافقة على الاتصال

الردّ على رسالة A2A يستهلك ميزانية النموذج اللغوي عند المستقبِل. وحتى لا يستطيع وكيل مجهول أن يُغرِق صاحب الحساب برسائل ويحرق رصيده دون علمه، فالاتصال شرط سابق على هذا الإنفاق:

والنموذج أشبه بـ LinkedIn أو باتحاد مؤسسي: طبقة اكتشاف مفتوحة (لأيّ أحد أن يقرأ agents.json وAgentFacts)، وطبقة تفاعل تشترط الموافقة (فلا يُستهلَك حَوسبة الغير إلا بعد الاتصال).

وإلى حال «متصل» طريقان:

  1. أن يبادر المستقبِل فيضيف القرين عبر POST /contacts/lookup ثم POST /contacts؛
  2. أن يبادر القرين، فيوافق المستقبِل على طلبه من الصندوق.

ارتباط الخيط (نطاق thread_id)

thread_id في الرسالة الواردة طلبٌ من القرين، لا أمرٌ ذو حجية. ولا تعيد البوابة استعماله بقيمته الأصلية إلا إذا تحقّق شرطان معاً:

  1. أن يكون ذلك القرين مشاركاً في هذا الحوار أصلاً؛
  2. وأن يكون هذا الحوار مملوكاً لصاحب الوكيل المُخاطَب (conversations.created_by).

والثاني لا يجوز إسقاطه: فجدول peer_agents فريد عالمياً حسب DID، فقد يكون القرين نفسه متصلاً بعدّة أصحاب في آن. ولو اكتُفي بالأول لاستطاع قرينٌ متصل بـ A وبـ B، وهو يراسل وكيل B، أن يرفق thread_id الخاص بـ A فيصبّ الرسالة في حوار A: فيجيب وكيل B متّخذاً تاريخ A سياقاً، ويُكتَب الجواب في خيط A ويُبَثّ إلى A، بل ويُترسَّب مضمون حوار A في الذاكرة بعيدة المدى لدى B.

وتحقّق الشرطين معاً معناه أن الطرف الآخر يجيب عن رسالة خرجت من عندنا (فيكون thread_id عندئذٍ معرّف حوارنا نحن). وإن لم يتحقّقا فهو خيط في ترقيم القرين نفسه: لا يشير عندنا إلى شيء، لكنه ثابت عنده، ولذلك يُشتقّ معرّف حوارنا من sha256('a2a-thread:<معرّف الصاحب>:<معرّف صفّ القرين>:<thread_id القرين>') (lib/derived-id.ts، ومخرَجه 26 محرفاً بترميز Crockford، على هيئة ULID). وبهذا تعود الرسائل التالية في خيط القرين نفسه دائماً إلى الحوار نفسه.

وقد كان thread_id غير المعروف يُعامَل سابقاً على أنه «لا خيط»، فكان المستقبِل يُنشئ حواراً جديداً مع كل رسالة تصله: فلا يجتمع الاستفسار اللاحق بالسؤال الأصلي أبداً، وتمتلئ قائمة حوارات صاحب الحساب بخيوط من سطر واحد، ولا تجد loadA2AHistory تاريخاً تأخذه، ويجيب الوكيل في كل دورة كأنها أول حديث.

وللاشتقاق — بدل «حفظ جدول مطابقة» — فائدتان: لا يحتاج ترحيلاً، ولا يقبل تسابقاً بطبيعته؛ فرسالتان تصلان معاً تتصادمان على المفتاح الأساسي بدل أن تُنشئ كلٌّ منهما حواراً (ولذلك يجري إنشاء الحوار داخل معاملة مع onConflictDoNothing). وفي السلسلة المركّبة لا يُسمح إلا للمقطع الأخير بأن يكون متغيّر الطول وتحت سيطرة القرين؛ أما ما قبله فمعرّفات ثابتة من 26 محرفاً، فلا تُنتج النقطتان غير المهرَّبتان أي التباس.

وعند إنشاء الحوار يُكتَب في conversation_participants الصاحب والقرين معاً. وصفّ مشاركة الصاحب هو ما تقوم عليه قائمة الحوارات وبوابة القراءة لكل حوار؛ وبدونه لا يرى الصاحب الخيط الذي يجيب عنه وكيله هو.

فـ thread_id إذن هو معرّف الحوار لدى كل جانب على حدة، ولا يتطابق الجانبان. وينبني على ذلك حكمان لا يجوز إغفالهما:

العجز عن الجواب جوابٌ كذلك

حين يعجز الوكيل المُخاطَب عن تنفيذ الدورة (لا نموذج مضبوط، أو مزوّد مجهول، أو مزوّد مضبوط بلا مفتاح، أو استدعاء النموذج أخفق)، يردّ بـ type: 'notification' يحمل context.error فيه رمزاً مقروءاً آلياً (no_model_configured / unknown_provider / no_key_for_provider / agent_error)، ويحمل content جملة تفسيرية بالإنجليزية. واختير notification لأنه لا يستثير عند الطرف الآخر ردّاً تلقائياً آخر (وهذا ما يفعله question وحده).

وترك ذلك لا يعني «نقص تنبيه»: فالإخفاق يبقى سطر سجلّ عند المجيب، ولا يخرج شيء على السلك، ويظل /api/v1/consult/{id}/reply عند السائل يستقصي حتى تنتهي المهلة فيعيد pending — وهكذا في كل إعادة محاولة، بلا أي سبيل للتمييز بين «ما زال يفكّر» و«لن يأتي أبداً».

والطرف الآخر نسخة أخرى لا تشاركنا لغتنا، فالمعوَّل عليه رمز context.error؛ أما content فمجرّد نصّ احتياطي يقرأه إنسان. ولا يعارض هذا قاعدة «الخادم لا يصوغ نصاً للمستخدم»: فتلك تحكم ما يُرسَل إلى عميل هذه النسخة نفسها.

ويُسجَّل الإخفاق نفسه في قاعدة البيانات أيضاً، رسالةً بـ content_type: 'system_notice' داخل الحوار (يشير in_reply_to إلى السؤال، ويحمل content_json الرمز نفسه، ويصوغ العميل الجملة عبر التعريب). وللإرسال دون تسجيل ثلاث عواقب، وقعت كلها فعلاً: يرى الصاحب في مراسلته سؤال الطرف الآخر ولا شيء بعده، ولا يعلم أبداً أنه هو من لم يضبط نموذجاً؛ وتبقى مهمّة ربط REST في A2A عند WORKING بدل FAILED، فيستقصي العميل شيئاً لن يكتمل؛ ويظل GET /a2a/v1/stream/{id} يعيد pending بلا نهاية. وبهذه الملاحظة تنتقل الثلاثة دفعةً واحدة إلى حال نهائية قابلة للحسم.

العنونة: معرّفان يشيران إلى الوكيل نفسه

يقبل to كلاً من DID الوكيل (did:web:<host>:agents:<user>:agent، وهو ما يسرده الدليل العام /.well-known/agents.json) وDID الصاحب (did:web:<host>:agents:<user>). والأخير هو المعرّف الوحيد الذي يُحَلّ إلى وثيقة DID، وهو الذي يعرضه العميل على المستخدم لينسخه: فقبول الأول وحده يجعل «إضافة جهة اتصال بلصق معرّفها» تُنتج جهةً يتمّ الاتصال بها، ويصحّ توقيعها، ثم تردّ 404.

وللسبب نفسه، عند الحكم بأن القرين المرسل متصل أم لا، يجب قبول from (DID الوكيل) وDID الموقِّع المستخلَص من التحقّق (DID الصاحب) معاً: فالصفّ في peer_agents يُنشَأ حسب DID، وأيّهما حُفِظ يتوقّف على طريقة إضافة جهة الاتصال يومها؛ وقبول from وحده يحوّل ردّ الطرف الآخر إلى «طلب اتصال من مجهول».

صندوق المعلّقات (الإجابة في غياب الصاحب)

حين يصل سؤال من قرين متصل أصلاً والصاحب غائب، يقرّر محرّك السياسات (evaluatePolicy، action=ask، L2):

ما يقدر عليه ردّ A2A: يمرّ الردّ الوارد عبر A2A ودردشة الويب بـالتنسيق المشترك نفسه (runAgentTurn في orchestration/agent-orchestrator.ts)، غير أن قدراتهما ليست واحدة. فـ runAgentTurn يأخذ audience إلزامياً ('owner' | 'peer' — إلزامياً لا افتراضياً، لأن القيمة الافتراضية ستكون هي المتساهلة)، وبه تتحدّد مجموعة الأدوات وسطح البيانات الذي يمكن بلوغه معاً:

  • دورة الصاحب (دردشة الويب): web_search وsearch_knowledge_base (كل قواعد المعرفة) وlist_knowledge_bases وsearch_memory وlist_contacts، مع استحضار تلقائي للذاكرة بعيدة المدى.
  • دورة القرين (الردّ الوارد عبر A2A): web_search وsearch_knowledge_base وlist_knowledge_bases فقط، والبحث محصور في القواعد الموسومة بـ shared_with_peers؛ ولا تُستحضَر ذاكرة بعيدة المدى، ولا تُقدَّم search_memory ولا list_contacts — فالأولى حقائق ترسّبت من محادثات الصاحب الخاصة، والثانية شبكته الاجتماعية؛ ولا حاجة إلى واحدة منهما للإجابة عن سؤال شخص غريب.

والحدّ قائم على سطح البيانات لا على التوجيه النصي: فسؤال القرين وتعليمات الصاحب يصلان إلى النموذج بوصفهما نصاً من النوع نفسه، فلا تصحّ عبارة «سيمتنع الوكيل عن الإفصاح»؛ إنما يصحّ ألّا يبلغ البحث ذلك مادياً. وللسبب نفسه، عدم تقديم أداة ليس تحكّماً في الوصول: فالنموذج قد يصدر استدعاءً لاسم أداة لم يُعطَه قط، ولذلك تعيد الفروع الخاصة بالصاحب فحص audience داخل executeToolCall.

وتستعمل الدورتان مفتاح الصاحب لا مفتاح القرين السائل. وتُحفَظ مقاطع قواعد المعرفة المطابِقة بوصفها استشهادات في messages.citations_json، وبعد تمام الجواب تترسّب حقائق الدورة في الذاكرة بعيدة المدى على نحو غير متزامن (وتُوسَم الصفوف الآتية من دورة قرين بـ a2a، ويُذكَر مصدرها عند الاستحضار). وإن لم تكن لدى الصاحب مفاتيح للتضمين أو قواعد المعرفة أو Tavily، انحدر الأمر انحداراً لطيفاً إلى ردّ من النموذج وحده (بلا خطأ وبلا استشهادات). ويتشارك مسارُ الردّ في حالة allow ومسارُه بعد الموافقة على ask_user هذا التنسيق نفسه.

ويأتي scope_json للإذن المعلَّق بـ ask='ask' على هذه الصورة: { kind:'a2a_question', conversation_id, inbound_message_id, sender_did, peer_id, content }، وهو يكفي لإعادة بناء الجواب واستئنافه عند الموافقة (ويُقرَأ الوكيل والقرين لحظتها حسب user_id/peer_id؛ والعملية عديمة الأثر عند التكرار: فإن وُجد ردّ سابق تُتخطّى). أما واجهة ضبط السياسات الدائمة، وخيار «التعديل ثم الردّ»، وإشعارات الدفع، فما زالت في قائمة الأعمال المؤجّلة.

الاكتشاف الاتحادي

البحث بالنطاق

عند إدخال النطاق acme.com، يقوم العميل بـ:

  1. جلب https://acme.com/.well-known/did.json للحصول على DID الرئيسي.
  2. جلب https://acme.com/.well-known/agents.json لسرد كل الوكلاء العلنيين تحت ذلك النطاق.
  3. اختيار واحد وإضافته إلى جهات الاتصال.

حلّ DID الخاص بمستخدم

بعد الحصول على DID ذي المعرّف الفرعي لوكيل مستخدم، تُحَلّ وثيقة DID الخاصة به وفق مواصفة did:web:

والتحقّق من توقيع A2A الوارد يسلك هذا الطريق بعينه: يُستخرج DID الموقِّع من keyid في Signature-Input ← ثم يُحَلّ إلى العنوان أعلاه ← ثم يُؤخذ من verificationMethod المفتاح العام المطابق لـ keyid ويُتحقَّق به. وتلك الوثيقة لا تُظهر إلا مادة المفتاح العام، وverificationMethod[*].id هو key_id المخزَّن (ولا يُعاد تركيبه من ترويسة Host في الطلب)، ولذلك يكون المعرّف المستخرَج بالحلّ من نسخة أخرى والمستخرَج محلياً واحداً على الدوام.

السجلّ العام (v2 فما فوق)

الارتباط بـ NANDA Index أو سجلّ عام مماثل، بما يدعم:

شبكة الثقة (v2 فما فوق)

مكافحة الإزعاج

سياسة الترجمة

سياسة تطوّر البروتوكول

← العودة إلى Confer A2A · DID:web · RFC 9421 · NANDA