Skip to content

Generic CDP

View on GitHub

The generic CDP provider points at any browser-level Chrome DevTools Protocol WebSocket endpoint and registers the core Browser capability over it. It is the shared floor for the whole field: a locally launched Chromium (--remote-debugging-port), obscura’s from-scratch partial CDP server, or a hosted vendor’s wss:// connect URL pasted straight in. Vendor-specific packages (a session-create REST call, an auth handshake) can layer on top and reuse this data plane.

The client is hand-rolled over Effect’s Socket. No playwright, puppeteer, or ws dependency; it speaks CDP directly over the platform’s native WebSocket.

Good fit for: driving a browser you already run (local Chromium, a CDP engine in a container), portable agent grounding over text. Less good fit for: provider features that live behind a vendor SDK rather than the CDP wire.

Install

Terminal window
pnpm add @effect-uai/core @effect-uai/browser effect

Wire it up

Pass the browser-level CDP WebSocket endpoint:

import { Effect } from "effect"
import * as Browser from "@effect-uai/core/Browser"
import { layer as cdpLayer } from "@effect-uai/browser/Connect"
const provider = cdpLayer({
endpoint: "ws://127.0.0.1:9222/devtools/browser",
})
const program = Effect.gen(function* () {
const session = yield* Browser.create({ timeout: "2 minutes" })
yield* session.goto("https://example.com")
return yield* session.content("markdown")
})
await Effect.runPromise(program.pipe(Effect.scoped, Effect.provide(provider)))

For a local Chromium, the browser-level endpoint (with its <uuid> path) comes from http://127.0.0.1:9222/json/version:

Terminal window
chromium --headless --remote-debugging-port=9222
curl -s http://127.0.0.1:9222/json/version # → "webSocketDebuggerUrl"

An endpoint that wants a token on the WebSocket handshake takes it through headers. Handshake headers need Node or Bun; a browser cannot set them.

const provider = cdpLayer({
endpoint: "ws://127.0.0.1:9222/devtools/browser",
headers: { Authorization: `Bearer ${token}` },
})

cdpLayer registers two service tags from one implementation:

  • CdpBrowser: the typed tag for this provider.
  • Browser: the generic, provider-portable tag. Yield this for code that should work across browser backends.

Sessions map to CDP targets

  • create opens a fresh target (tab) and attaches to it.
  • attach(id) re-attaches to an existing target by id; the finalizer detaches without closing, for a session kept warm across runs.
  • destroy(id) closes the target from anywhere.

A plain CDP endpoint has no server-side session cap, so the timeout and idleTimeout knobs on the create request are enforced locally by the session’s reaper fibers.

Running against obscura

obscura is a from-scratch headless CDP engine (no Chrome install needed), handy for local development and CI:

9222/devtools/browser
export OBSCURA_CDP_TOKEN="$(openssl rand -hex 32)"
docker run -d --name obscura -p 127.0.0.1:9222:9222 \
-e OBSCURA_CDP_TOKEN="$OBSCURA_CDP_TOKEN" h4ckf0r0day/obscura

Since 0.2.3, obscura requires that token whenever it listens beyond loopback, which inside Docker it always does. Pass it as a bearer token in headers (above); without it every request, /json/version included, answers 401.

obscura implements a partial CDP surface. Domain-enable calls it doesn’t recognize are tolerated at connect time, and verbs that need a missing domain fail cleanly rather than crashing the session. In practice snapshot (needs the Accessibility domain) returns BrowserUnsupported, while goto, click, fill, content, and query all work. That is exactly the vision-free surface the usability recipe is built on.

Next step

  • Agent usability testing: an observe / decide / act loop that drives this provider toward a plain-language goal and reports the UX friction it hit.