Sign inSign up

superbizon007/web-scraper-agent

By superbizon007

Updated 4 months ago

Web scraper agent

Image
0

315

superbizon007/web-scraper-agent repository overview

web_scraper_agent

Lightweight, always-stealth web-scraping agent. Driven by a text LLM in a ReAct loop; same tools also callable directly via A2A and MCP for deterministic single-tool usage.

Architecture

                    ┌──────────────────────────────────────┐
                    │  HTTPS :443  (TLS, optional API_KEY) │
                    └────────────────────┬─────────────────┘
                                         │
                  ┌──────────────────────┼──────────────────────┐
                  │                      │                      │
            A2A direct             A2A freeform               MCP /mcp
            (skill_id JSON)        (NL task → ReAct)          (mount_mcp)
                  │                      │                      │
                  ▼                      ▼                      ▼
            ┌────────────────────────────────────────────────────────┐
            │  Tool registry (app/agent/tools.py)                    │
            │  fetch_url · render_page · fill_form · click_and_wait  │
            │  scroll_to · extract · download_file · switch_engine   │
            │  finish                                                │
            └────────┬─────────────────────────┬─────────────────────┘
                     │                         │
              curl_cffi path             Playwright path
              (chrome131 JA3)            (rebrowser-Chromium / FF)
                     │                         │
                     │                  ┌──────┴───────┐
                     │                  │ ghost-cursor │
                     │                  │ humanized KB │
                     │                  └──────┬───────┘
                     │                         │
                     └──── cookie sync ────────┘
                                  │
                              storage_state.{engine}.json
                              (S3 + /tmp fallback, debounced 10s)
                                  │
                       optional tun2socks → SOCKS proxy
                       optional Xvfb + x11vnc + noVNC (HEADED)
  • TLS / HTTP fingerprint: curl_cffi with impersonate=chrome131 — defeats Cloudflare/PerimeterX/DataDome at the protocol layer.
  • Browser engines: rebrowser-playwright (CDP-leak-patched Chromium, default), vanilla Playwright Firefox (fallback for Chrome-specific bot rules), and opt-in cloakbrowser (stealth Chromium fork with binary-level fingerprint patches for Cloudflare Turnstile / reCAPTCHA v3 / FingerprintJS-style gates). LLM picks per task via switch_engine. The registry of supported engines lives in app/browser/engines.py. CloakBrowser is gated by CLOAK_ENABLED; its binary downloads from CloakHQ at first launch (BINARY-LICENSE.md prohibits redistribution in a public Docker image). Camoufox is deferred (PyYAML CLoader incompatibility on Python 3.12).
  • Behavioral: python-ghost-cursor Bezier mouse trajectories + inline humanized keyboard (typo simulation, Gaussian inter-key delays, RNG seeded per task_id).
  • State persistence: storage_state (cookies + localStorage) is debounced-saved to S3 every 10s during a task; restored on next task with the same task_id. Local /tmp/state fallback.
  • ReAct loop: LangGraph state machine — call_llm → execute_tool → loop. Default 25 steps max, 25k chars per HTML observation.
  • Routing: optional SOCKS proxy via tun2socks (transparent, fwmark 100 + tun0). Set SOCKS_PROXY=....
  • HEADED mode: HEADED=true boots Xvfb + x11vnc + noVNC on :6901 for live-watch debugging via the dashboard.
Source layout
app/
├── server/
│   ├── config.py     pydantic-settings
│   ├── a2a.py        WebScraperAgentExecutor (direct dispatch + ReAct)
│   ├── main.py       FastAPI + lifespan + mount_mcp(...)
│   └── __main__.py   uvicorn entry
├── agent/
│   ├── prompts.py    SYSTEM_PROMPT for the ReAct loop
│   ├── tools.py      9 @tool functions + SCRAPER_TOOLS / TOOLS_BY_ID
│   └── graph.py      LangGraph StateGraph
├── browser/
│   ├── context.py    rebrowser-Chromium + vanilla-Firefox runtimes
│   ├── state.py      storage_state save/load (S3 + /tmp), debounce
│   ├── cursor.py     ghost-cursor wrapper
│   ├── typing.py     humanized keyboard
│   ├── cookies.py    curl_cffi ↔ Playwright cookie sync
│   └── detect.py     anti-bot phrase scanner
└── http/
    └── session.py    per-task curl_cffi AsyncSession pool

