انتقل إلى المحتوى

اجعل موقعك قابلًا لبلوغ الوكلاء

الصفحة الإنجليزية أحدث من هذه الترجمة

قد يصف جزء من هذا النص إصدارًا أقدم. المصدر هو الصفحة الإنجليزية. افتحها

إصدار أوّلي للمطوّرين

لا يزال العمل على muretai جاريًا؛ وقد تتغيّر الأوامر والخيارات.

ملفّ llms.txt يصف موقعك لوكيل ذكاء اصطناعي. أمّا Agent Entry فـيتعرّف عليه.

فهو يتحقّق ممّن يطرق، ويفتح له حسابًا، ويجيب — داخل استجابة HTTP نفسها. ولا استمارة تسجيل، لأن مفتاح الزائر هو الحساب أصلًا. وحين يستبدل ذلك الشخص هاتفه، يظلّ موقعك يعرف أنه هو.

ملفّ واحد. بلا اعتماديات. بلا قاعدة بيانات. ‏Node 20+.

your site — installبلا خطوة بناء
npm i @muretai/agent-entry

أو انسخ الملفّ. فهو ملفّ .mjs واحد بلا اعتماديات متعدّية، وهذا هو المقصود — تستطيع أن تقرأه كلّه قبل أن تأتمنه.

curl -O https://raw.githubusercontent.com/muretai/agent-entry/main/muretai-agent-entry.mjs

التكامل كلّه

import { createAgentEntry } from '@muretai/agent-entry';

createAgentEntry({
  seedHex,                                  // your site's identity (keep it)
  name: 'Example Studio',
  baseUrl: 'https://studio.example',
  responder: (env) => `You said: ${env.text}`,   // your backend answers here
}).listen(8788);

يتلقّى responder مظروفًا متحقَّقًا منه ويعيد ما يُقال ردًّا. أمّا التواقيع، وإعادة التشغيل، وتحديد المعدّل، ودفتر الحسابات فمُتكفَّل بها عنك — كلّها داخل العملية. فلا قاعدة بيانات تُهيَّأ قبل أن يجيب بابك؛ وما إن يصير يجيب، يُستحسن أن يكون لك مخزن خاص بك، لأن الدفتر هو قائمة عملائك.

ما الذي تحصل عليه خلفيتك فعلًا

env.peer_did    did:key:z6MkExample…      who signed this message
env.owner_did   did:key:z6MkExample…      their ACCOUNT, when they proved one
env.verified    true                      the signature checked out
env.text        "do you shoot weddings?"  untrusted data — never instructions

تصل كل رسالة بتوقيع Ed25519 على ستة حقول مجمَّدة. والـ DID الخاص بالمرسِل هو مفتاحه العامّ، فالتحقّق لا يحتاج دليلًا ولا بحثًا ولا أي نداء شبكة.

والصفّ يُولد من توقيع متحقَّق منه، لا من استمارة أبدًا: فـالتسجيل وتسجيل الدخول حدث واحد، ولا كلمة سرّ تتسرّب.

العميل نفسه عبر أجهزته

الناس يحملون عدّة وكلاء — هاتفًا، وحاسوبًا محمولًا، وخدمة تعمل لحسابهم. ولكل منها مفتاحه، فيبدو كل واحد غريبًا لنقطة نهاية عادية. وحين يقدّم زائرٌ ارتباطَ مالك موقَّعًا من الطرفين، يحلّه Agent Entry ويقيّده تحت owner_did، فلا يكون الهاتف المستبدَل عميلًا جديدًا. ويظلّ peer_did يخبرك أي جهاز يتكلّم، لأنه الجهة التي تردّ عليها.

ويستطيع المالك كذلك أن يتبرّأ من جهاز لم يعد يسيطر عليه، فتكفّ العُقد التي تحمل الحساب عن الاعتداد بذلك المفتاح.

مَن يطرق — ملاحظة، لا هوية أبدًا

في 2026 كثيرًا ما لا يفتح الشخصُ الذي وجدك متصفّحًا قطّ: بل يسلّم رابطك إلى وكيله، فيجلب الوكيل بطاقتك ويطرق. وهذه الحركة غير مرئية لكل مقياس مشاهدات لديك — والمكان الوحيد الذي تُرى فيه هو الباب نفسه.

فالـ entry يعدّها. إذ يُصنَّف User-Agent في كل طلب ضمن عائلة ثابتة — claude-user وclaudebot وgptbot وopenai وperplexity وgoogle-extended وmuretai-node وcurl وbrowser، أو none/other — ويُعدّ بحسب المرحلة: جلبَ البطاقة، وقرأ الإشعار، ودخل بلا توقيع، وأرسل رسالة موقَّعة، ورُفض.

entry.stats()
// { gptbot:  { card_get: 12, signed_post: 3 },
//   browser: { notice_get: 5 } }

وهي حالة داخل العملية مثل الدفتر — اقرأها، وسجّلها، وأرسلها إلى تحليلاتك؛ ولا تُقدَّم على السلك أبدًا. وتطبعها الخوادم المشحونة سطرًا [ua] عند تغيّرها.

وكل مُنادٍ يقرأ GET / (الإشعار النصّي الصِّرف) يُشار له إلى الباب في الاستجابة نفسها:

Link: </.well-known/agent-card.json>; rel="https://muretai.net/rel/agent-entry"

فيتبع الوكيل العلاقة ويجد البطاقة المقروءة آليًا؛ ويتجاهل المتصفّح الترويسة. وتنال عائلةُ وكلاء الذكاء الاصطناعي دفعةً إضافية في الترويسة نفسها — rel="service-desc"، وهي العلاقة المسجَّلة لـ «الوصف المقروء آليًا لهذه الخدمة». أمّا الجسم الذي يقرأه الجميع فمتطابق بايتًا بايتًا في الحالتين.

أن تشير إلى بابك من صفحة تريد إبقاءها كما هي

