{
 "name": "Crawler Log Triage — AI Crawler Index",
 "description": "Paste access-log lines and get back who crawled you. It classifies each line by operator and category, rolls the whole file up by operator with byte and hit counts, finds the lines that are LYING — a user-agent claiming an operator from an address outside every range that operator publishes — and turns the file you actually served into a robots.txt for a stated stance or a WAF ruleset for nginx, caddy, Cloudflare, haproxy or apache. Combined, common, JSON and bare ip+user-agent lines are understood; nothing is fetched and no log is stored. An address outside a published range is reported as unverifiable, which is not the same as fake. Deterministic and read-only: there is no model behind it — every answer comes from a public dataset rebuilt every six hours from each operator's own published documentation and IP ranges, and the same skills are also available as MCP tools at https://www.pathwren.workers.dev/mcp/triage. No key, no signup, no quota. Independent and unaffiliated with any operator it documents.",
 "supportedInterfaces": [
  {
   "url": "https://www.pathwren.workers.dev/a2a/triage",
   "protocolBinding": "JSONRPC",
   "protocolVersion": "1.0"
  }
 ],
 "url": "https://www.pathwren.workers.dev/a2a/triage",
 "preferredTransport": "JSONRPC",
 "protocolVersion": "1.0",
 "provider": {
  "organization": "Pathwren",
  "url": "https://www.pathwren.workers.dev"
 },
 "version": "1.0.0",
 "documentationUrl": "https://www.pathwren.workers.dev/a2a.html",
 "iconUrl": "https://www.pathwren.workers.dev/icon.png",
 "capabilities": {
  "streaming": false,
  "pushNotifications": false,
  "extendedAgentCard": false,
  "extensions": [
   {
    "uri": "https://www.pathwren.workers.dev/changes.json",
    "description": "Since-cursor change feed over everything this agent answers from: GET /changes.json?since=<cursor> returns only what moved — operator IP-range lists that gained or lost prefixes, upstreams that failed or recovered, crawler records added or edited. Read `cursor` from the answer and send it back next time; it advances only on a real change, so an unchanged answer is proof and costs a few hundred bytes. The same feed is the changes_since skill on this endpoint.",
    "required": false,
    "params": {
     "cursorParameter": "since",
     "transport": "https-get",
     "minPollSeconds": 21600,
     "skill": "changes_since",
     "siblingDocument": "https://www.pathwren.workers.dev/data/agents.json"
    }
   }
  ]
 },
 "defaultInputModes": [
  "application/json",
  "text/plain"
 ],
 "defaultOutputModes": [
  "application/json",
  "text/plain"
 ],
 "skills": [
  {
   "id": "triage_log",
   "name": "Triage a paste of log lines",
   "description": "Paste log lines — combined, JSON, `uniq -c`, `<ip> <ua>` or bare user-agents, auto-detected — for per-line crawler, operator, purpose and cost of blocking, a rollup by operator and category, and every line whose address contradicts its claim. Log text, never a URL. Example: log='66.249.66.1 Googlebot/2.1' returns Googlebot, Google, search, verified.",
   "tags": [
    "logs",
    "crawlers",
    "classification",
    "triage"
   ],
   "examples": [
    "{\"skill\":\"triage_log\",\"log\":\"66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \\\"GET /pricing HTTP/1.1\\\" 200 5120 \\\"-\\\" \\\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\\\"\\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \\\"GET /docs HTTP/1.1\\\" 200 1234 \\\"-\\\" \\\"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\\\"\",\"detail\":\"summary\"}",
    "66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \"GET /pricing HTTP/1.1\" 200 5120 \"-\" \"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\"\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \"GET /docs HTTP/1.1\" 200 1234 \"-\" \"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\""
   ],
   "inputModes": [
    "application/json",
    "text/plain"
   ],
   "outputModes": [
    "application/json",
    "text/plain"
   ]
  },
  {
   "id": "find_impersonators",
   "name": "Find the lines that are lying",
   "description": "Only the lines claiming a crawler whose operator publishes address ranges, from an address in none of to run: this server makes no outbound request. Example: log='203.0.113.9 Googlebot/2.1' returns one impersonation.",
   "tags": [
    "logs",
    "spoofing",
    "security",
    "verification"
   ],
   "examples": [
    "{\"skill\":\"find_impersonators\",\"log\":\"66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \\\"GET /pricing HTTP/1.1\\\" 200 5120 \\\"-\\\" \\\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\\\"\\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \\\"GET /docs HTTP/1.1\\\" 200 1234 \\\"-\\\" \\\"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\\\"\"}"
   ],
   "inputModes": [
    "application/json",
    "text/plain"
   ],
   "outputModes": [
    "application/json",
    "text/plain"
   ]
  },
  {
   "id": "summarize_by_operator",
   "name": "Roll a log up by operator and category",
   "description": "Aggregate only: who crawled you, how many requests each, what share, which category, and what blocking each would cost. Eats a `uniq -c` table straight from a shell pipeline. Example: log='412 GPTBot/1.2' returns OpenAI, 412 requests, 100%, ai-training.",
   "tags": [
    "logs",
    "reporting",
    "operators",
    "summary"
   ],
   "examples": [
    "{\"skill\":\"summarize_by_operator\",\"log\":\"66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \\\"GET /pricing HTTP/1.1\\\" 200 5120 \\\"-\\\" \\\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\\\"\\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \\\"GET /docs HTTP/1.1\\\" 200 1234 \\\"-\\\" \\\"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\\\"\"}"
   ],
   "inputModes": [
    "application/json",
    "text/plain"
   ],
   "outputModes": [
    "application/json",
    "text/plain"
   ]
  },
  {
   "id": "robots_from_log",
   "name": "robots.txt from a log",
   "description": "A robots.txt naming only the crawlers in your log, each with its request count and cost of blocking, plus a warning for any that do not documentably obey it — there the file is a request, not enforcement. Example: log='412 GPTBot/1.2', stance='block-ai-training' blocks GPTBot only.",
   "tags": [
    "logs",
    "robots.txt",
    "policy",
    "generator"
   ],
   "examples": [
    "{\"skill\":\"robots_from_log\",\"log\":\"66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \\\"GET /pricing HTTP/1.1\\\" 200 5120 \\\"-\\\" \\\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\\\"\\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \\\"GET /docs HTTP/1.1\\\" 200 1234 \\\"-\\\" \\\"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\\\"\",\"stance\":\"block-ai-training\"}"
   ],
   "inputModes": [
    "application/json",
    "text/plain"
   ],
   "outputModes": [
    "application/json",
    "text/plain"
   ]
  },
  {
   "id": "waf_ruleset_from_log",
   "name": "WAF ruleset from a log",
   "description": "nginx, Caddy, Cloudflare, HAProxy or Apache rules for only the crawlers in your log. The reply warns that a UA rule stops only an honest client, and that impersonation is an address problem needing the published prefixes as an allowlist. Example: log='412 GPTBot/1.2', target='nginx', scope='ai-training'.",
   "tags": [
    "logs",
    "waf",
    "nginx",
    "cloudflare"
   ],
   "examples": [
    "{\"skill\":\"waf_ruleset_from_log\",\"log\":\"66.249.66.1 - - [01/Sep/2026:06:12:44 +0000] \\\"GET /pricing HTTP/1.1\\\" 200 5120 \\\"-\\\" \\\"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)\\\"\\n20.171.206.5 - - [01/Sep/2026:06:12:45 +0000] \\\"GET /docs HTTP/1.1\\\" 200 1234 \\\"-\\\" \\\"Mozilla/5.0 (compatible; GPTBot/1.2; +https://openai.com/gptbot)\\\"\",\"target\":\"nginx\",\"action\":\"block\"}"
   ],
   "inputModes": [
    "application/json",
    "text/plain"
   ],
   "outputModes": [
    "application/json",
    "text/plain"
   ]
  }
 ]
}