# openalice-browse

> Headless Chromium browser service — Playwright CDP screencast over WebSocket, JSON command channel for click/type/navigate. Sibling of openalice-pty.

## At a glance
| field | value |
|---|---|
| kind | `service` |
| priority | `P4` |
| default branch | `mvp` |
| last activity | 2026-05-22T01:02:45+00:00 |
| features catalogued | **5** |
| runtime deps declared | **1** |

## Manifest
`.atlas-deps.yml` — parsed cleanly: **yes**

### Runtime deps
- **`openalice`** · `ws` · via `INTERNAL_SECRET` — Agent Desktop BrowserApp connects over WS to receive JPEG screencast frames and send click/type/navigate commands

### Consumed env
- `INTERNAL_SECRET`
- `BROWSE_PORT`
- `BROWSE_HEADLESS`
- `BROWSE_DEFAULT_URL`
- `BROWSE_PUBLIC_HOST`
- `BROWSE_PUBLIC_WS_SCHEME`

### Exposes
- `http_endpoint` — POST /v1/sessions — create browser session
- `http_endpoint` — GET /v1/sessions — list sessions
- `http_endpoint` — DELETE /v1/sessions/:id — close session
- `http_endpoint` — POST /v1/sessions/:id/command — send command to session
- `ws_endpoint` — ws://.../v1/sessions/:id/stream — JPEG screencast stream
- `http_endpoint` — GET /health
- `http_endpoint` — GET /metrics — Prometheus exposition

## Env drift

**Code reads, manifest omits (1):**
- `BROWSE_INTERNAL_SECRET`

## Live reachability
- last probe: **up** · `https://browse.blal.pro/health` · HTTP 200 · 0ms
- uptime 24h: **100.0%**

## Features (5)

### browse/api
- **`browse.api.command-dispatch`** — Agent-side control endpoint — POST a single JSON command into an existing session. Body shape mirrors the WebSocket frames session.dispatch() already handles, so the openalice-live-control dispatcher (and any future server-side caller) can drive click / type / navigate / etc. without holding a WS open. 200 returns whatever session.dispatch produces (or `{ ok: true }` for fire-and-forget commands). 404 when session is missing, 400 when dispatch surfaces an `error` reply.  
  *src/main.js:164*
- **`browse.api.session-lifecycle`** — Maintains the authoritative `Map<sessionId, Session>`. Exposes add / get / list / remove for the HTTP control plane. The idle sweep (`startSweep`) runs every 15 s and closes sessions whose last WebSocket client disconnected more than `idleTimeoutMs` ago (default 60 s) — preventing zombie Chromium tabs from accumulating. @feature browse.ops.observability Tracks `metrics.totalScreencastFrames`, `totalSessionsCreated`, and `totalSessionsClosed` in-process counters that `GET /metrics` exposes as Prometheus text. v0 is single-process: one Chromium browser instance, one Map of sessionId -> Session. Per-tenant browser pods come later; for now we rely on Playwright's BrowserContext for isolated cookies and local-storage between sessions inside the same Chromium process. Idle sweep: every 15s we look for sessions that have had no subscribed WS clients for `idleTimeoutMs` (default 60s) and tear them down. Keeps zombie tabs from piling up if a frontend window is closed without sending DELETE.  
  *src/registry.js:4*

### browse/core
- **`browse.core.cdp-screencast`** — Boots a single shared headless Chromium via Playwright. Sessions use CDP `Page.startScreencast` to stream JPEG frames; each frame is forwarded as a binary WebSocket message to every subscribed client. @feature browse.api.session-lifecycle Express HTTP control plane: POST/GET/DELETE /v1/sessions manage the session lifecycle. GET /health and GET /metrics are public probes; all other routes require x-internal-secret or ?token= auth. Boots: - one shared headless Chromium via Playwright - an Express HTTP control plane (POST/GET/DELETE /v1/sessions, /health, /metrics) - a `ws` server bound to the SAME http.Server, only handling the /v1/sessions/:id/stream upgrade path Auth model — v0: Every HTTP entrypoint (except /health) requires `x-internal-secret: $INTERNAL_SECRET` OR `?token=$INTERNAL_SECRET` (WS upgrade only supports query-string tokens because browsers can't add custom headers to WebSocket handshakes). This service is intended to live behind Traefik on the internal `proxy` network and never be reached directly from the public internet. Process model — v0: Single Node process, single Chromium. Each session is its own BrowserContext (isolated cookies / localStorage). For real multi-tenant isolation, fan out to a per-tenant Docker pod and run one of these services in each.  
  *src/main.js:4*
- **`browse.core.cdp-screencast`** — Starts a CDP screencast (`Page.startScreencast`) on the session's Page. Each `Page.screencastFrame` event is decoded from base64 and broadcast as a binary WebSocket frame to every subscribed client. Frame acks (`Page.screencastFrameAck`) are sent immediately so CDP never stalls due to an unacknowledged backlog. @feature browse.api.command-dispatch `dispatch(message)` routes JSON command objects — click, mousemove, type, key, navigate, scroll, resize, screenshot, url — to the corresponding Playwright / CDP method. Both the WebSocket handler (main.js) and the HTTP command endpoint call this same method. Lifecycle: 1. constructor stashes options. 2. start() creates the BrowserContext from the shared Browser, opens a Page, navigates to the requested URL, attaches a CDPSession and starts the JPEG screencast. 3. attachClient(ws) subscribes a WS to receive binary frames. Multiple clients per session are allowed; useful for the "share my browser" review flow. 4. dispatch(message) accepts a single JSON command and routes it to the right Playwright/CDP method. 5. close() stops the screencast, closes the page, closes the context. Browser stays alive for the next session. Coordinate space: the screencast frames the frontend renders are `viewport.width x viewport.height` device-pixels. Click coords sent back to us MUST be in that same pixel space — the BrowserApp is responsible for mapping mouse events through the canvas bounding rectangle.  
  *src/session.js:4*

### browse/ops
- **`browse.ops.observability`** — no description  
  *src/main.js:100*

---

*Generated by [openalice-atlas](https://atlas.blal.pro) — single source of truth for the openalicelabs ecosystem.*
