# OpenDevBrowser Extension

Optional Chrome extension that enables relay mode (attach to existing logged-in tabs).

Status: active  
Last updated: 2026-06-30

Quick file-level overview: `<public-repo-root>/extension/README.md`

## What it does

- Connects to the local relay server (`ws://127.0.0.1:<port>/extension`).
- Uses the Chrome Debugger API to forward CDP commands across attached tabs/targets (with a primary tab used for handshake/status).
- Allows OpenDevBrowser to control tabs without launching a new browser.
- Lets extension-backed sessions participate in manager-owned browser replay capture through the existing screenshot primitive; there is no separate extension screencast relay family.
- Supports multi-tab CDP routing with flat sessions (Chrome 125+).
- Exposes top-level tabs and auto-attached child targets (workers/OOPIF) through `Target.getTargets`.
- Hosts the dedicated design-canvas runtime used by `/canvas` for design-tab, overlay, and workspace-scoped multi-child shell operations.
- Preserves additive canvas session-summary metadata such as `availableInventoryCount`, `catalogKitIds`, `availableStarterCount`, and the currently applied starter so the design tab stays in sync with starter and kit availability without introducing a second starter execution path in the extension.
- Routes popup/canvas/in-page annotation `Send` actions through `/annotation` `store_agent_payload`; core enqueues screenshot-free payloads into `AgentInbox`, delivering to one active chat scope or returning a stored-only receipt.
- Launch defaults to extension relay when available; managed/CDPConnect require explicit user choice.
- Extension mode is headed-only; extension-intent headless launch/connect is rejected with `unsupported_mode`.
- Desktop observation is not an extension feature; the shipped desktop commands and tools stay daemon/core-owned, public, and observe-only.
- Generated help surfaces the exact lookup labels `screencast / browser replay`, `desktop observation`, and `computer use / browser-scoped computer use`; the extension participates in relay-backed browser work, not the public read-only desktop plane.
- When hub mode is enabled, the hub daemon is the sole relay owner and enforces FIFO leases (no local relay fallback).

## Installation

Requires Node.js `>=18` and the npm package (`npx opendevbrowser` recommended; `npm install -g opendevbrowser` optional).

1. Run the CLI once with `--full` so the extension assets are extracted:
   ```bash
   npx opendevbrowser --full
   ```
2. Load the extension unpacked from:
   - `~/.config/opencode/opendevbrowser/extension`
   - Fallback: `~/.cache/opencode/node_modules/opendevbrowser/extension`
   - Pre-release local package onboarding: `<WORKDIR>/node_modules/opendevbrowser/extension`
3. Open the extension popup to configure relay settings.

For full first-run pre-release onboarding flow (local package install, isolated daemon, extension connect, first task), use:
- `<public-repo-root>/docs/FIRST_RUN_ONBOARDING.md`

## Popup settings

- **Relay port**: Port of the local relay server (default `8787`).
- **Auto-connect**: Reconnect on browser start (default on).
- **Auto-pair**: Fetch pairing token automatically from the plugin (default on).
- **Native fallback (experimental)**: Allow native messaging fallback when relay is unavailable (default off).
- **Require pairing token**: Require token for relay pairing (recommended).
- **Pairing token**: Manual token entry when auto-pair is off.

## Default settings

| Setting | Default |
|---------|---------|
| Relay port | `8787` |
| Auto-connect | `true` |
| Auto-pair | `true` |
| Native fallback (experimental) | `false` |
| Require pairing token | `true` |
| Pairing token | `null` (fetched on connect) |

## Auto-connect behavior

Auto-connect is enabled by default. The extension attempts to connect on browser startup, install, and when the toggle is enabled in the UI. Auto-connect respects the current relay port, pairing settings, and auto-pair toggle.
Native fallback is only attempted when the experimental native toggle is enabled.
The toolbar action uses the OpenDevBrowser icon set from `extension/icons/` (synced from `assets/extension-icons/` during `npm run extension:build`) and a badge dot indicator (green = connected, red = disconnected).

## Auto-pair flow

When auto-pair is enabled:

1. The extension calls the local discovery endpoint (`/config`, `http://127.0.0.1:8787/config` by default) to learn the relay port and pairing requirement.
2. If pairing is required, it fetches the token from `/pair`.
3. The extension connects to the relay with the pairing token.