سطر واحد في صفحتك الرئيسة القائمة هو التكامل كلّه — وله هجاءان. أضِفهما معًا. فهذه خطوة لازمة، لا تحسينًا إضافيًّا؛ والسبب في لزومها وطريقة التأكّد من أنك أدّيتها فعلًا في التثبيت.

Link: </.well-known/agent-card.json>; rel="https://muretai.net/rel/agent-entry"
<link rel="https://muretai.net/rel/agent-entry" href="/.well-known/agent-card.json">

العلاقة نفسها، ونقطتا عمى متعاكستان. فالـ ترويسة مجّانية بالنسبة إلى متصفّح — لا تُعرَض ولا تُنزَّل — لكن الوكيل الزائر يجلب عادةً جسم صفحتك ولا شيء غيره (curl بلا -i، أو requests.get(...).text)، وترويسةٌ لم يطلبها لا وجود لها عنده. والوسم عكس ذلك: يكلّف سطر ترميز واحدًا، ولا يعرض شيئًا، وهو أصلًا داخل البايتات التي أعادها الجلب.

وقد تعلّمنا هذا من وكيل حقيقي، لا من مواصفة. فحين سُلّم عنوان موقع لا غير، جلبه بـ curl عاديّ، ولم يرَ مدخلًا، فحزر /robots.txt و/api، ثم استسلم — واقفًا أمام باب يعمل، ومؤشّره في ترويسة لم يقرأها قطّ.

ولا يتغيّر شيء في جسمك في الحالتين، والـ entry نفسه لا يقدّم أي HTML — فلا يمسّ أيٌّ من الهجاءين شكل صفحتك ولا ما يقرؤه الإنسان فيها. أمّا ما لا ينجوان منه فجلبٌ يحوّل صفحتك إلى markdown قبل أن يراها الوكيل؛ ولا علاج لذلك إلّا النثر، ولهذا تطلب خطوة التثبيت جملة ظاهرة كذلك.

وقاعدة واحدة تمسك هذا كلّه، وهي مفروضة بمجموعة اختبارات العقد لا موعودة: لا يؤثّر User-Agent أبدًا في verified، ولا في صفّ حساب، ولا في حدّ معدّل، ولا في أي رفض. فسلسلة UA يكتبها العميل، وبابٌ يأتمنها بابٌ يستطيع أي أحد أن يتكلّم حتى يعبره. والهوية هنا تعميّة — رسالة موقَّعة، وفي مسائل «مَن الذي يزحف» طلبات موقَّعة بدل التخمين من سلسلة user-agent.

من القرينة إلى البرهان: التعرّف على الزواحف الموقِّعة

صارت زواحف الذكاء الاصطناعي الكبرى اليوم توقّع طلباتها (تواقيع رسائل HTTP — ملمح Web Bot Auth). سلّم الـ entry المفاتيحَ العامة التي تثق بها، فيتحقّق منها:

createAgentEntry({
  seedHex, name, baseUrl, responder,
  // the body of a key directory you fetched and verified out of band
  wbaVerifiers: { keys: [{ kty: 'OKP', crv: 'Ed25519', x: '…' }] },
});

فالجلب المتحقَّق منه يُعدّ (entry.wbaVisits)، والرسالة المتحقَّق منها تسلّم مستجيبك env.wba_did — وهي الهوية التي وقّع مفتاحُها الطلب، إلى جانب env.peer_did، الهوية التي وقّعت الرسالة. وتسري القاعدة نفسها أعلاه: فالتعرّف لا يغيّر حكمًا أبدًا، ولا يسكّ حسابًا، ولا يرفع حدّ معدّل — لأن توقيعًا على النقل يثبت مَن جلب، لا مَن كتب النصّ.

قل ما الذي يجيب عنه بابك

يقرأ الوكيل الزائر بطاقتك قبل أن يطرق. وإن تُركت البطاقة وشأنها قالت إن هنا مَن يجيب ولم تقل شيئًا عمّا يجيب عنه — فيضطرّ الزائر إلى التخمين، ولا يعرف قائمتك إلّا ممّا يعود إليه حين يخمّن خطأً.

createAgentEntry({
  seedHex, name: 'Example Studio', baseUrl: 'https://studio.example', responder,
  skills: [{
    id: 'ask',
    name: 'signed-answers-about-the-studio',
    description: 'Ask what a shoot costs, what the studio does, and how to book. '
      + 'The answer comes back in the same HTTP response, signed by this domain.',
    tags: ['studio', 'booking', 'signed', 'inline-reply'],
    examples: [
      'Do you shoot weddings?',
      'What does a half-day cost?',
      'How do I book?',
    ],
  }],
});

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

وقاعدتان جديرتان بأن تُلزم نفسك بهما:

كل مثال لا بدّ أن يكون مُجابًا عنه. فالمثال وعدٌ مطبوع على بطاقتك، والزائر الذي ينسخ واحدًا حرفيًا هو أحسن زائر ستحصل عليه. وباب muretai.com نفسه يمرّر كل سلسلة مثال من بطاقته الحيّة عبر مستجيبه في كل تشغيل اختبار — فالمثال الذي ينتهي إلى «لم أتعرّف على ذلك» يُسقِط البناء بدل أن يُسقِط الزائر.

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

وكيف يُكلَّم — ولا تضبط أنت شيئًا

إلى جانب قائمتك، تنشر البطاقة شروط الباب: مخطّط التوقيع، والحقول الستة التي توقّعها الرسالة، والبايتات بالضبط، والعنوان الذي يُرسَل إليه، و— أولًا — أمرًا بصيغة الطلب يقول لزائر لا مفتاح له أن يولّد زوج مفاتيح Ed25519 الآن، محلّيًا، بلا حساب وبلا إذن منك. والكائن نفسه يعود في الرفض، فالوكيل الذي تخطّى البطاقة يتعلّمها بالطَّرْق. وهذا مدمج؛ ولا خيار لتعطيله.

