Pourquoi commencer par le contexte d'acteur plutôt que par des règles ?
La plupart des équipes qui remarquent des agents IA dans leur trafic cherchent d'abord une règle. Bloquer un user agent, ajouter un challenge, durcir une limite de débit. Le problème, c'est qu'une règle écrite avant de comprendre le trafic touche souvent les mauvaises personnes. Des assistants légitimes sont bloqués, de vrais utilisateurs sont challengés, et l'automatisation qui vous inquiétait vraiment continue de passer parce qu'elle ne s'est jamais identifiée au départ.
Le contexte d'acteur est l'étape avant les règles. Pour chaque requête, vous enregistrez ce que votre application peut observer sur qui ou quoi agit: une personne en interaction, une personne qui travaille avec un assistant IA, un agent autonome, ou quelque chose que vous ne pouvez pas encore distinguer. Vous enregistrez la preuve derrière cette vue, et vous la gardez à côté du résultat de la requête. Après une ou deux semaines de ce suivi, vous disposez de quelque chose qu'aucun modèle de règle ne peut fournir: une carte de votre propre trafic. Si vous hésitez encore à savoir si des agents atteignent votre site, commencez par comment savoir si le trafic d'un site web provient d'agents IA.
Ce guide construit cette couche dans une application Node.js à l'aide d'un middleware simple. Il fonctionne avec Express, et les mêmes idées s'appliquent à Fastify, Koa, Hono ou un gestionnaire de route Next.js.
Ce que vous allez construire
À la fin, vous aurez:
- Un middleware qui extrait les preuves d'acteur de chaque requête.
- Une classification petite et explicite avec une base indiquée pour chaque résultat.
- Des journaux d'observation structurés qui ne modifient jamais la manière dont la requête est traitée.
- Un moyen d'attacher le résultat à la requête pour que les gestionnaires ultérieurs puissent le lire.
Rien ici ne bloque une requête. C'est volontaire.
Étape 1: Quelles preuves votre serveur peut-il réellement voir ?
Un serveur voit moins que ce que beaucoup imaginent. Avant d'écrire du code, dressez la liste des signaux disponibles à votre bordure:
- Signatures de requête. Certains clients automatisés signent les requêtes avec HTTP Message Signatures (RFC 9421), en envoyant les en-têtes
Signature-InputetSignature, souvent en même temps qu'un en-têteSignature-Agentqui pointe vers les clés publiées par l'opérateur, comme le décrit l'ébauche de protocole IETF Web Bot Auth. - Identité déclarée. Les clients peuvent se nommer dans l'en-tête
User-Agent, dans un en-têteFrom, ou via des métadonnées de protocole telles qu'un nom de client MCP. Les clients MCP indiquent un nomclientInfochoisi par eux-mêmes dans la requête initialize de la révision 2025-11-25, et la révision 2026-07-28 transporte l'identité du client comme métadonnées par requête. Ce sont des déclarations, et n'importe qui peut les copier. - Contexte d'authentification. Si la requête transporte un cookie de session, un bearer token ou une clé API, et de quel type d'identifiant il s'agit.
- Forme du protocole. Les en-têtes Accept, le fait que le client demande du HTML ou du JSON, s'il charge les ressources de la page, et la manière dont il gère les redirections et les cookies.
- Déclarations de l'application. Votre propre produit peut savoir des choses que le réseau ne peut pas voir, comme un utilisateur qui choisit une option "assisté par IA", ou une requête provenant de votre fonctionnalité d'assistant intégrée.
Chaque signal a une force différente. Une signature validée est vérifiable. Une chaîne user agent est une déclaration. Un cookie manquant est simplement absent. Garder tout cela séparé est tout l'objet de l'exercice.
Étape 2: Modélisez explicitement le résultat
Définissez la forme d'un résultat d'acteur avant d'écrire la logique de détection. Un petit modèle typé oblige tout le monde à rester honnête sur ce que signifie une classification. Les quatre classes utilisées ici sont expliquées dans Humain, assisté par IA ou agent: une taxonomie pratique pour le trafic applicatif.
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;
}Deux champs comptent le plus. actor est la classe. basis indique pourquoi vous y croyez. Un résultat d'agent fondé sur une signature validée et un résultat d'agent fondé sur une chaîne user agent sont tous deux "agent", mais ils méritent ensuite un traitement très différent, donc ne les fusionnez jamais.
Étape 3: Extraire les preuves dans le middleware
L'étape d'extraction lit les en-têtes et l'état de la requête. Elle ne décide encore de rien.
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",
};
}Quelques remarques sur ce code:
signaturePresentse contente d'enregistrer que des en-têtes de signature existent. La validation est une étape séparée, couverte dans le guide sur la vérification des requêtes d'agent signées.- La liste de jetons user agent est volontairement simple. Elle produit une déclaration, jamais une vérification.
x-app-assistreprésente ce que votre propre frontend envoie lorsqu'un utilisateur passe par votre fonctionnalité IA intégrée. Remplacez-le par votre vrai mécanisme.
Étape 4: Classifier avec une base indiquée
Transformez maintenant les preuves en résultat. Gardez les règles courtes, ordonnées de la preuve la plus forte à la plus faible, et retombez toujours sur 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 };
}Remarquez ce que cela ne fait pas. Cela n'élève jamais une déclaration au rang de vérification. Cela n'attribue jamais Human à une requête qui ne présente aucun signal interactif. Quand cela ne peut pas déterminer, cela dit Unknown. Unknown est une réponse valide et utile, et elle vous indique exactement où s'arrête votre visibilité. Nous présentons le cas complet dans pourquoi Unknown est un résultat valide pour le trafic IA.
Étape 5: L'intégrer en mode observation
Montez le middleware tôt, attachez le résultat à la requête et consignez-le. Ne branchez rien dessus.
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();
});Trois détails rendent cela sûr:
- Le bloc
trygarantit qu'une défaillance de détection n'affecte jamais la réponse. L'observation doit échouer ouvertement. - La journalisation se fait sur
finish, ce qui vous permet de capturer le vrai code d'état et de comparer ensuite les classifications aux résultats. verifySignatureest remplacé parfalsejusqu'à ce que vous implémentiez une vraie validation. Cela maintient les requêtes signées dans les catégories declared ou unknown plutôt que de les surestimer.
Étape 6: Journaliser pour l'analyse, pas seulement pour le débogage
Écrivez des journaux que vous pouvez agréger. Une ligne structurée par requête suffit.
function logActor(entry: Record<string, unknown>) {
process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}N'incluez pas de données personnelles dans ces journaux. Vous avez besoin de la route, de la classe, de la base, des raisons et du résultat. Vous n'avez pas besoin des adresses IP complètes, des cookies bruts ni du corps des requêtes pour comprendre votre mix de trafic. Si vous conservez un identifiant pour la corrélation, hachez-le avec un sel rotatif et définissez une période de conservation courte.
Étape 7: Lisez les données avant d'agir
Faites tourner le mode observation assez longtemps pour voir les schémas hebdomadaires normaux. Puis répondez à quelques questions:
- Quelles routes attirent des agents ? Les endpoints de documentation, de catalogue et de recherche, oui. Les flux de compte et de paiement, en principe non.
- Quelle part des résultats d'agent est vérifiée versus déclarée ? Ce ratio vous indique dans quelle mesure vous pouvez vous appuyer sur l'identité lorsque vous écrivez des politiques.
- Où Unknown se concentre-t-il ? Un taux élevé de Unknown sur une route signifie souvent qu'il manque un signal à cet endroit, par exemple une page qui n'exécute jamais le code de votre frontend.
- Les résultats diffèrent-ils selon la classe ? Comparez les taux d'erreur, les étapes de conversion ou les signalements d'abus entre les classes. C'est là que naissent les vraies idées de politique.
Les réponses surprennent souvent. Les équipes découvrent régulièrement qu'une grande partie de leur "problème de bot" est en réalité une automatisation légitime qui utiliserait volontiers un endpoint structuré si l'on en proposait un.
Étape 8: Exposer le résultat au reste de votre application
Une fois les données en circulation, laissez les gestionnaires lire res.locals.actor pour des usages non contraignants: balises d'analyse, choix d'un format de réponse, ou enregistrement du fait qu'un ticket de support provient d'une session assistée par IA. Ces usages améliorent l'expérience sans risquer de bloquer quelqu'un.
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 });
});L'application de politiques, comme les challenges, les limites de débit ou les contrôles de niveau supérieur, vient plus tard, après que vous avez mesuré les faux positifs. Le guide sur le déploiement de politiques sensibles à l'acteur couvre ce processus.
Quelles erreurs devez-vous éviter ?
- Traiter le user agent comme une identité. C'est l'en-tête le plus facile à modifier. Enregistrez-le comme une déclaration. Même les opérateurs IA le disent: la documentation d'Anthropic pour son navigateur hébergé note que n'importe qui peut envoyer le même en-tête User-Agent et recommande de vérifier ses adresses IP publiées avant de traiter le trafic comme authentique.
- Par défaut, Human. Si votre code dit Human chaque fois qu'il ne trouve pas de signaux de bot, vos chiffres Human seront gonflés et vos politiques feront confiance au mauvais trafic.
- Mélanger classe et base. Stockez toujours les deux. Un tableau de bord qui n'affiche que "Agent" masque la différence entre un opérateur vérifié et une chaîne copiée.
- Bloquer pendant l'observation. Même un blocage temporaire invalide vos mesures, parce que les clients bloqués modifient leur comportement.
Où Proof of Actor s'insère
Le middleware ci-dessus est un bon point de départ, et il montre le modèle central: d'abord les preuves, une base explicite pour chaque résultat, Unknown maintenu visible, et les décisions laissées à votre application. Proof of Actor s'appuie sur le même modèle avec une couverture de signaux plus profonde à travers les contextes serveur, edge, navigateur et MCP, et des analyses qui vous permettent de Detect, Understand et Route en toute confiance.
Si vous voulez aller plus loin sur une application de production, posez votre candidature pour un accès anticipé. Pour voir le flux de décision complet, lisez Comment fonctionne Proof of Actor.
Sources
- RFC 9421: HTTP Message Signatures: la norme derrière les requêtes signées
- draft-ietf-webbotauth-httpsig-protocol: ébauche IETF Web Bot Auth définissant l'en-tête
Signature-Agent(travail en cours en novembre 2026) - RFC 9110: HTTP Semantics, User-Agent and From: en-têtes d'identification client auto-déclarés
- Spécification MCP 2025-11-25: Lifecycle: la requête
initializeetclientInfo - Spécification MCP 2026-07-28: Versioning and Compatibility: métadonnées de version, d'identité et de capacités par requête
- Anthropic: comment les navigateurs hébergés par Anthropic s'identifient: jeton user agent, adresses IP publiées et avertissement indiquant que l'en-tête peut être copié