Close the intent gap and catch distributed scrapers (0.17.0) - #25
Merged
Conversation
Both of these came out of production traffic on openbankingtracker.com, not from review. PerplexityBot classified as `unknown`. `Perplexity-User` was in the retrieval list; the crawler itself was in no list at all, so 42 requests a day sat in no generated firewall rule — neither protected by the retrieval/search bypass nor bounded by the training rate limit. It goes to `search`: the `Bot` suffix reads like corpus collection, but Perplexity documents it as the crawler behind their search results and states it does not feed model training. Blocking it costs citations, same shape of loss as blocking Googlebot. Every link unfurler was unclassified too — facebookexternalhit, Twitterbot, LinkedInBot, Slackbot, Discordbot, TelegramBot, WhatsApp, redditbot. The protective bypass keys on retrieval|search, so none were covered: the first time anyone promotes a deny rule, link previews break silently everywhere the site gets shared. They get a new `preview` intent rather than joining `retrieval`. Retrieval is the demand signal this library sells; a Slack unfurl is not an assistant answering someone's question, and counting it as one inflates the single number the split exists to measure. PREVIEW is tested after SEARCH so Applebot stays a search crawler — Apple uses one token for both. The firewall recommender gains the rule class it was missing. Rule 3 asks "is any single address abusive?" and a rotating proxy pool is designed so the answer is no: 34 addresses, 11 countries, one UA each, none above 100 requests a day, all under any sane threshold while collectively sweeping the site and inflating its GA4 numbers. Volume can't separate that from real readers, so the new rule keys on rate via an optional `spanSeconds` — the address that made the case fetched 16 distinct pages in one second. It is safe to key on rate only because the condition excludes static assets: the WAF counts every asset request and the middleware producing the observations does not, so a naive limit on `Mozilla` would throttle a real visitor on their first page view. That exclusion is asserted by a test, and the test was mutation-checked. Unfurlers get their own bypass rule at `medium` risk rather than riding the `low` one. A bypass skips managed rulesets too, and these tokens are unverifiable and heavily forged — labelling that `low` next to vendors who publish IP ranges would understate what it hands out. firewallScript now reorders every bypass rule to the top instead of assuming there is one. Deliberately left alone: SEO crawlers (AhrefsBot, SemrushBot, Screaming Frog) and scraping tools (Firecrawl, Scrapy) still classify as `unknown`. They are not AI agents, blocking them is a legitimate choice, and inventing a bucket to make a number look complete would be worse than the gap. Minor rather than patch: AgentIntent gains a union member, so an exhaustive switch over it now fails to compile. firewall.js budget 3200 → 4300 gz. It never runs in middleware, so size buys correctness cheaply there. Root entry unchanged at 94% of budget. Verified against the 24h window that prompted it: 2 rules → 3, with Perplexity moving into the protective bypass and the burst rule reporting "16 addresses across 11 countries, 4 of 7 measurable slices exceeded 30 requests/60s, peaking at 1,860/min". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Both findings came out of a check on live openbankingtracker.com traffic, not from review.
PerplexityBotwas in no intent bucketPerplexity-Userwas in the retrieval list. The crawler itself was in none of the three, so it classified asunknown— which means it landed in no generated firewall rule at all: not protected by the retrieval/search bypass, not bounded by the training rate limit. 42 requests a day on that one site.It goes to
search. TheBotsuffix reads like corpus collection, but Perplexity documents it as the crawler behind their search results and states it does not feed foundation-model training. Blocking it costs citations — the same shape of loss as blocking Googlebot.Every link unfurler was unclassified
facebookexternalhit,Twitterbot,LinkedInBot,Slackbot,Discordbot,TelegramBot,WhatsApp,redditbot. The protective bypass keys onretrieval|search, so none of them were covered. The first time anyone promotes a deny rule, link previews break silently everywhere the site gets shared.They get a new
previewintent rather than joiningretrieval. Retrieval is the demand signal this library sells; a Slack unfurl is not an assistant answering someone's question, and counting it as one inflates the single number the split exists to measure.PREVIEWis tested afterSEARCHso Applebot stays a search crawler — Apple uses one token for both. There's a test pinning that ordering.The recommender had no rule for distributed abuse
Rule 3 asks "is any single address abusive?" and a rotating proxy pool is built precisely so the answer is no:
Volume cannot separate that from real readers, so the new rule keys on rate, via an optional
spanSecondsonTrafficObservation. The address that made the case fetched 16 distinct pages in one second.It is safe to key on rate only because the condition excludes static assets — the WAF counts every asset request and the middleware producing the observations does not, so a naive limit on
Mozillawould throttle a real visitor on their first page view. That exclusion is asserted by a test, and the test was mutation-checked (removing the exclusions fails it).Unfurlers get their own bypass rule at
mediumrisk rather than riding thelowone: a bypass skips managed rulesets too, and these tokens are unverifiable and heavily forged. Labelling thatlowalongside vendors who publish IP ranges would understate what it hands out.firewallScriptnow reorders every bypass rule to the top instead of assuming there is one.Deliberately not fixed
SEO crawlers (
AhrefsBot,SemrushBot, Screaming Frog) and scraping tools (Firecrawl, Scrapy) still classify asunknown. They are not AI agents, blocking them is a legitimate choice, and inventing a bucket to make the coverage number look complete would be worse than the gap.Verified against the traffic that prompted it
Same 24h window, before and after — 2 rules become 3:
Perplexity moved out of
unknownand into the protective bypass.Notes
AgentIntentgains a union member, so an exhaustiveswitchover it now fails to compile.firewall.jsbudget 3200 → 4300 gz, raised deliberately rather than with--update. It never runs in middleware, so size buys correctness cheaply there. Root entry unchanged at 94% of its budget.🤖 Generated with Claude Code