BrowserOxide exposes a Chrome DevTools Protocol (CDP) server so it can be driven by existing tools: Puppeteer, Playwright, scraper_oxide's browser layer.
- Drop-in replacement: Existing automation code that uses Puppeteer/Playwright works without changes
- scraper_oxide integration: scraper_oxide already speaks CDP — BrowserOxide just needs to respond to it
- Ecosystem compatibility: Testing tools, debugging tools, and CI/CD pipelines speak CDP
- Lightpanda validates this approach: They implemented CDP and achieved Puppeteer/Playwright compatibility
Full CDP has 40+ domains and thousands of commands. We implement the subset that scraping/automation tools actually use:
| Domain | Commands | Events |
|---|---|---|
| Target | createTarget, closeTarget, getTargets, attachToTarget |
targetCreated, targetDestroyed, targetInfoChanged |
| Page | navigate, reload, getFrameTree, setLifecycleEventsEnabled, addScriptToEvaluateOnNewDocument |
frameNavigated, loadEventFired, domContentEventFired, lifecycleEvent |
| Runtime | evaluate, callFunctionOn, getProperties, releaseObject |
executionContextCreated, executionContextDestroyed, consoleAPICalled |
| DOM | getDocument, querySelector, querySelectorAll, getOuterHTML, setOuterHTML, getAttributes |
documentUpdated |
| Domain | Commands | Events |
|---|---|---|
| Network | enable, disable, setCookies, getCookies, setExtraHTTPHeaders, setUserAgentOverride |
requestWillBeSent, responseReceived, loadingFinished, loadingFailed |
| Fetch | enable, disable, fulfillRequest, continueRequest, failRequest |
requestPaused |
| Domain | Commands |
|---|---|
| Input | dispatchMouseEvent, dispatchKeyEvent, dispatchTouchEvent |
| Emulation | setDeviceMetricsOverride, setUserAgentOverride, setGeolocationOverride, setTimezoneOverride, setLocaleOverride |
| Domain | Commands |
|---|---|
| Page | captureScreenshot, printToPDF |
These would require a minimal rendering pipeline — deferred.
protocol/
├── src/
│ ├── lib.rs # CdpServer — accepts WebSocket connections
│ ├── server.rs # WebSocket server (tokio-tungstenite)
│ ├── session.rs # Per-connection session state
│ ├── router.rs # Method dispatch (domain.method → handler)
│ ├── types.rs # CDP JSON types (auto-generated from protocol.json)
│ ├── domains/
│ │ ├── target.rs # Target domain handlers
│ │ ├── page.rs # Page domain handlers
│ │ ├── runtime.rs # Runtime domain handlers
│ │ ├── dom.rs # DOM domain handlers
│ │ ├── network.rs # Network domain handlers
│ │ ├── fetch.rs # Fetch domain handlers
│ │ ├── input.rs # Input domain handlers
│ │ └── emulation.rs # Emulation domain handlers
│ └── codegen/ # Generate Rust types from Chrome's protocol.json
│ ├── protocol.json # Official CDP protocol definition
│ └── generate.rs # Build script: JSON → Rust structs
├── tests/
│ ├── puppeteer_compat.rs # Run Puppeteer test suite against our server
│ └── playwright_compat.rs
└── Cargo.toml
CDP uses JSON-RPC over WebSocket:
// Request (client → BrowserOxide)
{"id": 1, "method": "Page.navigate", "params": {"url": "https://example.com"}}
// Response (BrowserOxide → client)
{"id": 1, "result": {"frameId": "main", "loaderId": "loader1"}}
// Event (BrowserOxide → client, no id)
{"method": "Page.loadEventFired", "params": {"timestamp": 1234567890.123}}We use V8 (same as Chrome), but our Runtime.enable implementation avoids the detection leak:
- We control the CDP server — We decide what
Runtime.enableactually does. Chrome's leak happens becauseRuntime.enabletriggers V8's console argument serialization (which invokes Proxy traps, error stack getters). Our CDP server can enable runtime event reporting WITHOUT calling V8'sRuntime.enableinternally. - Isolated execution contexts — All automation code runs in V8 isolated contexts invisible to page scripts (same technique as Patchright/rebrowser-patches).
- Console interception at the Rust layer — We intercept
console.*calls via V8 ops before they reach the CDP event stream. No V8 preview serialization is triggered. - No unconditional serialization — Only serialize console arguments when a CDP client explicitly requests it AND the arguments are from the page context (not from automation).
This means Runtime.enable is safe by default in BrowserOxide, even though we use V8.