Browser Extension Architecture

How the Chrome Extension communicates with the Python CLI for browser automation

Browser Extension Architecture

The PraisonAI browser automation uses a 3-layer architecture for AI-powered browser control.

Communication Flow

Python CLI ──► Bridge Server ◄──► Chrome Extension
     │              │                    │
     │              │                    │
 start_session  observation/action      CDP
  1. CLI sends start_session with goal
  2. Bridge Server routes to Extension
  3. Extension captures page state (screenshot, elements)
  4. Extension sends observation to Bridge
  5. Bridge processes with BrowserAgent (LLM decision)
  6. Bridge returns action to Extension
  7. Extension executes action via CDP
  8. Loop continues until goal complete

Architecture Diagram

┌─────────────────────────────────────────────────────────────┐
│                     Python CLI                               │
│  praisonai browser launch "goal"                            │
└─────────────────────┬───────────────────────────────────────┘
                      │ WebSocket
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                  Bridge Server (FastAPI)                     │
│  - Routes messages CLI ↔ Extension                          │
│  - Hosts BrowserAgent for LLM decisions                     │
│  - WebSocket endpoint: ws://localhost:8765/ws               │
└─────────────────────┬───────────────────────────────────────┘
                      │ WebSocket
                      ▼
┌─────────────────────────────────────────────────────────────┐
│                Chrome Extension                              │
│  - Background service worker                                │
│  - CDP debugger for page control                            │
│  - Captures screenshots, clickable elements                 │
│  - Executes click/type/scroll actions                       │
└─────────────────────────────────────────────────────────────┘

Running the Browser Agent

bash
# Simple usage (auto-selects engine)
praisonai browser launch "go to google and search for AI"

# Force extension mode
praisonai browser launch "go to google" --engine extension

# Force CDP mode (no extension required)
praisonai browser launch "go to google" --engine cdp

# Debug mode
praisonai browser launch "test" --debug

Message Types

TypeDirectionDescription
start_sessionCLI → ServerStart new automation
observationExt → ServerPage state snapshot
actionServer → ExtNext action to execute
statusServer → BothSession status updates
stop_sessionAny → ServerEnd session
cancel_sessionAny → ServerCancel an in-flight session (routes through the same handler as stop_session).
registerExt/CLI → ServerDeclares the socket’s role (extension or cli) for sockets that connected without an Origin header. Returns {"type": "status", "status": "registered", "role": "..."}, or {"type": "error", "code": "INVALID_ROLE"} if rejected. See Role Registration.

Extension Readiness Wait

Before sending start_session, run_browser_agent_with_progress polls the bridge /health endpoint until an extension connects.

Diagram

Readiness Poll

env var truthy

no

▶️ run_browser_agent

Skip wait?

⚡ straight to websocket

⏱ poll /health up to min 15s, timeout

📤 start_session

The poll caps at min(15.0, timeout) seconds — a caller passing timeout=2.0 waits at most 2 seconds. Set PRAISONAI_BROWSER_SKIP_EXTENSION_WAIT=1 to skip the poll entirely (for unit tests / mocked bridges). See Environment Variables.

Role Registration

Sockets that connect without an Origin header declare their role with a register message.

Chrome MV3 offscreen documents often omit Origin on ws://127.0.0.1:8765/ws. The bridge classifies such sockets as CLI, so without register the extension pool stays empty and start_session returns NO_EXTENSION.

Diagram
BrowserServer :8765Offscreen docBrowserServer :8765Offscreen docis_cli=true, no_origin=trueis_extension=true, is_cli=falseWS connect (no Origin){“type”:”register”,”client”:”extension”}{“type”:”status”,”status”:”registered”,”role”:”extension”}{“type”:”start_session”,”goal”:”…”}{“type”:”status”,”status”:”running”,”session_id”:”…”}

Send it once, on open:

js
ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ type: "register", client: "extension" }));
});
Diagram

WS accept

register client=extension

