agent-browser
agent-browser
Ein gehärteter lokaler Browser für KI-Agenten. Null Abhängigkeiten. Treibt das Chrome, das Sie bereits haben, über das DevTools-Protokoll, extrahiert token-effizientes Markdown und wird nicht trivial als Automatisierung erkannt.
Oberfläche | Installation | Verwendung für |
MCP-Server |
| Claude Code, Cursor, Codex, jeder MCP-Client |
CLI |
| Shells, Skripte, CI |
Bibliothek |
| Ihr eigener Node-Code |
DSH-/Cordis-Plugin | Kompositionszeile | native Tools in einer DSH-Umgebung |
npx -y @truenix/agent-browser markdown https://news.ycombinator.comAlles läuft lokal. Kein Konto, kein API-Schlüssel, kein Remote-Dienst, kein Kontingent.
Warum
Wenn man einem Agenten rohes HTML füttert, verschwendet das den Großteil seines Kontexts. Gemessen auf echten Seiten:
Und ein Browser, der sich als Automatisierung zu erkennen gibt, wird blockiert, degradiert oder erhält andere Inhalte – was stillschweigend das korrumpiert, was der Agent geschlossen hat.
Benchmark für schwere Seiten (live, curl vs. markdown)
Seite | rohes HTML | Markdown |
| Ersparnis |
| 927 kB | 120 kB | 55 kB | 7.7× / 16.8× |
| 273 kB | 11 kB | 8.7 kB | 23.8× / 31.4× |
| 323 kB | 10 kB | 7.3 kB | 32.0× / 44.2× |
| 648 kB | 69 kB | 13 kB | 9.3× / 48.9× |
| 360 kB | 4.6 kB | 3.5 kB | 78.6× / 102.8× |
| 35 kB | 10 kB | 3.2 kB | 3.4× / 11.0× |
Regenerieren Sie beide Diagramme und die zugrunde liegenden Messwerte dieser Tabelle mit npm run charts – es misst live und zeichnet dann die PNGs mit agent-browser selbst, sodass die Zahlen in den Bildern nicht von dem abweichen können, was der Code erzeugt.
Weniger Tokens für dieselbe Seite ausgeben
URLs machen ein Drittel der Ausgabe bei einem Wikipedia-Artikel und vier Fünftel bei github.com/trending aus. Wenn der Agent liest statt zu navigieren, lassen Sie sie weg:
agent-browser markdown https://github.com/trending --links text # 69 kB -> 13 kB
agent-browser markdown https://en.wikipedia.org/wiki/Rust --links relativeModus | rendert | Verwendung, wenn |
|
| der Agent als Nächstes navigieren wird |
|
| Crawling innerhalb derselben Site; behält Ziele, verwirft die Herkunft |
|
| Lesen, Zusammenfassen, Beantworten von Fragen |

