الجدار الذي يصطدم به كل وكيل
التسجيل في تجربة، أو إنشاء مساحة عمل، أو الحصول على مفتاح API، أو الانضمام إلى نسخة تجريبية: كل واحدة من هذه المهام تنتهي عند نموذج، وكل نموذج ينتهي عند صندوق بريد. الإنسان يلقي نظرة على هاتفه. أما الوكيل فليس لديه ما ينظر إليه — فلا عنوان لديه يستطيع قراءته، والوكلاء العقلاء يتوقفون ويطلبون منك الرمز، وهذا يُبطل الغرض من إرسال وكيل أصلاً.
والحل ليس أمرًا (prompt) أذكى. بل صندوق وارد يستطيع الوكيل قراءته برمجيًا، بلا حساب يجب إنشاؤه أولاً (فالوكيل الذي ينشئ حساب بريد يصطدم بالجدار نفسه في مستوى أدنى) وبلا مفتاح يجب إدارته. وصندوق البريد المؤقت هو ذلك بالضبط: يوجد صندوق البريد لحظة وصول البريد إليه، وقراءته طلب HTTP واحد.
طريقتان لإعطائه صندوق وارد
يمكن الوصول إلى صندوق البريد نفسه بطريقتين، ويعتمد الاختيار على كيفية بناء الوكيل لا على صندوق البريد:
| الخاصية | خادم MCP | دالة أداة REST |
|---|---|---|
| يناسب حين | يعمل الوكيل داخل عميل MCP — Claude Code، أو Cursor، أو Claude Desktop، أو إطار عمل فيه مهايئ MCP. | تكتب الوكيل بنفسك في الكود: LangChain، أو OpenAI Agents SDK، أو Vercel AI SDK، أو حلقتك الخاصة. |
| الانتظار | wait_for_message تنتظر من جهة الخادم حتى 25 ثانية وتعيد الرسالة كاملة. ولا تُستهلَك أي tokens أثناء الانتظار. | تكرر دالة الأداة التحقق مرة كل ثانية حتى موعدها النهائي. ولا تُستهلَك tokens هنا أيضًا — فالحلقة في كودك أنت، لا في النموذج. |
| الإعداد | رابط واحد في إعداد العميل. بلا كود. | دالتان، وأربعون سطرًا، ومكتبة HTTP واحدة. |
| ما الذي يراه النموذج | ست أدوات بأوصافها، إضافة إلى فقرة تعليمات يرسلها الخادم وقت الاتصال. | أيًا كان ما تقوله أوصاف أدواتك. وقد كُتبت الـdocstrings أدناه لتكون هي تلك الأوصاف. |
مسار MCP سطر واحد، ويحتوي دليل الإعداد الخاص بكل عميل على السطر الدقيق لسبعة عملاء؛ ويشرح دليل صندوق وارد يستطيع وكيل ذكاء اصطناعي قراءته الأدوات بتعمق. وبقية هذا الدليل هي مسار REST، للوكلاء الذين تبنيهم بنفسك.
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}دالة أداة: بلغة Python، لـLangChain وOpenAI Agents SDK
دالتان بسيطتان بـdocstrings دقيقة الصياغة. وهذه الـdocstrings أهم من الكود نفسه: ففي كلا الإطارين تصبح هي الوصف الذي يقرؤه النموذج ليقرر متى يستدعي الأداة وماذا يفعل بالإجابة، لذا فهي تقول الأمرين اللذين يخطئ فيهما الوكيل — المستعار مقابل العنوان، ومعنى timed_out.
"""inbox_tools.py — two plain functions any agent framework can wrap. No key, no account."""
import secrets
import time
import requests
API = "https://grabmail.io/api/v1"
def create_inbox() -> dict:
"""Create a fresh disposable email inbox for this task.
Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias
into forms; never hand out the address. Nothing is created server-side.
"""
address = f"agent-{secrets.token_hex(4)}@grabmail.io"
r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
r.raise_for_status()
return {
"address": address,
"alias": r.json().get("alias"),
"next_step": "Put the alias into the form. Then call wait_for_message with the address.",
}
def wait_for_message(address: str, subject_contains: str = "", timeout_seconds: int = 60) -> dict:
"""Wait for an email to arrive at the address, up to timeout_seconds.
Returns the message (from, subject, text, html) or {"status": "timed_out"}.
On timed_out, call again — up to three times — before concluding no mail was sent.
"""
deadline = time.monotonic() + timeout_seconds
while time.monotonic() < deadline:
r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
if r.status_code == 429: # slow down, do not fail
time.sleep(float(r.headers.get("Retry-After", 1)))
continue
r.raise_for_status()
for m in r.json()["messages"]:
if subject_contains.lower() in m["subject"].lower():
full = requests.get(f"{API}/message/{m['id']}", params={"mailbox": address}, timeout=15)
full.raise_for_status()
return full.json()
time.sleep(1) # one read a second, never throttled
return {"status": "timed_out", "hint": "Call again, up to three times, before giving up."}وتغليفهما استدعاء واحد لكل إطار عمل. فدالة tool في LangChain تقرأ الـdocstring وتلميحات النوع؛ وتفعل function_tool في Agents SDK الأمر نفسه وتضيف الأدوات إلى وكيل تكرر تعليماته الحلقة:
# LangChain: the docstring becomes the tool description the model reads.
from langchain_core.tools import tool
create_inbox_tool = tool(create_inbox)
wait_for_message_tool = tool(wait_for_message)
# agent = create_react_agent(model, tools=[create_inbox_tool, wait_for_message_tool, ...])# OpenAI Agents SDK: same two functions, same docstrings.
from agents import Agent, Runner, function_tool
signup_agent = Agent(
name="Signup agent",
instructions=(
"When a site needs an email address, call create_inbox once. Put the ALIAS in the form. "
"Right after submitting, call wait_for_message with the ADDRESS and a word from the expected "
"subject. If it returns timed_out, call it again, up to three times."
),
tools=[function_tool(create_inbox), function_tool(wait_for_message)],
)
result = Runner.run_sync(signup_agent, "Sign up for a trial at https://app.example.com/signup and report the login.")
print(result.final_output)الأداة نفسها بلغة TypeScript، لـVercel AI SDK
تأخذ tool() في AI SDK وصفًا، وschema، ودالة execute؛ ويحمل الوصف الجملتين نفسهما. مرِّر كلتا الأداتين إلى generateText أو streamText مع maxSteps أعلى من أربعة، لأن طول الحلقة أربعة استدعاءات أدوات:
// inbox-tools.ts — the same two tools for the Vercel AI SDK (v5 shape: inputSchema + execute).
import { tool } from 'ai';
import { z } from 'zod';
const API = 'https://grabmail.io/api/v1';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));
export const createInbox = tool({
description: 'Create a fresh disposable email inbox for this task. Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias into forms; never hand out the address.',
inputSchema: z.object({}),
execute: async () => {
const address = `agent-${crypto.randomUUID().slice(0, 8)}@grabmail.io`;
const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
const { alias } = (await res.json()) as { alias: string | null };
return { address, alias, next_step: 'Put the alias into the form. Then call waitForMessage with the address.' };
},
});
export const waitForMessage = tool({
description: 'Wait for an email to arrive at the address, up to timeoutSeconds. Returns the message (from, subject, text, html) or { status: "timed_out" }. On timed_out, call again — up to three times — before concluding no mail was sent.',
inputSchema: z.object({
address: z.string(),
subjectContains: z.string().optional(),
timeoutSeconds: z.number().int().min(5).max(120).default(60),
}),
execute: async ({ address, subjectContains = '', timeoutSeconds }) => {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
if (res.status === 429) { await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000); continue; }
if (!res.ok) throw new Error(`GET /mailbox answered ${res.status}`);
const { messages } = (await res.json()) as { messages: { id: string; subject: string }[] };
const hit = messages.find(m => m.subject.toLowerCase().includes(subjectContains.toLowerCase()));
if (hit) return (await fetch(`${API}/message/${hit.id}?mailbox=${encodeURIComponent(address)}`)).json();
await sleep(1000);
}
return { status: 'timed_out', hint: 'Call again, up to three times, before giving up.' };
},
});والشكل مطابق في أي إطار عمل لديه مفهوم للأداة: وصف يقرؤه النموذج، وschema للوسائط، ودالة تعمل من جهتك أنت. والأمران اللذان يجب نقلهما هما جملة المستعار وجملة timed_out؛ وكل ما عداهما هو العميل من دليل Node أو دليل Python.
الحلقة، في أربع خطوات
أيًا كان إطار العمل، فالتسجيل هو استدعاءات الأدوات الأربعة نفسها، وينبغي إخبار الوكيل بذلك في تعليماته بدل تركه يكتشفها بنفسه:
create_inbox، مرة واحدة لكل مهمة. فيعود عنوان، ومستعار، وجملة توضح أيهما أيهما.- المستعار يذهب إلى النموذج. أرسِله.
wait_for_messageعلى العنوان، فورًا، مع ضبطsubject_containsعلى كلمة ستحملها رسالة التأكيد — «code» أو «verify» أو «confirm». لا بعد مؤقِّت، ولا بعد أعمال أخرى: فالبريد في طريقه بالفعل.- يخرج الرمز من الرسالة التي أعادها الانتظار؛ فيكتبه الوكيل أو يتبع الرابط. وعادةً ستة أرقام بعد الكلمات التي يستخدمها القالب — ويحتوي دليل رموز OTP في الاختبارات الآلية على قواعد الاستخراج، وهي تنطبق على الوكيل تمامًا كما تنطبق على اختبار.
انتظار ستين ثانية ينتهي بـtimed_out ليس فشلاً؛ إنه «ليس بعد». وينبغي أن تقول التعليمات: استدعِ مرة أخرى، حتى ثلاث مرات. وثلاثة استدعاءات تعني أكثر من ثلاث دقائق، وهذا يغطي أي بريد معاملاتي أُرسل فعلاً — ويمنح الوكيل ثلاث فرص لملاحظة أن النموذج أظهر خطأً، أو أنه كتب المستعار خطأً.
قاعدة المستعار
لكل صندوق بريد عنوانان. العنوان هو الذي يقرأ به الوكيل؛ ويستطيع أي شخص يملكه فتح صندوق البريد، لأنه لا يوجد حساب والعنوان هو المفتاح الوحيد. أما المستعار فهو عنوان ثانٍ على نطاق منفصل يسلِّم إلى الصندوق نفسه ولا يمكن استخدامه لقراءته.
لذا يحصل الموقع على المستعار ويحتفظ الوكيل بالعنوان. والوكيل الذي يلصق العنوان في نموذج يكون قد سلَّم الموقع — وأي جهة يسرّبه إليها الموقع — القدرة على قراءة كل رسالة سيتلقاها الوكيل هناك على الإطلاق. وتعيد دالتا الأداة أعلاه كليهما مع حقل next_step يوضح أيهما يذهب إلى أين، وتكرر التعليمات ذلك، لأن القاعدة المذكورة مرتين قاعدة تُتَّبع.
ضوابط أمان لوكيل يعمل دون إشراف
مجموعة الاختبارات تفشل وتتوقف. أما الوكيل الذي يسيء قراءة موقف ما فيواصل العمل، ويواصل الإنفاق. وخمسة حدود تمنع خطوة البريد الإلكتروني من أن تصبح الجزء الأغلى من المهمة:
- صندوق وارد واحد لكل مهمة
- لا تُعِد استخدام عنوان أبدًا عبر مهام أو تشغيلات مختلفة. فرسالة قديمة تحمل رمزًا يبدو معقولاً هي أسرع طريقة لوكيل ليفعل الشيء الخطأ بثقة تامة.
create_inboxلا تكلِّف شيئًا؛ استدعِها في كل مرة. - ميزانية لعمليات الانتظار
- ثلاثة استدعاءات لـ
wait_for_message، ثم توقف وأبلِغ. فالوكيل الذي ينتظر إلى ما لا نهاية بريدًا لم يُرسَل أبدًا يستهلك فتحة عامل وفاتورة. - موعد نهائي للخطوة كلها
- خمس دقائق من الإرسال إلى الرمز، من البداية إلى النهاية. وبعد تجاوزها، فالتصرف الصحيح هو إخبار إنسان بما حدث، لا إعادة محاولة النموذج مرة أخرى.
- ترشيح الموضوع
- مرِّر
subject_containsدائمًا. وإلا فإن رسالة ترحيب تصل قبل رسالة الرمز تصبح هي «الرسالة»، ويستخرج الوكيل ستة أرقام من تذييل تسويقي. - تسجيل العنوان في السجلّ
- اكتب العنوان في سجلّ المهمة. وتبقى الرسائل 5 يومًا، بحيث يستطيع إنسان فتح صندوق البريد لاحقًا ورؤية ما رآه الوكيل بالضبط — وهذا أفيد شيء على الإطلاق حين تسوء تشغيلة ما.
وكلاء المتصفح
الوكيل الذي يقود متصفحًا حقيقيًا — Browser Use، أو خادم Playwright MCP، أو نموذج computer-use — هو الحالة التي تؤلم فيها خطوة البريد الإلكتروني أكثر ما يكون، لأنه سيواجه النموذج قبل أن يخطط أحد لذلك. وثلاثة أمور تجعله يعمل:
- أعطه الخادمين كليهما. أدوات المتصفح وأدوات صندوق الوارد في الجلسة نفسها، بحيث تصبح «تحقق من بريدك» استدعاء أداة لا طريقًا مسدودًا.
- ضع الحلقة في أمر النظام (system prompt). أربعة أسطر: أنشئ صندوق واردٍ عند أول حقل بريد إلكتروني؛ وضع المستعار في النموذج؛ وانتظر على العنوان فور الإرسال؛ وثلاث محاولات إعادة عند
timed_out. - توقَّع الرفض. فالنموذج الذي يرفض نطاق المستعار سيقول ذلك في الصفحة؛ وينبغي أن يقرأ الوكيل الخطأ ويتوقف، لا أن يجرّب أسماء أخرى. والطرق الصادقة للالتفاف على الرفض — نطاق تملكه أنت، أو نطاق من المجموعة التي تُبقى بعيدة عن قوائم الحظر — قرارات إعداد تخصك أنت، لا الوكيل.
ولقراءة الوكيل نفسه، ينشر الموقع llms.txt، وهو خريطة نصية عادية تقول الأشياء نفسها التي تقولها هذه الصفحة بالصيغة التي يفضلها النموذج، ووثيقة OpenAPI يستطيع وكيل يكتب الكود بناء العميل منها.
قبل أن تدعه يعمل دون إشراف
- أوصاف أدوات تذكر قاعدة المستعار ومعنى
timed_out. - تعليمات فيها حلقة الخطوات الأربع وميزانية ثلاث محاولات انتظار.
- استدعاء
create_inboxمرة واحدة لكل مهمة، دون إعادة استخدام أبدًا. subject_containsفي كل عملية انتظار.- العنوان مكتوب في سجلّ المهمة.
- سطر يمنع إرسال أي شيء سرّي إلى صندوق الوارد.
هذا كل ما يحتاجه الوكيل. ويخدم صندوق البريد نفسه مجموعة اختبارات نهارًا ووكيلاً ليلاً، لأنه من الداخل استدعاءات HTTP الثلاثة نفسها — وإذا رفض الموقع الذي يسجِّل فيه الوكيل النطاقات العامة، فإن نطاقًا تملكه أنت أو المجموعة التي تُبقى بعيدة عن القوائم يُدرَج دون أي تغيير في الأدوات.
أسئلة
هل أحتاج إلى مفتاح API للوكيل؟
لا. فالنطاقات العامة لا تتطلب مفتاحًا ولا حسابًا ولا ترويسة، عبر REST وMCP على حد سواء. ولا تستخدم رمز حامل (bearer token) إلا مجموعة النطاقات المدفوعة التي تُبقى بعيدة عن قوائم حظر البريد المؤقت، ودوال الأداة مطابقة لها فيما عدا ذلك.
MCP أم أداة REST — أيهما أختار؟
إذا كان الوكيل يعمل أصلاً داخل عميل MCP، فاختر MCP: فهو سطر واحد والانتظار من جهة الخادم. وإذا كنت تكتب الوكيل في إطار عمل، فاختر دالة أداة: فهي أربعون سطرًا، وتعمل مع أي نموذج، وتتحكم أنت في الوصف الذي يقرؤه النموذج. وكلاهما يصل إلى صندوق البريد نفسه.
كم تكلِّف عملية الانتظار من tokens؟
لا شيء، في كلتا الحالتين. فانتظار MCP يحدث من جهة الخادم؛ وأداة REST تكرر التحقق في كودك أنت. وينفق النموذج tokens على استدعاء الأداة وعلى قراءة النتيجة، لا على الستين ثانية بينهما — وهذا هو السبب الكامل وراء عدم ترك النموذج يستطلع صندوق بريد بنفسه.
هل يمكن لعدة وكلاء العمل في الوقت نفسه؟
نعم. فلكل مهمة صندوق واردها الخاص ولا توجد حالة جلسة. والحدود هي قراءة واحدة في الثانية لكل عنوان و1200 طلب في الدقيقة لكل عميل عبر REST، و8 من استدعاءات wait_for_message المتزامنة عبر MCP — وبعد تجاوز ذلك تجيب الأداة فورًا بـtimed_out ويستدعيها الوكيل مرة أخرى.
هل يستطيع الوكيل إرسال بريد من صندوق الوارد؟
لا. فالخدمة تستقبل فقط، بحكم التصميم — فأي صندوق وارد مجاني بلا حساب يستطيع الإرسال سيتحول إلى مُرحِّل بريد مزعج خلال ساعة واحدة. والوكيل الذي عليه إرسال بريد يحتاج إلى مزوّد إرسال وبيانات اعتماد خاصة به.
ماذا لو رفض الموقع نطاق المستعار؟
فهذا يعني أنه مدرج في قائمة حظر نطاقات مؤقتة، ولن يغيّر أي اسم أمام الـ@ ذلك. وجِّه نطاقًا تملكه إلى الخدمة (سجل MX واحد، مجانًا)، أو استخدم نطاقًا من المجموعة المدفوعة التي تُبقى بعيدة عن القوائم — وكلاهما يُدرَج في الأدوات نفسها بتغيير ثابت النطاق فقط. ويشرح دليل لماذا تحظر نماذج التسجيل البريد المؤقت أي فحص هو الذي رفضك.
هل صندوق الوارد خاص بوكيلي؟
لا. فأي شخص يعرف العنوان يستطيع قراءته، ولهذا يوجد المستعار، ولهذا لا ينبغي إرسال أي شيء سرّي إلى هناك أبدًا. وهذا أمر مقبول لرمز يعيش عشر دقائق؛ وهي القاعدة الوحيدة التي يجب أن تنص عليها تعليمات الوكيل بوضوح.


