feat(rules): let rules act on who the User-Agent says it is - #17
Merged
Conversation
The docs have a "Content Protection Strategy" section telling customers to branch on ai_scraper_category === 'training_crawler', and a threat table whose CRITICAL row reads "Block or serve alternative content". Nothing in the request path could do either. RuleContext exposed ip, path, method, userAgent, headers, enrichment, the Web Bot Auth verdict and the edge class. No agent classification, and no registry in the SDK at all — zero occurrences of GPTBot anywhere in packages/. The only adjacent signal was edge.class, a four-way script/crawler/verified/browser bucket that cannot separate a training crawler from a search crawler and only exists when Cloudflare fronts the request. So the headline policy of this product category — "block AI training crawlers, allow search crawlers" — was documented and unimplementable. WHY A GENERATED TABLE AND NOT A HAND-WRITTEN ONE pkg/agents in WebDecoy/app is the source of truth and already has 168 entries. A second hand-maintained copy would drift the first time someone adds a crawler, which is the exact failure the monorepo already documents for the deprecated php-sdk/wordpress mirror. registry.generated.ts is emitted by `go run ./cmd/export-agent-registry`, and parity-vectors.generated.json records what Go's MatchUserAgent answers for 360 User-Agents so parity.test.ts can prove the two implementations agree. ORDER IS LOAD-BEARING MatchUserAgent returns the first hit, walking categories by priority. The export preserves that order and the matcher takes the first match, so there is no second ordering to keep in sync. Sorting the table is not cosmetic: alphabetical order makes `applebot` shadow `applebot-extended`, silently reclassifying Apple's AI training crawler as a search crawler. The parity test catches it. ONE VOCABULARY Categories use agents.DetectionCategory, not the registry constants, so bot.category == "training_crawler" in a rule means the same thing as the training_crawler column in the dashboard. That mapping now lives in one place and ingest's scorer is pinned to it by a test in the app repo. WHAT THIS DELIBERATELY CANNOT DO It matches a self-declared User-Agent, so it only ever acts on agents that identify honestly. That is correct for this policy — GPTBot and ClaudeBot declare themselves, and the decision is about a cooperative agent's access to content, not about catching a liar. It is not a threat signal and does not pretend to be: BotRule reports confidence 70, not the 100 a tripwire reports, because a tripwire proves behaviour while this repeats a claim. Anything spoofing Chrome passes through, and the docs say so in a caution block. allow[] is applied before every other clause, because an SEO outage from a rule someone thought they had scoped costs far more than a scraper getting through. An empty config is inert rather than default-deny for the same reason. The UA cache is bounded and clears wholesale on overflow: the key is client-controlled, so an unbounded map would turn a memoisation into a memory exhaustion vector. Adds ~28 KB to the bundle (37.7 KB gzipped total) and stays edge-compatible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Companion to WebDecoy/app#498, which adds the registry exporter.
The gap
The docs describe branching on
ai_scraper_category === 'training_crawler'and blocking at CRITICAL.RuleContextexposedip,path,method,userAgent,headers,enrichment, the Web Bot Auth verdict andedge— no agent classification, and no registry in the SDK at all (zero occurrences ofGPTBotanywhere inpackages/).So the headline policy of this product category — block AI training crawlers, allow search crawlers — was documented and unimplementable.
API
New filter namespace:
bot.known,bot.ai,bot.category,bot.name,bot.id,bot.organization,bot.score,bot.respects_robots.ai: truecovers the four AI categories and deliberately excludessearch_crawler— blocking Googlebot would deindex the customer's site.Generated, not hand-written
registry.generated.tscomes frompkg/agentsvia the exporter;parity-vectors.generated.jsonrecords what Go'sMatchUserAgentanswers for 360 User-Agents, andparity.test.tsreplays every one.Order is load-bearing. Verified the parity test fails on reorder: sorting alphabetically makes
applebotshadowapplebot-extended, silently reclassifying Apple's AI training crawler.What this deliberately cannot do
It matches a self-declared User-Agent. That is correct for this policy — GPTBot and ClaudeBot identify honestly, and the decision is about a cooperative agent's access, not about catching a liar. Anything spoofing Chrome passes through, and the docs say so in a caution block.
It is not a threat signal and does not pretend to be:
BotRulereportsconfidence: 70, not the100a tripwire reports, because a tripwire proves behaviour while this repeats a claim.Safety choices
allow[]applies before every other clause — an SEO outage from a mis-scoped rule costs more than a scraper getting throughVerification
turbo test --force)check:edgepasses — still edge-runtime compatible