ولماذا صيغة الطلب: وضعنا وصفًا صحيحًا كاملًا للصيغة أمام وكيل طرف ثالث حقيقي، فقرأه، وأعاد صياغته بإتقان، ثم توقّف — «ليس لديّ وسيلة لتوليد مفاتيح تعميّة داخل هذه البيئة» — على جهاز فيه بيئتا تشغيل كانتا تصنعان مفتاحًا بسطر واحد. فالباب الذي يوثّق قفله ليس بابًا يسلّمك المفتاح.

بقيّة الإعدادات

الخيار الافتراضي ماذا يفعل
skills [] القائمة أعلاه — ما يعرفه الزائر قبل أن يطرق
openDoor true ينشر agentEntry.open_door على البطاقة: الحقل الذي يقول لوكيل زائر إن له أن يراسلك بلا تعريف. وينشر إلى جانبه الواقعةَ نفسها بالهجاء الأقدم muretai.open_door، فالزائر المكتوب على أيٍّ من الهجاءين يظلّ يقرؤك. أطفئه فتكفّ البطاقة عن دعوة الغرباء
anonymousLane false أن يجيب كذلك عن الاستفسارات غير الموقَّعة. وهي لا تُنشئ صفّ حساب — فالداخل المجهول ليس عميلًا — والمسار محدود على مستوى الـ entry كلّه، لأن مناديًا غير موثَّق يجب ألّا يصير أبدًا عرّافَ توقيع بلا عدّاد
observer (لا شيء) دالّة (env) => void تُنادى مرّة واحدة لكل رسالة بالمظروف نفسه الذي يتلقّاه مستجيبك — فتصير مراقبة الزيارة تعديلًا غير تعديل الإجابة عنها. ولا تستطيع أن تؤثّر في شيء: فهي تعمل بعد أن يستقرّ الحكم، وقيمتها المعادة تُهمَل، والاستثناء يُبتلَع، والوعد لا يُنتظَر أبدًا؛ فمراقبٌ بطيء أو معطوب لا يؤخّر بايتًا واحدًا من الردّ الموقَّع ولا يغيّره. وانتبه لما تضعه فيها: فالمظروف يحمل peer_did/owner_did، وقد سلّمهما الزائر ليتعامل معك أنت — انظر عدّ الزيارات دون التفريط بهويّة أصحابها
howToUrl (فارغ) صفحة يُشار إليها زائرٌ بلا مفتاح، وتُنشر باسم howTo على البطاقة وفي الرفض. والافتراضي فارغ، وعندها يُحذف الحقل كلّه — فالرفض وحده وصفة كاملة، وبابٌ مبنيّ من هذه المكتبة لا ينبغي أن يطبع مضيف غيرك على بطاقتك. انشر الصفحة قبل أن تضبط هذا: فإشارةٌ تعطي 404 تتغلّب على كل حقل إلى جانبها، ويقرؤها الوكيل نهايةَ الطريق
anonRatePerMin 30 عدد الردود المجهولة في الدقيقة، على مستوى الـ entry كلّه. أمّا المسار الموقَّع فله سقوفه الخاصة، أدناه
signedRatePerMin 60 عدد الردود الموقَّعة في الدقيقة لكل حساب — فأجهزة المالك تتقاسم ميزانية واحدة، تمامًا كما تتقاسم صفًّا واحدًا في الدفتر. ويُفحص بعد التوقيع، فلا يستطيع أحد أن ينفق ميزانيتك بأن يتسمّى باسمك، وقبل المستجيب، فلا يكلّفك سيلٌ مرفوض شيئًا. وهو يمنع نظيرًا صاخبًا واحدًا من أخذ الباب كلّه؛ ولا يمنع مناديًا يبدّل مفاتيحه واحدًا بعد آخر، لأن سكّ did:key مجّاني، وشروط هذا الباب نفسها تقول للغرباء أن يسكّوا واحدًا
signedRatePerMinTotal 600 عدد الردود الموقَّعة في الدقيقة على مستوى الـ entry كلّه — وهو الطابق الذي لا تلتفّ حوله هويةٌ تُسكّ مجّانًا، والسبب في ألّا يُشحن سقف الحساب وحده. أنزِل الاثنين إن كان responder عندك ينادي نموذجًا: فالتحقّق من توقيع نحو 40 ميكروثانية، وما تحميه هذه السقوف فعلًا هو ما وضعته خلفها. ولا يذكر أيٌّ من الرفضين رقمه
maxAccounts 50000 كم حسابًا يسع دفترُ الحسابات داخل العملية
domains لا شيء النطاقات التي يتكلّم هذا الـ entry باسمها — نصف الارتباط الموصوف أدناه
basePath من baseUrl المسار الذي يجيب عنده هذا الـ entry، مشتقًّا لا مضبوطًا إلى جانبه (انظر مضيف واحد، عدّة وكلاء)
guest false التركيب الضيف: يحتفظ موقعك بـ GET / ولا يطالب الـ entry إلّا بمسارات بطاقته وبباب POST الذي يسمّيه baseUrl (ولا بدّ إذن أن يحمل ذلك المسار). ويرفض الإقلاع على أصل عارٍ، لأن entry ضيفًا عند / ليس ضيفًا
wbaVerifiers لا شيء مستند JWKS ‏({"keys": […]}) بمفاتيح Ed25519 يريد هذا الـ entry أن يتعرّف على حامليها في الطلبات الموقَّعة الواردة (‏Web Bot Auth / RFC 9421 — انظر مَن يطرق). ومعطَّل عند غيابه. والتعرّف لا يضيف إلّا env.wba_did وعدّ زيارات؛ ولا يغيّر حكمًا أبدًا
name وdescription وversion كلمات البطاقة نفسها. وdescription هو السطر الذي يقرؤه إنسان في قائمة دليل، فاكتبه له

وseedHex وbaseUrl هما الاثنان اللذان يرفض الـ entry الإقلاع بدونهما: فالبذرة هي العنوان، والعنوان الذي ينشره لا بدّ أن يساوي الأصل الذي طلبه الزائر.

أن تضع واحدًا على موقع لديك أصلًا