Token-Kosten pro YC-Check (HN + YC-Blog gelesen mit --links text, 51,1k → 1,5k Tokens, +500 Ausgabe) — Preisliste August 2026: Anthropic Haiku 4.5 $1/$5, Sonnet 5 $3/$15, Opus 5 $5/$25 • OpenAI GPT-4o $2.50/$10, GPT-4o mini $0.15/$0.60
Seiten, die nach dem Laden rendern
Die Navigation wartet auf das load-Ereignis, gesteuert durch CDP-Lebenszyklus-Ereignisse statt einer Polling-Schleife und einer festen Verzögerung. Auf echten Seiten ist das 2–3× schneller bei byteidentischer Ausgabe:
Seite |
| alte feste 250 ms Wartezeit |
| 7 ms | 258 ms |
| 115 ms | 340 ms |
| 160 ms | 365 ms |
| 218 ms | 466 ms |
Eine client-gerenderte App, deren Inhalt nach dem Laden eintrifft, braucht ein echtes Signal, keine größere Vermutung – die alte 250-ms-Wartezeit hat diesen Inhalt ebenfalls verpasst:
agent-browser markdown https://some-spa.example --wait idleModus | wartet auf | Verwendung, wenn |
| das DOM geparst ist | Sie nur Markup benötigen, das im HTML enthalten ist |
| das load-Ereignis | fast immer |
| das Netzwerk ruhig wird | das Ergebnis wie eine leere Hülle aussieht |
Related MCP server: Browser-MCP Navigator
Jede andere Umgebung
Wenn Ihr Framework JSON Schema spricht, kann es agent-browser steuern, ohne dass dieses Projekt von der Existenz des Frameworks weiß. agent-browser tools gibt den Katalog in der Form aus, die Sie benötigen:
agent-browser tools # MCP: {name, description, inputSchema}
agent-browser tools --format openai # OpenAI: {type:"function", function:{...}}
agent-browser tools --format anthropic # Anthropic: {name, description, input_schema}Jedes Tool läuft dann auf beide Arten, je nachdem, was Ihr Harness kann:
import { findTool, TOOLS } from '@truenix/agent-browser/tools';
import { withBrowser } from '@truenix/agent-browser';
// in-process: the harness can hold a CDP session
const tool = findTool('browser_markdown');
const text = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return tool.run(session, { url: 'https://example.com', links: 'text' });
});
// out-of-process: the harness can only run a command (sandboxes, shells)
tool.cli({ url: 'https://example.com', links: 'text' });
// => ['markdown', 'https://example.com', '--links', 'text']Installation
Als MCP-Server
claude mcp add browser -- npx -y @truenix/agent-browser mcp{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "@truenix/agent-browser", "mcp"]
}
}
}Tools: browser_markdown, browser_text, browser_html, browser_links, browser_screenshot, browser_evaluate, browser_accessibility_tree, browser_pdf, browser_probe.
Jede Oberfläche stellt dieselben neun Tools aus einem gemeinsamen Katalog bereit, sodass CLI, MCP-Server und DSH-Plugin nie auseinanderdriften können.
Als Bibliothek
npm install @truenix/agent-browserimport { withBrowser } from '@truenix/agent-browser';
const md = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});Als DSH-/Cordis-Plugin
Einzeiler (empfohlen – verdrahtet automatisch, wenn Sie DSH meinen):
npx -y @truenix/agent-browser install # adds the bundle to ~/.dsh/profiles/web/package.json, then pnpm install (the mount ships in the package's own cordis.patch.yml layer)
# npx -y @truenix/agent-browser install --profile web --dry-run # preview
# npx -y @truenix/agent-browser uninstall # remove againStarten Sie dsh neu – alle neun browser_*-Tools erscheinen als native Tools. Bei einer einfachen npm install wird kein Handler ausgeführt; eine absichtliche install ist erforderlich.
Manuell (wenn Sie die Komposition lieber selbst bearbeiten):
npm i -g @truenix/agent-browser
# or inside the harness checkout: pnpm add @truenix/agent-browserErfordert Node ≥ 18 und eine Chrome/Chromium-Installation. Fügen Sie dann zur Host-Komposition hinzu (die Tool-Registry lebt auf dem Host, nicht pro Agent):
# ~/.dsh/profiles/web/cordis.patch.yml — persists for every web session
- insert:
- id: agent-browser
name: '@truenix/agent-browser/cordis'
config:
timeoutMs: 180000 # per-tool call budget; default respects AGENT_BROWSER_BIN / ENDPOINT
# cli: 'npx -y @truenix/agent-browser' # override only if neededKurzform (wenn die Komposition bereits insert umschließt):
- '@truenix/agent-browser/cordis':
timeoutMs: 180000Env-Overrides: AGENT_BROWSER_BIN (Chrome-Binärdatei), AGENT_BROWSER_ENDPOINT (an langlebigen Browser über --endpoint anhängen) oder config.cli.
CLI
agent-browser <command> [options]
markdown <url> Extract the whole page as Markdown (main content by default)
text <url> Visible text only
html <url> Full serialized DOM after JavaScript runs
links <url> Every anchor as JSON
screenshot <url> PNG/JPEG (-o file, --full)
pdf <url> PDF (-o file)
a11y <url> Filtered accessibility tree
eval <url> <expr> Evaluate JS, return only its value (cheapest)
probe Browser, GPU and capability report
mcp Run as an MCP server on stdio
tools Print tool schemas (--format mcp|openai|anthropic)Optionen: --headful, --no-stealth, --block-images, --gpu/--no-gpu, --width, --height, --viewport WxH, --main, --raw, --links, --max-rows, --limit, --wait, --settle, --full, --endpoint <ws>, --timeout, --json, -o.
markdown zum Lesen, eval zum Nachschlagen
Der teuerste Fehler, den ein Agent mit diesem Tool macht, ist das Rendern einer ganzen Seite, um eine einzeilige Frage zu beantworten. Drei gezielte Fragen über drei schwere Seiten kosten 235 Bytes über eval, gegenüber 77 kB Markdown:
agent-browser eval https://github.com/trending \
"JSON.stringify(Array.from(document.querySelectorAll('article h2 a')).slice(0,3).map(a=>a.innerText.trim()))"
# ["openai / codex","mattpocock / skills","affaan-m / ECC"] -> 58 bytesGreifen Sie zu innerText, nicht zu textContent. textContent gibt den rohen Quell-Whitespace zurück ("openai /\n\n codex"); innerText liefert, was tatsächlich gerendert wird ("openai / codex"). Ein Agent, der das falsch macht, zahlt eine ganze Wiederholungsrunde, die weit mehr kostet als die gesparten Bytes.
Verwenden Sie markdown, wenn Sie die Seite tatsächlich lesen, zusammenfassen oder durchsuchen müssen.
Jeder Aufruf startet einen eigenen Browser (~1,4 s). Für mehrere aufeinanderfolgende Abfragen halten Sie einen Browser am Leben und richten --endpoint darauf – dieselben drei Fragen dauern 5,9 s über drei Kaltstarts, 3,2 s gegen einen warmen.
--endpoint hängt an einen bereits laufenden Browser an, statt einen zu starten – nützlich, um einen einzelnen langlebigen Browser über viele Aufrufe hinweg wiederzuverwenden.
Speicher
Chromes Untergrenze liegt bei etwa 420 MB PSS über 18 Prozesse, bevor es etwas lädt, und diese Untergrenze gehört zu Chrome, nicht zu diesem Paket – Flag-Tuning verschiebt sie um ~5%, und das Flag, das sie weiter verschiebt (--enable-low-end-device-mode), meldet navigator.deviceMemory: 2 neben 24 Kernen, eine unmögliche Maschine und genau die Art von Inkonsistenz, die einen Browser auffliegen lässt. Der Hebel ist also weniger Browser, nicht kleinere.
Der MCP-Server hält einen Browser und gibt jedem Tool-Aufruf einen eigenen isolierten Kontext. Drei schwere Seiten gleichzeitig abgerufen:
Spitzen-PSS | Prozesse | Echtzeit | |
ein Browser pro Aufruf | 1289 MB | 44 | 2114 ms |
ein Browser, 3 Kontexte | 654 MB | 20 | 2049 ms |
Die Isolation bleibt unverändert – der Browser-Kontext pro Aufruf war schon immer das, was sie bereitstellte. Der Browser fährt nach 30 s Inaktivität herunter (AGENT_BROWSER_IDLE_MS, 0 zum sofortigen Schließen), sodass ein langlebiger Server nicht zwischen Gesprächen auf 420 MB sitzt. Ein wiederholter Aufruf, während er warm ist, überspringt den Start vollständig und läuft etwa doppelt so schnell.
Der Kompromiss: Aufgaben teilen sich einen Prozessbaum, sodass ein Absturz auf Browser-Ebene alles in Bearbeitung betrifft statt nur einen Aufruf. Ein toter Browser wird erkannt und beim nächsten Aufruf neu gestartet. Wenn Sie eine Isolierung des Schadensradius pro Aufgabe benötigen, verwenden Sie withBrowser, das jedem Aufruf weiterhin einen eigenen Browser gibt.
import { withPooledSession, shutdownPool } from '@truenix/agent-browser/pool';
await withPooledSession({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});
await shutdownPool(); // or let it idle outFür die CLI ist jeder Aufruf ein eigener Prozess, Wiederverwendung bedeutet also, --endpoint auf einen Browser zu richten, den Sie selbst am Leben halten.
Der Daemon: ein Browser für jeden CLI-Aufruf
Jeder CLI-Aufruf ist ein eigener Prozess, also startet standardmäßig jeder einen eigenen Chrome – zehn gleichzeitige Aufrufe bedeuten zehn Browser. --daemon teilt stattdessen einen residenten Browser, wobei jeder Aufruf weiterhin seinen eigenen isolierten Kontext erhält:
agent-browser markdown https://example.com --daemon
agent-browser daemon --status
agent-browser daemon --stopEchtzeit | Spitzen-Chrome-Prozesse | |
3 sequenzielle Abfragen, ohne Daemon | 5330 ms | — |
3 sequenzielle Abfragen, | 3254 ms | — |
10 gleichzeitige Aufrufe, ohne Daemon | 1443 ms | 140 |
10 gleichzeitige Aufrufe, | 1144 ms | 32 |
Es ist opt-in (--daemon oder AGENT_BROWSER_DAEMON=1), weil das Starten eines Hintergrundprozesses, der Ihren Befehl überlebt, ein Nebeneffekt ist, um den man bitten sollte. Er startet bei Bedarf und beendet sich nach fünf Minuten Inaktivität (AGENT_BROWSER_DAEMON_IDLE_MS). Das Setzen der Umgebungsvariable leitet auch den MCP-Server dorthin, was sinnvoll ist, wenn mehrere MCP-Clients eine Maschine teilen.
--headful, --width, --height, --block-images und --gpu werden beim Start eines Browsers festgelegt, sodass ein geteilter sie nicht berücksichtigen kann. Wenn Sie eines davon übergeben, gewinnt das: Dieser Aufruf erhält stillschweigend einen eigenen privaten Browser und meldet das auf stderr.
Vier Fehlermodi, um die herum es gebaut ist, jeder durch Tests verifiziert:
Genau ein Daemon. Zehn gleichzeitige Erstaufrufer starten alle einen; neun verlieren das Rennen um die Socket-Bindung und beenden sich, bevor sie etwas starten. Die Socket-Bindung ist die Sperre, also gibt es keine Lockdatei, die veralten kann.
Der offene Socket ist der Referenzzähler. Ein Client hält seine Verbindung, während er arbeitet, sodass ein mit
SIGKILLgetöteter Client trotzdem freigibt – der Kernel schließt den Socket. Eine „Bitte freigeben“-Nachricht hätte eine Referenz für immer verloren.Beendigung bei Inaktivität, sodass er nicht zwischen Gesprächen auf ~420 MB sitzt.
Ein mit
SIGKILLgetöteter Daemon hinterlässt nichts: Sein Profilmarker nennt seine PID, sodass der unten beschriebene Durchlauf den Browser beim nächsten Start zurückholt.
Aufräumen nach sich selbst
Jeder Start schreibt einen Besitzermarker in sein temporäres Profil und durchsucht nach Profilen, deren Besitzer gestorben ist, und beendet den Browser, der noch daran hängt. Zusammen mit SIGINT/SIGTERM/SIGHUP-Handlern ergibt das:
wie der Aufruf endet | verwaiste Prozesse | nach dem nächsten Start |
normal | 0 | 0 |
SIGTERM / SIGINT | 0 | 0 |
SIGKILL (nicht abfangbar) | 1 Browser | 0 |
Ein harter Kill kostet also höchstens einen verwaisten Browser, nicht einen pro unterbrochenem Aufruf. Zehn gleichzeitige CLI-Aufrufe, wiederholt, hinterlassen nichts. Ein laufender Browser wird nie weggeräumt – sein Besitzerprozess läuft noch, und ein nicht zuordenbares Profil bleibt in Ruhe, bis niemand mehr es offen hat und es 60 s lang im Leerlauf war.
Bot-Erkennung
Teste es selbst: npm run test:bot. Letztes Ergebnis:
Detektor | Ergebnis |
31 bestanden, 0 fehlgeschlagen | |
6 grün, 0 rot, | |
|
Normales Headless-Chrome scheitert bei vier sannysoft-Zeilen (HEADCHR_UA, CHR_MEMORY, WebGL SwiftShader, alte UA) und wird als Bot gemeldet.
Ein nicht erreichbarer Detektor zählt als SKIP, nie als bestanden, und das Gate verlangt drei erreichbare Bestätigungen – so kann ein Tag, an dem die Testseiten down sind, nicht als Erfolg fehlinterpretiert werden.
Die Umgebung zählt mehr als das Patchen
Derselbe Code, an zwei Orten gemessen:
dieser Arbeitsplatz | GitHub-Actions-Runner | |
IP | privat | Rechenzentrum |
GPU | echt (NVIDIA) | keine → SwiftShader |
| 31 bestanden, 0 fehlgeschlagen | 30 bestanden, 1 fehlgeschlagen ( |
| 6 grün, 0 rot | 6 grün, 0 rot |
|
|
|
Alle CDP-Ebene-Signale bleiben in beiden Fällen sauber – das ist Code, und der Code ist richtig. Was das Urteil kippt, ist die Umgebung: eine Rechenzentrums-ASN plus Software-Rendering löst einen Schwachsignal-Verbund aus, den kein noch so viel Fingerprint-Patching behebt.
Das ist die ehrliche Form des Problems. Das Härten des Browsers entfernt die trivialen Anzeichen. Wo du es ausführst, entscheidet den Rest.
Was die Härtung bewirkt und warum
Jeder Punkt kam von einem Detektor, der uns sagte, dass wir falsch lagen:
Kein
--enable-automation. Dieses Flag – das Puppeteer und Playwright hinzufügen – setztnavigator.webdriver = true. Rohes CDP setzt es nicht, also bleibt esfalse, ohne dass etwas gepatcht wird.Kein
Runtime.enable. Es ist das lauteste CDP-Signal und treibt den klassischen Console/Error.stack-Detektor.Runtime.evaluatefunktioniert auch ohne.Fenster und Bildschirm bewegen sich zusammen.
--window-sizeohne--ozone-override-screen-sizeergibtouterWidth > screen.width, was physikalisch unmöglich ist – ein stärkeres Signal als reines Headless.Keine Standard-Device-Metrics-Überschreibung. Die übliche 1280×720 ist Playwrights Standard-Viewport, und Detektoren markieren sie beim Namen. Setze
--viewportnur, wenn du es brauchst.Echte GPU, wenn verfügbar, was einen echten
ANGLE (NVIDIA …)-Renderer statt SwiftShader ergibt.UA beim Start gesetzt, nicht nur über CDP.
Emulation.setUserAgentOverrideerreicht Web Workers nicht, also meldet ein Worker weiterhin die Headless-UA, während die Seite die saubere meldet (hasInconsistentWorkerValues).Kein
acceptLanguage-Override. CDP leitetnavigator.languagesab, indem es diesen Header aufteilt, also wird"en-US,en;q=0.9"zu["en-US","en;q=0.9"]– ein q-Wert, wo keiner legal existieren kann, und eine weitere Seite/Worker-Diskrepanz.--langmacht es korrekt.Client Hints, abgeleitet aus der eigenen Version des Binärs, sodass
Sec-CH-UAnicht mitnavigator.userAgentkollidieren kann.Isolierter Browser-Kontext pro Sitzung, beim Schließen entsorgt – sauberer Zustand pro Aufgabe ohne zweiten Browserprozess.
Die wiederkehrende Lektion: Konsistenz schlägt Abdeckung. Vier davon sind Fälle, in denen teilweises Spoofing die Erkennung erleichterte, nur entdeckt durch das Ausführen echter Detektoren.
Warum WebGL-Spoofing standardmäßig deaktiviert ist
spoofWebgl existiert und ist sorgfältig implementiert – ein Proxy um natives getParameter, sodass Function.prototype.toString weiterhin [native code] meldet. Es ist aus, weil Messungen zeigen, dass es nach hinten losgeht. Aus test/webgl-spoof-experiment.mjs:
Arm | behaupteter Renderer | maxTexture | Erweiterungen | sannysoft | Urteil |
echte GPU, kein Spoof | NVIDIA | 32768 | 37 | 0 fehlgeschlagen |
|
SwiftShader, ehrlich | SwiftShader | 8192 | 35 | 1 fehlgeschlagen |
|
SwiftShader + Spoof | NVIDIA | 8192 | 35 | 0 fehlgeschlagen |
|
Hardware zu behaupten, die du nicht hast, behebt eine kosmetische Zeile und lässt den Verbunddetektor scheitern: Das injizierte Skript erreicht Web Workers nicht, also meldet der Worker weiterhin SwiftShader, und MAX_TEXTURE_SIZE bleibt beim Software-Wert, während die Renderer-Zeichenkette eine diskrete GPU behauptet.
Ehrliches SwiftShader besteht. Eine überzeugende Lüge nicht. Gib dem Browser stattdessen eine echte GPU – es ist kostenlos.
Was das NICHT tut
Fingerprint-Erkennung ist der gesamte Umfang. Es besiegt nicht und versucht nicht zu besiegen:
TLS/JA3-JA4- und HTTP/2-Fingerprinting – entschieden, bevor irgendein JavaScript läuft
IP-Reputation – Rechenzentrum vs. private ASN, oft der eigentliche Blocker
Verhaltensanalyse – Mauspfade, Timing, Verweildauer
Kommerzielle Challenge-Produkte stützen sich darauf, also bedeutet „das Gate bestehen“ nicht trivial als Automatisierung markiert zu werden, nie unerkennbar. Gedacht für deine eigenen Seiten, Tests, Barrierefreiheitsarbeit und normales Agenten-Browsing.
Speicher
Ein Chrome-Stack kostet ungefähr 450 MB. Der Hebel ist Architektur, nicht Flags: Starte einen Browser und viele isolierte Kontexte statt einen Browser pro Aufgabe. Starte einen Browser einmal und richte dann jeden Aufruf mit --endpoint / AGENT_BROWSER_ENDPOINT darauf. --block-images hilft bei Textarbeit.
Null Abhängigkeiten
dependencies ist leer, einschließlich des WebSocket-Transports.
Nodes globales WebSocket (WHATWG) kann keine Request-Header senden, was jeder authentifizierte oder proxierte CDP-Endpunkt braucht, und undici ist nicht eigenständig importierbar. Also implementiert src/ws.mjs RFC 6455 direkt über node:http(s) – Handshake, Maskierung, Fortsetzungsfragmente, 64-Bit-Längen, Ping/Pong – was alles ist, was CDP benötigt.
Der Markdown-Konverter durchläuft das DOM mit einem expliziten Stack, hält die JS-Aufruftiefe bei O(1) unabhängig von der Verschachtelung und verwendet natives innerText für Inline-Knoten auf Blattebene. Das macht ihn sowohl rekursionssicher bei tief verschachtelten Dokumenten als auch deutlich schneller bei großen Seiten.
Umgebung
| Pfad zu einem Chrome/Chromium-Binär |
| an diesen CDP-Endpunkt anhängen statt starten |
Anforderungen
Node ≥ 18 und eine Chrome/Chromium-Installation. Kein Build-Schritt.
Danksagungen
Die Härtung dieses Projekts ist fast vollständig aus der veröffentlichten Erkennungsforschung anderer Leute abgeleitet – siehe CREDITS.md. Besonderer Dank an rebrowser-bot-detector, bot.sannysoft.com, deviceandbrowserinfo.com und Camoufox dafür, dass sie gezeigt haben, wie man es richtig macht.
Lizenz
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Web scraping MCP server: any URL to Markdown or JSON, with JavaScript rendering and batch jobs.
MCP server for Firecrawl — web search, scraping, and biomedical/arXiv paper search.
Stealth web browser for agents: search, fetch, click, download and type in persistent MCP sessions.
Related MCP Servers
- AlicenseAqualityBmaintenanceMCP server for browser automation with anti-detection. Scout pages, find elements, interact with websites, and monitor network traffic from any AI client that supports the Model Context Protocol.211MIT
- FlicenseBqualityCmaintenanceUltra-fast browser automation server over Chrome DevTools Protocol (CDP), exposed as MCP, enabling AI agents to control a real Chrome browser with low latency and minimal token usage.212-
- AlicenseAqualityBmaintenanceA fault-tolerant, stealth-enabled Model Context Protocol (MCP) server for web searching and content fetching. Built for AI Agents (Cursor, Claude Code, OpenCode), it uses a stealth browser engine to fetch pages, dynamically handles SPAs/React, and converts bloat into token-optimized Markdown.213 npm3MIT
- AlicenseAqualityAmaintenanceMCP server that drives your real Chrome/Edge/Opera browser through a Chrome extension and DevTools Protocol, preserving logins and session state, and can also perform OS-level mouse and keyboard input behind approval.5134 PyPI2MIT