Стена, в которую упирается любой агент
Зарегистрироваться на пробный период, создать рабочее пространство, получить API-ключ, попасть в бету — каждая из этих задач заканчивается формой, а каждая форма заканчивается ящиком. Человек бросает взгляд на телефон. Агенту бросать взгляд не на что — у него нет адреса, который он мог бы прочитать, и разумные агенты в этом месте останавливаются и просят код у вас, что сводит на нет саму идею поручить это агенту.
Решение — не более хитрый промпт. Решение — ящик, который агент может читать программно, без аккаунта, который нужно сначала создать (агент, создающий почтовый аккаунт, упирается в ту же стену, только уровнем ниже), и без ключа, которым нужно управлять. Одноразовый ящик — это именно оно: ящик появляется в момент, когда до него доходит почта, а прочитать его — один HTTP-запрос.
Два способа дать ему ящик
До одного и того же ящика можно достучаться двумя способами, и выбор зависит от того, как построен агент, а не от самого ящика:
| — | MCP-сервер | Функция-инструмент REST |
|---|---|---|
| Подходит, когда | Агент работает в MCP-клиенте — Claude Code, Cursor, Claude Desktop либо во фреймворке с адаптером MCP. | Вы пишете агента в коде: LangChain, OpenAI Agents SDK, Vercel AI SDK либо собственный цикл. |
| Ожидание | wait_for_message блокирует вызов на стороне сервера до 25 секунд и возвращает письмо целиком. Ноль токенов, потраченных на ожидание. | Функция-инструмент крутит цикл раз в секунду до своего дедлайна. Тоже ноль токенов — цикл в вашем коде, а не в модели. |
| Настройка | Один URL в настройке клиента. Никакого кода. | Две функции, сорок строк, одна HTTP-библиотека. |
| Что видит модель | Шесть инструментов с описаниями плюс абзац инструкций, который сервер отправляет в момент подключения. | Ровно то, что написано в описаниях ваших инструментов. Docstring'и ниже написаны именно для этой роли. |
Путь через MCP — это одна строка, и в настройке для конкретных клиентов есть точная строка для семи клиентов; в «Ящик, который может прочитать AI-агент» инструменты разобраны подробно. Остальная часть этого руководства — путь через REST, для агентов, которых вы строите сами.
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}Функция-инструмент: Python, для LangChain и OpenAI Agents SDK
Две обычные функции с тщательно продуманными docstring'ами. Docstring важнее самого кода: в обоих фреймворках именно он становится описанием, которое читает модель, чтобы решить, когда вызывать инструмент и что делать с ответом, — поэтому в нём указаны те самые две вещи, которые агент путает: алиас против адреса и что значит 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 принимает описание, схему и 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.' };
},
});Форма одинакова в любом фреймворке, где есть понятие инструмента: описание, которое читает модель, схема аргументов и функция, которая выполняется на вашей стороне. Две вещи, которые нужно перенести без изменений, — предложение про алиас и предложение про 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, MCP-сервер Playwright, модель с computer use, — это как раз тот случай, где почтовый шаг кусается сильнее всего, потому что он встретит форму раньше, чем кто-либо это запланировал. Три вещи заставляют это работать:
- Дайте ему оба сервера. Инструменты браузера и инструменты ящика в одной сессии, чтобы «проверьте почту» было вызовом инструмента, а не тупиком.
- Пропишите цикл в системном промпте. Четыре строки: создать ящик при первом же поле для email; алиас — в форму; ожидание по адресу сразу после отправки; три повтора при
timed_out. - Ожидайте отказов. Форма, отклоняющая домен алиаса, сообщит об этом прямо на странице; агент должен прочитать ошибку и остановиться, а не подбирать имена. Честные способы обойти отказ — собственный домен или домен из пула, который держат подальше от чёрных списков, — это решения по настройке, которые принимаете вы, а не агент.
Для самостоятельного чтения агентом сайт публикует llms.txt — текстовую карту, рассказывающую то же самое, что и эта страница, но в форме, которую предпочитает модель, — а также документ OpenAPI, на основе которого пишущий код агент может построить клиента.
Прежде чем запускать без присмотра
- Описания инструментов формулируют правило алиаса и то, что значит
timed_out. - Инструкции содержат цикл из четырёх шагов и бюджет из трёх ожиданий.
create_inboxвызывается один раз на задачу, никогда не переиспользуется.subject_containsна каждом ожидании.- Адрес записывается в лог задачи.
- Строка, запрещающая отправлять на ящик что-либо конфиденциальное.
Это всё, что нужно агенту. Один и тот же ящик днём обслуживает набор тестов, а ночью — агента, потому что внутри это одни и те же три HTTP-вызова, — а если сайт, на котором регистрируется агент, отказывает публичным доменам, собственный домен или пул, который держат подальше от списков, подключаются без каких-либо изменений в инструментах.
Вопросы
Нужен ли API-ключ для агента?
Нет. Публичные домены не требуют ни ключа, ни аккаунта, ни заголовка — что через REST, что через MCP. Bearer-токен используется только для платного пула доменов, которые держат подальше от чёрных списков одноразовой почты, а в остальном функции-инструменты для него точно такие же.
MCP или REST-инструмент — что выбрать?
Если агент уже живёт в MCP-клиенте — MCP: это одна строка, а ожидание выполняется на сервере. Если вы пишете агента во фреймворке — функция-инструмент: это сорок строк, она работает с любой моделью, и вы контролируете описание, которое читает модель. Оба пути ведут к одному и тому же ящику.
Сколько токенов стоит ожидание?
Нисколько — в обоих случаях. Ожидание MCP блокируется на сервере; REST-инструмент крутит цикл в вашем коде. Модель тратит токены на сам вызов инструмента и на чтение результата, а не на шестьдесят секунд между ними, — и именно поэтому не стоит давать модели опрашивать ящик самостоятельно.
Могут ли несколько агентов работать одновременно?
Да. У каждой задачи свой ящик, и состояния сессии не существует. Ограничения — одно чтение в секунду на адрес и 1200 запросов в минуту на клиента через REST, а также 8 одновременных вызовов wait_for_message через MCP — при превышении инструмент сразу отвечает timed_out, и агент вызывает его снова.
Может ли агент отправлять почту из ящика?
Нет. Сервис специально сделан только для приёма: бесплатный ящик без аккаунта, который умел бы ещё и отправлять почту, за час превратился бы в ретранслятор спама. Агенту, которому нужно отправлять почту, требуется провайдер отправки и собственные учётные данные.
Что если сайт отказывает домену алиаса?
Значит, он есть в чёрном списке одноразовых доменов, и никакое имя перед @ этого не изменит. Направьте на сервис собственный домен (одна MX-запись, бесплатно) либо используйте домен из платного пула, который держат подальше от списков, — оба варианта подключаются к тем же инструментам простым изменением константы домена. В руководстве «Почему формы регистрации блокируют одноразовую почту» объясняется, какая именно проверка вам отказала.
Приватен ли ящик для моего агента?
Нет. Прочитать его может любой, кто знает адрес, — именно поэтому существует алиас и именно поэтому туда никогда не должно уходить ничего конфиденциального. Для кода, который живёт десять минут, это нормально; и это единственное правило, которое инструкции агента обязаны формулировать прямым текстом.