Tools

ToolPurpose
fetch_urlTLS-impersonated HTTP. First choice for static HTML, JSON APIs.
render_pageBrowser navigate + wait + snapshot.
fill_formMulti-field humanized form fill ± submit.
click_and_waitClick selector, wait for state.
scroll_toHumanized scroll.
extractApply schema to HTML — selectors (CSS) or llm mode.
download_fileStream URL bytes to a run artifact.
switch_engineFlip between Chromium / Firefox / Cloak per task.
finishTerminate the ReAct loop with a final answer.

Surfaces

Same tools, three callable shapes:

  1. A2A direct dispatch — POST {"skill_id": "fetch_url", "params": {...}} as the message text. Tool runs synchronously, no LLM cost.
  2. A2A freeform — POST a natural-language task. The ReAct loop runs; the LLM picks tools.
  3. MCPmount_mcp(...) exposes every tool as an MCP tool at /mcp. Frontier-LLM clients (Claude Desktop, Cursor, Windsurf) use their own LLM and call tools deterministically.

Build

The image is multi-arch (linux/amd64,linux/arm64) and ships from superbizon007/web-scraper-agent.

Local build (dev)

Native arch only — required on Apple Silicon because QEMU emulation breaks Chromium's new_page under linux/amd64.

cd apps/web_scraper_agent
docker build \
  --build-context a2a_pkg=../../packages/a2a_agent \
  -f Dockerfile.ubuntu \
  -t superbizon007/web-scraper-agent:latest \
  .

Import into a local k3d cluster:

k3d image import superbizon007/web-scraper-agent:latest -c forfetch-local
Multi-arch publish
cd apps/web_scraper_agent
./docker_hub_push.sh

Reads version from pyproject.toml, runs docker buildx build --platform linux/amd64,linux/arm64 --push against both :VERSION and :latest tags.

Run

Locally with Docker
docker run -d --rm \
  --name web-scraper \
  --shm-size=2g \
  -p 9443:443 \
  -e API_KEY=test123 \
  -e LLM_API_KEY=$OPENAI_API_KEY \
  superbizon007/web-scraper-agent:latest

Smoke test (skill_id direct dispatch):

curl -sk https://localhost:9443/ \
  -H "Authorization: Bearer test123" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{
        "id":"task-1",
        "message":{"role":"user","messageId":"m1",
          "parts":[{"type":"text","text":"{\"skill_id\":\"fetch_url\",\"params\":{\"url\":\"https://example.com\"}}"}]
        }}}'

Freeform (ReAct loop) — same shape, but the message text is a NL task instead of a JSON skill_id envelope.

HEADED mode (live-watch via VNC)
docker run -d --rm \
  --name web-scraper-headed \
  --shm-size=2g \
  -p 9443:443 -p 6901:6901 \
  -e HEADED=true \
  -e API_KEY=test123 \
  -e LLM_API_KEY=$OPENAI_API_KEY \
  superbizon007/web-scraper-agent:latest

Open http://localhost:6901/vnc.html for the noVNC client.

With SOCKS routing
docker run -d --rm \
  --name web-scraper-proxied \
  --cap-add=NET_ADMIN --device=/dev/net/tun \
  --shm-size=2g \
  -p 9443:443 \
  -e SOCKS_PROXY=socks5://user:[email protected]:1080 \
  -e API_KEY=test123 \
  -e LLM_API_KEY=$OPENAI_API_KEY \
  superbizon007/web-scraper-agent:latest
With CloakBrowser engine

