なぜ rule ではなく actor context から始めるのか?
トラフィックの中に AI agent がいると気づいた多くのチームは、まず rule を作ろうとします。user agent を block する、challenge を追加する、rate limit を厳しくする。しかし、traffic を理解する前に書かれた rule は、たいてい意図しない相手に当たります。正当な assistant が block され、実在の user が challenge され、本当に警戒していた automation は、自分を名乗っていなかったためにそのまま通り続けます。
actor context は rule の前段です。各 request について、アプリが観測できる「誰が、あるいは何が action しているか」を記録します。interactive な person なのか、AI assistant と一緒に作業している person なのか、自律的な agent なのか、あるいはまだ判別できない何かです。その見方の根拠も記録し、request の結果の隣に残します。これを 1 週間から 2 週間続けると、どんな rule template でも得られないものが手に入ります。自分の traffic の地図です。そもそも agent がサイトに来ているのかまだ判断している段階なら、まず website traffic が AI agents 由来かを見分ける方法 から始めてください。
このガイドでは、通常の middleware を使ってその層を Node.js アプリに実装します。Express で動作し、同じ考え方は Fastify, Koa, Hono, Next.js の route handler にもそのまま適用できます。
何を作るか
最終的に次の状態にします。
- 各 request から actor evidence を抽出する middleware。
- 各結果に明示的な basis を持つ、小さく明確な classification。
- request の扱い方を変えない structured な observation log。
- 後続の handler が読めるように、結果を request に付与する方法。
ここでは request を block しません。意図的です。
Step 1: サーバーが実際に見られる evidence は何か?
サーバーが見える範囲は、人が想像するより狭いものです。コードを書く前に、edge で利用できる signal を洗い出してください。
- Request signatures. いくつかの自動クライアントは HTTP Message Signatures (RFC 9421) を使って request に署名し、
Signature-InputとSignatureheader を送り、しばしば operator が公開した key を指すSignature-Agentheader も併用します。これは IETF の Web Bot Auth protocol draft に記載されています。 - Declared identity. クライアントは
User-Agentheader やFromheader で自分を名乗ることがあります。また、MCP client name のような protocol metadata 経由でも名乗れます。MCP clients は 2025-11-25 改訂の initialize request で自分で選んだclientInfoname を報告し、2026-07-28 改訂では client identity が per-request metadata として扱われます。これらは declaration であり、誰でも複製できます。 - Authentication context. request に session cookie, bearer token, API key のどれが載っているか、そしてそれがどの種類の credential か。
- Protocol shape. Accept header、クライアントが HTML か JSON かを要求しているか、page asset を読み込むか、redirect と cookie をどう扱うか。
- Application declarations. 自分の product が network では分からないことを知っている場合もあります。たとえば user が「AI assisted」オプションを選んだ、あるいは request が組み込み assistant 機能から来た、というような場合です。
各 signal の強さは異なります。検証済みの signature は確認できます。user agent string は主張です。欠けている cookie は、単に欠けているだけです。これらを分けて扱うことが、この演習の核心です。
Step 2: 結果を明示的にモデル化する
検出ロジックを書く前に、actor result の shape を定義してください。小さな型付き model にしておくと、classification が何を意味するのかを全員が正確に理解できます。ここで使う 4 つの class は、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;
}最も重要なのは 2 つの field です。actor は class です。basis は、その判断をなぜ信じるのかを示します。検証済みの signature に基づく agent result と、user agent string に基づく agent result は、どちらも「agent」ですが、後での扱いは大きく異なるため、決して同一視しません。
Step 3: middleware で evidence を抽出する
抽出段階では header と request state を読み取ります。まだ何も判断しません。
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",
};
}この code について、いくつか補足します。
signaturePresentは signature header が存在することだけを記録します。validation は別の step であり、署名された agent request の検証ガイドで扱います。- user agent token list は意図的に単純化しています。これは declaration を生成するだけで、verification ではありません。
x-app-assistは、user が組み込みの AI 機能を通じて作業するときに、あなたの frontend が送るものを表す placeholder です。実際の仕組みに置き換えてください。
Step 4: 明示された basis で classification する
ここで evidence を result に変換します。rule は短く保ち、最も強い evidence から最も弱い evidence へ順に並べ、必ず 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 };
}ここで何をしていないかに注目してください。declaration を verification に格上げしていません。interactive signal がない request に Human を割り当ててもいません。判別できない場合は Unknown と言います。Unknown は有効で役に立つ答えであり、visibility の限界がどこにあるかを正確に示します。詳しくは AI traffic における Unknown が有効な結果である理由 で説明しています。
Step 5: observation mode で組み込む
middleware は早い段階で mount し、result を request に付け、log してください。そこで分岐してはいけません。
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();
});これを安全にする 3 つの要素があります。
tryブロックにより、detect の失敗が response に影響しません。observation は open に失敗すべきです。- logging は
finishで行われるため、実際の status code を記録でき、後で classification と outcome を比較できます。 verifySignatureは、実際の validation を実装するまでfalseのままの stub です。これにより、署名された request を declared あるいは unknown の bucket に留め、過大評価しないようにできます。
Step 6: デバッグだけでなく分析のために log する
集計できる log を書いてください。request ごとに 1 行の structured log で十分です。
function logActor(entry: Record<string, unknown>) {
process.stdout.write(JSON.stringify({ type: "actor_observation", ...entry }) + "\n");
}これらの log には個人データを含めないでください。必要なのは route, class, basis, reason, outcome です。traffic の内訳を理解するために、完全な IP address, 生の cookie, request body は必要ありません。相関のために identifier を保持する場合は、rolling salt で hash し、保持期間を短くしてください。
Step 7: 実行する前に data を読む
observation mode で十分な期間運用し、通常の週次パターンが見えるようにします。そのうえで、次の問いに答えてください。
- どの route に agent が集まるか? ドキュメント, catalog, search の endpoint はそうなりやすいです。account と checkout の flow は通常そうあるべきではありません。
- agent result のうち verified と declared の比率はどれくらいか? その比率が、policy を書くときに identity をどれだけ安全に頼れるかを示します。
- Unknown はどこに集中しているか? ある route で Unknown rate が高いなら、たとえば frontend code が一度も動かない page など、そこで signal が不足していることが多いです。
- class ごとに outcome は異なるか? error rate, conversion step, abuse report を class 間で比較してください。ここから本当の policy のアイデアが生まれます。
答えはしばしば意外です。チームはよく、自分たちの「bot problem」の大部分が、実際には正当な automation であり、structured endpoint があれば進んで使っていたはずのものだと気づきます。
Step 8: 結果をアプリの他の部分に公開する
data が流れ始めたら、enforce しない用途のために handler から res.locals.actor を読めるようにします。たとえば analytics tag, response format の選択、あるいは support ticket が AI-assisted session から来たことの記録です。これらは、誰かを締め出すリスクなしに体験を改善します。
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 limit, step-up check などの enforcement は、その後です。false positive を測定してから行ってください。actor-aware policy の rollout に関するガイドで、その process を扱います。
どんな mistakes を避けるべきか?
- user agent を identity とみなすこと。 これは最も簡単に変えられる header です。declaration として記録してください。AI operator もそう述べています。Anthropic の hosted browser agent のガイダンス では、誰でも同じ User-Agent header を送信できるとし、traffic を genuine と見なす前に公開されている IP addresses を確認するよう推奨しています。
- Human を default にすること。 bot signal が見つからないたびに Human を返す code だと、Human 数が膨らみ、policy が誤った traffic を信頼することになります。
- class と basis を混同すること。 必ず両方を保存してください。"Agent" だけを示す dashboard では、検証済みの operator と複製された string の違いが見えません。
- observation 中に block すること。 一時的な block であっても測定は無効になります。block された client は振る舞いを変えるからです。
Proof of Actor はどこに位置づくか
ここまでの middleware は堅実な出発点であり、核となる model を示しています。つまり、evidence を先に置き、各 result に明示的な basis を与え、Unknown を可視のままにし、decision はアプリに委ねるということです。Proof of Actor は同じ model を土台に、server, edge, browser, MCP の各 context にまたがるより深い signal coverage と、Detect, Understand, Route を確信を持って行える analytics を提供します。
本番アプリでこれをさらに進める支援が必要なら、early access に申し込んでください。decision flow 全体を見るには、Proof of Actor の仕組み を読んでください。
Sources
- RFC 9421: HTTP Message Signatures: 署名付き request の基盤となる standard
- draft-ietf-webbotauth-httpsig-protocol:
Signature-Agentheader を定義する IETF Web Bot Auth draft (2026年11月時点では作業中) - RFC 9110: HTTP Semantics, User-Agent and From: 自己申告の client identification header
- MCP specification 2025-11-25: Lifecycle:
initializerequest とclientInfo - MCP specification 2026-07-28: Versioning and Compatibility: request ごとの version, identity, capability metadata
- Anthropic: how Anthropic-hosted browsers identify themselves: user agent token, 公開 IP addresses, そして header は複製可能だという注意喚起