No description
  • JavaScript 97.9%
  • Dockerfile 2.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
brennoflavio 57534b31e9
All checks were successful
Publish container image / Validate source (push) Successful in 2m38s
Publish container image / publish (push) Successful in 6m14s
attach audio device
2026-10-05 03:37:21 -03:00
.forgejo/workflows replace chromium 2026-10-05 01:15:11 -03:00
config/pipewire attach audio device 2026-10-05 03:37:21 -03:00
.dockerignore first commit 2026-09-30 00:57:16 -03:00
Dockerfile attach audio device 2026-10-05 03:37:21 -03:00
entrypoint.mjs attach audio device 2026-10-05 03:37:21 -03:00
entrypoint.test.mjs attach audio device 2026-10-05 03:37:21 -03:00
maintenance.mjs better restart 2026-10-02 15:26:36 -03:00
maintenance.test.mjs better restart 2026-10-02 15:26:36 -03:00
proxy.mjs better restart 2026-10-02 15:26:36 -03:00
proxy.test.mjs better restart 2026-10-02 15:26:36 -03:00
README.md attach audio device 2026-10-05 03:37:21 -03:00
server.mjs basic webgl support 2026-10-05 01:40:06 -03:00
server.test.mjs attach audio device 2026-10-05 03:37:21 -03:00

Coding agent browser

A single, always-on Google Chrome Stable process for coding agents. It exposes Chrome DevTools Protocol (CDP) over HTTP/WebSocket on port 9222. Chrome itself listens only on loopback; the Node proxy publishes its CDP endpoint and rewrites discovery URLs so clients in other containers can connect.

Chrome renders offscreen through --ozone-platform=headless, without --headless or a custom user-agent string. This preserves its native desktop user agent and Google Chrome Client Hints branding in pages, dedicated/shared/service workers, and HTTP headers. Blink is configured for a fine pointer with hover, matching a desktop mouse. --keep-alive-for-test keeps Chrome running even after the last tab closes. This is still an offscreen browser, not a real desktop or hardware GPU.

The container starts private PipeWire, pipewire-pulse and a minimal pipewire-media-session routing service before Chrome, with a stereo null sink exposed through Chrome's native media-device APIs. Routing enables only metadata/default-node selection and stream linking; ALSA, camera and Bluetooth discovery are disabled. pipewire-pulse supplies the PulseAudio protocol; no PulseAudio daemon, systemd, D-Bus, host audio socket, microphone, camera or fake-capture flags are required. The output discards audio rather than playing through physical speakers. Its configuration lives in config/pipewire/, and sockets/state use a private temporary runtime directory. The entrypoint supervises all four process groups, stops them on shutdown or a child failure, and removes the runtime directory after the groups have exited. Surviving descendants are force-killed after 8 seconds; shutdown has a 10-second deadline. Audio remains running across the daily Chrome profile reset.

WebGL and WebGL2 use Chrome's ANGLE + SwiftShader backend (--use-gl=angle --use-angle=swiftshader) for CPU rendering. Vendor, renderer and capabilities remain native and identify SwiftShader, not a hardware GPU. This adds CPU cost and exposes the graphics backend to untrusted pages. WebGPU is not opted in, and neither --enable-unsafe-webgpu nor --enable-unsafe-swiftshader is enabled.

The image installs regular Google Chrome Stable from Google's official Linux package, not Chromium or Chrome for Testing, and supports linux/amd64 only. Uncached image builds download the current Stable release; use docker build --no-cache to refresh Chrome after a new release, since cached installation layers retain their installed version. The executable defaults to /usr/bin/google-chrome-stable; CHROMIUM_BIN remains the executable override. The image includes Liberation, DejaVu and Noto core fonts. Chrome defaults to the America/Sao_Paulo timezone when TZ is unset; an explicit TZ overrides it. The daily browser reset remains scheduled at 07:00 UTC.

docker build --platform=linux/amd64 -t coding-agent-browser .
docker run -d --name coding-agent-browser --restart=always --shm-size=1g -p 127.0.0.1:9222:9222 coding-agent-browser

Rebuild and deploy the image to apply launch, font or timezone changes; editing the checkout does not change a running browser. Replacing the deployed browser disconnects all clients and loses its temporary profile.

Check http://127.0.0.1:9222/json/version. Clients can use that HTTP URL with Playwright chromium.connectOverCDP("http://<browser-host>:9222") or playwright-cli attach --cdp=http://<browser-host>:9222. Multiple clients can connect; each client should create and close its own browser context instead of operating on the default context or closing other clients' pages. The browser stays up independently of client connections.

Every minute the service checks CDP page targets and closes pages first observed more than one hour ago (including tabs left behind by detached clients). A page that predates a service start gets a full grace period because CDP does not report its creation time. This can interrupt long-running tasks; other target types are left alone. At 07:00 UTC (04:00 São Paulo time) the service replaces Chrome and its temporary profile without restarting the container, dropping all connections, contexts, cookies, and tabs. The CDP proxy stays running, but discovery and connections are briefly unavailable until the new browser is ready. Clients must reconnect and start new tabs afterward. The reset has a 25-second readiness deadline; a hung browser is force-killed after 5 seconds, and failure to exit within another 2 seconds terminates the service. Unexpected Chrome exits and failed resets still cause the service to exit nonzero; Kubernetes restarts its container, or Docker must run with a restart policy as shown above.

For an ingress with TLS, set CDP_PUBLIC_URL=https://browser.internal.example (the origin seen by clients); this makes /json/version advertise a wss:// endpoint. The ingress must route both HTTP and WebSocket traffic at the root path. Chrome uses the loopback port immediately above CDP_PORT (or 65534 when CDP_PORT=65535); keep both ports available. Without this setting, the advertised endpoint uses the request's Host header and ws://. A container restart policy is required for crash recovery and failed resets, not routine daily maintenance.

Tests

Run node --test *.test.mjs for lifecycle/proxy/container-supervision tests. Real-browser tests also run when CHROMIUM_BIN points to an installed regular Google Chrome executable and pipewire, pipewire-pulse, pipewire-media-session and pw-dump are installed. They launch the container entrypoint and check the shared and isolated contexts, native audio-output enumeration without capture permissions, active stereo playback links and AudioContext clock progression, WebGL/WebGL2 pixel rendering and native SwiftShader identity, mouse capability, native webdriver behavior, timezone defaults/overrides, native Google Chrome branding and matching Client Hints across pages, workers and HTTP headers, and opening a new tab after every page has been closed.

CI runs the Node-only tests without Docker; real-browser tests are skipped when CHROMIUM_BIN is unset. To include them locally, using only loopback fixtures:

CHROMIUM_BIN=/usr/bin/google-chrome-stable node --test *.test.mjs

CDP grants full control over Chrome. Only expose this service to trusted clients on the private network; individual browser contexts prevent accidental interference but do not provide an authorization boundary. Profiles are temporary and are not retained after browser restarts. Fingerprint/stealth init scripts remain the responsibility of each Playwright client context, as in the existing coding-agent implementation.