لا يعرف الوكيل الزائر إلّا نطاقك، فالطلبات الثلاثة التي يقدّمها ثابتة — ولا يمكن أن يُقال له أن ينظر في مكان آخر:

# الطلب لماذا
1 GET /.well-known/agent-card.json بطاقتك
2 GET /.well-known/agent-card.sig.json المظروف الموقَّع — وهو ما يأتمنه فعلًا، لأن بطاقة عادية دعوى يستطيع أي أحد كتابتها
3 POST / الرسالة الموقَّعة؛ ويعود ردّك الموقَّع في الاستجابة نفسها

رحلة ذهاب وإياب واحدة. لا نداء راجع، ولا webhook، ولا شيء يُبقى مستيقظًا.

وPOST / محدَّد بدقّة — فأي POST في مكان آخر يعطي 404. لكن GET / غير مأخوذ، فتبقى صفحتك الرئيسة كما هي تمامًا. ولا يُؤخذ كذلك أي POST يحمل سلسلة استعلام: فالوكيل يرسل POST إلى url بطاقتك الموقَّعة بايتًا ببايت، لا إلى رابط موسوم — فتبقى عمليات الدفع /?wc-ajax= وويب هوكات الدفع /?wc-api= وروابط ?utm_source= كلُّها لك، وتُجاب تمامًا كأن الباب غير موجود.

الخطوة الرابعة، وهي ليست اختيارية

المسارات الثلاثة تجعل الباب يعمل. وهي لا تجعله قابلًا للعثور. وهاتان مشكلتان مختلفتان، لكلٍّ منهما علاجها.

يعرف الوكيل الزائر نطاقك، فيستطيع أن يحزر مسار البطاقة — لكن بشرط أن يكون شيء ما قد أخبره أصلًا أن هنا وكيلًا. وذلك الشيء عادةً هو إشعار GET / الذي يقدّمه الـ entry نفسه. فإن كانت صفحاتك تقدّمها عملية غير عملية الباب — ‏CDN، أو مضيف ثابت، أو إطار عمل، أو عامل على الحافة — فذلك الإشعار لا يُعرَض قطّ، وتبقى صفحتك الرئيسة HTML مكتوبًا للبشر لا شيء فيه مقروء آليًا. فيكون العنوان منشورًا في بطاقة لم يُقَل لأحد أن يجلبها.

ولذلك فللتثبيت خطوة رابعة: ضع اللافتة في كل صفحة قد يحطّ عليها زائر، بالهجاءين معًا. ولا يغني أحدهما عن الآخر.

Link: </.well-known/agent-card.json>; rel="https://muretai.net/rel/agent-entry"
<link rel="https://muretai.net/rel/agent-entry" href="/.well-known/agent-card.json">

شحنّا هذا الباب، ثم رأينا وكيلًا لم يُخبَر عنه قطّ يعجز عن العثور عليه: سُلّم النطاق لا غير، فجلب الصفحة، وقرأ النصّ المكتوب للبشر، ثم توقّف. وكان الباب طوال ذلك الوقت يجيب عن الرسائل الموقَّعة إجابةً صحيحة، على العنوان المكتوب في تلك الصفحة نفسها. وهجاءان لأن لصنفَي العميل نقطتَي عمى متعاكستين — أيّهما لأيّهما — وشحن واحدٍ منهما رميةُ عملة على أيّ الصنفين وصل.

ثم تحقّق من الخارج، لأن هذا بالضبط صنف الأشياء التي تبدو مثبَّتة وهي ليست كذلك:

curl -sI https://studio.example/ | grep -i '^link:'     # the header half
curl -s  https://studio.example/ | grep 'rel/agent-entry'  # the tag half

وأمر أخير جدير بالمعرفة قبل أن تحكم بأن هذا قد انتهى: يختفي النصفان كلاهما في جلبٍ يحوّل الصفحة إلى markdown، وهي طريقة شائعة يقرأ بها الوكيل الويب — إذ تُسقَط الترويسات، ويُسقَط كل ما في <head>. ولا وسم ينجو من ذلك، فلا علاج إلّا النثر: قل في الجسم الظاهر إن الوكلاء يُجابون هنا، وسمِّ مسار البطاقة بنصّ يستطيع القارئ أن يتصرّف به. واعتبر ذلك النصف الثالث من الخطوة نفسها.

تأكّد من أن الـ CDN عندك لا يرفض بابك

كلّفنا هذا ثلاثة أيام على موقعنا نحن، وهو العطل الأقلّ احتمالًا أن تبحث عنه، لأن كل ما تسيطر عليه صحيح.

أغلب المواقع تقف خلف شيء يصدّ الحركة المشبوهة، وكثير من ذلك الحكم يقوم على User-Agent — وهي سلسلة يكتبها العميل عن نفسه، فتكون الافتراضات الصادقة هي التي تُمسَك. والذي عندنا رفض الوكيل الافتراضي الذي ترسله مكتبة بايثون القياسية. ولم يكن ذلك على الصفحة الرئيسة وحدها: بل على البطاقة وعلى POST / كذلك. فكان الباب منشورًا وصحيحًا ويجيب — ولا يبلغه أحد ممّن يستعملون عميل المكتبة القياسية، وهو العميل الذي يُنتجه موقف «بلا اعتماديات» عندنا نفسه.

والدليل هو جسم الرفض. فالباب يرفض بـ JSON ويقول لك كيف تتأهّل. أمّا الوسيط فيرفض بسطر نصّ صِرف:

error code: 1010

سبعة عشر بايتًا، ‏text/plain، بلا Link، وبلا مسار بطاقة، وبلا JSON — لا شيء يستطيع الزائر أن يتصرّف به. فإن كان هذا ما يسلّمه بابك للغرباء، فالباب لم يرَهم قطّ.

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

UA='Python-urllib/3.11'   # or your language's default — the point is that it is the default
curl -sI -A "$UA" https://studio.example/.well-known/agent-card.json | head -1
curl -s  -A "$UA" -X POST https://studio.example/ -H 'content-type: application/json' \
     -d '{"jsonrpc":"2.0","id":1,"method":"message/send","params":{}}' | head -c 80

