---
name: hermes-browser-setup
description: "Install, configure, and troubleshoot Hermes browser automation tools — local Chromium via Playwright, cloud providers (browser-use, Browserbase, Camofox), and CDP-based headless mode. Covers the full lifecycle from zero to working browser_* tools."
version: 1.2.0
author: Hermes Agent
license: MIT
platforms: [linux]
metadata:
  hermes:
    tags: [hermes, browser, automation, playwright, chromium, cdp, browser-use]
    related_skills: [hermes-agent, hermes-migration, computer-use]
---

# Hermes Browser Setup

Hermes ships a `browser` toolset with ~10 tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_scroll`, etc.) that can run via a **local Chromium CDP** or **cloud providers** (browser-use.com, Browserbase, Camofox).

This skill covers getting from zero to working browser tools, including the common failure mode where the toolset is enabled but no tools appear because no credential or CDP URL is configured.

## Quick Success Check

```bash
# Are browser tools enabled?
hermes tools list | grep browser

# Is a CDP URL or cloud key set?
grep -i 'browser\|cdp\|base\|fox' ~/.hermes/.env
grep -A5 'browser:' ~/.hermes/config.yaml
```

**Tools gated out with no error?** Check `.env` for `BROWSER_CDP_URL`, `BROWSER_USE_API_KEY`, `BROWSERBASE_API_KEY`, or `CAMOFOX_URL`. If none are set, the browser tools will be silently disabled regardless of `hermes tools list` showing them enabled.

## Mode A: Local Chromium via Playwright (free, recommended default)

### 1. Install Playwright + Chromium

```bash
pip3 install playwright
python3 -m playwright install chromium
python3 -m playwright install-deps chromium
```

Chromium binary lands at `~/.cache/ms-playwright/chromium-<version>/chrome-linux64/chrome`.

### 2. Start headless CDP server

**Quick test:**
```bash
~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome \
    --headless=new --no-sandbox --disable-gpu --disable-dev-shm-usage \
    --remote-debugging-port=9222 --user-data-dir=/tmp/chromium-data &

# Verify
curl -s http://127.0.0.1:9222/json/version
# Expected: Chrome/<version> + webSocketDebuggerUrl
```

**Persistent systemd service:**

```bash
cat > /etc/systemd/system/hermes-browser.service << 'EOF'
[Unit]
Description=Hermes Headless Chromium (CDP Browser)
After=network.target

[Service]
Type=simple
User=root
ExecStart=/root/.cache/ms-playwright/chromium-<version>/chrome-linux64/chrome \
    --headless=new \
    --no-sandbox \
    --disable-gpu \
    --disable-dev-shm-usage \
    --remote-debugging-port=9222 \
    --user-data-dir=/tmp/chromium-data
Restart=always
RestartSec=5
Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable hermes-browser.service
systemctl start hermes-browser.service
```

### 3. Wire up Hermes config

```bash
# Set CDP URL in .env
echo 'BROWSER_CDP_URL=ws://127.0.0.1:9222' >> ~/.hermes/.env

# Set local mode in config
hermes config set browser.cloud_provider local

