Automatizez browsere de ani de zile. Selenium, Puppeteer, Playwright, le-am folosit pe toate, le-am văzut pe toate prinse. Cursa înarmării dintre detecția de boți și automatizarea de browsere durează de la începuturile scrapingului, și ghici cine pierde? Absolut fiecare unealtă de automatizare bazată pe Chromium de pe planeta asta nenorocită.
Problema nu sunt uneltele în sine. Playwright e software bun. Puppeteer merge bine. Problema e Chrome DevTools Protocol, mecanismul prin care toate vorbesc cu browserul. CDP e felul în care framework-ul tău de automatizare zice “apasă butonul ăsta” sau “scrie în câmpul ăsta”. Tot el e felul în care Cloudflare, DataDome, PerimeterX și orice alt serviciu de detecție de boți de pe Pământ știu că nu ești om. Poți instala pluginuri de stealth, poți petici navigator.webdriver, poți falsifica amprente până îți sar ochii din cap, CDP tot acolo e, și ăia tot îl găsesc.
Așa că am construit docker-stealthy-auto-browse. Să fiu clar, nu am inventat eu nimic din căcatul ăsta de stealth. Munca grea o fac Camoufox, Playwright, PyAutoGUI și browserforge. Astea sunt proiecte geniale, făcute de oameni mai deștepți ca mine. Ce am făcut eu e că am luat tot căcatul ăsta, l-am legat împreună într-un container Docker și i-am pus un API HTTP deasupra, ca să poți controla toată treaba de la distanță cu comenzi curl. Un container, un endpoint, zero bătaie de cap la instalare.
Problema de Fond a Tuturor Celorlalte Abordări
Uite ce face fiecare unealtă de automatizare bazată pe Chromium: deschide Chrome, se conectează la el prin CDP și trimite comenzi prin protocolul ăsta. Browserul știe. JavaScriptul care rulează în pagină știe. Scriptul serviciului de detecție de boți, ăla care s-a încărcat înaintea conținutului tău? Ăla sigur știe.
Poți încerca să ascunzi:
- Peticești
navigator.webdriversă întoarcăfalse, detectoarele verifică dacă a fost peticit - Instalezi pluginuri de stealth, detectoarele verifică efectele secundare ale pluginurilor alea
- Falsifici amprente, detectoarele compară amprenta contextului principal cu cea din web workeri și găsesc inconsistențe
- Folosești mod headless, detectoarele verifică semnalele de headless
E un joc de-a șoarecele și pisica în care pisica are toate avantajele. CDP lasă urme peste tot, în runtime-ul JavaScript, în felul în care sunt trimise evenimentele, în tiparele de timing, în starea internă a browserului. Încerci să te prefaci că o marionetă nu e marionetă, în timp ce sforile se văd cât casa.
Abordarea: Fără Sfori Deloc
docker-stealthy-auto-browse nu ascunde semnalele de automatizare. Le elimină cu totul.
Camoufox în loc de Chromium. Un fork de Firefox făcut la comandă. Nu există Chrome DevTools Protocol, pentru că Firefox nu îl folosește. Detectoarele de boți care caută semnale CDP nu găsesc nimic, nu pentru că le-am ascuns noi, ci pentru că nu există. navigator.webdriver e false, nu peticit să întoarcă false, ci fals cu adevărat, pentru că Camoufox nici nu îl setează.
Playwright pentru controlul browserului. Se ocupă de treburile la nivel de DOM, navigare, selecție de elemente, inspecție de pagină. Modul de input comod, dar detectabil, trece prin Playwright. Combinat cu Camoufox, nu scurge semnalele obișnuite de automatizare CDP pe care le scurg configurațiile bazate pe Chromium.
PyAutoGUI în loc de evenimente DOM. Când ai nevoie de stealth, mouseul se mișcă fizic pe ecranul virtual, cu curbe ca de om, jitter aleator și accelerație lină. Când scrii, se generează apăsări de taste reale, la nivel de sistem de operare, cu întârzieri randomizate între caractere. Browserul le primește ca input de utilizator autentic. Niciun JavaScript din lumea asta nu poate face diferența dintre input de la PyAutoGUI și un om în carne și oase care stă la tastatură.
Amprente reale prin browserforge. Amprenta e generată o dată și aplicată consistent în contextul principal și în web workeri. Fără falsificare înseamnă fără inconsistențe, un vector de detecție obișnuit, care prinde majoritatea uneltelor de falsificat amprente.
Xvfb pentru un display adevărat. Browserul rulează cu display grafic complet în container, printr-un framebuffer virtual. Fără mod headless, fără semnale de headless. Din punctul de vedere al browserului și al oricărui script de detecție, treaba merge pe un desktop normal.
Contribuția mea e lipiciul: un API HTTP în Python care leagă toate astea, containerul Docker care împachetează totul într-o singură comandă docker run, sistemul de page loadere pentru automatizare declanșată de URL, abstracția celor două moduri de input (system și playwright) și integrarea noVNC pentru vizionare live. Tehnologia de stealth e geniul altora. Împachetarea și API-ul sunt ale mele.
Cum Funcționează
Pornești containerul, expune un API HTTP pe portul 8080. Trimiți comenzi JSON, primești răspunsuri JSON. Asta e toată interfața.
docker run -d --name browser
-p 8080:8080
-p 5900:5900
psyb0t/stealthy-auto-browsePortul 8080 e API-ul. Portul 5900 e un viewer noVNC, ca să te uiți la browser în timp real din propriul tău browser: deschizi http://localhost:5900/ și vezi exact ce vede browserul automatizat.
Navighează undeva:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{"action": "goto", "url": "https://example.com"}'De la v2.6.0, goto (plus refresh și new_tab) acceptă controale de navigare opționale, per apel: timeout în secunde (implicit 30), retry_count pentru reîncercări limitate când încărcarea expiră (implicit 1) și retry_delay între reîncercările alea (implicit 1s). Aceleași trei butoane, fie că îl chemi peste HTTP, prin unealta MCP, sau ca pas înăuntrul unui run_script.
Citește pagina:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{"action": "get_text"}'Găsește tot ce se poate apăsa în pagină:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{"action": "get_interactive_elements"}'Asta îți întoarce fiecare buton, link și input, cu coordonatele lor din viewport, textul și selectorii CSS. Acum apasă unul cu o mișcare de mouse adevărată:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{"action": "system_click", "x": 500, "y": 300}'Scrie cu apăsări de taste reale:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{"action": "system_type", "text": "hello world"}'Fă o captură de ecran:
curl https://ciprian.51k.eu80/screenshot/browser?whLargest=512 -o screenshot.pngRulează scripturi cu mai mulți pași într-o singură cerere, cu run_script, fără să trimiți câte un curl pentru fiecare acțiune:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{
"action": "run_script",
"steps": [
{"action": "goto", "url": "https://example.com", "wait_until": "domcontentloaded"},
{"action": "sleep", "duration": 2},
{"action": "get_text", "output_id": "text"},
{"action": "eval", "expression": "document.title", "output_id": "title"}
]
}'Acceptă și "yaml": "...", cu același format folosit în modul script. În modul cu o singură instanță, cererile sunt serializate automat, trimiți mai multe scripturi în paralel și se pun la coadă în loc să se calce în picioare.
Două Moduri de Input, și Asta Contează
Containerul îți dă două feluri de a interacționa cu paginile, iar alegerea celui potrivit e diferența dintre a trece și a fi blocat.
Input de Sistem: Nedetectabil
system_click, mouse_move, system_type, send_key, scroll, toate astea folosesc PyAutoGUI ca să genereze evenimente reale la nivel de sistem de operare. Mouseul se mișcă pe curbe ca de om. Apăsările de taste au timing randomizat. Browserul nu are absolut nicio cale să afle că nu vin de la o persoană reală.
Lucrezi cu coordonate din viewport, le iei din get_interactive_elements.
Input Playwright: Detectabil, Dar Comod
click, fill, type, astea folosesc automatizarea DOM a lui Playwright, cu selectori CSS sau XPath. Mai rapid, mai simplu, fără calcule de coordonate. Dar tiparele de injectare a evenimentelor sunt teoretic detectabile de o analiză comportamentală serioasă.
Regula e simplă: situl are detecție de boți? Folosești input de sistem. Mereu. Doar scrapezi ceva ce nu se apără? Inputul Playwright e în regulă.
Un Flux de Login Adevărat
Uite cum arată un login nedetectabil, fiecare interacțiune folosește input la nivel de sistem:
API=https://ciprian.51k.eu80
# Navigate to login
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "goto", "url": "https://example.com/login"}'
# Find all interactive elements
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "get_interactive_elements"}'
# Click the email field (coordinates from above)
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "system_click", "x": 400, "y": 200}'
# Type email with human-like keystrokes
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "system_type", "text": "[email protected]"}'
# Tab to password field
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "send_key", "key": "tab"}'
# Type password
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "system_type", "text": "secretpassword"}'
# Submit
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "send_key", "key": "enter"}'
# Wait for redirect
curl -X POST $API -H 'Content-Type: application/json'
-d '{"action": "wait_for_url", "url": "**/dashboard", "timeout": 15}'Situl vede un om adevărat care scrie cu viteză naturală și întârzieri randomizate. Fără semnale CDP. Fără amprente de automatizare. Nimic.
Page Loadere: Automatizare pe Pilot Automat
Page loaderele sunt ca userscripturile de Greasemonkey, dar pentru API-ul HTTP. Scrii un fișier YAML care zice “de câte ori browserul intră pe domeniul ăsta, rulează pașii ăștia automat”. Le montezi în container și uiți de ele.
# loaders/news_site.yaml
name: News Site Cleanup
match:
domain: news-site.com
steps:
- action: goto
url: "${url}"
wait_until: networkidle
- action: wait_for_element
selector: "article"
timeout: 10
- action: eval
expression: "document.querySelector('.cookie-consent')?.remove()"
- action: eval
expression: "document.querySelector('.newsletter-overlay')?.remove()"
- action: scroll_to_bottom
delay: 0.3Acum fiecare goto spre news-site.com așteaptă automat conținutul, omoară popupul de cookies, omoară modalul de newsletter și derulează ca să declanșeze imaginile încărcate leneș. Gata cu trimis 5 comenzi după fiecare navigare.
API-ul Complet
API-ul HTTP acoperă tot ce ți-ar trebui:
- Navigare:
goto,refresh, cu condiții de așteptare configurabile - Input de sistem:
system_click,mouse_move,system_type,send_key,scroll, toate la nivel de sistem de operare, toate nedetectabile - Inspecție de pagină:
get_interactive_elements,get_text,get_html,eval - Condiții de așteptare:
wait_for_element,wait_for_text,wait_for_url,wait_for_network_idle, pentru căsleepe pentru amatori - Administrare de taburi:
list_tabs,new_tab,switch_tab,close_tab - Cookies și storage: CRUD complet pe cookies, localStorage, sessionStorage
- Descărcări și încărcări: gestionezi descărcări de fișiere și inputuri de fișier programatic
- Logare de rețea: înregistrezi toate cererile HTTP pe care le face pagina, găsești endpointuri de API, depanezi, verifici
- Capturi de ecran: viewportul browserului sau desktopul întreg, cu parametri de redimensionare
- Gestionare de dialoguri: acceptare automată sau răspunsuri configurate pentru alert, confirm și prompt
- Înregistrare de ecran:
start_recording,stop_recording,recording_status, MP4 cu pixelii chiar randați
Două endpointuri de captură îți dau viewportul browserului (cum arată pagina) sau desktopul virtual întreg (inclusiv interfața browserului). Ambele suportă parametri de redimensionare, ca să nu descarci PNG-uri de 1920×1080 de fiecare dată:
# Resize longest side to 512px
curl https://ciprian.51k.eu80/screenshot/browser?whLargest=512 -o shot.png
# Full desktop including browser chrome
curl https://ciprian.51k.eu80/screenshot/desktop?whLargest=512 -o desktop.pngCameră și Microfon Virtuale
Pui un fișier video și/sau audio într-un director /media montat, iar paginile primesc piste de cameră și microfon din fișierele alea, prin navigator.mediaDevices.getUserMedia(). Căile sunt validate la pornire, symlinkurile rezolvate, ca să rămână în directorul de media configurat. Ceri un tip care nu e configurat și cererea pică, nu trece pe tăcute pe un dispozitiv real, ceea ce e chiar comportamentul pe care îl vrei când tot rostul e că nu există niciun dispozitiv real.
Fișierele statice au fost prima versiune. Setezi VIRTUAL_MEDIA_DYNAMIC=true și poți schimba sursa în timpul rulării: set_virtual_media_source alege un fișier existent dinăuntru, upload_virtual_media ia un payload base64 limitat. Ambele păstrează identitățile pistelor de cameră și microfon pe care pagina le-a obținut deja, deci o pagină poate schimba ce vede fără să ceară din nou getUserMedia() și fără să observe că s-a întâmplat ceva.
Încărcările nu sunt o portiță: nume de fișier generate ferite de coliziuni, în loc să suprascrie o sursă cu nume, decodare base64 strictă, un plafon configurabil VIRTUAL_MEDIA_UPLOAD_MAX_BYTES (implicit 50 MiB) și o verificare ffprobe pentru fluxul cerut, înainte ca ceva să fie stocat sau activat. Selecția sursei acceptă doar fișiere obișnuite aflate în VIRTUAL_MEDIA_DIR, fără URL-uri la distanță, fără fluxuri WebSocket, fără căi arbitrare de pe gazdă, fără altă intrare vie. get_virtual_media_state raportează starea modului dinamic, numele sursei active și un contor de revizie, fără să scurgă căi de sursă sau octeți încărcați.
Scraping Fără Să Scrii JavaScript
Patru acțiuni care acoperă ce ai fi scris altfel de mână într-un apel evaluate, de fiecare dată: get_page_info, get_element, get_elements și get_computed_style. Date de pagină și de CSS direct, fără JS de scris, fără coșmarul de ghilimele ca să le treci prin JSON. get_elements are acum implicit 20 de rezultate, consistent, în API-ul HTTP, în documentația MCP și în fixture-ul de test, ceea ce nu era mereu cazul.
Înregistrare de Ecran
Capturile îți zic cum a arătat o pagină. Nu îți zic ce s-a întâmplat. Așa că acum browserul se înregistrează singur: ffmpeg cu x11grab pe displayul Xvfb, scriind MP4 într-un volum /recordings montat. Pixelii chiar randați, inclusiv cursorul de mouse la nivel de sistem care se plimbă, pentru că în chestia asta cursorul e real.
Trei moduri: window ia fereastra Camoufox întreagă, viewport taie interfața browserului folosind offseturile calibrate mozInnerScreenX/Y (nu ghicituri hardcodate), iar desktop ia tot ecranul Xvfb.
mkdir -p ./recordings
docker run -d -p 8080:8080 -v ./recordings:/recordings psyb0t/stealthy-auto-browse
# start
curl -X POST https://ciprian.51k.eu80/action
-d '{"action": "start_recording", "mode": "viewport", "fps": 20}'
# ... drive the browser ...
# stop — you name the file at stop time, after you know how it went
curl -X POST https://ciprian.51k.eu80/action
-d '{"action": "stop_recording", "slug": "my-flow"}'
# → ./recordings/my-flow.mp4Slugul se dă la oprire, intenționat, îți botezi înregistrarea după ce se termină rularea, când chiar știi dacă e login-success sau login-broke-again. Slugurile sunt curățate împotriva traversării de căi, iar coliziunile se redenumesc singure. O singură înregistrare activă per container. Rezistent la crash: închidere curată pe SIGINT, plus o trecere la pornire care mătură fișierele temporare orfane.
show_cursor e implicit true, dar îl stingi când faci capturi de regresie vizuală și pixelii de cursor ți-ar otrăvi diferența. Descriptorul din răspuns îți dă flagul înapoi, ca apelantul să confirme ce a primit.
Merge peste API-ul HTTP și prin MCP. În modul cluster, pornirea și oprirea trebuie să stea înăuntrul aceluiași apel run_script, ca să nimerească aceeași instanță, altfel zici unui container să oprească o înregistrare pornită de alt container.
Rezoluție. Înregistrarea se rupea peste 1920×1080, pentru că entrypointul pornea Xvfb la mărimea aia, iar xrandr nu poate crește framebufferul rădăcină după aceea, deci ffmpeg captura fericit în afara ecranului real. Framebufferul e alocat acum la XVFB_RESOLUTION din start, iar redimensionarea a dispărut. Rezoluțiile pătrate și înalte merg, plafonul de 1920×1080 nu mai există.
Când Moare Camoufox, Se Întoarce
Un utilizator a dat de “Connection closed while reading from the driver” după câteva rulări n8n pe Facebook. Cauza era urâtă: browserul ținea în cache un obiect Page mort după ce Camoufox murise, deci fiecare cerere de după încerca să vorbească cu un cadavru.
Acum există un health check adevărat. is_healthy() face dus-întors până la driver, în loc să se încreadă în starea din cache, iar ensure_healthy() relansează contextul persistent dacă dus-întorsul pică, profilul supraviețuiește, deci cookie-urile și amprenta trec mai departe. Și getterul intern de pagină, și accesorul de pagină activă vindecă întâi și folosesc după. Cererea care declanșează recuperarea mănâncă vreo 4-5 secunde, tot ce vine după merge la viteză maximă.
Fiecare recuperare lasă și un postmortem la nivel WARNING: liniile de OOM din dmesg, meminfo, loadavg și lista proceselor camoufox-bin rămase. Deci când moare, primești cauza adevărată în logul JSON, în loc de o oprire misterioasă de container.
Configurarea de Stealth Care Chiar Contează
Câteva variabile de mediu care chiar influențează dacă ești prins:
Potrivirea fusului orar. Detectoarele de boți compară fusul orar al browserului cu geolocația IP-ului tău. Dacă IP-ul zice România și fusul zice UTC, ăla e un steag roșu. Setezi TZ=Europe/Bucharest (sau ce se potrivește cu IP-ul tău) și vectorul ăsta dispare.
Suport de proxy. Trimiți tot traficul printr-o ieșire, cu PROXY_URL, fie http://user:pass@host:port, fie socks5://host:port, ce ai. Combinat cu potrivirea fusului, arăți ca un utilizator real din locația ieșirii ăleia, motiv pentru care documentația zice pe șleau că ar trebui să fie o ieșire autorizată, a cărei locație se potrivește cu amprenta pe care o testezi. Repo-ul documentează acum și o configurație în care ieșirea e a ta, nu una închiriată: o celulă WireGuard pr0xteus de unică folosință. Ghidul ăla a fost rescris pentru pr0xteus v0.11.0, care întoarce două URL-uri per lease în loc de unul, iar browserul îl ia acum pe cel HTTP: jq -er '.proxies.http'. Nu e o preferință de stil. Firefox nu face SOCKS5 autentificat în mod fiabil, deci să îndrepți Camoufox spre URL-ul SOCKS e felul în care obții un proxy care merge exact până în clipa în care are nevoie de credențiale. Restul decurge de acolo: ajungi la controller prin --network host, nu intrând într-o rețea de egress, ții API-ul de control pe 127.0.0.1:8000 și proxy-ul HTTP pe 127.0.0.1:8080, și muți API-ul browserului pe 8090, cu HTTP_LISTEN_PORT=8090, ca să nu se bată cele două pe 8080. Nimic din aranjamentul ăsta nu e accesibil din afara gazdei. Ghidul complet e în docs/configuration.md.
Profiluri persistente. Montezi un director la /userdata și cookie-urile, localStorage, sesiunile și amprenta supraviețuiesc repornirilor de container. Fără asta, fiecare repornire e o identitate nouă, ceea ce uneori e exact ce vrei, iar alteori e suspect ca dracu.
docker run -d
-e TZ=Europe/Bucharest
-e PROXY_URL=http://user:pass@proxy:8888
-v ./my-profile:/userdata
-p 8080:8080
-p 5900:5900
psyb0t/stealthy-auto-browseExtensii Preinstalate
Fiecare container vine cu extensii de confidențialitate deja configurate:
- uBlock Origin, blochează reclame, trackere și alte enervări. Mai puțin zgomot, mai puține scripturi de tracking în execuție
- LocalCDN, interceptează cererile spre CDN-uri și servește resursele local. Google și Cloudflare nu te mai pot urmări de la sit la sit
- ClearURLs, curăță parametrii de tracking (utm_source, fbclid, gclid) din URL-uri
- Consent-O-Matic, respinge automat popupurile de consimțământ pentru cookies, ca să nu te mai ocupi tu de căcatul ăla
Vrei mai multe? Montezi un profil persistent, deschizi VNC, navighezi la about:addons și instalezi ce vrei. Vor rezista peste reporniri.
Rezultate la Testele de Detecție de Boți
Testat pe tot ce contează și trecut peste tot:
- CreepJS, consistență de amprentă canvas și WebGL, detecție de minciuni, comparație cu workerii: trecut
- BrowserScan, flagul WebDriver, semnale CDP, proprietăți de navigator: trecut
- Pixelscan, coerență de amprentă, potrivire fus orar cu IP, scurgeri WebRTC: trecut
- Cloudflare, pagini de challenge, Turnstile, bot management: trecut
- SannySoft, teste Intoli plus scanner de amprentă: trecut
- Incolumitas, tehnici moderne de detecție: trecut
- Rebrowser, detecție de scurgeri CDP, webdriver, analiză de viewport: trecut
- BrowserLeaks WebRTC, detecție de scurgeri de IP prin WebRTC: trecut
- DeviceAndBrowserInfo, 19 verificări, toate verzi, “You are human!”: trecut
- IpHey, calificativ “Trustworthy”: trecut
- Fingerprint.com, identificat ca Firefox normal, fără steaguri de bot: trecut
Trece pentru că nu e nimic de detectat. Niciun CDP de găsit, pentru că Firefox nu are așa ceva. Nicio amprentă falsificată, pentru că amprenta e reală și consistentă. Niciun flag de automatizare, pentru că navigator.webdriver e fals de-adevăratelea. Niciun eveniment de input fals, pentru că PyAutoGUI le generează reale, la nivel de sistem de operare.
Îți Zice Că E un CAPTCHA, și Nimic Altceva
detect_challenge raportează dovezi limitate și reduse la minimul de date pentru integrări documentate de Turnstile, reCAPTCHA, hCaptcha, Friendly Captcha, ALTCHA, Arkose, AWS WAF și GeeTest, plus indicii prudente pentru cele generice vizibile. Disponibil peste HTTP, în modul script, în run_script și prin MCP.
Citește verbele cu atenție, pentru că omisiunile sunt tot designul: nu apasă niciodată, nu rezolvă niciodată, nu intră niciodată în frame-ul de challenge și nu expune niciodată query stringuri, chei de sit sau tokenuri de răspuns. Dovezile de resursă Arkose au segmentele de cale care poartă chei redactate înainte ca vreun răspuns de API, script sau MCP să le poată întoarce. Asta îți zice că un challenge e acolo. Nu te trece de el și nici nu încearcă.
Însoțitorul e scroll_into_view: true, care aduce primul frame sau widget detectat și randat în viewport, fără să îl apese, să îl focuseze, să îl rezolve, să îl trimită sau să intre în el. Aia e pentru predare: automatizarea ta se izbește de un zid, derulează zidul în vizor, iar un om îl preia peste sesiunea noVNC care stătea acolo tot timpul. Browserul a fost mereu de privit, acum îți poate zice și când să te uiți.
Server MCP
Agenții AI pot controla browserul prin Model Context Protocol, cu Streamable HTTP la /mcp pe portul 8080. Toate acțiunile browserului sunt expuse ca unelte MCP: navigare, capturi, apăsat, scris, evaluare de JavaScript, cookies, tot.
Conectezi orice client compatibil MCP, Claude Desktop, Claude Code, agenți proprii, la https://ciprian.51k.eu80/mcp/ și începi să navighezi. Merge și în mod standalone, și în mod cluster, HAProxy rutează traficul MCP cu aceleași sesiuni lipicioase ca API-ul HTTP.
În modul cluster, serverul MCP expune doar run_script (plus ping și sleep) ca unelte. Acțiunile individuale ca goto, get_text, screenshot și așa mai departe nu sunt disponibile ca unelte MCP separate când rulezi în spatele unui cluster. Asta e intenționat, vezi secțiunea de mod cluster de mai jos pentru motiv.
Asta e separat de abordarea cu directorul .agents/.skills/ menționată mai jos. Skillurile învață AI-ul cum să folosească API-ul HTTP cu curl. MCP îi dă AI-ului acces nativ la unelte, fără curl, fără HTTP, acțiunile browserului apar direct ca unelte apelabile. Folosește ce ți se potrivește.
Autentificare
Setezi AUTH_TOKEN ca să ceri un bearer token la toate cererile (în afară de /health):
docker run -d -p 8080:8080 -e AUTH_TOKEN=mysecretkey psyb0t/stealthy-auto-browseTrimiți tokenul în headerul Authorization:
curl -H "Authorization: Bearer mysecretkey" https://ciprian.51k.eu80 ...v2.0.0 a omorât forma cu query param. Înainte accepta și ?auth_token=mysecretkey, ceea ce era comod pentru clienții MCP care nu puteau seta headere și oribil pentru tot restul, tokenurile în poziția aia se scurg în logurile de acces, în istoricul browserului și în headerele Referer. Acum headerul e singura formă acceptată, iar simpla prezență a unui query param auth_token înseamnă 401 instant.
Merită știut când migrezi: verificarea aia de query rulează înainte de verificarea de header, deci un client care trimite un header Authorization perfect valid plus un ?auth_token= uitat pe acolo tot mănâncă 401. Scoate query paramul, nu doar adăuga headerul și pleca liniștit. Comparația e acum și în timp constant (hmac.compare_digest), nu un simplu !=, deci nu poți deduce tokenul bucată cu bucată cronometrând răspunsurile.
Autentificarea rămâne opțională totuși: lași AUTH_TOKEN nesetat și fiecare endpoint în afară de /health e deschis pentru orice ajunge la port.
Făcut Pentru Agenți AI
Uite chestia despre care nu vorbește nimeni la automatizarea de browsere: cel mai bun caz de utilizare în 2026 nu e vreun script Python care rulează o buclă de scraping. Sunt agenții AI care trebuie să interacționeze cu webul ca un om.
Folosesc Claude Code tot timpul, și jumătate din ce am nevoie să facă implică pagini web: completat formulare, verificat dashboarduri, luat date de pe situri care nu au API, umblat prin panouri de admin. Problema cu a-i da un browser unui LLM a fost mereu interfața. Selenium? Prea complicat. API-ul Playwright? Prea multe piese în mișcare. LLM-ul ajunge să scrie 50 de linii de setup înainte să apese un singur buton.
docker-stealthy-auto-browse a fost gândit din temelii să fie prietenos cu AI-ul. Toată interfața e comenzi curl cu JSON. Atât. Un LLM nu trebuie să importe biblioteci, să administreze instanțe de browser, să se lupte cu contexte async sau cu vreun alt gunoi de-ăsta. Trimite doar cereri HTTP.
Gândește-te ce îi trebuie unui agent AI ca să navigheze pe web:
- Să ajungă undeva, un curl spre
goto - Să înțeleagă ce e în pagină, un curl spre
get_text. AI-ul citește textul și știe la ce se uită. Dacă textul nu ajunge,get_interactive_elementsîntoarce fiecare lucru apăsabil, cu coordonate și etichete. Dacă tot e confuz, faci o captură, Claude știe să citească imagini - Să interacționeze cu elementele, un curl spre
system_clickcu coordonate x,y, un curl spresystem_typepentru text - Să aștepte rezultatele, un curl spre
wait_for_textsauwait_for_element - Să verifice rezultatul, încă un curl spre
get_text
Fără SDK. Fără instalat drivere. Fără administrat ciclul de viață al browserului. Containerul se ocupă de tot. AI-ul doar vorbește cu un endpoint HTTP.
Am pus Claude Code să facă lucruri ca:
- Să se logheze în dashboarduri web, să navigheze la pagini anume, să extragă date și să le rezume
- Să completeze formulare cu mai mulți pași pe situri care cer randare JavaScript
- Să monitorizeze pagini pentru schimbări și să mă anunțe când se actualizează ceva
- Să umble prin panouri de admin care nu au API, apăsând butoane, schimbând setări, descărcând exporturi
- Să caute chestii pe situri care blochează cererile HTTP obișnuite în spatele Cloudflare
Repo-ul vine cu un director .agents/.skills/ care conține o definiție completă de skill pentru agenții de cod AI. Clonezi repo-ul (sau doar directorul .agents/) în proiectul tău și Claude Code îl descoperă automat. Setezi STEALTHY_AUTO_BROWSE_URL=https://ciprian.51k.eu80 și agentul are referința completă de API, ambele moduri de input, fluxurile tipice și exemplele, tot ce îi trebuie ca să navigheze pe web din prima.
E disponibil și pe ClawHub. Îl instalezi cu clawhub install psyb0t/stealthy-auto-browse și orice agent AI compatibil OpenClaw poate folosi browserul la cerere.
Combinația dintre un API HTTP simplu ca bună ziua, stealth complet împotriva detecției de boți și instrucțiuni încorporate pentru agenți AI face din asta cea mai bună unealtă de automatizare de browsere pentru LLM-uri pe care am găsit-o. Și am căutat, crede-mă. Tot restul fie cere un setup de SDK complicat, care buimăcește AI-ul, fie e prins de Cloudflare din prima cerere, fie amândouă.
Modul Script: Rulează și Ieși
Rulezi un script YAML la pornirea containerului, execută pașii, îți dă rezultatele ca JSON pe stdout, iar containerul iese. Fără server HTTP, fără proces de lungă durată. Bun pentru CI, joburi cron, scraping într-o singură trecere, sau orice unde vrei să automatizezi o secvență și să iei output.
# Pipe a script in, get JSON results out
cat my-script.yaml | docker run --rm -i
psyb0t/stealthy-auto-browse --script > results.json
# Parameterize with environment variables
cat my-script.yaml | docker run --rm -i
-e TARGET_URL=https://example.com
psyb0t/stealthy-auto-browse --scriptFormatul de script e aceleași acțiuni ca API-ul HTTP, dar în YAML:
name: Scrape Example
on_error: stop # "stop" (default) or "continue"
steps:
- action: goto
url: ${env.TARGET_URL}
wait_until: networkidle
- action: save_screenshot
output_id: page_screenshot
whLargest: 1024
- action: get_text
output_id: page_text
- action: eval
expression: "document.title"
output_id: titlePașii cu output_id ajung în JSON-ul de ieșire. Capturile ies ca PNG-uri codate base64. ${env.VAR_NAME} e înlocuit cu variabile de mediu. Logurile se duc pe stderr, deci redirectând stdout obții JSON curat. Cod de ieșire 0 dacă toți pașii reușesc, 1 dacă vreunul pică. Page loaderele tot se declanșează pe goto, dacă sunt configurate.
Scripturile Pot Ramifica și Bucla Acum (În Limite)
O listă plată de pași rămâne repede fără drum. “Apasă accept dacă bannerul de cookies e acolo.” “Tot derulează până dispare butonul de pagina următoare.” Modul script și run_script le fac pe ambele acum: ramuri if imbricate, plus bucle repeat și while.
Condițiile acoperă starea elementelor CSS, textul vizibil, globuri de URL, rezultate booleene de JavaScript și ieșirile denumite ale pașilor anteriori, deci o ramură de mai târziu poate reacționa la ce a găsit chiar un pas de mai devreme, în loc să ghicești tu la momentul trimiterii.
Cuvântul care face toată treaba în titlul ăla e limite. Numărul de iterații de buclă, munca totală de buclă, timeoutul condiției și adâncimea de imbricare sunt toate plafonate. Un script trimis trebuie să fie finit, pentru că chestia asta acceptă scripturi peste HTTP, iar o buclă while fără plafon e o primitivă de denial-of-service pe care ai împărțit-o intenționat.
Modul Cluster
Ai nevoie să tratezi cereri concurente? Rulezi mai multe instanțe de browser în spatele HAProxy, cu o coadă de cereri și sincronizare de cookies prin Redis. Fiecare browser tratează o cerere pe rând, proxy-ul le pune pe restul la coadă până se eliberează un loc.
curl -LO https://raw.githubusercontent.com/psyb0t/docker-stealthy-auto-browse/main/docker-compose.cluster.yml
curl -LO https://raw.githubusercontent.com/psyb0t/docker-stealthy-auto-browse/main/haproxy.cfg.template
docker compose -f docker-compose.cluster.yml up -dAsta pornește Redis, 5 containere de browser (configurabile prin NUM_REPLICAS) și queue-proxy-ul HAProxy. Punctul de intrare e https://ciprian.51k.eu80, același API ca în modul cu un singur container. MCP la /mcp/ merge și el prin proxy.
În modul cluster, se acceptă doar run_script. Dacă trimiți acțiuni individuale ca goto, get_text, click sau screenshot direct, primești eroare. Asta e intenționat: fiecare cerere dintr-o secvență cu mai mulți pași poate nimeri pe altă instanță de browser, dacă clientul nu administrează atent sesiunile lipicioase, iar când nu o face, capeți buguri de conținut vechi, subtile și înnebunitoare. run_script e atomic. Fiecare pas din script rulează pe aceeași instanță de browser, în aceeași cerere. Nicio sesiune lipicioasă de administrat. Nicio scurgere de stare între instanțe.
În modul standalone (un singur container, fără cluster), acțiunile individuale merg în continuare bine, cererile sunt serializate automat, deci nu e nicio problemă de concurență.
Sintaxa e identică cu ce ai folosi în modul standalone. Secvență completă de login, navigare, extragere, toate dintr-un foc:
curl -X POST https://ciprian.51k.eu80
-H "Content-Type: application/json"
-d '{
"action": "run_script",
"steps": [
{"action": "goto", "url": "https://example.com/login", "wait_until": "domcontentloaded"},
{"action": "system_click", "x": 400, "y": 200},
{"action": "system_type", "text": "[email protected]"},
{"action": "send_key", "key": "tab"},
{"action": "system_type", "text": "secretpassword"},
{"action": "send_key", "key": "enter"},
{"action": "wait_for_url", "url": "**/dashboard", "timeout": 15},
{"action": "get_text", "output_id": "page"}
]
}'HAProxy se ocupă de rutare pe dinăuntru, alege o instanță de browser liberă și ține tot scriptul pe instanța aia. Nu te atingi niciodată de cookie-uri INSTANCEID. Nu te gândești deloc la rutare.
Sincronizarea de cookies prin Redis e atuul cel mare. Cookie-urile setate pe orice instanță se propagă instantaneu la toate celelalte, prin Redis PubSub. Te loghezi pe browser1 și browser2 până la browser10 sunt imediat autentificate. Rulezi un singur script de login pe orice instanță și toată flota e logată, fără să repeți loginul pe fiecare browser.
HAProxy expune un dashboard de statistici pe portul 8081: trafic live, adâncime de coadă, sănătatea serverelor, rate de cereri per instanță.
Skill și Plugin
Repo-ul livrează un skill de agent și un plugin OpenClaw sub .agents/, ambele publicate pe ClawHub de CI la push-uri de tag. Îndrepți un agent spre plugin și el conduce direct endpointul MCP al unei instanțe care rulează, fără cod de legătură, fără să explici lista de acțiuni la fiecare sesiune.
Pe Scurt
Orice altă unealtă de automatizare de browsere joacă în apărare, ascunde semnale CDP, petecește vectori de detecție, speră că următorul update de Cloudflare nu o să îi strice pluginul de stealth. docker-stealthy-auto-browse nu joacă jocul ăla. Nu e niciun CDP de ascuns. Nu sunt semnale de automatizare de peticit. Browserul chiar nu știe că e automatizat.
Un container Docker. Un API HTTP. Trece de fiecare detector de boți pe care i l-am aruncat în față.
Ia-l de aici: github.com/psyb0t/docker-stealthy-auto-browse
Licențiat sub WTFPL, Do What The Fuck You Want To Public License. Pentru că evident.
Cum Îl Instalezi în Agentul Tău
Având în vedere că tot rostul e ca agenții să conducă browsere, calea de instalare contează aici mai mult decât în alte părți. Tot ce e sub .agents/ e catalogat într-un singur marketplace, deci sunt două comenzi:
claude plugin marketplace add psyb0t/agents
claude plugin install stealthy-auto-browse@psyb0tCodex folosește același marketplace cu alt verb, codex plugin add stealthy-auto-browse@psyb0t, pentru că nu există codex plugin install. Găsește singur și skillul într-un checkout al repo-ului, pentru că scanează .agents/skills/ nativ, fără să fie instalat absolut nimic. E listat acum și pe MCP Registry-ul oficial, deci un client care rezolvă servere de acolo îl poate găsi fără să i se dea un URL.