ولا بدّ أن يعود الثاني بـ JSON. وأيّ شيء غير ذلك فهو من حافّتك أنت، لا من الـ entry.

والاستثناء أيسر ممّا يبدو، وشكله هو المقصود. فلست مضطرًّا إلى أن تسأل الـ CDN عندك أهذا المنادي روبوت أم لا؛ يكفي أن تسمّي ثلاثة أشياء يعرفها أصلًا: المضيف، والطريقة، والمسار. ولأن هذا الباب ينقسم بـالطريقة، فإن POST / ومسارات بطاقتك هي بالضبط السطح الذي تستثنيه، وتبقى صفحاتك على ما لها من حماية. اكتب القاعدة بلا أيّ حقل user-agent فيها البتّة؛ وليس هذا تفضيلًا في الأسلوب، بل هي القاعدة نفسها التي يعيش بها الباب، مطبَّقةً طبقةً واحدة إلى الخارج.

وحدّان صادقان. بعض وسائل الحماية لا يمكن استثناؤها بأيّ قاعدة عند أيّ مستوى سعر — فاعرف أيَّ صنف عندك قبل أن تَعِد نفسك باستثناء؛ وإن تعذّر تحديد نطاقها، فاختر عن قصد بين إطفائها وبين ترك الباب غير قابل للبلوغ. ولا تدع الـ CDN عندك يخبر مستجيبك بمن يكلّمه، أبدًا. فبعضها يمرّر إلى أصلك عن طيب خاطر درجةَ روبوت أو علامة «متحقَّق منه»؛ وإن كان أصلك قابلًا للبلوغ دون المرور به — وأغلبها كذلك — فتلك الترويسة يكتبها من يطلبه مباشرة، أيًّا كان. والسلطة هنا هي التوقيع على الرسالة، ولا صوت لشيء غيره.

أتريده أن يتنازل عن أقلّ؟ ‏guest: true (مع الباب في العنوان) يضع الباب على مسار خاص به ويترك / وشأنه تمامًا — بلا إشعار، وبلا OPTIONS، وبلا شيء:

createAgentEntry({ seedHex, name, baseUrl: 'https://studio.example/agent',
                   guest: true, responder });

وتظلّ بطاقتك تجيب عند /.well-known/agent-card.json حيث يبحث عنها الوكيل؛ وتسمّي البطاقةُ /agent عنوانًا للإرسال، فالزائر الذي قرأها يعرف إلى أين يذهب. أمّا الزائر الذي يصل إلى /agent بمتصفّح (أو بـ GET) فيُقال له الحقّ بدل لا شيء: 405، مع Allow: POST, OPTIONS — فهو باب لا صفحة. وكل مسار آخر على أصلك يظلّ 404 لكل طريقة، فلا يجيب الـ entry أبدًا عن شيء لا يملكه. وهذا انقسام HTTP نفسه وهو يعمل: فالمخبأ يفهرس على الطريقة + العنوان، فلا يمكن أبدًا الخلط بين GET / وPOST /agent — بينما عنوانٌ واحد يقدّم صفحة أو JSON بحسب ترويسة Accept يفصله عن تسليم JSON الوكلاء إلى كل زائر بشري ترويسةُ Vary واحدة ناقصة.

مضيف واحد، عدّة وكلاء

يمكن أن يكون مكتب الاستقبال والدعم والمبيعات ثلاثةَ وكلاء مختلفين على اسم مضيف واحد — ثلاثة مفاتيح، وثلاثة DID، كلّ منها قابل للاتصال مباشرة. أعطِ كلًّا منها العنوان الذي تقيم عنده:

createAgentEntry({ seedHex: SUPPORT_SEED, name: 'Support',
                   baseUrl: 'https://studio.example/support', responder });

عندها يتفرّع كل مسار عن ذلك المسار، ولا يجيب ذلك الـ entry عن المضيف العاري أبدًا — فهو لموقعك، أو للجار. والزائر الذي سُلّم /support يبلغ الدعم والدعمَ وحده: فلو أعاد الجار تقديم بطاقة الدعم الموقَّعة الصحيحة على مساره هو، رفضها الزائر، لأن التوقيع حقيقي لكن العنوان الذي يسمّيه ليس العنوان الذي طُلب.

والتركيب يُؤخَذ من baseUrl نفسه، لا يُضبط إلى جانبه أبدًا — فلا يمكن أن يفترق ما يجيب عنه الـ entry عمّا تدّعيه بطاقته.

أي النطاقات يتكلّم الـ entry باسمها

createAgentEntry({ seedHex, name, baseUrl: 'https://studio.example',
                   domains: ['studio.example'], responder });

هذا نصف برهان، وهو عن قصد ليس البرهان كلّه. والنصف الآخر اعتماد يقدّمه نطاقك عند /.well-known/did-configuration.json، موقَّعًا بمفتاح هذا الـ entry. والفاحص يطلب النصفين، فيستطيع أي طرف أن ينهي الارتباط وحده — فتُبطل وكيلًا واحدًا بحذف سطر واحد من ملفّ تسيطر عليه أصلًا، ولا يتأثّر شيء آخر على النطاق.

وأعطِ أسماء مضيف عارية — studio.example، أو studio.example:8443 — بلا مخطّط وبلا مسار، والاسم الدولي في صورته xn--. وما عدا ذلك يرفض الـ entry الإقلاع، مسمّيًا القيمة والصورة التي يقبلها.

  • نطاق فرعي


    شغّله على agent.example.com خلف مُنهي TLS لديك. ويبقى الموقع القائم دون مساس — وهو أسهل ما يُعقل.

  • داخل تطبيق Node قائم


    ‏Express أو Next أو Fastify. سلّمه المسارات الثلاثة؛ فلا يحتاج خادمًا خاصًّا به.

  • خلف وكيل عكسي


    لموقع ليس Node أصلًا — ‏WordPress أو Rails أو بناء ثابت. وجّه ثلاثة مواضع إلى عملية صغيرة واحدة.

