Pourquoi Grub
Nous avons intégré les fonctionnalités de chaque grand crawler — puis ajouté ce qu'aucun d'entre eux n'a.
Crawlers auto-hébergés
Crawlers cloud / gérés
Seul Grub dispose du Ghost Protocol — un repli automatique basé sur la vision qui capture des captures d'écran des pages bloquées et extrait le contenu via LLM lorsque le crawling standard échoue. La prévention (Camoufox + proxy + furtivité) gère 95 % des blocages. Le Ghost Protocol gère le reste.
Points d'API
Crawling principal
Agent (Mode B)
Gestion des tâches
Cache distant
Gestion des sessions
Flux en direct
Mesh
Système
Outils MCP (grub-crawl.py)
Le pont MCP expose toutes les capacités à tout hôte compatible MCP :
Modules internes
Cœur de l'agent (app/agent/)
Adaptateurs de fournisseurs (app/agent/providers/)
Passerelles de politique (app/policy/)
Observabilité (app/observability/)
Couche API
Anti-détection (app/)
| Fichier | Rôle | Statut |
|---|
stealth.py | Correctifs playwright-stealth, blocage des domaines de traqueurs | Terminé |
proxy.py | Résolution de proxy par requête avec repli sur les variables d'environnement | Terminé |
Mesh (app/mesh/)
Infrastructure
Machine à états de l'agent```
INIT -> PLAN -> EXECUTE_TOOL -> OBSERVE -> PLAN -> ... -> RESPOND -> STOP
| |
+-- policy_denied ---------------------->+
+-- max_steps / max_wall_time / max_failures -> STOP
+-- no_op_loop (3x empty) ------------> STOP
+-- blocked (ghost trigger) -----------> GHOST -> OBSERVE
Conditions d'arrêt appliquées à chaque itération :
- `max_steps` (par défaut : 12)
- `max_wall_time` (par défaut : 90s)
- `max_failures` (par défaut : 3)
- `no_op_loop` (3 réponses vides consécutives)
- `policy_denied` (outil/domaine bloqué)
- `completed` (l'agent répond par un texte)
## Anti-Détection
Trois couches d'anti-détection qui se cumulent. La prévention arrête les blocages avant qu'ils ne se produisent. Ghost Protocol les gère ensuite.
### Camoufox Engine
Navigateur anti-détection enfichable avec usurpation d'empreinte numérique au niveau C++. Aucune astuce manuelle de user-agent — Camoufox génère des empreintes réalistes par contexte au niveau du navigateur, y compris canvas, WebGL, polices et propriétés du navigateur.```bash
# Switch engine (default: chromium)
BROWSER_ENGINE=camoufox
Proxy par requête
Acheminez le trafic de crawling via des pools de proxys résidentiels, de centres de données ou personnalisés. Remplacement par requête avec des valeurs par défaut basées sur les variables d'environnement. Configuration proxy entièrement compatible avec Playwright.```bash
Env-based default
PROXY_SERVER=http://proxy.example.com:10001
PROXY_USERNAME=your_username
PROXY_PASSWORD=your_password
Or per-request
curl -X POST http://localhost:6792/api/crawl
-H "Content-Type: application/json"
-d '{
"url": "https://example.com",
"options": {
"proxy": {
"server": "http://proxy.example.com:10001",
"username": "your_username",
"password": "your_password"
}
}
}'
### Mode furtif
Correctifs `playwright-stealth` facultatifs pour Chromium (ignorés pour Camoufox, où ils sont intégrés). Bloque plus de 20 domaines de suivi/analyse (Google Analytics, DataDome, PerimeterX, etc.) pour réduire la surface d'empreinte numérique.```bash
STEALTH_ENABLED=true
BLOCK_TRACKING_DOMAINS=true
Ghost Protocol
Lorsqu'un résultat d'exploration signale un blocage anti-bot (défi Cloudflare, CAPTCHA,
coquille SPA vide), l'agent peut passer en mode furtif :
- Prendre une capture d'écran pleine page via Playwright
- Envoyer l'image à un LLM doté de capacités de vision (Claude Sonnet ou GPT-4o)
- Extraire le contenu des pixels rendus
- Renvoyer le texte extrait avec
render_mode: "ghost" dans la trace
Cela contourne entièrement la détection anti-bot basée sur le DOM.
Nécessite AGENT_GHOST_ENABLED=true. Déclenchement automatique sur les blocages détectés lorsque AGENT_GHOST_AUTO_TRIGGER=true.
Mesh
Des agents qui dialoguent entre eux. Chaque instance Grub est à la fois un worker et un coordinateur. Le nœud local délègue au cloud, le cloud délègue au local. Les appels d'outils transitent par le réseau de manière transparente.```
Node A (local) Node B (cloud)
┌─────────────┐ ┌─────────────┐
│ AgentEngine │ │ AgentEngine │
│ ↓ │ │ ↓ │
│ MeshDispatcher ──── HTTP ────→ MeshDispatcher │
│ ↓ │ │ ↓ │
│ Dispatcher │ │ Dispatcher │
│ ↓ │ │ ↓ │
│ ToolRegistry │ │ ToolRegistry │
└─────────────┘ └─────────────┘
↕ heartbeat (15s) ↕
└────────────────────────────────┘
**Comment ça marche:**
- **Découverte** — les nœuds se connectent via la liste de pairs seed, puis utilisent le gossip (1-hop) pour découvrir les autres
- **Heartbeat** — toutes les 15s, les nœuds échangent les métriques de charge. 3 manqués = non sain. 2 min = retiré
- **Routage** — MeshDispatcher évalue tous les nœuds selon la charge, la localité et l'affinité, puis achemine les appels d'outils vers le meilleur nœud
- **1 saut max** — Nœud A → B uniquement, jamais A → B → C. Empêche les boucles de routage
- **Repli local** — si l'exécution à distance échoue, repli sur le Dispatcher local
- **Authentification HMAC** — tout le trafic mesh est signé avec un secret partagé (SHA-256, 60s TTL)
### Exécuter un mesh à 2 nœuds localement```bash
# Docker Compose (recommended)
./deploy.sh mesh # Linux/Mac
./deploy.ps1 -Target mesh # Windows
# Verify
curl http://localhost:6792/mesh/peers # Node A sees Node B
curl http://localhost:6793/mesh/peers # Node B sees Node A
Connecter le local à Cloud Run```bash
Deploy to Cloud Run with mesh
./deploy.sh cloudrun latest --mesh-peer http://your-local-ip:6792 --mesh-secret mysecret
Start local node
MESH_ENABLED=true MESH_SECRET=mysecret MESH_PEERS=https://your-cloud-run-url
MESH_ADVERTISE_URL=http://your-local-ip:6792
uvicorn app.main:app --port 6792
### Configuration manuelle```bash
# Node A
MESH_ENABLED=true MESH_NODE_NAME=local MESH_SECRET=test123 \
MESH_ADVERTISE_URL=http://localhost:6792 \
uvicorn app.main:app --port 6792
# Node B
MESH_ENABLED=true MESH_NODE_NAME=cloud MESH_SECRET=test123 \
MESH_PEERS=http://localhost:6792 \
MESH_ADVERTISE_URL=http://localhost:8081 \
uvicorn app.main:app --port 8081
Lorsque le maillage est désactivé (MESH_ENABLED=false, par défaut), Grub fonctionne comme un crawler classique à nœud unique, sans aucun surcoût lié au maillage.
Flux en direct
Regardez le crawler travailler en temps réel. Un pool persistant d'instances Chromium chaudes diffuse les images de la fenêtre d'affichage via WebSocket ou MJPEG.
WebSocket — connectez-vous et envoyez des commandes interactives :```javascript
const ws = new WebSocket("ws://localhost:6792/stream/my-session?url=https://example.com");
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
if (msg.type === "frame") document.getElementById("viewport").src = "data:image/jpeg;base64," + msg.data;
};
// Navigate, click, scroll, type — all over the same socket
ws.send(JSON.stringify({ action: "navigate", url: "https://example.com/pricing" }));
ws.send(JSON.stringify({ action: "click", selector: "#signup-btn" }));
ws.send(JSON.stringify({ action: "scroll", direction: "down" }));
**MJPEG** — placez-le dans une balise ``, vidéo instantanée :```html
<img src="http://localhost:6792/stream/my-session/mjpeg?url=https://example.com" />
Requiert BROWSER_STREAM_ENABLED=true. Chaque instance Chromium utilise ~150-300 Mo de RAM.
Démarrage rapide
Développement local```bash
git clone
cd grub-crawl
cp .env.example .env
pip install -r requirements.txt
uvicorn app.main:app --reload --host 0.0.0.0 --port 6792
### Activer le mode Agent B```bash
# Add to .env
AGENT_ENABLED=true
OPENAI_API_KEY=sk-...
# or
ANTHROPIC_API_KEY=sk-ant-...
AGENT_PROVIDER=anthropic
Soumettre une tâche d'Agent```bash
curl -X POST http://localhost:6792/api/agent/run
-H "Content-Type: application/json"
-d '{
"task": "Find the pricing page on example.com and extract plan details",
"max_steps": 10,
"allowed_domains": ["example.com"]
}'
### Docker```bash
# Single node
./deploy.sh local # or ./deploy.ps1 -Target local
# 2-node mesh
./deploy.sh mesh # or ./deploy.ps1 -Target mesh
# Cloud Run
./deploy.sh cloudrun v1.0.0 # or ./deploy.ps1 -Target cloudrun -Tag v1.0.0
# Cloud Run + mesh (connect to local node)
./deploy.sh cloudrun v1.0.0 --mesh-peer http://your-ip:6792 --mesh-secret mykey
Anti-détection (Camoufox + Proxy)```bash
Add to .env
BROWSER_ENGINE=camoufox
STEALTH_ENABLED=true
BLOCK_TRACKING_DOMAINS=true
Optional: proxy
PROXY_SERVER=http://proxy.example.com:10001
PROXY_USERNAME=your_username
PROXY_PASSWORD=your_password
### Ghost Protocol (contournement anti-bot)```bash
# Add to .env
AGENT_GHOST_ENABLED=true
curl -X POST http://localhost:6792/api/agent/ghost \
-H "Content-Type: application/json" \
-d '{"url": "https://blocked-site.com"}'
Flux de navigateur en direct```bash
Add to .env
BROWSER_STREAM_ENABLED=true
BROWSER_POOL_SIZE=2
MJPEG (open in browser)
open "http://localhost:6792/stream/demo/mjpeg?url=https://example.com"
## Configuration
### Serveur
- `HOST` (défaut : 0.0.0.0)
- `PORT` (défaut : 6792)
- `DEBUG` (défaut : false)
### Stockage
- `STORAGE_PATH` (défaut : ./storage)
- `RUNNING_IN_CLOUD` (défaut : false)
- `GCS_BUCKET_NAME`
- `GOOGLE_CLOUD_PROJECT`
### Authentification
- `DISABLE_AUTH` (défaut : false)
- `GNOSIS_AUTH_URL` (défaut : http://gnosis-auth:5000)
### Moteur de navigation
- `BROWSER_ENGINE` — chromium | camoufox (défaut : chromium)
### Exploration
- `MAX_CONCURRENT_CRAWLS` (défaut : 5)
- `CRAWL_TIMEOUT` (défaut : 30)
- `ENABLE_JAVASCRIPT` (défaut : true)
- `ENABLE_SCREENSHOTS` (défaut : false)
### Proxy
- `PROXY_SERVER` — URL du proxy (p. ex. http://proxy:10001)
- `PROXY_USERNAME`
- `PROXY_PASSWORD`
- `PROXY_BYPASS` — liste de contournement séparée par des virgules
### Furtivité
- `STEALTH_ENABLED` (défaut : false) — correctifs playwright-stealth
- `BLOCK_TRACKING_DOMAINS` (défaut : false) — bloquer les requêtes d'analyse/pistage
### Agent (mode B)
- `AGENT_ENABLED` (défaut : false)
- `AGENT_MAX_STEPS` (défaut : 12)
- `AGENT_MAX_WALL_TIME_MS` (défaut : 90000)
- `AGENT_MAX_FAILURES` (défaut : 3)
- `AGENT_ALLOWED_TOOLS` — liste d'autorisation séparée par des virgules
- `AGENT_ALLOWED_DOMAINS` — liste d'autorisation séparée par des virgules
- `AGENT_BLOCK_PRIVATE_RANGES` (défaut : true)
- `AGENT_REDACT_SECRETS` (défaut : true)
### Fournisseurs LLM
- `AGENT_PROVIDER` — openai | anthropic | ollama (défaut : openai)
- `OPENAI_API_KEY`
- `OPENAI_MODEL` (défaut : gpt-4.1-mini)
- `ANTHROPIC_API_KEY`
- `ANTHROPIC_MODEL` (défaut : claude-3-5-sonnet-latest)
- `OLLAMA_BASE_URL` (défaut : http://localhost:11434)
- `OLLAMA_MODEL` (défaut : llama3.1:8b-instruct)
### Protocole Ghost
- `AGENT_GHOST_ENABLED` (défaut : false)
- `AGENT_GHOST_AUTO_TRIGGER` (défaut : true)
- `AGENT_GHOST_VISION_PROVIDER` — hérite de AGENT_PROVIDER
- `AGENT_GHOST_MAX_IMAGE_WIDTH` (défaut : 1280)
### Mesh
- `MESH_ENABLED` (défaut : false) — interrupteur principal
- `MESH_PEERS` — URL des pairs seed séparées par des virgules
- `MESH_NODE_NAME` — nom lisible (défaut : hostname)
- `MESH_SECRET` — secret HMAC partagé pour l'authentification inter-nœuds
- `MESH_ADVERTISE_URL` — URL utilisée par les pairs pour atteindre ce nœud
- `MESH_PREFER_LOCAL` (défaut : true) — privilégier l'exécution locale
- `MESH_HEARTBEAT_INTERVAL_S` (défaut : 15)
- `MESH_PEER_TIMEOUT_S` (défaut : 45) — marquer comme non sain après ce délai
- `MESH_PEER_REMOVE_S` (défaut : 120) — retirer de la table des pairs après ce délai
- `MESH_REMOTE_TIMEOUT_MS` (défaut : 35000) — délai d'attente pour les appels d'outils distants
### Flux en direct
- `BROWSER_POOL_SIZE` (défaut : 1)
- `BROWSER_STREAM_ENABLED` (défaut : false)
- `BROWSER_STREAM_QUALITY` (défaut : 25) — qualité JPEG 1-100
- `BROWSER_STREAM_MAX_WIDTH` (défaut : 854)
- `BROWSER_STREAM_MAX_LEASE_SECONDS` (défaut : 300)
## Contrat de réponse
`POST /api/markdown` retourne :
`success`, `url`, `final_url`, `status_code`, `markdown`, `markdown_plain`, `content`, `render_mode`, `wait_strategy`, `timings_ms`, `blocked`, `block_reason`, `captcha_detected`, `http_error_family`, `body_char_count`, `body_word_count`, `visible_char_count`, `visible_word_count`, `visible_similarity`, `quarantined`, `quarantine_reason`, `policy_flags`, `content_quality`, `extractor_version`, `normalized_url`, `content_hash`
### Qualité du contenu
- `blocked` — anti-bot/captcha/défi
- `empty` — signal très faible
- `minimal` — pages fines/erreurs
- `sufficient` — utilisable pour la synthèse
Ne pas résumer sauf si `content_quality == "sufficient"`.
### Défense contre l'injection de prompt
- `quarantined=true` signifie que l'extracteur a détecté un texte de type instruction dans le contenu extrait, absent du texte visible rendu de la page (courant dans l'abus de `.sr-only`/contenu masqué visuellement).
- En cas de quarantaine, `content_quality` est dégradée à `minimal`, `policy_flags` inclut `hidden_text_suspected` et `quarantined`, et les sorties `content`/`markdown` sont vidées (échec fermé).
### Format d'erreur```json
{"error": "http_error|validation_error|internal_error", "status": 400, "details": {}}
Benchmarks
Arène de combat — benchmarks en tête-à-tête contre Crawl4AI, Firecrawl (auto-hébergé) et Scrapy. Tous les tests sont exécutés sur la même machine, avec les mêmes URL et les mêmes conditions. Grub s'exécute en premier comme référence, les autres adaptateurs dans un ordre aléatoire avec un délai de 10 s entre chacun pour éviter les biais de limitation de débit.
Vitesse sur URL unique (ms, plus bas = mieux)
Grub remporte 4/5 courses de vitesse sur URL unique. La conversion Markdown s'exécute en 0 à 21 ms via le moteur Rust natif (grub_md).
Répartition des phases de Grub (ms côté serveur)
La navigation domine ; la conversion Markdown est inférieure à la milliseconde sur la plupart des pages grâce au moteur Rust.
Débit par lot (ms, plus bas = mieux)
Grub remporte 2/3 des tailles de lot. Coût par URL : 163-312 ms (Grub) contre 255-477 ms (autres).
Start Grub
docker compose up -d
Start Firecrawl (optional)
docker compose -f combat/firecrawl-compose.yaml up -d
Install combat deps
pip install crawl4ai scrapy markdownify tabulate
Run the arena
pytest combat/ -m combat -v
Generate report
python -m combat.report
## Statut de développement
### Phase 1: Infrastructure de base ✅
### Phase 2: Exploration ✅
### Phase 3: Module Agent ✅
- [x] Cœur de l'agent — machine à états, types, erreurs (W1)
- [x] Contrat d'outil unifié — répartiteur avec délai d'expiration/nouvelle tentative (W2)
- [x] Contrôles de politique — liste blanche de domaines, refus des plages privées, masquage (W3)
- [x] Observabilité — persistance EventBus, TraceCollector, RunSummary (W4)
- [x] Câblage API — `/api/agent/run`, `/api/agent/status`, JobType.AGENT_RUN (W5)
- [x] Adaptateurs de fournisseur — OpenAI, Anthropic, Ollama avec repli (W6)
- [x] Options de configuration — agent, provider, ghost, paramètres de flux (W7)
### Phase 4: Protocole Ghost ✅
- [x] Détection de déclenchement du mode Cloak (W8)
- [x] Pipeline de capture d'écran (W8)
- [x] Extraction visuelle via Claude/GPT-4o (W8)
- [x] Chaîne de repli dans le moteur (W8)
- [x] Outil Ghost pour les appelants externes (W8)
- [x] Outil MCP Ghost + point de terminaison REST (W8)
### Phase 5: Flux de navigateur en direct ✅
- [x] Pool de navigateurs persistant avec prêt/retour (W9)
- [x] Relais de screencast CDP (W9)
- [x] Point de terminaison WebSocket avec commandes interactives (W9)
- [x] Flux de repli MJPEG (W9)
- [x] Points de terminaison d'état du flux + d'état du pool (W9)
### Phase 5.5: Anti-détection ✅
- [x] Moteur de navigateur anti-détection Camoufox (W10)
- [x] Proxy par requête avec repli sur les variables d'environnement (W10)
- [x] Patches furtifs pour Chromium (W10)
- [x] Blocage des domaines de suivi/analyse (W10)
- [x] Correction de la détection du format de vision Anthropic (W10)
### Phase 6: Coordinateur Mesh ✅
- [x] Découverte de pairs avec gossip (1-hop) (W11)
- [x] Authentification inter-nœuds HMAC-SHA256 (W11)
- [x] Boucle Heartbeat avec métriques de charge + nouvelle tentative de seed (W11)
- [x] MeshDispatcher — routage transparent des outils entre nœuds (W12)
- [x] Score basé sur la charge avec bonus de localité/affinité (W12)
- [x] Scripts de déploiement — local, mesh, Cloud Run (W12)
- [x] Topologie mesh Docker Compose à 2 nœuds (W12)
- [x] Page d'atterrissage intégrée (grub-site) (W12)
### Phase 7: Performances + Durcissement
- [x] Moteur markdown Rust (`grub_md`) — extension native PyO3, conversion sub-milliseconde
- [x] Arène de combat — benchmarks automatisés vs Crawl4AI, Firecrawl, Scrapy
- [x] Suite de tests unitaires — 176 tests sur tous les modules
- [ ] Améliorations de la gestion des erreurs
- [ ] Surveillance et alertes
Voir [MASTER_PLAN.md](https://github.com/deepbluedynamics/grubcrawler/blob/HEAD/MASTER_PLAN.md) pour le plan d'architecture complet.
## Licence
Licence du projet Grub Crawler