# Restart gateway (or /reset in chat)
hermes gateway restart
```

### 4. Verify

```bash
hermes chat -q "Use browser_navigate to visit http://whatsmyuseragent.com and report the browser string" --toolsets browser
```

Expected: successful navigation, browser reports `HeadlessChrome/<version>`.

## Mode B: browser-use.com Cloud (when local Chromium isn't enough)

Use cloud mode when sites detect headless Chromium, require real mobile/desktop fingerprints, or you need browser-use's anti-detection features.

```bash
# Get API key from https://browser-use.com/
# Then:
echo 'BROWSER_USE_API_KEY=<your-key>' >> ~/.hermes/.env
hermes config set browser.cloud_provider browser-use
hermes gateway restart
```

Cost: ~$0.02/hr per browser instance. No minimums.

## Mode C: Browserbase (alternative cloud)

```bash
echo 'BROWSERBASE_API_KEY=<key>' >> ~/.hermes/.env
echo 'BROWSERBASE_PROJECT_ID=<project>' >> ~/.hermes/.env
hermes config set browser.cloud_provider browserbase
hermes gateway restart
```

Has a free tier suitable for light usage.

## Mode D: Camofox (local anti-detection)

```bash
echo 'CAMOFOX_URL=http://localhost:<port>' >> ~/.hermes/.env
```

## Configuration Reference

| Config key / env var | Default | Mode | Description |
|---|---|---|---|
| `browser.cloud_provider` | `browser-use` | All | `local`, `browser-use`, `browserbase`, or `camofox` |
| `BROWSER_CDP_URL` | — | Local | WebSocket URL of local Chromium (e.g. `ws://127.0.0.1:9222`) |
| `BROWSER_USE_API_KEY` | — | Cloud | Key from browser-use.com |
| `BROWSERBASE_API_KEY` | — | Cloud | Key from browserbase.com |
| `BROWSERBASE_PROJECT_ID` | — | Cloud | Project ID from Browserbase |
| `CAMOFOX_URL` | — | Local | Camofox anti-detection URL |
| `browser.inactivity_timeout` | `120` | All | Seconds before idle browser session is closed |

## SPA Loading Behavior

JS-rendered API doc sites (Redoc, Swagger UI, Stoplight) often show "Loading..." in the page title and have an **empty accessibility tree** on first `browser_snapshot`. This is normal — the SPA bootstraps, then renders the sidebar tree, then lazy-loads the content pane. The `browser_snapshot` tool captures the current accessibility tree state and will grow as the page renders.

**Proven resolution sequence (netcup KVM, Chromium 149):**
1. `browser_navigate(url)` — returns `{title: "Loading...", element_count: 0}`. This is fine.
2. `browser_snapshot(full=true)` — after ~2-3s, the sidebar appears with 7,000+ elements. The content pane may still be sparse.
3. If the content pane is empty, use `browser_click(ref)` on a sidebar link to navigate to a specific endpoint, then `browser_snapshot` again. Redoc renders endpoint details on click.
4. For bulk extraction, use `browser_cdp` with `Runtime.evaluate` on the correct target (identified via `Target.getTargets`) targeting the page's title/URL, not the chrome://newtab page. Execute `document.body.innerText.substring(N, M)` to page through the full rendered text.

See `references/js-rendered-api-docs.md` for the full probe pattern.

## Auto-Provisioning Flow (any phone, any source)

Phones purchased from any retail source (Amazon, Best Buy, eBay, second-hand from a previous provider) can auto-provision on RingLogix/NetSapiens. The critical insight: **MAC registration is the only per-phone step**. Everything else (DHCP, provisioning server URL) is network-level config.

### The Flow

| Step | What happens | Who does it |
|---|---|---|
| **1. Factory reset** (used phones only) | Hold reset pin or use phone menu to wipe old config | Customer or you, one-time |
| **2. DHCP discovery** | Phone boots, requests IP. Router serves DHCP options pointing to provisioning server | Router/DHCP server (configured once) |
| **3. MAC registration** | Register the phone's MAC address in RingLogix with model info | API or portal — `?object=mac&action=create` |
| **4. Device linking** | Link MAC to a subscriber extension | API or portal — `?object=device&action=create` |
| **5. Provisioning request** | Phone contacts the NDP (NetSapiens Device Provisioning) server, sends its MAC | Phone does this automatically |
| **6. Config push** | Server matches MAC, pushes SIP credentials, extension, caller ID, features | RingLogix NDP server |
| **7. Auto-reboot** | Phone reboots with new config, registers to VoIPSimplicity | Automatic |
| **8. Verify** | Check subscriber status, registration, and call capability | API — `?object=subscriber&action=read` |

### DHCP Options Required (configure once on your router/MikroTik)

| DHCP Option | Provider/Standard | What it sets |
|---|---|---|
| **Option 156** | Yealink / Poly | Provisioning server URL |
| **Option 66** (TFTP server) | Generic SIP phones | TFTP server address for config files |
| **Option 160** | Grandstream | HTTP provisioning URL |
| **Option 43** (Vendor-specific) | Cisco | Cisco-specific provisioning |