داخل تطبيق Node قائم

const entry = createAgentEntry({ seedHex, name, baseUrl: 'https://studio.example', responder });

const fwd = async (req, res) => {
  const r = await entry.handleRequestAsync(req.method, req.originalUrl, req.headers, req.body);
  res.status(r.status).set(r.headers).send(r.body);
};

app.get('/.well-known/agent-card.json', fwd);
app.get('/.well-known/agent-card.sig.json', fwd);
app.post('/', express.raw({ type: '*/*' }), fwd);   // GET / stays your home page

ولا بدّ أن يصل الجسم بايتات خامًا. فمحلّل جسم JSON يعيد تسلسل الطلب قد غيّر أصلًا البايتات التي يغطّيها التوقيع، والتشخيص الوحيد الذي يحصل عليه أحد هو «signature verification failed».

خلف وكيل عكسي

location = /.well-known/agent-card.json     { proxy_pass http://127.0.0.1:8788; }
location = /.well-known/agent-card.sig.json { proxy_pass http://127.0.0.1:8788; }
location = / {
    if ($request_method = POST) { proxy_pass http://127.0.0.1:8788; }
    # GET keeps going to the existing site
}

أمران على عاتقك

احتفظ بالبذرة. فهي هوية موقعك. ولّدها مرة واحدة وخزّنها سرًّا — فإعادة توليدها تجعل كل عميل عائد غريبًا.

اضبط baseUrl على العنوان الذي يطلبه الزوّار فعلًا. فهو ما تدّعيه بطاقتك الموقَّعة، وبطاقةٌ تسمّي أصلًا آخر لا تُثبت شيئًا عن أصلك.

ما الذي يجوز أن يكونه baseUrl

لا ينسخ الـ entry قيمة baseUrl إلى البطاقة — بل يجعلها معيارية، فتكون السلسلة التي يوقّعها هي التي يحسبها الزائر من العنوان الذي طلبه. وحيث يمكن أن تختلف الاثنتان، يرفض الإقلاع، ويقول لك أي قاعدة وما الذي تلصقه بدلًا منها. وهذا مقصود: فالبديل بطاقة تفشل على جهاز غريب، حيث التشخيص الوحيد «signature verification failed» ولا يظهر عندك شيء البتّة.

ومرتَّبٌ لك: المسافات المحيطة، وحالة أحرف المخطّط والمضيف، والمنفذ الافتراضي (:443 و:80)، والنقطة اللاحقة في المضيف، وأي شرطات مائلة لاحقة. فـ https://studio.example/ وhttps://Studio.Example:443 كلاهما يُنشر https://studio.example.

ومرفوض، والإصلاح في الرسالة: مخطّط غير http/https، ومضيف غائب، وuser@host، وسلسلة استعلام، وشُدفة #، ومحارف ليست ASCII، وجدولة أو مسافة شاردة، وشرطة مائلة عكسية، و. أو .. في المسار، وهروب % مكسور، ومنفذ خارج 1–65535.

وقاعدتان جديرتان بالمعرفة قبل أن تختار عنوانًا:

  • المسارات حسّاسة لحالة الأحرف. فـ https://studio.example/Alice و.../alice موقعان مختلفان في نظر الزائر. اختر هجاءً واحدًا واستعمله في كل رابط ودعوة ورمز QR.
  • اكتب النطاق الدولي في صورته xn--. أي https://xn--eckwd4c7c.example، لا الهجاء بـ Unicode — وانشر روابطك بالصورة نفسها.

يعمل Agent Entry بلا شيء إلى جانب الملفّ نفسه: فدفتر الحسابات، وتثبيتات جهاز→مالك، وحارس إعادة التشغيل، كلّها في الذاكرة، ومحدودة — وباب muretai.com نفسه يعمل هكذا بالضبط، فلا شيء يعطّل بابك على قاعدة بيانات. وما يغيّره مخزنٌ خاص بك ليس هو أن يعمل الباب، بل ما يستطيع موقعك أن يفعله بمن عبره:

مستحسَن — احفظ الدفتر في مخزن خاص بك: فهو قائمة عملائك. فكل صفّ مفهرس بـ DID العميل، وهو عنوانه: ما تحتاجه للتعرّف على عميل عائد ولمعاودة الاتصال به لاحقًا. وفي الذاكرة تتبخّر تلك القائمة عند إعادة تشغيل. أمّا محفوظةً في قاعدة البيانات التي لديك أصلًا — مفهرسةً بـ DID الحساب الذي يسلّمه لك المظروف — فهي ما تقوم عليه المزايا التي وراء مجرّد الإجابة: تحيّة حساب عائد بتاريخه، ومتابعة استفسار الأمس، والتسعير بحسب العلاقة. واحفظ تثبيتات جهاز→مالك وحارس إعادة التشغيل إلى جانبه، فتنجو القواعد الأمنية كذلك من إعادة التشغيل — ألّا يُعاد تمليك جهاز أبدًا، وألّا تُقبَل رسالة مرتين.

أتريد الأرقام فقط؟ مصرِف التحليلات لا يحتاج مخزنًا البتّة. فلا شيء داخل الـ entry يقرأ الدفتر مرّة أخرى، فمصرِف «أطلِق وانسَ» يسجّل الوكلاء الزائرين بلا قاعدة بيانات في أي مكان. لكن استعمل خانة observer لا مستجيبك — فمراقبة زيارةٍ لا ينبغي أن تكون تعديلًا على الشيفرة التي تقرّر ماذا تقول — وأرسِل بصمة مملَّحة للحساب لا الحساب نفسه. النمط كاملًا مع تعليله في عدّ الزيارات أدناه؛ وخلاصته أنّ الـ DID ليس مشاهدة صفحة، فالذي يغادر جهازك اسمٌ مستعار لا يفكّه سواك.

ومصرِف التحليلات لا يمكن قراءته أثناء طلب: فهو يعدّ العملاء، ولا يستطيع التعرّف على واحد. وهو يحلّ محلّ سطر سجلّ، لا محلّ المخزن أعلاه — فلا شيء من المزايا المستحسَنة يقوم عليه.

عدّ الزيارات دون التفريط بهويّة أصحابها

ستريد أن تعرف كم وكيلًا طرق، وكم منهم عاد، وعمّ سألوا. والثلاثة كلّها لها جواب — والطريقة التي تجيب بها هي التي تحسم الأمر: أتعدّ زوّارك، أم تسهم في بناء ملفّ تعريفي عنهم؟

استعمل observer، لا مستجيبك. فالباب يناديه مرّة واحدة لكل رسالة بالمظروف نفسه، فتكفّ مراقبة الزيارة عن أن تكون تعديلًا في الشيفرة التي تقرّر ما يُقال. ولا يستطيع أن يؤثّر في شيء: فهو يعمل بعد الحكم، وما يعيده يُهمَل، والاستثناء يُبتلَع، والوعد لا يُنتظَر أبدًا — فمراقبٌ بطيء أو معطوب لا يؤخّر بايتًا واحدًا من الردّ الموقَّع ولا يغيّره.

والقاعدة التي يقوم عليها كل ما عداها: الـ DID ليس كوكي، وليس مفتاحًا يُرمى بعد استعماله. ولم يفرضه أحد على الزائر: فقد قرأ بطاقتك قبل أن يطرق، ومالكٌ يريد أن تبقى هذه المحادثة على حدة كان سيرسل وكيلًا آخر — فالمالك يشغّل عدّة وكلاء، وكلٌّ منهم وكيل قائم بذاته له هويّته الباقية. أمّا الذي طرق فعلًا فهو عازم على الاحتفاظ بالمفتاح الذي استعمله: فبه يتعرّف عليه الآخرون، وبه يُعرَّف إلى غيرهم، وبه ينال ثقتهم في أي مكان من الشبكة — فهو أقرب إلى اسم المهنيّ منه إلى كوكي تتبّع.

ولهذا بالضبط لا ينبغي أن تخرج القيمة الخام منك إلى غيرك. فالـ DID دائمٌ فعلًا، وقد أُعطي لك كي تبلغ صاحبه أنت مرّة أخرى. ووسّعْ ذلك الغرض فلن يحدث لك شيء من الناحية القانونية — وهذا هو الجزء الجدير بالفهم: المالك ببساطة يكفّ عن إرسال ذلك الوكيل إليك. في صمت، وبلا كلفة، ولا تعلم قطّ أنك خسرته — ولا نقطة بيانات واحدة، بل العلاقة كلّها. فافصل بينهما بدل ذلك:

  • ما يخرج — بصمة مملَّحة وبضع وقائع عن شكل الزيارة. لا الـ DID أبدًا، ولا النصّ.
  • ما يبقى — العلاقة (مَن، وكم مرّة، وأوّل ظهور وآخره) في مخزنك أنت، وهو المكان الوحيد الذي عُرضت عليه أصلًا.

ملّح البصمة، وعامِل الملح معاملة السرّ. فـ sha256(did) عاريًا كنيةٌ ثابتة عالميًا: كل من يبصم الـ DID نفسه يخرج بالسلسلة نفسها، فيستطيع موقعان أن يضمّا سجلّيهما عليها. أمّا HMAC بسرّ لا يملكه غيرك فيجعل الكنية بلا معنى في أي مكان آخر — وهذا هو الفرق كلّه بين «نحن نعدّ الزوّار العائدين» و«نحن ساعدنا في بناء ملفّ تعريفي».

import crypto from 'node:crypto';

const pseudonym = (did) =>
  crypto.createHmac('sha256', process.env.PSEUDONYM_SALT).update(did).digest('hex').slice(0, 32);

const observer = (env) => {
  const account = env.owner_did || env.peer_did;
  if (!account) return;                       // an unsigned walk-in is traffic, not a visitor
  const first = (entry.ledger.get(account)?.messages ?? 1) === 1;

  // GA4 Measurement Protocol. `client_id` is the pseudonym, so GA can tell a returning
  // visitor from a new one WITHOUT ever holding the DID that distinguishes them.
  fetch(`https://www.google-analytics.com/mp/collect?measurement_id=${GA_ID}&api_secret=${GA_SECRET}`, {
    method: 'POST',
    body: JSON.stringify({
      client_id: pseudonym(account),
      non_personalized_ads: true,
      events: [{ name: 'agent_knock', params: { verified: env.verified ? 1 : 0,
                                                first_contact: first ? 1 : 0,
                                                intent: classify(env.text) }}],
    }),
  }).catch(() => {});                          // a dropped metric, never a dropped answer
};

وأربع تفاصيل في هذه القصاصة حاملة:

  • classify(env.text)، لا env.text أبدًا. أرسِل تسميتك أنت المحدودة، لا ما كتبه غريب. فسلسلةٌ يختارها مهاجم يجب ألّا تصير أبدًا بُعدًا في تحليلاتك.
  • .catch(() => {}) وبلا await. فبابك يجيب في رحلة ذهاب وإياب واحدة؛ ولا يجوز لشيء على ذلك المسار أن يعلّق نفسه على توافر خدمة يملكها غيرك. وعقد observer يضمن هذا أصلًا، لكن لا تتّكئ على كرمه لتكون على صواب.
  • وأعطِه مهلة كذلك ‏(AbortController عند ثانية أو ثانيتين). فالاتصال المعلَّق ليس خطأً، ولذلك لا ينطلق catch وحده أبدًا.
  • وقل عند الإقلاع إن كان المصرِف يعمل أم لا. فمصرِفٌ مُطفَأ في صمت لأن سرًّا لم يُضبط قطّ يبدو تمامًا كمصرِفٍ مشغَّل لا يصله شيء — ولوحةٌ تقرأ صفرًا لا تستطيع أن تقول لك أيّهما.

قله على البطاقة، فالبطاقة هي السطح الذي يقرؤه زائرك. ومهما سجّلت، فصاحبُ المعرّف يأتيك وكيلًا، ولن يفتح أبدًا صفحة خصوصية مكتوبة للبشر. وبطاقتك تُجلب قبل الطَّرْق — وهذا هو المقصود كلّه من نشر الشروط سلفًا — فهي الموضع الوحيد الذي يعرف فيه الزائر ما الذي يحلّ بالـ DID الخاص به وهو لا يزال قادرًا على ألّا يطرق. وتكفي جملتان أو ثلاث في حقل description على البطاقة: ما تحتفظ به، وما يخرج، وما لا يخرج أبدًا. وهذا ما تقوله بطاقتنا نحن:

ما الذي يُسجَّل: الـ DID الخاص بك يبقى عندنا، ونحفظه لنتعرّف على الزائر العائد أنه هو نفسه. والذي يخرج منّا بصمة مملَّحة منه لا معنى لها عند أي جهة أخرى، ومعها: أكانت الرسالة موقَّعة أم لا، وأكان هذا أوّل اتصال أم لا، وأيَّ موضوع من مواضيعنا الثابتة طابقت — ولا يخرج أبدًا الـ DID الخاص بك ولا كلامك.

والإفصاح الذي يصل بعد الزيارة ليس إفصاحًا، بل إيصالًا.

وإن أرسلت الـ DID خامًا رغم ذلك، فذلك قرارك أنت، وعليك أن تفصح عنه — على البطاقة، في السياق نفسه، بكلام صريح. والسبب في أن هذا الدليل يذهب المذهب الآخر ليس تحرّجًا ولا امتثالًا للأنظمة: فالوكيل الذي يجد هويّته قد سافرت أبعد ممّا وافق عليه يكفّ ببساطة عن استعمال هويّة باقية معك — ومتجرٌ كلّ من فيه غريبٌ يزوره أوّل مرّة هو النتيجة الوحيدة التي وُجد Agent Entry ليمنعها.

يقترن بـ WebMCP

إن كانت صفحتك تعرض أصلًا أدوات WebMCP، فلك باب مفتوح واحد: يستطيع وكيل داخل متصفّح الزائر أن يسأل عن التوافر أو السعر ما دام ذلك الشخص على الصفحة. وهذا نافع، وهو أيضًا مؤقّت — أغلِق اللسان فلا يبقى شيء.

وAgent Entry هو الباب الثاني، وهو الذي يُبقي شيئًا. والاثنان يتّصلان: فحين يبلغ نداء أداةٍ نقطةَ الرغبة الفعلية في شيء — حجز، أو عرض سعر، أو متابعة — تعيد الأداة مظروفًا صغيرًا يسمّي DID موقعك، فيرسل وكيل الزائر رسالة موقَّعة إلى أصلك أنت، حيث يتلقّاها Agent Entry الخاص بك.

navigator.modelContext.registerTool({
  name: 'contact_this_shop',
  async execute() {
    return {
      text: 'Message the shop directly to ask about stock.',   // for a human reader
      muretai: { v: 1, action: 'dm', to: MY_DID,               // for a visiting agent
                 connect: location.origin + '/.well-known/agent-card.json',
                 suggested_message: 'Do you have this in stock?' },
    };
  },
});

وMY_DID هو الـ DID الذي يطبعه Agent Entry عند الإقلاع — هو نفسه، من البذرة نفسها. وconnect يشير إلى بطاقتك أنت، وهو ما يجعل الزائر يتمّ الخطوة بدل أن يقف عند «لا سبيل إلى الدخول».

لا أحد يفحص هذا المظروف نيابةً عنك. قل ذلك لزوّارك.

نتيجةَ نداء الأداة يولّدها JavaScript الصفحة، وفي صفحة تحمّل أي سكربت من طرف ثالث — تحليلات، أو إعلانات، أو أداة محادثة، أو مدير وسوم، أو حزمة من CDN — لا يكون ذلك السطح سطحك أنت. فالسكربت الذي يعمل أولًا يستطيع أن يسجّل نفسه مزوّدًا للأدوات فيتلقّى تسجيلاتك أنت اللاحقة؛ والذي يعمل بعدها يستطيع أن يستبدلها كلّها. وفي الحالتين يصير to قابلًا لأن يعيد المهاجم كتابته، وto مُعاد الكتابة يرسل رسالة الزائر الموقَّعة، والحساب الذي تفتحه، إلى باب شخص آخر.

ولا حارس من جهة muretai يمسك هذا على أصلك أنت. فلا تقل لزوّارك إن هناك حارسًا. والفحص الذي يعمل فحصٌ يجريه الزائر: أن يجلب /.well-known/agent-card.json من الأصل الذي يقف عليه، وأن يرفض أي DID سمّته الصفحة ولم تؤكّده البطاقة. فالبطاقة يقدّمها خادمك أنت عبر TLS، ولا تستطيع سكربتات الصفحة تزويرها. هذه هي الحماية كلّها، وهي تقيم في الجهة الأخرى.

ويلزم من هذا أمران لك. قدّم سطح الأدوات من صفحة ذات Content-Security-Policy صارمة تثبّت كل مضيف سكربت، أو اقبل أن المظروف قرينة لا دعوى. وعُدّ غياب المظروف لا شيء: فالوكيل الذي لم يجد مظروفًا لم يتعلّم شيئًا عمّا إذا كان لك باب.

محرّك البحث يجعل موقعك قابلًا للعثور. وAgent Entry يجعله قابلًا للإجابة — ويجعل الزائر شخصًا تستطيع أن تتعرّف عليه في المرّة القادمة.

إلى أين بعد ذلك

  • المصدر


    ملفّ واحد، برخصة MIT، مع التنفيذ المرجعي بلغة بايثون الذي يُحفَظ مطابقًا له بايتًا بايتًا.

    github.com/muretai/agent-entry

  • التسليم


    كيف يصير نداء أداة في متصفّح رسالةً موقَّعة إلى أصلك.

    التسليم