The cloak engine is available by default — the ReAct LLM picks it via switch_engine when blocked_signals indicate a Turnstile / reCAPTCHA v3 / FingerprintJS gate. The stealth-Chromium binary is not baked into the image (its BINARY-LICENSE.md prohibits redistribution in a public Docker artifact); the entrypoint downloads it from CloakHQ on first boot (~200 MB, ~30 s on a fast link). Mount a volume at /cache/cloakbrowser so the download survives pod restarts:

docker run -d --rm \
  --name web-scraper-cloak \
  --shm-size=2g \
  -p 9443:443 \
  -e API_KEY=test123 \
  -e LLM_API_KEY=$OPENAI_API_KEY \
  -v cloakbrowser-cache:/cache/cloakbrowser \
  superbizon007/web-scraper-agent:latest

To force cloak as the default engine for every task: pass -e DEFAULT_ENGINE=cloak. To opt out entirely (no outbound traffic to CloakHQ at any point): pass -e CLOAK_ENABLED=false. Startup fail-fasts if DEFAULT_ENGINE=cloak and CLOAK_ENABLED=false.

NET_ADMIN + /dev/net/tun are required for tun2socks to set up the tun0 device and iptables mangle rules.

In-cluster (k8s)

Core seeds and launches the agent automatically — see apps/agents_mcp/app/seed.py for the published agent definition. The agent registers as web_scraper_agent with capability tag {"stealth": true}.

Environment

LLM (text)
VarDefaultNotes
LLM_URLhttps://api.openai.com/v1OpenAI-compatible base URL. Used by ReAct loop and extract LLM mode.
LLM_API_KEY""Bearer for the LLM.
LLM_MODELgpt-4.1-miniCheap text model is fine; tools have selectors mode.
Auth + transport
VarDefaultNotes
API_KEY""Inbound bearer; auth skipped when unset.
PORT443Both A2A JSON-RPC and /mcp mount on this port.
SSL_CERTFILE/tmp/agent.crtSelf-signed cert auto-generated at startup if missing.
SSL_KEYFILE/tmp/agent.key
SSL_CA_CERTFILE/tmp/agent-ca.crt
AGENT_URL(auto)Public URL published in AgentCard. Core injects in-cluster URL in prod.
Stealth knobs
VarDefaultNotes
CURL_IMPERSONATEchrome131curl_cffi profile; should match shipped Chromium major.
DEFAULT_ENGINEchromiumchromium | firefox | cloak. ReAct loop can flip per task via switch_engine. Startup fails fast if the selected engine isn't available.
CLOAK_ENABLEDtruecloak engine is available by default; the ReAct LLM decides when to escalate. Set false to opt out entirely (compliance, no outbound traffic to CloakHQ). First use triggers ~200 MB binary download.
HEADEDfalsetrue boots Xvfb + x11vnc + noVNC; browser headless=False.
SOCKS proxy (entrypoint, not server)
VarDefaultNotes
SOCKS_PROXYunsetsocks5://user:pass@host:port; routed transparently via tun2socks.
PROXY_BYPASS_PRIVATE1Skip RFC1918 ranges. Set 0 to force everything through proxy.
PROXY_BYPASS_CIDRSunsetComma-separated extra CIDRs to exclude from the proxy.
S3 (artifacts + storage_state)
VarDefaultNotes
S3_BUCKETunsetOptional. Without it, artifacts go to /tmp/artifacts only.
S3_ENDPOINT_URLunsetFor S3-compatibles (MinIO, R2).
S3_ACCESS_KEYunsetRun-scoped STS creds preferred (Core injects).
S3_SECRET_KEYunset
S3_SESSION_TOKENunset
S3_PREFIX""e.g. agent-sessions/{session_id} — keys nested under this.
RUN_ARTIFACTS_ROOTrunsTop-level prefix for artifact uploads.
ReAct loop bounds
VarDefaultNotes
MAX_STEPS25Hard ceiling on tool calls per task.
MAX_HTML_CHARS25000Truncate HTML observations passed back to the LLM.
HISTORY_WINDOW10(reserved for summarization)
SUMMARY_THRESHOLD15(reserved for summarization)
HIL (human-in-the-loop, async)
VarDefaultNotes
HUMAN_RELAY_AGENT_URLunsetPoints to hil_agent for captcha / 2FA escalation.
HUMAN_RELAY_AGENT_API_KEYunset
HUMAN_INPUT_TIMEOUT86400Seconds (24h).
Trajectory + identity
VarDefaultNotes
TRAJECTORY_DIRunsetIf set, dumps per-turn LLM I/O for debugging.
AGENT_NAMEweb.scraper
AGENT_VERSION0.1.0
AGENT_IDweb_scraper_agentRegistry key Core uses to discover the agent.
AGENT_DESCRIPTION(long)Published in the AgentCard.