The actual provisioning server URL (e.g., `prov.ringlogix.com`) is configured in the RingLogix portal UI — it is **not exposed** through the REST API. The API provides the building blocks (MAC registration, device model queries) but not the provisioning server config itself.

### MAC Endpoints (from RingLogix API docs)

All POST to `https://api.ringlogix.com/pbx/v1/?object=mac&action=<action>`.

**Create:** `?object=mac&action=create`
Permission: OMP (Office Manager / Reseller / Super User)
| Param | Required | Description |
|---|---|---|
| `mac` | Yes | MAC address, hex only, 12 chars, no `-` `:` `.` |
| `domain` | Yes | Domain of new device |
| `model` | Yes | Phone model (query via Device_Model endpoint) |
| `transport` | No | NDP transport type |
| `server`, `last_pull`, `date_created` | No | Metadata |
| `device1..device8` | No | Device field references |
| `territory`, `notes` | No | Organization |
| `line1_ext..line2_ext` | No | Extension lines |
| `line1_enable..line8_enable` | No | Per-line enable flags |
| `line1_share..line8_share` | No | Line sharing |
| `overrides`, `dir_inc`, `presence` | No | NDP overrides |

**Read:** `?object=mac&action=read` — Permission: Reseller
| Param | Required | Description |
|---|---|---|
| `mac` | Yes | MAC to look up (hex, colons removed) |
| `extension` | Yes | Numerical string, searches lines with this extension associated with the MAC |
| `checkExistance` | Yes | Boolean — check if MAC exists |

Response includes: mac, server, territory, dir_inc, presence, domain, model, device1-8, notes, line1-8_share, overrides, phone_ext, transport, fxs, sla, sidecar, resync, directory_support, user_agent, contact, registration information.

**Update:** `?object=mac&action=update` — Permission: Reseller
Requires `mac`; all other fields optional (model, line, device, transport, server, notes, overrides, etc.).

**Delete:** `?object=mac&action=delete` — Permission: Reseller
Requires `mac` (hex, colons removed) and `domain`.

**Count:** `?object=mac&action=count` — Permission: Reseller
Requires `domain`. Optional: `territory`, `mac`, `mac_LIKE` (partial match), `model`, `notes`.

### Device Model Query

```http
POST https://api.ringlogix.com/pbx/v1/?object=devicemodel&action=read
```
Permission: Reseller
Optional: `brand`, `model`, `portal_view`, `include_ndp_defs`, `ndp_syntax`

### Important API Notes

- All endpoints use **POST** — parameters in form body
- Routing via `?object=` and `?action=` query params (not RESTful paths)
- Reseller permission required for provisioning ops; `subscriber&action=list` needs only Basic User
- RingLogix provides a Postman test collection from the docs site
- The docs site uses Redoc JS viewer — see `references/js-rendered-api-docs.md` for SPA navigation tips
- **Client ID/Secret** from RingLogix support (separate from PBX user credentials) — this is required before any API call works

## Reference Files

| File | Contents |
|---|---|
| `references/js-rendered-api-docs.md` | Navigating JS-heavy API doc sites (Redoc, Swagger) — sidebar vs main pane behavior, probe sequence |
| `references/ringlogix-api.md` | RingLogix (NetSapiens/CPaaS) API reference — OAuth2 auth, subscriber management, device provisioning, MAC endpoints, CDRs, and full endpoint catalog |

## Available Browser Tools

| Tool | Purpose | Requires |
|---|---|---|
| `browser_navigate` | Load a URL | First call that initializes session |
| `browser_snapshot` | Get accessibility tree with ref IDs | `browser_navigate` first |
| `browser_click` | Click element by ref ID | `browser_snapshot` first |
| `browser_type` | Type into a field by ref ID | `browser_snapshot` first |
| `browser_scroll` | Scroll up/down/left/right | `browser_navigate` first |
| `browser_back` | Navigate backward in history | `browser_navigate` first |
| `browser_press` | Press a keyboard key | `browser_navigate` first |
| `browser_console` | Get JS console output | `browser_navigate` first |
| `browser_get_images` | List images on current page | `browser_navigate` first |
| `browser_vision` | Screenshot + vision analysis | `browser_navigate` first |

