Skip to content
5 changes: 4 additions & 1 deletion cmd/gateway_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,12 @@ func setupToolRegistry(
// Browser automation tool
if cfg.Tools.Browser.Enabled {
var opts []browser.Option
if cfg.Tools.Browser.Backend != "" {
opts = append(opts, browser.WithBackend(browser.Backend(cfg.Tools.Browser.Backend)))
}
if cfg.Tools.Browser.RemoteURL != "" {
opts = append(opts, browser.WithRemoteURL(cfg.Tools.Browser.RemoteURL))
slog.Info("browser tool enabled", "remote", cfg.Tools.Browser.RemoteURL)
slog.Info("browser tool enabled", "remote", cfg.Tools.Browser.RemoteURL, "backend", cfg.Tools.Browser.Backend)
} else {
opts = append(opts, browser.WithHeadless(cfg.Tools.Browser.Headless))
slog.Info("browser tool enabled", "headless", cfg.Tools.Browser.Headless)
Expand Down
41 changes: 41 additions & 0 deletions docker-compose.lightpanda.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Lightpanda sidecar overlay — lightweight CDP browser.
#
# Usage:
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.lightpanda.yml up -d --build
#
# Lightpanda (https://lightpanda.io) is a low-memory headless browser with CDP
# support. Opt-in alternative to the Chrome sidecar (docker-compose.browser.yml).
# See docs/browser-backends.md for the compatibility matrix.

services:
lightpanda:
# Official Lightpanda CDP image: https://hub.docker.com/r/lightpanda/browser
image: lightpanda/browser:latest
# Invoke the lightpanda binary explicitly — the image's default entrypoint
# isn't `lightpanda`. Verify against image docs if upgrading.
command: ["lightpanda", "serve", "--host", "0.0.0.0", "--port", "9222"]
ports:
- "127.0.0.1:${LIGHTPANDA_CDP_PORT:-9222}:9222"
healthcheck:
# Minimal TCP probe via sh's /dev/tcp. If the image is scratch-based
# without a shell, drop healthcheck and change depends_on below to
# `condition: service_started`.
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9222 || exit 1"]
interval: 5s
timeout: 3s
retries: 5
deploy:
resources:
limits:
# Lightpanda is ~10x lighter than Chrome.
memory: 512M
cpus: '1.0'
restart: unless-stopped

goclaw:
environment:
- GOCLAW_BROWSER_REMOTE_URL=ws://lightpanda:9222
- GOCLAW_BROWSER_BACKEND=lightpanda
depends_on:
lightpanda:
condition: service_healthy
61 changes: 61 additions & 0 deletions docs/browser-backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Browser Backends

Goclaw's browser automation tool (`pkg/browser/`) connects to any CDP-compatible browser. Two backends are supported:

| Backend | Image | Overlay | Status |
|---|---|---|---|
| **Chrome** (default) | `chromedp/headless-shell:latest` | `docker-compose.browser.yml` | Stable |
| **Lightpanda** | `lightpanda/browser:latest` | `docker-compose.lightpanda.yml` | Experimental |

## Switching backends

Both overlays set `GOCLAW_BROWSER_REMOTE_URL` to their respective sidecar. Only one should be active at a time.

```bash
# Chrome (default)
docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.browser.yml up -d

# Lightpanda
docker compose -f docker-compose.yml -f docker-compose.postgres.yml -f docker-compose.lightpanda.yml up -d
```

Set `GOCLAW_BROWSER_BACKEND=chrome|lightpanda` to pick the backend explicitly. If unset, goclaw probes `/json/version` on the remote and auto-detects from the `Browser` field.

## Compatibility matrix

| Feature | Chrome | Lightpanda | Notes |
|---|---|---|---|
| Navigate / reload | ✅ | ✅ | |
| AX snapshot (`Accessibility.getFullAXTree`) | ✅ | ✅ | Primary "see the page" path for the agent. Required Lightpanda fix [lightpanda-io/browser#2232](https://github.com/lightpanda-io/browser/pull/2232) (merged 2026-04) |
| Click / type / hover / press | ✅ | ✅ | |
| Wait (text / URL / stable) | ✅ | ✅ | |
| Evaluate JS | ✅ | ✅ | go-rod's `Page.Eval` requires a function form (`() => document.title`), not a bare expression — same on both backends |
| Screenshot (`Page.captureScreenshot`) | ✅ | ❌ | Lightpanda returns a placeholder image. The tool returns an error on Lightpanda directing the agent to use `snapshot` instead |
| Multiple tabs per connection | ✅ | ❌ | Lightpanda: 1 CDP connection = 1 tab. Goclaw opens a fresh connection per tab transparently |
| Browser contexts / incognito | ✅ | Implicit | On Lightpanda every connection is already a fresh browser — isolation is automatic, no `Target.createBrowserContext` multiplexing |
| Cookies / localStorage shared across tabs | ✅ within a context | ❌ | Lightpanda: each tab is a fresh browser. A login on one tab is not visible to another |
| List open tabs from server | ✅ | ❌ | Lightpanda: no `/json/list`. Goclaw tracks tabs in its local map (URL/title cached at OpenTab time, since `page.Info()` is also unreliable post-open) |
| Auto-reconnect on WS drop | ✅ | ❌ | Lightpanda: connection death = that tab is gone server-side. Goclaw drops the tab from the map and surfaces a clear error |

## Minimum Lightpanda version

The AX-tree (`Accessibility.getFullAXTree`) snapshot path requires Lightpanda with [lightpanda-io/browser#2232](https://github.com/lightpanda-io/browser/pull/2232) merged. Earlier images return `nodeId` as a JSON number (CDP spec: string), causing the typed go-rod decoder to fail. Use `lightpanda/browser:latest` or any image built after that PR landed.

## When to choose which

**Lightpanda:**
- Memory-constrained deployments (desktop / Lite edition, small VPS).
- Stateless workflows — navigate, snapshot, extract, done.
- Cases where per-tab isolation is a feature (every tab is a fresh browser).

**Chrome:**
- Multi-tab flows (OAuth popup → main window, tab-to-tab navigation).
- Long-lived sessions with shared login / cookies / localStorage.
- Screenshot-based workflows.
- Anything requiring full JS engine fidelity.

## References

- Lightpanda: https://lightpanda.io
- Lightpanda + go-rod demos: https://github.com/lightpanda-io/demo/tree/main/rod
- Tracking issue: https://github.com/nextlevelbuilder/goclaw/issues/223
7 changes: 4 additions & 3 deletions internal/config/config_channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,10 @@ type WebFetchPolicyConfig struct {

// BrowserToolConfig controls the browser automation tool.
type BrowserToolConfig struct {
Enabled bool `json:"enabled"` // enable the browser tool (default false)
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote Chrome sidecar, e.g. "ws://chrome:9222"
Enabled bool `json:"enabled"` // enable the browser tool (default false)
Backend string `json:"backend,omitempty"` // "chrome" (default) or "lightpanda"; auto-detected from /json/version if empty
Headless bool `json:"headless,omitempty"` // run Chrome in headless mode (ignored when RemoteURL is set)
RemoteURL string `json:"remote_url,omitempty"` // CDP endpoint for remote sidecar, e.g. "ws://chrome:9222" or "ws://lightpanda:9222"
ActionTimeoutMs int `json:"action_timeout_ms,omitempty"` // per-action timeout in ms (default 30000)
IdleTimeoutMs int `json:"idle_timeout_ms,omitempty"` // idle page auto-close in ms (default 600000, 0=disabled)
MaxPages int `json:"max_pages,omitempty"` // max open pages per tenant (default 5)
Expand Down
1 change: 1 addition & 0 deletions internal/config/config_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func (c *Config) applyEnvOverrides() {

// Browser (for Docker-compose browser sidecar overlay)
envStr("GOCLAW_BROWSER_REMOTE_URL", &c.Tools.Browser.RemoteURL)
envStr("GOCLAW_BROWSER_BACKEND", &c.Tools.Browser.Backend)
if c.Tools.Browser.RemoteURL != "" {
c.Tools.Browser.Enabled = true
}
Expand Down
Loading