`/config` and `/pair` reject explicit non-extension origins. Chrome extension requests may omit the `Origin` header, so the relay also accepts missing-Origin requests. CLI/tools may call `/config` and `/pair` to auto-fetch relay settings and tokens.
Keep `discoveryPort` at `8787` for normal extension discovery; set it to the same isolated value as `relayPort` when running a separate daemon with isolated config/cache roots.

Relay ops endpoint: `ws://127.0.0.1:<relayPort>/ops`. The CLI/tool `connect` command accepts base relay WS URLs
(for example `ws://127.0.0.1:<relayPort>`) and normalizes them to `/ops`.
Relay canvas endpoint: `ws://127.0.0.1:<relayPort>/canvas` for live design-canvas preview and overlay commands. Public canvas commands are still the `canvas.*` commands exposed by the CLI/tool; extension `canvas.tab.sync` and `canvas.overlay.sync` are internal runtime capabilities.
Relay annotation endpoint: `ws://127.0.0.1:<relayPort>/annotation` for interactive annotate capture plus one-off `store_agent_payload` and `fetch_stored` requests.
Legacy relay `/cdp` is still available but must be explicitly opted in (CLI: `--extension-legacy`).
Legacy `/cdp` is mutually exclusive with an active `/ops` lease on the extension target. If the CLI returns `cdp_attach_blocked`, disconnect the `/ops` session first, then retry the legacy path.
When pairing is enabled, `/ops`, `/canvas`, `/annotation`, and `/cdp` require a relay token (`?token=<relayToken>`). Tools and the CLI auto-fetch `/config` and `/pair`
to obtain the token before connecting, so users should not manually pass or share tokenized URLs.

Readiness checks:

```bash
npx opendevbrowser status --daemon --output-format json
npx opendevbrowser --help
node scripts/chrome-store-compliance-check.mjs
```

Expected extension-ready daemon fields:
- `extensionConnected=true`
- `extensionHandshakeComplete=true`
- `opsConnected=false` until a default extension session connects through `/ops`
- `canvasConnected=false` unless a design-canvas session is actively using relay preview/overlay flows
- `annotationConnected=false` unless annotation capture or stored-payload retrieval is active

Auto-connect behavior:
- If a second extension client replaces the current relay client, reconnect suppression lasts only while that replacement remains active. When the relay no longer reports an active extension client, the background auto-connect loop should retry the stored relay instead of requiring a manual popup click.

## Chrome version requirement

Extension relay uses flat CDP sessions and requires **Chrome 125+**. Older versions will fail fast with a clear error.

## Multi-tab + primary tab behavior

- Target discovery (`Target.getTargets`) includes top-level tabs and child targets.
- Child targets are auto-attached recursively for session-aware routing.
- A single **primary tab** is used for relay handshake/status; switching tabs updates the handshake without disconnecting others.
- Design-canvas flows can open dedicated extension-hosted design tabs (`canvas.html`) and mount overlays on existing tabs through the `/canvas` runtime.
- The extension design tab is a same-origin infinite-canvas editor: it persists full `CanvasPageState` snapshots in workspace and child scoped `IndexedDB` keys, fans out same-origin convergence over workspace and child scoped `BroadcastChannel` names, sends editor-originated patch requests back through `/canvas`, keeps arbitrary page overlays in sync through the same runtime, and exposes page selection, hierarchical layers, a property inspector, lease-aware undo/redo controls, keyboard shortcuts, extension-stage region annotation, and a dedicated token panel for collection or mode creation, token value or alias editing, selected-node binding, and token usage inspection.
- `CanvasPageState`, `CanvasSessionSummary`, page messages, current selection, drafts, cached previews, and preview sync payloads carry optional `workspaceId` and `childId`. When present, reload, relay reconnect, and MV3 restart restore the same workspace child without cross-child draft, selection, or preview leakage.
- Workspace design tabs render a 4x2 agent shell with coordinator lane, active child detail, worker panes, activity log, review lane, checkpoint lane, preview budget indicators, and visible delivered/degraded/paused/conflict/lease/revision/sync states.
- Extension-hosted design tabs must register their synthetic target through `targets.registerCanvas` before `/ops` `targets.use` can activate that surface.

## Annotation send behavior