## Troubleshooting

### Tools enabled but not appearing in function list

```
# Check if this machine
hermes tools list | grep browser    # should show "enabled"
grep BROWSER ~/.hermes/.env         # must show CDP URL or API key
```

**Root cause:** The browser toolset checks for a configured backend at startup. If no key/CDP URL is found, all browser tools silently gate out. Setting `cloud_provider` alone is NOT sufficient — the corresponding env var must exist.

### Chromium exits with code 127 (binary not found) or path mismatch

```bash
$ ~/.cache/ms-playwright/chromium-1228/chrome-linux/chrome
bash: .../chrome: No such file or directory
```

**The Playwright Chromium binary is NOT at `chrome-linux/chrome`.** Since Playwright ~1.50, the path is `chrome-linux64/chrome`. This was confirmed on Playwright 1.61.0 / Chromium 1228 installed Jul 7, 2026 on Debian 13. Always use:

```bash
find ~/.cache/ms-playwright/chromium-*/ -name chrome -type f
```

Then use the full discovered path in the systemd unit's `ExecStart`. The incorrect `chrome-linux/chrome` path causes exit code 127 immediately with no CDP port responding.

### Chromium fails to start with exit code 133

```
Error: dbus/bus.cc failed to connect to the bus
```

**Fix 1:** Start the D-Bus daemon:
```bash
dbus-daemon --system --fork
```

**Fix 2:** Set the env var in the systemd unit:
```
Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket
```

**Fix 3:** Install dbus-x11 if missing:
```bash
apt-get install -y dbus dbus-x11
```

### Headless Chrome crashes on KVM VPS (Debian 13)

**Symptoms:** Process exits with 133 or 255 immediately, no CDP port responding.

**Known issue on netcup KVM (nested virtualization):** The system CPU scaling files (`/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq`) are missing. These errors are **non-fatal warnings** — the real cause is usually:
1. Missing `--no-sandbox` flag
2. D-Bus socket unavailable
3. Wrong path to the `chrome` binary (Playwright installs to `chrome-linux64/chrome`, not `chrome-linux/chrome`)

Always check the binary path with `find`:
```bash
find ~/.cache/ms-playwright/chromium-*/ -name chrome -type f
```

### Test: verify browser is working

From any Hermes session:
```bash
hermes chat -q "Open browser_navigate to http://example.com and report the page title" --toolsets browser
```

Expect: `Example Domain` title returned.

### SPA site returns "Loading..." and empty snapshot — that's normal

When browsing JS-rendered API docs (Redoc, Swagger), the first `browser_navigate` call returns `title: "Loading..."` with `element_count: 0`. This is expected behavior — the SPA has not finished rendering. Resolution sequence proven on netcup KVM with Chromium 149:

1. Call `browser_navigate(url)` — accept the "Loading..." title
2. Call `browser_snapshot(full=true)` — after ~2-3s, sidebar appears with 7,000+ elements
3. If content pane is empty, use `browser_click(ref)` on a sidebar link, then `browser_snapshot` again
4. For bulk extraction, use `browser_cdp` with `Runtime.evaluate` on the correct page target (identified via `Target.getTargets`) targeting the page's URL/title, not the `chrome://newtab` page. Execute `document.body.innerText.substring(N, M)` to page through rendered content

See `references/js-rendered-api-docs.md` for the full probe pattern.

### Multiple page targets exist — use Target.getTargets to find the right one

When `browser_cdp` calls with `Runtime.evaluate` fail with `method not found`, the issue is often that the browser has multiple open tabs (new tab page + your page). Use `Target.getTargets` to find the page target by URL, then pass `target_id` in subsequent CDP calls. The `browser_navigate` tool interacts with the most recent target, but raw CDP calls need the explicit id.

## Related Skills

- `computer-use` — desktop GUI driving (separate from browser tools; use `browser_*` for web automation)
- `server-architecture-plan` — service placement on this server (browser runs well on 8C/15G boxes)
- `reboot-with-health-check` — verify hermes-browser.service comes back after server reboot