register client=cli

no register

start_session from panel

start_session client=cli

start_session → NO_EXTENSION

observation / action

done action

Connected

RegisteredExt

RegisteredCli

Unregistered

Running

DeliverAutomation

Reject

ObsLoop

Completed

RuleBehavior
client: "extension"Accepted when the socket connected without Origin (or is already the extension).
client: "cli"Accepted when the socket connected without Origin and has no active session.
Web-origin socketRejected with INVALID_ROLE — it cannot claim either role.
Unregistered, no OriginStays CLI. This default blocks extension hijacking (advisory GHSA-8x8f-54wf-vv92).

Success response:

json
{"type": "status", "status": "registered", "role": "extension"}

Rejection response:

json
{"type": "error", "code": "INVALID_ROLE", "error": "Cannot register as client: extension"}

Troubleshooting

Extension Not Connecting

If you see “Extension did not connect after 15s”:

  1. Check extension console:
  2. Go to chrome://extensions/
  3. Find “PraisonAI Browser Agent”
  4. Click “service worker” link
  5. Look for errors
  1. Kill stale Chrome processes:
bash
pkill -f "Google Chrome"
  1. Rebuild extension:
bash
cd /path/to/praisonai-chrome-extension
npm run build
  1. Check bridge server:
bash
curl http://localhost:8765/health

Expected output:

json
{
  "status": "ok",
  "connections": 2,
  "extension_connections": 1,
  "sessions": 0,
  "extension_busy": false,
  "active_session_id": null
}

connections counts all WebSocket clients (extension, CLI, curl’d probes). Check extension_connections >= 1 instead — it counts only clients whose Origin starts with chrome-extension://, so a stray CLI probe won’t make it look connected. connections >= 1 alone is no longer sufficient.

extension_busy and active_session_id (added in PraisonAI #3095 hardening) tell you whether the extension is currently owned by a session — a healthy-and-idle bridge shows extension_busy: false. praisonai browser run fails fast with an “Extension already running a session” error rather than hanging when this is true. Servers older than that commit omit both fields; treat missing as false / null.

start_session returns NO_EXTENSION even though the WebSocket is open

The offscreen document has not registered. Add this to its onopen handler:

js
ws.addEventListener("open", () => {
  ws.send(JSON.stringify({ type: "register", client: "extension" }));
});

Expect the server log line [SERVER][REGISTER] Connection registered as extension. Once the extension has sent register, /health (extension_connections) and session routing agree. If /health shows extension_connections: 1 but start_session still returns NO_EXTENSION, check that the start_session is sent after the registered status and from the expected socket.

No Observations Sent

If session starts but times out:

  • Check extension console for [PraisonAI] onStartAutomation FATAL ERROR
  • CDP debugger may fail to attach
  • Page may be a chrome:// URL (unsupported)

Debug Mode

bash
praisonai browser launch "test" --debug

This enables verbose logging and saves screenshots to ~/.praisonai/browser_screenshots/.

Performance Profiling

Track timing breakdown for each automation step:

bash
# Basic profiling with timing summary
praisonai browser launch "search for AI" --engine cdp --profile

# Deep profiling with cProfile trace
praisonai browser launch "search for AI" --engine cdp --deep-profile

Sample Output

📊 Performance Profile
──────────────────────────────────────────────────────────────────────
Total Time: 24.2s | Steps: 3 | Avg: 8.1s/step

Step |    LLM | Screen | Action | Verify | Stable |  Total
   0 |   2.4s |   0.0s |   1.0s |   0.0s |   0.0s |   3.5s
   1 |   2.2s |   0.0s |   0.2s |   0.0s |   0.0s |   2.5s

Bottlenecks: LLM 51% | Verify 0% | Stable 0%

Timing Breakdown

MetricDescription
LLMTime spent waiting for LLM decision
ScreenScreenshot capture time
ActionCDP action execution time
VerifyAction verification time
StablePage stability wait time