36 — control a remote browser over CDP (no xdotool/ydotool, Wayland-safe)
Drive a node's real browser from the host over the URI contract, using the browser-control connector's Chrome DevTools Protocol surface (browser://<node>/cdp/...). CDP runs headed under Wayland and needs no xdotool/ydotool — the practical way to control Chrome on a Linux desktop node.
host node (Chrome + a urirun node)
browser://NODE/cdp/session/command/launch {headless} ── launch Chrome w/ debug port
browser://NODE/cdp/session/query/find {domain} ── find an existing CDP browser session
browser://NODE/cdp/page/command/navigate {url} ── go to a page
browser://NODE/cdp/page/query/eval {expr} ── run JS in the page, get the value
browser://NODE/cdp/page/query/screenshot ── PNG over CDP
browser://NODE/cdp/page/query/tabs ── list tabs
The surface is stateful: launch first, then navigate / eval / screenshot against the live page. The session probe is read-only and does not launch, navigate, type, click, or expose cookie values.
Run
Against any node that serves the CDP surface (e.g. a remote box):
NODE_URL=http://192.168.188.201:8765 NODE=laptop python3 drive_cdp.py https://example.com
Read-only unattended browser observation, with physical-screen KVM/OCR detection first, screen://.../portal/query/capture + local OCR fallback second, and CDP fallback third:
NODE_URL=http://192.168.188.201:8766 NODE=laptop \
python3 unattended_browser.py --unattended "sprawdź czy na ekranie jest LinkedIn"
The unattended runner is intentionally read-only. It refuses social writes and login/payment actions such as publishing posts, sending messages, commenting, liking, following, entering passwords, or buying. Use it to inspect the screen/page, capture evidence, and prepare drafts for a human to approve.
When the node can capture the desktop through screen://<node>/portal/query/capture, the runner saves the PNG under ~/.urirun/artifacts/screenshots/ and OCRs it locally with tesseract when available. This is the practical GNOME/Wayland path when grim is missing and gnome-screenshot hangs inside the node process.
For local development and fake/staging surfaces, the runner has a dev override:
python3 unattended_browser.py --unattended --dev-allow-write-actions \
--url http://localhost:8080/fake-linkedin \
"opublikuj testowy post w lokalnym fake LinkedIn"
The override only applies to allowlisted non-public hosts (localhost, .test, .local, .internal, .lan, plus repeated --dev-allow-host). It intentionally does not override public social sites such as linkedin.com.
Self-contained local proof (spins a local node, deploys the CDP handler, drives a local Chrome) — needs a Chrome/Chromium on this machine:
./e2e.sh
== deploy the CDP surface ==
deploy ok= True routeCount= 6
== drive the local browser over CDP ==
✓ launch {"ok":true,"browser":"Chrome/149.0.7827.155","debugPort":9222,...}
✓ navigate https://example.com
✓ eval title 'Example Domain'
✓ eval links 1 <a> on the page
✓ screenshot 17149-byte PNG
✓ tabs ['Example Domain', 'about:blank']
6/6 CDP browser steps ok
Verified live the same way against a remote Fedora node ("lenovo", 192.168.188.201).
Why CDP (and not the GUI/KVM path)
The browser-control connector also has a browser-agnostic browser://<node>/kvm/* path (launch + navigate + type + click-text + capture by GUI automation). On a GNOME/Wayland node that path needs ydotool+grim and the node running inside the graphical user session. CDP sidesteps all of that for Chrome-family browsers: the debug protocol drives the page directly, headed or headless, no input tools, no portal. For Chrome on Linux desktops, prefer browser://.../cdp/*.
Files
drive_cdp.py— host-side driver: launch → navigate → eval → screenshot → tabs (NODE_URL,NODE).unattended_browser.py— KVM/OCR-first, screen-portal/OCR fallback, then CDP read-only
unattended observation with policy gates for social-write/login/payment actions.
cdp-bindings.json— the 6 CDP routes (templated onNODE), mapping to the connector'scdp-flat-handler.py.e2e.sh— local node + signed/deployof the CDP handler +drive_cdp.py(self-contained; skips if no Chrome).
The handler is urirun-connector-browser-control/examples/cdp-flat-handler.py (stdlib-only CDP WebSocket client; deployable as a single file).