- Popup `Annotate` resolves against the opener window's active http(s) tab first. If the focused surface is `canvas.html`, another extension page, or a restricted tab, the background falls back to the last annotatable web tab it stored instead of trying to inject into the extension page itself, so popup annotate can recover after an MV3 service-worker restart.
- Popup, canvas, and in-page annotation `Send` actions dispatch `annotation:sendPayload` to the background, and the background then posts `/annotation` `store_agent_payload`.
- The relay handles `store_agent_payload` locally through core bootstrap's `AgentInbox` handler and returns a shared-inbox receipt.
- Successful scoped delivery reports `Delivered to agent`.
- When delivery cannot be scoped safely, the receipt includes `stored_only` with `no_active_scope` or `ambiguous_scope`.
- When the relay path fails, the extension stores the sanitized payload locally and reports `Stored only; fetch with annotate --stored`.
- Shared inbox persistence strips screenshots; `annotate --stored --include-screenshots` only affects the extension-local fallback copy when it is still available in memory.

## Security notes

- Relay connections are local-only by default.
- Pairing tokens are stored in `chrome.storage.local` and never sent to third parties.
- The extension does not log page content or tokens.
- Non-local relay endpoints are not supported unless explicitly configured in the plugin.

## Troubleshooting

- **Extension not connecting**: Confirm the relay is running (`opendevbrowser serve`) and the port matches the popup.
- **Temp-profile unpacked extension automation does nothing**: Google Chrome stable may ignore startup flags like `--disable-extensions-except` and `--load-extension`. For isolated automation harnesses, prefer Chromium or Chrome for Testing, or use the already-installed unpacked extension in your real Chrome profile.
- **Auto-pair failing**: Ensure the plugin is running and the relay server is available on the configured port.
- **Pairing token required**: Enable "Require pairing token" and provide the value from your `opendevbrowser.jsonc`.
- **No active tab / restricted tab**: The popup cannot attach to `chrome://`, `chrome-extension://`, or Chrome Web Store pages. Focus a normal http(s) tab before connecting.
- **Popup annotate says `Annotation UI did not load in the page`**: The popup could not confirm the page-side annotation bridge after injection. The popup now sends its opener-tab id directly and otherwise restores the last stored annotatable web tab, but you should still focus the intended http(s) tab once, reload that page, and retry. If you just rebuilt the unpacked extension, reload it in Chrome before retesting so the new background and content-script bundles are active.
- **Canvas design-tab overlay fails with `restricted_url` on `chrome-extension://.../canvas.html`**: Chrome is still running stale unpacked-extension runtime code. Rebuild if needed, then reload the unpacked extension in Chrome before retrying `canvas.overlay.mount` or related design-tab commands; reconnect the extension after reload so the fresh background bundle owns the relay again.
- **Canvas workspace panes, drafts, selection, or previews look mixed after rebuild or reload**: Reload the unpacked extension, reconnect the relay, and re-open the workspace child route. The design tab stores workspace and child scoped cache keys, so persistent mixing usually means Chrome is still running stale extension code.
- **Debugger attach failed**: Close DevTools on the target tab (or any other debugger) and retry.
- **Chrome too old**: Extension relay requires Chrome 125+ for flat sessions.
- **Headless extension launch/connect fails**: expected. Extension mode is headed-only; use `launch --no-extension --headless` for managed headless sessions.
- **Launch fails due to missing extension**: The CLI/tool will print exact commands for Managed or CDPConnect fallbacks when the extension is not connected.
- **Popup shows Connected but launch says not connected**: Check the popup note for the relay port/instance (it now includes the relay identity) and ensure it matches the daemon relay port.
- **Canvas code sync shows `unsupported`**: Run `npx opendevbrowser canvas --command canvas.code.status --params '{"canvasSessionId":"<canvas-session-id>","bindingId":"<binding-id>"}' --output-format json` and inspect `frameworkAdapterId`, `declaredCapabilities`, `grantedCapabilities`, `capabilityDenials`, and `reasonCode`. Built-in lanes currently cover React TSX v2, static HTML, custom elements, Vue SFC, and Svelte SFC; legacy `tsx-react-v1` bindings migrate to `builtin:react-tsx-v2` on load.
- **Local adapter plugin will not load**: Repo-local BYO plugins only load from local `package.json`, `.opendevbrowser/canvas/adapters.json`, or explicit config declarations. Out-of-worktree package declarations are rejected with `trust_denied`, and malformed manifests or broken entrypoints surface deterministic plugin load failures.
- **Annotation send says stored only**: no safe chat scope was available for the current worktree or relay enqueue failed. Use `annotate --stored` to fetch the payload explicitly, or keep one target chat active and retry the send.