Fingerprint regression test

tests/test_fingerprint_check.py is a manual / nightly smoke that catches the day TLS impersonation or webdriver patches silently break. Six checks:

  1. JA3 doesn't match Python's stdlib default; UA doesn't leak python-*.
  2. Cloudflare basic mode (https://www.cloudflare.com/) doesn't return a "Just a moment" interstitial via fetch_url.
  3. Sannysoft loads on Chromium (screenshot artifact = audit trail).
  4. navigator.webdriver not flagged on Chromium.
  5. Sannysoft loads on Firefox.
  6. navigator.webdriver not flagged on Firefox.

Run against a live container:

docker run -d --rm --name web-scraper-smoke --shm-size=2g \
  -p 9443:443 -e API_KEY=test123 \
  superbizon007/web-scraper-agent:latest

AGENT_URL=https://localhost:9443 API_KEY=test123 \
  python3 tests/test_fingerprint_check.py
# exit 0 = green; exit 1 = at least one regression

Re-run after every curl_cffi, rebrowser-playwright, or base-image bump.

Status

All five plan phases shipped:

PhaseScope
1skeleton + curl_cffi tools + dual-path executor + mount_mcp
2Chromium + humanized cursor/keyboard + cookie sync + bot-signal detection
3Firefox engine + switch_engine (vanilla Playwright FF; Camoufox deferred)
4extract LLM mode + storage_state.{engine}.json persistence
5tun2socks proxy verify + HEADED Xvfb/VNC verify + fingerprint regression suite

See specs/PLAN_WEB_SCRAPER_AGENT.md for the full plan.

MCP tools

The agent image exposes an MCP (Model Context Protocol) server on the same port as A2A at /mcp. Any MCP client — Claude Desktop, Cursor, Windsurf, ChatGPT Connectors, OpenAI Agents SDK — can list and invoke these tools using the pod's API_KEY as Bearer.

In production, Core proxies https://web-scraper-agent.agents.forfetch.ai/mcp → the worker pod's /mcp.

Tool (skill_id)Description
fetch_urlTLS-impersonated HTTP fetch. First choice for static HTML, JSON APIs, sitemaps.
render_pageNavigate the stealth browser (Chromium or Firefox) to a URL; wait, snapshot HTML + screenshot.
fill_formMulti-field humanized form fill (text/select/check) with optional submit.
click_and_waitHumanized click on a selector and wait for the next page state.
scroll_toHumanized incremental scroll to trigger lazy-load.
extractApply a CSS-selector schema to HTML and return structured data.
download_fileStream a binary URL directly to a run-scoped artifact.
switch_engineFlip the active browser engine between chromium (default), firefox, and cloak (opt-in). Call when blocked_signals indicate a bot challenge. Returns error if the requested engine isn't available.
list_filesList customer-uploaded files visible to this agent (file_ref envelopes).
read_fileRead a customer-uploaded file by file_ref (text|bytes|meta).
apply_storage_stateReplay a Playwright storage_state.json upload on the next browser context.
finishTerminate the ReAct loop with a final answer.
scrapeRun a freeform scraping task via the LLM-driven ReAct loop. Pass the task as a text string.

Input schemas are authored in apps/agents_mcp/app/seed.py. Skills without an explicit schema advertise a single {task: str} freeform parameter.

Tag summary

Content type

Image

Digest

sha256:c8d1cb32b

Size

1.1 GB

Last updated

4 months ago

docker pull superbizon007/web-scraper-agent