Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@ jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
# 20 is the supported floor; 22 exercises live streaming and CDP
# attach mode (global WebSocket client).
node-version: [20, 22]
steps:
- name: Check out repository
# v4.2.2
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
# No later step runs authenticated git; don't leave the token in
# the checkout for repository-controlled test code to read.
persist-credentials: false
- name: Set up Node.js
# v4.0.3
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b
with:
node-version: 20
node-version: ${{ matrix.node-version }}
- name: Run tests
run: npm test
- name: Validate shell syntax
run: bash -n scripts/*.sh
Expand Down
82 changes: 75 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# herdr-browser

[![CI](https://github.com/StructuPath/herdr-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/StructuPath/herdr-browser/actions/workflows/ci.yml)

A drivable browser pane for [Herdr](https://herdr.dev), built around
[agent-browser](https://github.com/vercel-labs/agent-browser).

Expand All @@ -19,6 +21,10 @@ Conductor).
- **Shared agent sessions** — one isolated browser session per Herdr workspace.
- **Attach to any CDP browser** — observe a Playwright, Puppeteer, or Browser Use
run (or any Chrome started with `--remote-debugging-port`) without owning it.
- **Zero-setup launch** — press `l` and the pane launches a local Chromium of
its own and attaches to it; no agent-browser install required.
- **Observe-only mode** — press `o` and pane input stops being forwarded, so
watching a live automation run can never perturb it.
- **Live push streaming** — frames, URL/title changes, console messages, and
page errors arrive over WebSocket, with transparent polling fallback.
- **Failed network requests** — 4xx/5xx and no-response xhr/fetch/document
Expand All @@ -39,8 +45,9 @@ Conductor).
| Component | Requirement | Notes |
| --- | --- | --- |
| Herdr | `>= 0.7.0` | Tested with Herdr 0.7.4 |
| Node.js | `>= 20` | Node 22+ enables live WebSocket streaming and CDP attach mode |
| agent-browser | Required | Tested with agent-browser 0.33.x; failed-request reporting needs the `network requests` command |
| Node.js | `>= 20` | Node 22+ enables live WebSocket streaming, CDP attach mode, and launch mode |
| agent-browser | Optional | Required for shared agent sessions; tested with agent-browser 0.33.x; failed-request reporting needs the `network requests` command |
| Chromium/Chrome | Optional | Any Chromium-based browser enables launch mode (`l`) and attach mode |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| chafa | Optional | ANSI rendering and streamed JPEGs in Kitty mode |
| carbonyl | Optional | Only required for the separate interactive Browse action |

Expand Down Expand Up @@ -140,6 +147,9 @@ Use these controls to drive the shared session directly:
| --- | --- |
| `u` | Open the address prompt; `https://` is assumed when omitted |
| `a` | Attach to a CDP endpoint (`http://host:port` or `ws://…`) |
| `l` | Launch a local Chromium the pane owns and attach to it |
| `t` | Attach mode: cycle the pane between the browser's page targets (tabs) |
| `o` | Toggle observe-only: pane input is dropped instead of forwarded |
| Click the screenshot | Send real Chrome mouse move/down/up events at that page coordinate |
| `i` | Type into the currently focused page element |
| `b` / `f` | Navigate backward / forward |
Expand Down Expand Up @@ -212,10 +222,7 @@ seconds without a status). Only xhr, fetch, and document requests are watched
images, stylesheets, and held-open streams (SSE, WebSocket) stay out. Failures
from before the pane attached are intentionally not replayed, a repeating
identical failure paints once and stays collapsed until it has been quiet for
60 seconds, and on very long
from before the pane attached are intentionally not replayed, repeated
identical failures are collapsed within a 60-second window, and on very long
sessions the feed turns itself off with a one-time note once the daemon's
60 seconds, and on very long sessions the feed turns itself off with a one-time note once the daemon's
request log outgrows the pane's read buffer.

## Attach to any CDP browser
Expand All @@ -236,7 +243,20 @@ printf 'http://127.0.0.1:9222\n' > "$(herdr plugin config-dir structupath.browse

Press `a` in the pane to attach at runtime. `u` still means "navigate" — the
keys are separate because `localhost:9222` is a valid destination as well as a
valid endpoint.
valid endpoint. While attached, the pane header shows the endpoint's
`host:port` instead of a session name, `t` cycles between the browser's page
targets when your automation has more than one tab open, and `o` toggles
**observe-only**: every pane click, wheel event, keystroke, and navigation —
including Cmd/Ctrl+click link handoffs in attach mode — is dropped at the
pane instead of forwarded, so watching a live run cannot blur the field your
automation is typing into or dismiss the element it is waiting on.
Observe-only is a pane-side latch; nothing about the observed browser
changes when you toggle it, and pane input is guarded in agent-browser mode
too. One caveat for the runtime toggle: in agent-browser mode a Cmd+click
navigates the session daemon directly, outside the pane. Set
`HERDR_BROWSER_OBSERVE=1` (or the `observe` config file) for
watch-the-agent workspaces — the pane starts observe-only *and* the open
action itself refuses link navigation, closing that gap in both modes.

Launcher recipes: Playwright `chromium.launch({args:['--remote-debugging-port=9222']})`,
Puppeteer the same `args`, Browser Use its `chrome_remote_debugging_port` option.
Expand Down Expand Up @@ -269,6 +289,37 @@ report it the way the agent-browser polling feed's timeout heuristic does.
Attach mode needs Node 22 or newer (for the built-in WebSocket client); the pane
says so plainly on older Node and keeps working in agent-browser mode.

## Launch a browser from the pane

Press `l` and the pane launches a local Chromium with a loopback DevTools
port and attaches to it — no agent-browser, no configuration. This is the
zero-setup path: open the pane, press `l`, press `u`, browse. Like attach
mode, launching needs Node 22 or newer (the built-in WebSocket client); the
pane says so instead of starting a browser it could never attach to.

The launcher looks for `HERDR_BROWSER_CHROMIUM` (or the `chromium` config
file), then probes `google-chrome`, `google-chrome-stable`, `chromium`,
`chromium-browser`, `chrome`, and the macOS Chrome/Chromium app bundles —
`google-chrome` first because Ubuntu's `chromium` is often a snap wrapper
whose confinement cannot use a profile outside `$HOME`.
The browser starts headless with a fresh ephemeral DevTools port
(`--remote-debugging-port=0`, read back from `DevToolsActivePort`) and a
per-workspace profile under the plugin state directory, so cookies and
localStorage survive relaunches. Set `HERDR_BROWSER_LAUNCH_HEADED=1` to get
a visible browser window instead.

Unlike plain attach mode, the pane owns what it launches: quitting the pane
— or attaching to a different endpoint — kills the launched browser rather
than leaking a headless Chrome. Every attach-mode guarantee about the
*endpoint* still holds: the DevTools port binds to loopback, and the
capability token is never displayed.

For a workspace that should always work this way, set
`HERDR_BROWSER_LAUNCH=1` (or write `1` to the `launch` config file): the
pane launches its Chromium on open, no keypress needed. A configured
`cdp-url` endpoint still wins, and the launch is attempted once — if it
fails, the banner says why and the keys take over.

## Session model

By default, each Herdr workspace uses:
Expand Down Expand Up @@ -298,6 +349,11 @@ echo "my-agent-session" \

## Recording

Recording captures the workspace's **agent-browser session**. Attach and
launch workspaces have no such session, so the record actions refuse with
an explanation instead of silently recording a fresh, unrelated headless
browser — record from the automation client that owns the browser instead.

Start and stop recording through the existing recording actions. Each new
capture is a run-scoped observation bundle:

Expand Down Expand Up @@ -340,6 +396,10 @@ Plugin config files contain one value on their first line:
| `session` | Session name | `herdr-ws-<workspace-id>` | Watch a different agent-browser session |
| `run-id` | Valid run ID | Generated | Correlate a recording bundle with an external run |
| `render` | `kitty`, `symbols`, `text` | Automatic probe | Force a rendering mode |
| `cdp-url` | `http://host:port` or `ws://…` | None | Attach to this CDP endpoint at startup |
| `chromium` | Path to a browser binary | Probed | Browser used by launch mode (`l`) |
| `launch` | `1`/`true`/`yes`/`on` | Off | Launch a Chromium on open instead of waiting for a session |
| `observe` | `1`/`true`/`yes`/`on` | Off | Start observe-only; `o` still toggles |

Equivalent environment controls:

Expand All @@ -348,6 +408,11 @@ Equivalent environment controls:
| `HERDR_BROWSER_SESSION` | Workspace session | Override the watched session |
| `HERDR_BROWSER_RUN_ID` | Config or generated ID | Select the recording run ID |
| `HERDR_BROWSER_RENDER` | Automatic probe | Override the rendering mode |
| `HERDR_BROWSER_CDP_URL` | None | Attach to this CDP endpoint at startup |
| `HERDR_BROWSER_CHROMIUM` | Probed | Browser binary used by launch mode |
| `HERDR_BROWSER_LAUNCH_HEADED` | Unset | `1` launches a visible window instead of headless |
| `HERDR_BROWSER_LAUNCH` | Unset | `1` launches a Chromium on open instead of waiting for a session |
| `HERDR_BROWSER_OBSERVE` | Unset | `1` starts the pane observe-only |
| `HERDR_BROWSER_INTERVAL_MS` | `1000` | Polling interval; clamped to safe bounds |
| `AGENT_BROWSER_IDLE_TIMEOUT_MS` | `1800000` | Idle timeout for plugin-created browser daemons |

Expand All @@ -364,6 +429,9 @@ Environment variables take precedence over config files.
- WebM recordings are intentionally retained under the plugin state directory.
- Browser sessions are a trusted local boundary: any local process that knows a
session name can drive it, including authenticated pages.
- The `chromium`/`cdp-url` configuration files and their environment
equivalents are trusted local configuration: whoever can write them chooses
which binary launch mode executes and which endpoint the pane dials.

If an agent browses sensitive or authenticated content, that content is visible
in the pane and briefly present in its cached frame. Treat screen sharing and
Expand Down
12 changes: 11 additions & 1 deletion bin/cdp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,17 @@ export function makeCdpBrowser(endpointInput, opts = {}) {
} catch {
/* older engines: page-level feed only */
}
await startScreencast();
try {
await startScreencast();
} catch (err) {
// Firefox's CDP subset has no Page.startScreencast: name the reason
// instead of surfacing a raw protocol error nobody can act on.
if (/wasn't found|not found|not supported|unknown method/i.test(err?.message ?? ""))
throw new Error(
"browser has no CDP screencast (Firefox?) — attach needs a Chromium-based browser",
);
throw err;
}
};

const onCdpEvent = (m) => {
Expand Down
10 changes: 9 additions & 1 deletion bin/record.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@ import path from "node:path";
import { fileURLToPath } from "node:url";

const PLUGIN_ID = "structupath.browser";
const PLUGIN_VERSION = "0.6.0";
// Single-sourced from package.json so a release bump cannot miss it. The
// Start/Stop manifest check still compares versions strictly: a recording
// that crosses an upgrade fails closed rather than completing ambiguously.
const PLUGIN_VERSION = JSON.parse(
fs.readFileSync(
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
"utf8",
),
).version;
const SCHEMA_VERSION = 1;
const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
const WORKSPACE_ID_RE = /^[A-Za-z0-9_-]+$/;
Expand Down
Loading
Loading