Зачем начинать с контекста акторов, а не с правил?
Большинство команд, заметивших AI-агентов в своем трафике, сначала тянется к правилу. Заблокировать user agent, добавить challenge, ужесточить rate limit. Проблема в том, что правило, написанное до понимания трафика, обычно задевает не тех. Легитимные помощники блокируются, настоящим пользователям показывают challenge, а автоматизация, которой вы и правда опасались, продолжает ходить, потому что с самого начала никак себя не обозначила.
Контекст актора - это шаг перед правилами. Для каждого запроса вы записываете, что ваше приложение может наблюдать о том, кто или что действует: интерактивный человек, человек, работающий с AI-ассистентом, автономный агент или что-то, что пока нельзя уверенно различить. Вы фиксируете основание этого вывода и храните его рядом с результатом запроса. Через неделю или две у вас появляется то, что не даст ни один шаблон правила: карта собственного трафика. Если вы еще решаете, вообще ли агенты доходят до вашего сайта, начните с как определить, что трафик сайта идет от AI-агентов.
Это руководство строит такой слой в приложении Node.js с помощью обычного middleware. Оно работает с Express, и те же идеи переносятся на Fastify, Koa, Hono или обработчик маршрута Next.js.
Что вы построите
К концу у вас будет:
- middleware, которое извлекает доказательства актора из каждого запроса.
- Небольшая, явная классификация с указанным основанием для каждого результата.
- Структурированные журналы наблюдения, которые никогда не меняют способ обработки запроса.
- Способ прикрепить результат к запросу, чтобы последующие обработчики могли его прочитать.
Ничто здесь не блокирует запрос. Это сделано намеренно.
Шаг 1: Какие доказательства ваш сервер действительно может видеть?
Сервер видит меньше, чем обычно предполагают. Прежде чем писать код, перечислите сигналы, доступные на вашем краю:
- Подписи запроса. Некоторые автоматизированные клиенты подписывают запросы с помощью HTTP Message Signatures (RFC 9421), отправляя заголовки
Signature-InputиSignature, часто вместе с заголовкомSignature-Agent, который указывает на опубликованные ключи оператора, как описано в черновике протокола IETF Web Bot Auth. - Заявленная идентичность. Клиенты могут называть себя в заголовке
User-Agent, в заголовкеFromили через метаданные протокола, например имя клиента MCP. Клиенты MCP сообщают выбранное ими самим имяclientInfoв запросе initialize редакции 2025-11-25, а редакция 2026-07-28 передает идентичность клиента как метаданные на каждый запрос. Это заявления, и скопировать их может кто угодно. - Контекст аутентификации. Есть ли у запроса cookie сессии, bearer token или API key, и что это за тип учетных данных.
- Форма протокола. Заголовки Accept, запрашивает ли клиент HTML или JSON, загружает ли он ресурсы страницы и как обрабатывает redirects и cookies.
- Заявления приложения. Ваш собственный продукт может знать то, чего не видит сеть, например что пользователь выбрал вариант "AI assisted" или что запрос пришел из встроенной функции ассистента.
У каждого сигнала разная сила. Проверяемую подпись можно валидировать. Строка user agent - это утверждение. Отсутствующий cookie - это просто отсутствие. Разделять эти вещи - и есть смысл всей работы.
Шаг 2: Явно смоделируйте результат
Определите форму результата актора до того, как писать логику определения. Небольшая типизированная модель помогает всем честно понимать, что означает классификация. Четыре класса, используемые здесь, описаны в Human, AI-assisted or agent: a practical taxonomy for application traffic.
export type ActorClass = "human" | "human_ai" | "agent" | "unknown";
export type EvidenceBasis = "verified" | "declared" | "inferred" | "unknown";
export interface ActorEvidence {
signaturePresent: boolean;
signatureAgent?: string;
declaredClient?: string;
credentialType: "session" | "api_key" | "bearer" | "none";
acceptsHtml: boolean;
appDeclaredAssist: boolean;
}
export interface ActorResult {
actor: ActorClass;
basis: EvidenceBasis;
reasons: string[];
evidence: ActorEvidence;
observedAt: string;
}Наиболее важны два поля. actor - это класс. basis говорит, почему вы так считаете. Результат агента, основанный на проверяемой подписи, и результат агента, основанный на строке user agent, оба относятся к "agent", но в дальнейшем им нужен совершенно разный подход, поэтому их нельзя смешивать.
Шаг 3: Извлекайте доказательства в middleware
Этап извлечения читает заголовки и состояние запроса. Он еще ничего не решает.
import type { Request, Response, NextFunction } from "express";
const KNOWN_AGENT_TOKENS = ["bot", "crawler", "spider", "agent", "gpt", "claude"];
export function extractEvidence(req: Request): ActorEvidence {
const ua = (req.get("user-agent") || "").toLowerCase();
const auth = req.get("authorization") || "";
const hasSession = Boolean(req.cookies?.session);
let credentialType: ActorEvidence["credentialType"] = "none";
if (hasSession) credentialType = "session";
else if (auth.toLowerCase().startsWith("bearer ")) credentialType = "bearer";
else if (req.get("x-api-key")) credentialType = "api_key";
const declaredToken = KNOWN_AGENT_TOKENS.find((t) => ua.includes(t));
return {
signaturePresent: Boolean(req.get("signature") && req.get("signature-input")),
signatureAgent: req.get("signature-agent") || undefined,
declaredClient: declaredToken ? ua.slice(0, 200) : undefined,
credentialType,
acceptsHtml: (req.get("accept") || "").includes("text/html"),
appDeclaredAssist: req.get("x-app-assist") === "1",
};
}Несколько замечаний по этому коду:
signaturePresentтолько фиксирует наличие заголовков подписи. Валидация - отдельный шаг, он описан в руководстве по проверке подписанных запросов агентов.- Список токенов user agent намеренно прост. Он создает заявление, но никогда не дает валидации.
x-app-assistздесь обозначает любой механизм, который ваш frontend отправляет, когда пользователь работает через встроенную AI-функцию. Замените его на ваш реальный механизм.
Шаг 4: Классифицируйте с указанным основанием
Теперь превратите доказательства в результат. Держите правила короткими, располагайте их от самого сильного доказательства к самому слабому и всегда завершайте ветку на Unknown.
export async function classify(
evidence: ActorEvidence,
verifySignature: () => Promise<boolean>,
): Promise<ActorResult> {
const reasons: string[] = [];
const observedAt = new Date().toISOString();
if (evidence.signaturePresent && (await verifySignature())) {
reasons.push("request signature validated");
return { actor: "agent", basis: "verified", reasons, evidence, observedAt };
}
if (evidence.appDeclaredAssist) {
reasons.push("application reported its built-in assistant");
return { actor: "human_ai", basis: "declared", reasons, evidence, observedAt };
}
if (evidence.declaredClient) {
reasons.push("client declared itself as automated");
return { actor: "agent", basis: "declared", reasons, evidence, observedAt };
}
if (evidence.credentialType === "session" && evidence.acceptsHtml) {
reasons.push("interactive browser session");
return { actor: "human", basis: "inferred", reasons, evidence, observedAt };
}
reasons.push("insufficient evidence");
return { actor: "unknown", basis: "unknown", reasons, evidence, observedAt };
}Обратите внимание, чего здесь нет. Здесь никогда не происходит повышения заявления до валидации. Здесь не присваивается Human запросу, в котором нет интерактивного сигнала. Когда определить нельзя, код говорит Unknown. Unknown - это корректный и полезный ответ, и он точно показывает, где заканчивается ваша видимость. Полный разбор мы приводим в почему Unknown - это корректный результат для AI-трафика.
Шаг 5: Подключите это в режиме наблюдения
Подключите middleware как можно раньше, прикрепите результат к запросу и запишите его в лог. Не ветвитесь по нему.
import express from "express";
import cookieParser from "cookie-parser";
const app = express();
app.use(cookieParser());
app.use(async (req, res, next) => {
try {
const evidence = extractEvidence(req);
const result = await classify(evidence, async () => false);
res.locals.actor = result;
res.on("finish", () => {
logActor({
route: req.route?.path ?? req.path,
method: req.method,
status: res.statusCode,
actor: result.actor,
basis: result.basis,
reasons: result.reasons,
});
});
} catch {
res.locals.actor = undefined;
}
next();
});Три детали делают это безопасным:
- Блок
tryгарантирует, что сбой в определении никогда не повлияет на ответ. Наблюдение должно fail open. - Логирование происходит на
finish, поэтому вы фиксируете реальный статус-код и позже можете сравнить классификации с результатами. verifySignatureпо умолчанию заглушен какfalse, пока вы не реализуете настоящую проверку. Это удерживает подписанные запросы в корзинах declared или unknown, а не завышает их статус.
Шаг 6: Логируйте для анализа, а не только для отладки
Пишите логи, которые можно агрегировать. Достаточно одной структурированной строки на запрос.
function logActor(entry: Record<string, unknown>) {
process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}Не включайте в эти логи персональные данные. Вам нужен маршрут, класс, основание, причины и результат. Для понимания структуры трафика не нужны полные IP-адреса, сырые cookies или тела запросов. Если вы все же храните какой-либо идентификатор для сопоставления, хэшируйте его с rotating salt и задайте короткий срок хранения.
Шаг 7: Изучите данные до того, как действовать
Запустите режим наблюдения достаточно долго, чтобы увидеть обычные недельные паттерны. Затем ответьте на несколько вопросов:
- Какие маршруты привлекают агентов? Обычно это документация, каталог и поисковые endpoint. Обычно не должны быть такими account и checkout flows.
- Какова доля результатов agent, которые подтверждены, а не только заявлены? Это соотношение показывает, насколько можно безопасно полагаться на идентичность при написании политик.
- Где сосредоточен Unknown? Высокая доля Unknown на маршруте часто означает, что там не хватает сигнала, например на странице, которая никогда не запускает код вашего frontend.
- Отличаются ли результаты по классам? Сравните уровни ошибок, этапы конверсии или отчеты о злоупотреблениях между классами. Именно отсюда обычно возникают реальные идеи для политики.
Ответы часто удивляют. Команды регулярно обнаруживают, что большая часть их "bot problem" на самом деле - это легитимная автоматизация, которая с радостью использовала бы структурированный endpoint, если бы он существовал.
Шаг 8: Сделайте результат доступным остальной части приложения
Когда данные начинают поступать, дайте обработчикам читать res.locals.actor для нефорсирующих сценариев: аналитические теги, выбор формата ответа или запись того, что обращение в службу поддержки пришло из AI-assisted сессии. Такие сценарии улучшают опыт, не создавая риска блокировки.
app.get("/docs/:page", (req, res) => {
const actor = res.locals.actor as ActorResult | undefined;
if (actor?.actor === "agent" && req.accepts("text/markdown")) {
return res.type("text/markdown").send(renderMarkdown(req.params.page));
}
return res.render("doc", { page: req.params.page });
});Принуждение, например challenge, rate limits или step-up checks, приходит позже, после того как вы измерили false positives. Процесс внедрения actor-aware policy описан в отдельном руководстве.
Каких ошибок следует избегать?
- Считать user agent идентичностью. Это самый простой заголовок для изменения. Записывайте его как заявление. Даже операторы AI так говорят: в рекомендациях Anthropic для его hosted browser agent указано, что любой может отправить тот же заголовок User-Agent, и рекомендуется проверять опубликованные IP-адреса, прежде чем считать трафик подлинным.
- По умолчанию ставить Human. Если код назначает Human всякий раз, когда не находит сигналов бота, ваши числа Human будут завышены, а политики начнут доверять не тому трафику.
- Смешивать класс и основание. Всегда храните оба значения. Панель, где показан только "Agent", скрывает разницу между подтвержденным оператором и скопированной строкой.
- Блокировать во время наблюдения. Даже временная блокировка искажает измерения, потому что заблокированные клиенты меняют свое поведение.
Как сюда вписывается Proof of Actor
Приведенное выше middleware - хорошая отправная точка, и оно показывает основную модель: сначала доказательства, для каждого результата - явное основание, Unknown остается видимым, а решения остаются за вашим приложением. Proof of Actor строится на той же модели, но с более глубоким покрытием сигналов на уровнях server, edge, browser и MCP, а также с аналитикой, которая позволяет Detect, Understand и Route уверенно.
Если вам нужна помощь, чтобы пойти дальше в production-приложении, подайте заявку на ранний доступ. Чтобы увидеть полный поток принятия решения, прочитайте How Proof of Actor works.
Источники
- RFC 9421: HTTP Message Signatures: стандарт, на котором основаны подписанные запросы
- draft-ietf-webbotauth-httpsig-protocol: черновик IETF Web Bot Auth, определяющий заголовок
Signature-Agent(в работе по состоянию на ноябрь 2026) - RFC 9110: HTTP Semantics, User-Agent and From: самостоятельно сообщаемые заголовки идентификации клиента
- MCP specification 2025-11-25: Lifecycle: запрос
initializeиclientInfo - MCP specification 2026-07-28: Versioning and Compatibility: метаданные версии, идентичности и возможностей на каждый запрос
- Anthropic: how Anthropic-hosted browsers identify themselves: токен user agent, опубликованные IP-адреса и предупреждение о том, что заголовок можно скопировать