mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
docs(optional-skills): rewrite optional skills from built-in browser_* tools to browser_exec
unbroker's browser_backend 'agent-browser' value becomes 'local' (legacy value mapped on load); har-derived-api-client, sketch, finance, web-pentest, page-agent and others drop agent-browser / Camofox-in-core text. Regenerated skill pages.
This commit is contained in:
1 parent
ad5e44daa2
commit
1211e42ae2
39 files changed
+144
-116
No files matched your search
@@ -44,7 +44,7 @@ Start a new figure by copying the closest example into `$WORK/figures/<name>/` a
|
||||
|
||||
**Looking at your work.** The upstream workflow says "read every PNG". In Hermes that means `vision_analyze` on each capture, and the 4× close-ups use its `region` crop on a high-DPR capture or a `--zoom` capture. Ask specific questions ("does the pipe at the flange end on the flange face?"), not "does this look good".
|
||||
|
||||
**Browser.** The capture, drive, line-end and 3D checks start a headless Chrome over a loopback DevTools port. They use `CHROME_PATH` if set, then the Playwright headless shell that Hermes' browser tool installs (`chromium_headless_shell-*` in the Playwright cache), then a system Chrome or Chromium. Prefer the headless shell: it renders the WebGL layers through SwiftShader and makes no outbound connections. A full desktop Chrome opens background connections to Google services even with a throwaway profile, and it fails to start ("Socket path too long", then "Chrome did not start") when `TMPDIR` is a long path, as Hermes' per-profile scratch dir can be. If you must use one, set `TMPDIR` to a short directory for that command. Building a figure needs no browser at all.
|
||||
**Browser.** The capture, drive, line-end and 3D checks start a headless Chrome over a loopback DevTools port. They use `CHROME_PATH` if set, then a Playwright headless shell if one is installed (`chromium_headless_shell-*` in the Playwright cache, e.g. from `npx playwright install chromium`), then a system Chrome or Chromium. Prefer the headless shell: it renders the WebGL layers through SwiftShader and makes no outbound connections. A full desktop Chrome opens background connections to Google services even with a throwaway profile, and it fails to start ("Socket path too long", then "Chrome did not start") when `TMPDIR` is a long path, as Hermes' per-profile scratch dir can be. If you must use one, set `TMPDIR` to a short directory for that command. Building a figure needs no browser at all.
|
||||
|
||||
**Long runs.** `node build.mjs --audit` takes about two minutes on a dense Hero, and a full 3D orbit audit takes 15 to 20 minutes for 30 parts. Run those with `terminal(background=true, notify=true)` and keep working on something else.
|
||||
|
||||
|
||||
@@ -87,14 +87,18 @@ Each variant is a **single self-contained HTML file**:
|
||||
|
||||
Open it in a browser. If it looks broken, fix it before showing the user.
|
||||
|
||||
**Verify variants visually — use Hermes' browser tools.** Don't just write HTML and hope it renders; load each variant and look at it:
|
||||
**Verify variants visually — use Hermes' `browser_exec`.** Don't just write HTML and hope it renders; load each variant and look at it:
|
||||
|
||||
```
|
||||
browser_navigate(url="file:///absolute/path/to/sketches/001-calm-editorial/index.html")
|
||||
browser_vision(question="Does this layout look clean and readable? Any visible bugs (overlapping text, unstyled elements, broken images)?")
|
||||
browser_exec(code='''
|
||||
# Checking sketch 001 renders cleanly
|
||||
new_tab("file:///absolute/path/to/sketches/001-calm-editorial/index.html")
|
||||
wait_for_load()
|
||||
capture_screenshot()
|
||||
''')
|
||||
```
|
||||
|
||||
`browser_vision` returns an AI description of what's actually on the page plus a screenshot path — catches layout bugs that pure source inspection misses (e.g. a font import that silently failed, a flex container that collapsed). Fix and re-navigate until each variant looks right.
|
||||
Look at the attached screenshot and ask: does this layout look clean and readable, any visible bugs (overlapping text, unstyled elements, broken images)? (non-vision models: `vision_analyze` the printed path with that question). It catches layout bugs that pure source inspection misses (e.g. a font import that silently failed, a flex container that collapsed). Fix and reload (`goto_url(...)` with the same URL) until each variant looks right.
|
||||
|
||||
**Default CSS reset + system font stack** for fast starts:
|
||||
|
||||
@@ -209,8 +213,8 @@ Propose 2-4 named candidates. Let the user pick.
|
||||
terminal("mkdir -p sketches/001-calm-editorial")
|
||||
write_file("sketches/001-calm-editorial/index.html", "<!doctype html>...")
|
||||
write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...")
|
||||
browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html")
|
||||
browser_vision(question="How does this look? Any obvious layout issues?")
|
||||
browser_exec(code='new_tab("file:///abs/path/sketches/001-calm-editorial/index.html"); wait_for_load(); capture_screenshot()')
|
||||
# then check the screenshot: any obvious layout issues?
|
||||
```
|
||||
|
||||
Repeat for each variant, then present the comparison table.
|
||||
|
||||
@@ -424,7 +424,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -653,7 +653,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -1261,7 +1261,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Don't use for: one-off status questions (answer directly), price/availability th
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- At least one source connector the dashboard will read from: email/calendar via `himalaya` or `google-workspace`, websites via `web_extract` or `browser_navigate`, local files via `read_file`. If none is configured, renegotiate the sources in step 1 before writing any artifact.
|
||||
- At least one source connector the dashboard will read from: email/calendar via `himalaya` or `google-workspace`, websites via `web_extract` or `browser_exec`, local files via `read_file`. If none is configured, renegotiate the sources in step 1 before writing any artifact.
|
||||
- `cronjob` for the recurring tick.
|
||||
- Optional: the `desktop_preview` tool (Hermes desktop app sessions). When it is in the toolset, dashboards render in the in-app preview pane; otherwise the user is given the file path.
|
||||
|
||||
@@ -45,7 +45,7 @@ From the user's sentence, pin down: the dashboard's purpose in one line, the ent
|
||||
|
||||
### 2. Verify each source with one live read
|
||||
|
||||
For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_navigate`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user.
|
||||
For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_exec`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user.
|
||||
|
||||
### 3. Build the dashboard artifact
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ fallback**. Scope is **US-first**, with EU/UK (GDPR) and global coverage on the
|
||||
|
||||
The design is **Hermes-native**: a small deterministic Python CLI (`scripts/pdd.py`) owns the state
|
||||
(config, dossiers, broker DB, tier planning, ledger, drafts, reports), while the agent does the
|
||||
scanning and submitting with native tools (`web_extract`, `browser_*`, email, `cronjob`,
|
||||
scanning and submitting with native tools (`web_extract`, `browser_exec`, email, `cronjob`,
|
||||
`delegate_task`). [`SKILL.md`](SKILL.md) is the authoritative reference.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -27,7 +27,7 @@ person controls.
|
||||
The Python CLI (`scripts/pdd.py`) owns the deterministic state - config, dossiers + consent, the
|
||||
broker database, tier planning, the ledger, drafts, reports, **email sending (SMTP), verification-link
|
||||
polling (IMAP), and the autonomous action queue (`next`)**. You (the agent) do the scanning and
|
||||
form-driving with native tools: `web_extract` and `browser_navigate` for searching and web forms, and
|
||||
form-driving with native tools: `web_extract` and `browser_exec` for searching and web forms, and
|
||||
`cronjob` for recurring re-scans.
|
||||
|
||||
## Autonomy contract
|
||||
@@ -76,13 +76,13 @@ verifying re-scan.
|
||||
- Email automation, two credential-free-or-not options:
|
||||
- **Browser mode (no password): `setup --email-mode browser`.** The agent sends opt-out/CCPA
|
||||
emails and opens verification links through the operator's **logged-in webmail** using
|
||||
`browser_*` tools. Nothing is stored. This requires Hermes to be pointed at the operator's own
|
||||
`browser_exec`. Nothing is stored. This requires Hermes to be pointed at the operator's own
|
||||
logged-in browser, **NOT** a cloud browser: a headless cloud browser (Browserbase) holds no
|
||||
webmail session and is itself Cloudflare/DataDome-gated on webmail and on session-bound broker
|
||||
gates (e.g. PeopleConnect guided-mode). Drive the operator's real Chrome over CDP - launch
|
||||
`chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.hermes/chrome-debug"` (a dedicated
|
||||
debug profile signed into the webmail once, not the Default profile) and connect the browser
|
||||
tools to `127.0.0.1:9222`. **`$PDD cdp` launches this for you** (finds Chrome/Chromium/Brave/Edge,
|
||||
debug profile signed into the webmail once, not the Default profile) and attach Hermes's browser
|
||||
to `127.0.0.1:9222` with `/browser connect`. **`$PDD cdp` launches this for you** (finds Chrome/Chromium/Brave/Edge,
|
||||
starts it detached on the dedicated profile, prints the CDP endpoint; `--check` to test, `--print`
|
||||
for the command). See `references/methods.md` -> "Browser backends: scan vs execute".
|
||||
Falls back to drafts for an email if the inbox isn't reachable.
|
||||
@@ -141,7 +141,7 @@ For anything past a couple of brokers, run this as **map → reduce → act**, n
|
||||
effects, so it is safe to parallelize and retry. Getting the full exposure map *before* acting is
|
||||
what unlocks cluster dedup and prioritization below. **Default: the parent drives `web_extract`
|
||||
probes directly** - most people-search sites render name/phone/address results as static HTML that
|
||||
`web_extract` reads in seconds. Escalate to `browser_*` only for the few JS-only sites, and to
|
||||
`web_extract` reads in seconds. Escalate to `browser_exec` only for the few JS-only sites, and to
|
||||
`delegate_task` subagents only for genuinely *reasoning*-heavy work (large-scale namesake/relative
|
||||
disambiguation). **Do NOT hand a browser-toolset subagent a big list of brokers to crawl** - in the
|
||||
field this timed out repeatedly (600s, ~5-6 brokers each, no summary) because browser navigation is
|
||||
@@ -223,7 +223,7 @@ recording `found` and before any deletion.
|
||||
`delegate_task` subagent per `batch`, in parallel, passing that batch's ready-made `brief`** - do
|
||||
not scan all brokers yourself sequentially. For `scan_inline`: scan the few brokers yourself.
|
||||
Either way, each broker gets **every** `search_vectors` entry via the `references/methods.md`
|
||||
ladder (`web_extract` → `site:` probe → `browser_navigate` → `scrapling`), a 404 is INCONCLUSIVE
|
||||
ladder (`web_extract` → `site:` probe → `browser_exec` → `scrapling`), a 404 is INCONCLUSIVE
|
||||
(not `not_found`), `blocked` is recorded when `antibot` is set and no stealth browser is available,
|
||||
and subject vs namesake/relative is confirmed before recording:
|
||||
`$PDD record <subject> <broker> <found|not_found|indirect_exposure|blocked> --found <bool> --evidence '{"listing_urls":[...]}'`.
|
||||
@@ -235,8 +235,8 @@ recording `found` and before any deletion.
|
||||
just the hide-my-listing flow. When it carries `prefer_suppression` instead (**PeopleConnect** -
|
||||
deleting removes your suppressions and does not stop re-listing), do the suppression flow and keep
|
||||
it maintained; use their Delete button only for a deliberate data-purge. Per method:
|
||||
- **web_form** → drive `optout_url` with `browser_navigate`/`browser_type`/`browser_click`, submit
|
||||
only `disclosure_fields`, screenshot the confirmation, then the action's `after` record command.
|
||||
- **web_form** → drive `optout_url` with `browser_exec` (`new_tab` + `wait_for_load`, `fill_input`,
|
||||
a `js()` click to submit), submit only `disclosure_fields`, `capture_screenshot()` the confirmation, then the action's `after` record command.
|
||||
Playbooks may end with a right-to-delete `send-email` follow-up - do it (full erasure, not just
|
||||
listing suppression).
|
||||
- **email** → `$PDD send-email <subject> <broker> --kind <ccpa|gdpr|generic> --to <addr>
|
||||
@@ -244,7 +244,7 @@ recording `found` and before any deletion.
|
||||
record declares; `next` picks the kind from residency - never claim CCPA/GDPR for someone who
|
||||
can't). In **browser** mode it returns a recipient-locked `compose` payload: compose a new
|
||||
message to `compose.to` with `compose.subject`/`compose.body` exactly in the operator's webmail
|
||||
via `browser_*` and send (no password); in **programmatic** mode it SMTP-sends. `next` also
|
||||
via `browser_exec` and send (no password); in **programmatic** mode it SMTP-sends. `next` also
|
||||
routes human-gated forms (phone-callback/gov-ID) through a broker's deletion email when one
|
||||
exists - the **rescue lane** (verified Whitepages pattern). Draft-only falls back to
|
||||
`render-email` + a digest entry.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"ADDRESS path is fully hyphen-joined and joins street+city+state+ZIP with hyphens (e.g. /address/123-Main-St-Anytown-NY-12345) and REQUIRES the ZIP. The older slash form (/address/Street/City-ST) and any ZIP-less form 404.",
|
||||
"NAME and PHONE and EMAIL paths use a slash before city/state and do NOT need a ZIP: /name/First-Last/City-ST , /phone/AAA-PPP-LLLL , /email/addr@host.",
|
||||
"Street abbreviations are kept verbatim (Ave, Pl, St) - do NOT expand to Avenue/Place/Street; expanding 404s.",
|
||||
"A 404 on a constructed URL means WRONG PATTERN, not 'no record present'. Treat as INCONCLUSIVE: fall back to the on-site search box (browser_type into the address/name field + submit) and read the resulting canonical URL."
|
||||
"A 404 on a constructed URL means WRONG PATTERN, not 'no record present'. Treat as INCONCLUSIVE: fall back to the on-site search box (browser_exec fill_input into the address/name field + submit) and read the resulting canonical URL."
|
||||
]
|
||||
},
|
||||
"optout": {
|
||||
|
||||
@@ -70,8 +70,9 @@ URLs.
|
||||
(b) a broad `"First Last" <ZIP OR unique-address>` search (no `site:`) surfaces **brokers not yet in
|
||||
your DB** (e.g. information.com, peoplefinders.com) - record those as bonus exposures. Note: empty
|
||||
`site:` results are INCONCLUSIVE (many broker pages aren't indexed / are `noindex`), not `not_found`.
|
||||
2. If the page is JS-rendered or returns nothing useful, `browser_navigate` + `browser_snapshot`
|
||||
(and `browser_type`/`browser_click` to run the site's search box).
|
||||
2. If the page is JS-rendered or returns nothing useful, use `browser_exec`: `new_tab(url)` +
|
||||
`wait_for_load()`, then `page_info()` / `js()` to read the results (`fill_input()` + a `js()`
|
||||
click to run the site's search box).
|
||||
3. If blocked by stealth/Cloudflare, use the `scrapling` skill via `terminal`. **If the broker record
|
||||
has `search.antibot` set (e.g. `datadome`), results are behind a device-check CAPTCHA**: a
|
||||
cloud/stealth browser (Browserbase) or `scrapling` may get through; if none is available, do **not**
|
||||
@@ -99,8 +100,9 @@ If a listing genuinely does not exist: `pdd.py record <subject> <broker> not_fou
|
||||
A constructed search URL that 404s almost always means the **URL pattern is wrong**, not that the
|
||||
person is absent. Never record `not_found` off a 404. Instead:
|
||||
1. Re-check the broker's `search.url_patterns` / `url_format_quirks` and rebuild the URL.
|
||||
2. Fall back to the **on-site search box**: `browser_navigate` to the search page, `browser_type`
|
||||
the raw query, `browser_click` Search, then read the **canonical result URL** the site lands on.
|
||||
2. Fall back to the **on-site search box**: in one `browser_exec` call, `new_tab()` the search page,
|
||||
`fill_input()` the raw query, click Search via `js()`, `wait_for_load()`, then read the
|
||||
**canonical result URL** the site lands on (`js('location.href')`).
|
||||
3. Only after the site's own search returns an empty result set do you record `not_found`.
|
||||
4. If a pattern was wrong, fix it in `references/brokers/<id>.json` (`url_patterns` +
|
||||
`url_format_quirks`) so the next run is correct - see the rule below.
|
||||
@@ -156,10 +158,11 @@ re-deriving the listing URL). If a `found` turns out to be a false positive, cor
|
||||
|
||||
## web_form
|
||||
|
||||
1. `browser_navigate` to `optout.url`; `browser_snapshot` to read the form.
|
||||
2. Fill only the planned `disclosure_fields` with `browser_type`/`browser_click`; for `profile_url`,
|
||||
paste the confirmed listing URL from evidence.
|
||||
3. Submit; `browser_snapshot` to confirm the success state; screenshot to `evidence/`.
|
||||
1. `browser_exec`: `new_tab(optout.url)` + `wait_for_load()`; read the form with `page_info()` /
|
||||
`js()` (field names, labels, required flags).
|
||||
2. Fill only the planned `disclosure_fields` with `fill_input()` (checkboxes/radios via a `js()`
|
||||
click); for `profile_url`, paste the confirmed listing URL from evidence.
|
||||
3. Submit; `page_info()` to confirm the success state; `capture_screenshot()` into `evidence/`.
|
||||
4. `pdd.py record <subject> <broker> submitted --disclosed <field> --disclosed <field> --channel web_form`.
|
||||
5. If the broker requires email verification, follow **Verification loop** below.
|
||||
|
||||
@@ -198,7 +201,7 @@ actually sends depends on `email_mode`:
|
||||
|
||||
1. **browser mode (no password, autonomous):** the command returns a recipient-locked `compose`
|
||||
payload (`to`/`subject`/`body`). Compose a NEW message in the operator's **logged-in webmail** via
|
||||
`browser_*` (paste `compose.body` exactly, disclosing nothing beyond it) and send. No credentials
|
||||
`browser_exec` (paste `compose.body` exactly, disclosing nothing beyond it) and send. No credentials
|
||||
stored. Requires the inbox signed in in the browser Hermes uses.
|
||||
2. **programmatic mode (SMTP creds):** the command SMTP-sends it directly, no human.
|
||||
3. **draft_only fallback:** `pdd.py render-email <subject> <broker> --listing <url>`; a digest entry
|
||||
@@ -209,13 +212,13 @@ Then follow the **Verification loop** if the broker emails a confirmation link.
|
||||
## Verification loop (email_verification brokers)
|
||||
|
||||
- **browser mode (autonomous, no password):** open the broker's confirmation email in the operator's
|
||||
webmail (`browser_*`), then `pdd.py verify-link <subject> <broker> --text '<email body>'` returns
|
||||
the anti-phishing-scored link. `browser_navigate` it **in the same browser** (several brokers, e.g.
|
||||
webmail (`browser_exec`), then `pdd.py verify-link <subject> <broker> --text '<email body>'` returns
|
||||
the anti-phishing-scored link. Open it with `browser_exec` (`goto_url(link)`) **in the same browser** (several brokers, e.g.
|
||||
PeopleConnect, bind the session to the browser that opens the link), finish the flow, record
|
||||
`awaiting_processing`.
|
||||
- **programmatic mode (IMAP):** `pdd.py poll-verification <subject>` polls IMAP for every in-flight
|
||||
case, extracts the link (anti-phishing scored: only opt-out-looking links on the broker's own
|
||||
domains), and auto-advances `submitted → verification_pending`. Then `browser_navigate` the link in
|
||||
domains), and auto-advances `submitted → verification_pending`. Then open the link with `browser_exec` in
|
||||
the agent's own browser, finish the flow, record `awaiting_processing`.
|
||||
- **draft_only:** the digest tells the operator to click the link in the subject's inbox; the agent
|
||||
records `awaiting_processing` on their word.
|
||||
@@ -281,10 +284,10 @@ run stalling in Phase 2.
|
||||
multi-step flows that matter (e.g. PeopleConnect guided-mode, whose verify link is session- and
|
||||
device-bound to the browser that opens it - a cloud browser both fails the challenge and breaks the
|
||||
binding).
|
||||
- **How to drive the operator's browser (CDP).** Point Hermes's browser tools at the operator's real
|
||||
Chrome over the DevTools protocol: launch
|
||||
`chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.hermes/chrome-debug"` and connect the
|
||||
browser backend to `127.0.0.1:9222`. Use a **dedicated debug profile** (`chrome-debug`), NOT the
|
||||
- **How to drive the operator's browser (CDP).** Point Hermes's browser (`browser_exec`) at the
|
||||
operator's real Chrome over the DevTools protocol: launch
|
||||
`chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.hermes/chrome-debug"` and attach Hermes
|
||||
with `/browser connect` (or `browser.cdp_url` / `BROWSER_CDP_URL`) to `127.0.0.1:9222`. Use a **dedicated debug profile** (`chrome-debug`), NOT the
|
||||
operator's Default Chrome profile, and have the operator sign into their webmail (and any needed
|
||||
broker accounts) in that profile once. That single browser then carries residential IP + real
|
||||
fingerprint + logged-in sessions, which is precisely what Phase-2 flows need. (This is a Hermes-side
|
||||
@@ -293,7 +296,7 @@ run stalling in Phase 2.
|
||||
binary, starts it detached on the dedicated profile, waits for the debug port, and prints the CDP
|
||||
endpoint (`webSocketDebuggerUrl`). `pdd.py cdp --check` reports whether a debug browser is already
|
||||
live (and never launches a second one); `pdd.py cdp --print` just emits the exact command for the
|
||||
operator to run themselves. Point the browser tools at the `endpoint` it returns.
|
||||
operator to run themselves. Attach Hermes's browser to the `endpoint` it returns (`/browser connect`).
|
||||
- **Always-available fallback:** if no CDP browser is wired up, use the operator-in-the-loop path
|
||||
(scan ladder 3b) - hand over paste-ready URLs and field-by-field least-disclosure guidance, pausing
|
||||
before submit. It never fails; it just needs a human present.
|
||||
|
||||
@@ -136,7 +136,7 @@ def _optout_action(row: dict, playbook: dict[str, dict], subject_id: str, dossie
|
||||
kind = request_kind(dossier, deletion.get("kinds"))
|
||||
via = "browser" if email_mode == "browser" else "smtp"
|
||||
then = ("send-email records it + returns a recipient-locked payload; compose and send it in "
|
||||
"the operator's webmail via browser_*, then `verify-link` on the reply and open the link"
|
||||
"the operator's webmail via browser_exec, then `verify-link` on the reply and open the link"
|
||||
if via == "browser" else
|
||||
"state auto-records as submitted; poll-verification picks up their verification reply, "
|
||||
"open its link, then record")
|
||||
@@ -282,15 +282,15 @@ def next_actions(dossier: dict, brokers_list: list[dict], cfg: dict,
|
||||
"type": "poll_verification", "via": "imap",
|
||||
"broker_id": bid,
|
||||
"command": f"python3 scripts/pdd.py poll-verification {subject_id} --broker {bid}",
|
||||
"then": "browser_navigate the returned link IN THE SAME AGENT BROWSER (sessions are "
|
||||
"then": "open the returned link with browser_exec IN THE SAME AGENT BROWSER (sessions are "
|
||||
"browser-bound), complete the flow, then record: awaiting_processing",
|
||||
})
|
||||
elif email_mode == "browser":
|
||||
actions.append({
|
||||
"type": "poll_verification", "via": "browser", "broker_id": bid,
|
||||
"how": "open the broker's confirmation email in the operator's logged-in webmail "
|
||||
f"(browser_*), then `python3 scripts/pdd.py verify-link {subject_id} {bid} "
|
||||
"--text '<email body>'` to score the link, browser_navigate it in the SAME "
|
||||
f"(browser_exec), then `python3 scripts/pdd.py verify-link {subject_id} {bid} "
|
||||
"--text '<email body>'` to score the link, open it with browser_exec in the SAME "
|
||||
"browser, then record awaiting_processing",
|
||||
})
|
||||
else:
|
||||
@@ -335,7 +335,7 @@ def next_actions(dossier: dict, brokers_list: list[dict], cfg: dict,
|
||||
"type": "indirect_web_form",
|
||||
"broker_id": bid, "confirm_first": confirm_first,
|
||||
"optout_url": row.get("optout_url"),
|
||||
"steps": [f"browser_navigate {row.get('optout_url')}",
|
||||
"steps": [f"browser_exec: new_tab({row.get('optout_url')!r}) + wait_for_load()",
|
||||
"submit ONLY the subject's own identifiers (the fields the form requires) to "
|
||||
"remove them from the third party's record; disclose nothing extra",
|
||||
"confirm the success state, screenshot into evidence/"],
|
||||
|
||||
@@ -6,7 +6,7 @@ session-bound multi-step opt-out gates (e.g. PeopleConnect guided-mode) -- must
|
||||
operator's OWN browser: real fingerprint, residential IP, and the operator's signed-in sessions.
|
||||
A headless cloud browser (Browserbase) is the wrong tool there (it has no webmail session and is
|
||||
itself anti-bot-gated on those exact flows). This module launches the operator's real Chrome with
|
||||
remote debugging on a DEDICATED profile so Hermes's browser tools can attach at 127.0.0.1:<port>.
|
||||
remote debugging on a DEDICATED profile so Hermes's browser (`/browser connect`) can attach at 127.0.0.1:<port>.
|
||||
|
||||
Stdlib only; cross-platform (macOS / Linux / Windows). Nothing here touches a password or PII.
|
||||
"""
|
||||
|
||||
@@ -36,20 +36,28 @@ VALID = {
|
||||
# email_mode:
|
||||
# draft_only - render drafts; the operator sends + clicks verify links (zero setup)
|
||||
# browser - the agent sends + opens verify links through the operator's logged-in
|
||||
# webmail via browser_* tools (NO password stored; needs a browser the
|
||||
# webmail via browser_exec (NO password stored; needs a browser the
|
||||
# operator's inbox is signed into)
|
||||
# programmatic - CLI sends via SMTP + reads verify links via IMAP (needs EMAIL_* creds)
|
||||
# alias - AgentMail agent-owned inboxes / per-broker aliases
|
||||
"email_mode": {"draft_only", "browser", "programmatic", "alias"},
|
||||
"browser_backend": {"auto", "browserbase", "agent-browser", "camofox"},
|
||||
# browser_backend: local = Hermes's own Chromium via browser_exec; camofox = the `camofox`
|
||||
# catalog plugin. Only browserbase (or auto + key) changes planning (soft-CAPTCHA clearing).
|
||||
"browser_backend": {"auto", "browserbase", "local", "camofox"},
|
||||
"tracker_backend": {"local-json", "google-sheets"},
|
||||
"encryption": {"none", "age"},
|
||||
}
|
||||
|
||||
|
||||
# Values written by older installs, renamed when Hermes dropped agent-browser.
|
||||
LEGACY_VALUES = {"browser_backend": {"agent-browser": "local"}}
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
cfg = dict(DEFAULT_CONFIG)
|
||||
cfg.update(storage.read_json(paths.config_path(), {}) or {})
|
||||
for key, renames in LEGACY_VALUES.items():
|
||||
cfg[key] = renames.get(cfg.get(key), cfg.get(key))
|
||||
return cfg
|
||||
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ def browser_send_payload(broker: dict, body_text: str, to: str | None = None) ->
|
||||
|
||||
No network and no credentials: the deterministic part (recipient-lock to the broker's own
|
||||
declared address, subject/body split) happens here; the agent then composes and sends it in
|
||||
the operator's logged-in webmail with browser_* tools. Same recipient guard as `send()`, so
|
||||
the operator's logged-in webmail with browser_exec. Same recipient guard as `send()`, so
|
||||
the browser lane cannot be pointed at an arbitrary person either.
|
||||
"""
|
||||
allowed = broker_addresses(broker)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"""unbroker - deterministic CLI helper.
|
||||
|
||||
The Hermes agent orchestrates scanning and opt-out submission with native tools
|
||||
(`web_extract`, `browser_navigate`, email mechanisms). THIS CLI owns the
|
||||
(`web_extract`, `browser_exec`, email mechanisms). THIS CLI owns the
|
||||
deterministic state: config, dossiers + consent, the broker DB, tier planning,
|
||||
the ledger + audit log, draft/template rendering, and reports.
|
||||
|
||||
@@ -260,7 +260,7 @@ def cmd_cdp(args) -> None:
|
||||
_out({"running": True, "endpoint": f"127.0.0.1:{port}",
|
||||
"browser": live.get("Browser"),
|
||||
"webSocketDebuggerUrl": live.get("webSocketDebuggerUrl"),
|
||||
"note": "a debuggable browser is already listening; point Hermes's browser tools at "
|
||||
"note": "a debuggable browser is already listening; attach Hermes's browser (`/browser connect`) to "
|
||||
f"127.0.0.1:{port} and make sure the operator's webmail is signed in in THAT browser."})
|
||||
return
|
||||
|
||||
@@ -292,7 +292,7 @@ def cmd_cdp(args) -> None:
|
||||
_out({"running": bool(live), "launched_pid": pid, "browser": browser,
|
||||
"profile": str(profile), "endpoint": f"127.0.0.1:{port}",
|
||||
"webSocketDebuggerUrl": (live or {}).get("webSocketDebuggerUrl"),
|
||||
"next": ([f"point Hermes's browser tools at 127.0.0.1:{port} (CDP)",
|
||||
"next": ([f"attach Hermes's browser (browser_exec) with `/browser connect` to 127.0.0.1:{port} (CDP)",
|
||||
"in the launched browser, sign into the operator's webmail ONCE (dedicated debug profile)",
|
||||
"then run email/verify flows in browser mode -- they use this logged-in session"]
|
||||
if live else
|
||||
@@ -595,7 +595,7 @@ def cmd_send_email(args) -> None:
|
||||
|
||||
if mode == "browser":
|
||||
# No network / no credentials: hand the agent a recipient-locked payload to send in the
|
||||
# operator's webmail via browser_* tools. State still records deterministically here.
|
||||
# operator's webmail via browser_exec. State still records deterministically here.
|
||||
payload = emailer.browser_send_payload(b, body, to=args.to)
|
||||
ledger_mod.log_disclosure(args.subject, args.broker, list(disclosed), f"email_browser:{kind}")
|
||||
case = ledger_mod.transition(args.subject, args.broker, "submitted",
|
||||
@@ -604,7 +604,7 @@ def cmd_send_email(args) -> None:
|
||||
"state": case["state"], "next_recheck_at": case.get("next_recheck_at"),
|
||||
"instruction": "In the operator's logged-in webmail, compose a NEW email to compose.to "
|
||||
"with compose.subject/body EXACTLY (disclose nothing beyond it) and send "
|
||||
"it via browser_* tools. Then use `verify-link` on any confirmation reply.",
|
||||
"it via browser_exec. Then use `verify-link` on any confirmation reply.",
|
||||
"note": "recipient is locked to the broker's declared address"})
|
||||
return
|
||||
|
||||
@@ -635,7 +635,7 @@ def cmd_verify_link(args) -> None:
|
||||
sys.exit("error: provide --text '<email body>' (or --file) from the broker's confirmation email")
|
||||
link = email_modes.extract_verification_link(text, b)
|
||||
_out({"broker": args.broker, "verification_link": link,
|
||||
"next": ("browser_navigate the link IN THE SAME browser (sessions are browser-bound), "
|
||||
"next": ("open the link with browser_exec IN THE SAME browser (sessions are browser-bound), "
|
||||
f"complete the flow, then `record {args.subject} {args.broker} awaiting_processing`"
|
||||
if link else
|
||||
"no broker/opt-out-scoped link found in that text; confirm you opened the right email")})
|
||||
@@ -674,7 +674,7 @@ def cmd_poll_verification(args) -> None:
|
||||
**ledger_mod.followup_fields("verification_pending", b, d))
|
||||
results.append({"broker": bid, "verification_link": hit["link"],
|
||||
"email_from": hit.get("from"), "email_subject": hit.get("subject"),
|
||||
"next": f"browser_navigate the link IN THE AGENT'S OWN BROWSER, complete "
|
||||
"next": f"open the link with browser_exec IN THE AGENT'S OWN BROWSER, complete "
|
||||
f"the flow, then `record {args.subject} {bid} awaiting_processing` "
|
||||
f"(or confirmed_removed only after a verifying re-scan)"})
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Stdlib fetch helper for simple url_pattern brokers (osint-style).
|
||||
|
||||
For JS-rendered or anti-bot pages the agent should use the `web_extract` or
|
||||
`browser_navigate` tools (and the `scrapling` skill for stealth/Cloudflare).
|
||||
`browser_exec` tools (and the `scrapling` skill for stealth/Cloudflare).
|
||||
This helper only covers plain static pages and is intentionally network-light so
|
||||
it can be mocked in tests.
|
||||
"""
|
||||
|
||||
@@ -182,11 +182,11 @@ payloads yet. Still scope-bounded.
|
||||
```
|
||||
|
||||
4. **Endpoint discovery:**
|
||||
- Crawl the app with the browser tool (`browser_navigate`,
|
||||
`browser_get_images`, follow links).
|
||||
- Crawl the app with `browser_exec` (`new_tab`/`goto_url`, collect
|
||||
links and images via `js()`, follow links).
|
||||
- Inspect `robots.txt`, `sitemap.xml`, `.well-known/*`.
|
||||
- Use the developer tools network panel via browser tool to capture
|
||||
XHR/fetch calls.
|
||||
- Capture XHR/fetch calls with `browser_exec`:
|
||||
`js("performance.getEntriesByType('resource').map(e => [e.initiatorType, e.name])")`.
|
||||
|
||||
5. **Auth surface:** Identify login, registration, password reset,
|
||||
session cookie names, token formats. Do NOT send credentials yet —
|
||||
|
||||
@@ -45,7 +45,7 @@ or URL and confirm it's in scope.
|
||||
| `curl --resolve HOST:PORT:ADDR` | HOST | reject — resolve overrides scope |
|
||||
| `nmap TARGET` | Each TARGET arg | `nmap 10.0.5.5 staging.example.com` |
|
||||
| `whatweb URL` | The URL | `whatweb https://staging.example.com` |
|
||||
| `browser_navigate(url)` | The URL | python-side: extract host from `url` |
|
||||
| `browser_exec` `new_tab(url)` / `goto_url(url)` | The URL | python-side: extract host from `url` |
|
||||
| Tool-driven HTTP (sqlmap, wfuzz, gobuster) | `-u`, `-h`, target arg | depends on tool |
|
||||
|
||||
For URLs: `urllib.parse.urlparse(url).hostname.lower()`.
|
||||
|
||||
@@ -22,7 +22,7 @@ backend routing in [Agent Reach](https://github.com/Panniantong/Agent-Reach).
|
||||
|
||||
- "What is r/LocalLLaMA saying about X", "find Reddit threads on Y", "summarise this
|
||||
Reddit thread", "what has u/someone posted lately".
|
||||
- Any `reddit.com` URL the user shares. `web_extract`, `browser_navigate` and the
|
||||
- Any `reddit.com` URL the user shares. `web_extract`, `browser_exec` and the
|
||||
`.json` endpoints all fail from server IPs (403 or a "Prove your humanity" wall);
|
||||
this skill is the working path.
|
||||
- Not for posting, voting, messaging, or anything needing a user login.
|
||||
@@ -108,7 +108,7 @@ account. Never ask for a Reddit password or browser cookies.
|
||||
- `www.reddit.com/…/.json`, `api.reddit.com` and `old.reddit.com` return 403 or an
|
||||
empty "Welcome to Reddit" shell for datacentre IPs. Do not fall back to them; do not
|
||||
spoof a browser User-Agent (also 403).
|
||||
- `r.jina.ai` and the `browser_navigate` tool hit the same block ("blocked by network
|
||||
- `r.jina.ai` and the `browser_exec` tool hit the same block ("blocked by network
|
||||
security" / humanity check). `blocked-page-recovery`'s Wayback route can still recover
|
||||
an **old** thread that was archived; it cannot fetch fresh ones.
|
||||
- Anonymous thread feeds only contain the post plus top-level comments (Reddit caps the
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
Optional skills for client-side web development workflows — embedding agents, copilots, and AI-native UX patterns into user-facing web apps.
|
||||
|
||||
These are distinct from Hermes' own browser automation (Browserbase, Camofox), which operate *on* websites from outside. Web-development skills here help users build *into* their own websites.
|
||||
These are distinct from Hermes' own browser automation (`browser_exec`, local or cloud browsers), which operate *on* websites from outside. Web-development skills here help users build *into* their own websites.
|
||||
@@ -24,8 +24,8 @@ session, you carry its headers/cookies forward, you don't forge them.
|
||||
The scripts are stdlib-plus-Playwright: capture needs Playwright, derivation
|
||||
is pure stdlib, replay needs only `requests`/`httpx` (or `curl`).
|
||||
|
||||
Covers **every Hermes browser pathway**: the default local `browser_navigate`
|
||||
backend, plus the cloud/remote backends (Browserbase, Browser-Use, Firecrawl)
|
||||
Covers **every Hermes browser pathway**: the default local Chromium that
|
||||
`browser_exec` drives, plus the cloud/remote backends (Browserbase, Browser-Use, Firecrawl)
|
||||
and any `/browser connect` CDP endpoint. There are two capture scripts — one
|
||||
for a browser you launch, one for a browser you attach to over CDP — because
|
||||
HAR recording works differently in each case (see How to Run).
|
||||
@@ -34,7 +34,7 @@ HAR recording works differently in each case (see How to Run).
|
||||
|
||||
- "Build a CLI/client for <website>" — derive its API instead of scripting clicks.
|
||||
- "This site has no public API but the page clearly fetches JSON."
|
||||
- You're about to loop `browser_navigate` for the same query repeatedly — stop and derive the endpoint once.
|
||||
- You're about to loop `browser_exec` navigations for the same query repeatedly — stop and derive the endpoint once.
|
||||
- Reverse-engineering an autocomplete, search, feed, or checkout XHR.
|
||||
- You captured a session on a cloud backend (Browserbase / Browser-Use / Firecrawl) or via `/browser connect` and want the API without re-renting the browser.
|
||||
|
||||
@@ -56,8 +56,7 @@ Scripts under this skill's `scripts/`, invoked through the `terminal` tool.
|
||||
|
||||
| Browser pathway | How Hermes reaches it | Capturer |
|
||||
|---|---|---|
|
||||
| Local `browser_navigate` (default, agent-browser/Playwright) | launched locally | `har_capture.py` |
|
||||
| Camofox (`CAMOFOX_URL` set) | local REST/CDP | `har_capture_cdp.py` if it exposes CDP, else drive it yourself |
|
||||
| Local Chromium (default `browser_exec` browser) | launched locally | `har_capture.py` |
|
||||
| Browserbase / Browser-Use / Firecrawl (cloud) | **CDP** (`cdpUrl`) | `har_capture_cdp.py` |
|
||||
| `/browser connect <url>` / `BROWSER_CDP_URL` | **CDP** | `har_capture_cdp.py` |
|
||||
|
||||
@@ -97,7 +96,7 @@ python3 scripts/har_to_client.py out.har --host SITE --max-body 400
|
||||
har_capture.py <url> <out.har> [--wait S] [--headed] [--action SPEC ...]
|
||||
action SPEC: fill:SELECTOR:TEXT | press:SELECTOR:KEY | click:SELECTOR
|
||||
goto:URL | sleep:SECONDS (run in order after page load)
|
||||
use when Hermes LAUNCHED the browser (local browser_navigate default)
|
||||
use when Hermes LAUNCHED the browser (local browser_exec default)
|
||||
|
||||
har_capture_cdp.py <cdp_url> <out.har> [--goto URL] [--wait S] [--action SPEC ...]
|
||||
same action SPEC; attaches to an existing CDP browser and does NOT close it
|
||||
@@ -113,7 +112,7 @@ har_to_client.py <in.har> [--host SUBSTR] [--include-static] [--max-body N]
|
||||
## Procedure
|
||||
|
||||
0. **Pick the capturer by pathway** (see How to Run table). Launched-locally → `har_capture.py`; reached over CDP → `har_capture_cdp.py`. On Hermes, `/browser connect` tells you the CDP endpoint when a cloud/remote backend is active.
|
||||
1. **Find the interaction.** Open the site with `browser_navigate` (or `--headed` capture) to see which selector to type into / click, and confirm a JSON XHR fires in devtools/network.
|
||||
1. **Find the interaction.** Open the site with `browser_exec` (`new_tab(url)` + `wait_for_load()`, or a `--headed` capture) to see which selector to type into / click via `js()`, and confirm a JSON XHR fires: `js("performance.getEntriesByType('resource').filter(e => ['xmlhttprequest','fetch'].includes(e.initiatorType)).map(e => e.name)")`.
|
||||
2. **Capture the HAR** via the `terminal` tool. Order `--action` to reach the request: `fill` the box, then `sleep` long enough for the debounced XHR, and always leave `--wait` at the end so late responses flush. Both capturers embed response bodies, so the derived client sees real payload shapes.
|
||||
3. **Derive** with `har_to_client.py --host <domain>`. Read off: the method, the URL/path template (numeric/UUID segments collapse to `{id}`), query params, request-body JSON, and the `### Replay hints` block.
|
||||
4. **Write the client.** Recreate the request exactly — same method, path, query params, body. Send the headers the site actually needs: at minimum copy the **User-Agent** from the replay hints. If hints report cookies or an auth/token header, resend those too.
|
||||
@@ -145,7 +144,7 @@ for p in r.json()["pages"]:
|
||||
- **`record_har_content="embed"` makes big HARs.** Use `--max-body` to cap what's printed; the file itself can be large for media-heavy pages.
|
||||
- **Endpoints shift.** Sites change private APIs without notice. Re-run the capture→derive loop when a client breaks rather than patching URLs by hand.
|
||||
- **Wrong capturer = empty/no HAR.** `har_capture.py` on a cloud/CDP backend records nothing (it launches its own local browser instead of the one you meant). `har_capture_cdp.py` needs the endpoint; on Hermes get it from `/browser connect` or `BROWSER_CDP_URL`. Match the capturer to the pathway (How to Run table).
|
||||
- **Headless-Chrome UA is a weak tell.** Local/agent-browser capture yields a `HeadlessChrome/...` User-Agent; some sites sniff the "Headless" token. Cloud backends (Browserbase/Browser-Use) send a real desktop-Chrome UA, so a client derived from a cloud capture replays more reliably. If a headless-derived client 403s where the browser didn't, swap the "Headless" UA for a normal Chrome UA string before assuming the endpoint changed.
|
||||
- **Headless-Chrome UA is a weak tell.** Local headless capture yields a `HeadlessChrome/...` User-Agent; some sites sniff the "Headless" token. Cloud backends (Browserbase/Browser-Use) send a real desktop-Chrome UA, so a client derived from a cloud capture replays more reliably. If a headless-derived client 403s where the browser didn't, swap the "Headless" UA for a normal Chrome UA string before assuming the endpoint changed.
|
||||
- **CDP capture doesn't close the browser.** `har_capture_cdp.py` attaches to a browser it doesn't own and leaves it running — correct for cloud/remote sessions Hermes manages. Don't add a close; let the owning backend tear it down.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Use this when the browser is owned by someone else and only reachable over the
|
||||
Chrome DevTools Protocol: Hermes cloud backends (Browserbase, Browser-Use,
|
||||
Firecrawl), a Camofox session exposing CDP, or anything wired via
|
||||
Firecrawl), or anything wired via
|
||||
`/browser connect <url>` / BROWSER_CDP_URL / browser.cdp_url in config.
|
||||
|
||||
Why this exists: Playwright's record_har_path only works on a context you
|
||||
|
||||
@@ -27,7 +27,7 @@ Load this skill when a user wants to:
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- User wants **Hermes itself to drive a browser** → use Hermes' built-in browser tool (Browserbase / Camofox). page-agent is the *opposite* direction.
|
||||
- User wants **Hermes itself to drive a browser** → use Hermes' built-in `browser_exec` tool (local Chromium or a cloud browser). page-agent is the *opposite* direction.
|
||||
- User wants **cross-tab automation without embedding** → use Playwright, browser-use, or the page-agent Chrome extension
|
||||
- User needs **visual grounding / screenshots** → page-agent is text-DOM only; use a multimodal browser agent instead
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import emailer
|
||||
import pdd
|
||||
import legal
|
||||
import ledger
|
||||
import paths
|
||||
import report
|
||||
import storage
|
||||
import tiers
|
||||
@@ -86,11 +87,21 @@ def _consenting(full_name="Jane Q. Public"):
|
||||
|
||||
def test_browser_clears_captcha_logic():
|
||||
assert config.browser_clears_captcha({"browser_backend": "browserbase"}) is True
|
||||
assert config.browser_clears_captcha({"browser_backend": "agent-browser"}) is False
|
||||
assert config.browser_clears_captcha({"browser_backend": "local"}) is False
|
||||
assert config.browser_clears_captcha({"browser_backend": "auto"}, env={}) is False
|
||||
assert config.browser_clears_captcha({"browser_backend": "auto"}, env={"BROWSERBASE_API_KEY": "x"}) is True
|
||||
|
||||
|
||||
def test_legacy_agent_browser_backend_loads_and_resaves_as_local():
|
||||
"""Configs saved before agent-browser was removed must keep loading and saving."""
|
||||
with temp_env():
|
||||
storage.write_json(paths.config_path(), {"browser_backend": "agent-browser"})
|
||||
cfg = config.load_config()
|
||||
assert cfg["browser_backend"] == "local"
|
||||
config.save_config(cfg)
|
||||
assert config.load_config()["browser_backend"] == "local"
|
||||
|
||||
|
||||
# --- storage ------------------------------------------------------------------
|
||||
|
||||
def test_storage_json_and_jsonl_roundtrip():
|
||||
|
||||
@@ -60,7 +60,7 @@ Start a new figure by copying the closest example into `$WORK/figures/<name>/` a
|
||||
|
||||
**Looking at your work.** The upstream workflow says "read every PNG". In Hermes that means `vision_analyze` on each capture, and the 4× close-ups use its `region` crop on a high-DPR capture or a `--zoom` capture. Ask specific questions ("does the pipe at the flange end on the flange face?"), not "does this look good".
|
||||
|
||||
**Browser.** The capture, drive, line-end and 3D checks start a headless Chrome over a loopback DevTools port. They use `CHROME_PATH` if set, then the Playwright headless shell that Hermes' browser tool installs (`chromium_headless_shell-*` in the Playwright cache), then a system Chrome or Chromium. Prefer the headless shell: it renders the WebGL layers through SwiftShader and makes no outbound connections. A full desktop Chrome opens background connections to Google services even with a throwaway profile, and it fails to start ("Socket path too long", then "Chrome did not start") when `TMPDIR` is a long path, as Hermes' per-profile scratch dir can be. If you must use one, set `TMPDIR` to a short directory for that command. Building a figure needs no browser at all.
|
||||
**Browser.** The capture, drive, line-end and 3D checks start a headless Chrome over a loopback DevTools port. They use `CHROME_PATH` if set, then a Playwright headless shell if one is installed (`chromium_headless_shell-*` in the Playwright cache, e.g. from `npx playwright install chromium`), then a system Chrome or Chromium. Prefer the headless shell: it renders the WebGL layers through SwiftShader and makes no outbound connections. A full desktop Chrome opens background connections to Google services even with a throwaway profile, and it fails to start ("Socket path too long", then "Chrome did not start") when `TMPDIR` is a long path, as Hermes' per-profile scratch dir can be. If you must use one, set `TMPDIR` to a short directory for that command. Building a figure needs no browser at all.
|
||||
|
||||
**Long runs.** `node build.mjs --audit` takes about two minutes on a dense Hero, and a full 3D orbit audit takes 15 to 20 minutes for 30 parts. Run those with `terminal(background=true, notify=true)` and keep working on something else.
|
||||
|
||||
|
||||
@@ -107,14 +107,18 @@ Each variant is a **single self-contained HTML file**:
|
||||
|
||||
Open it in a browser. If it looks broken, fix it before showing the user.
|
||||
|
||||
**Verify variants visually — use Hermes' browser tools.** Don't just write HTML and hope it renders; load each variant and look at it:
|
||||
**Verify variants visually — use Hermes' `browser_exec`.** Don't just write HTML and hope it renders; load each variant and look at it:
|
||||
|
||||
```
|
||||
browser_navigate(url="file:///absolute/path/to/sketches/001-calm-editorial/index.html")
|
||||
browser_vision(question="Does this layout look clean and readable? Any visible bugs (overlapping text, unstyled elements, broken images)?")
|
||||
browser_exec(code='''
|
||||
# Checking sketch 001 renders cleanly
|
||||
new_tab("file:///absolute/path/to/sketches/001-calm-editorial/index.html")
|
||||
wait_for_load()
|
||||
capture_screenshot()
|
||||
''')
|
||||
```
|
||||
|
||||
`browser_vision` returns an AI description of what's actually on the page plus a screenshot path — catches layout bugs that pure source inspection misses (e.g. a font import that silently failed, a flex container that collapsed). Fix and re-navigate until each variant looks right.
|
||||
Look at the attached screenshot and ask: does this layout look clean and readable, any visible bugs (overlapping text, unstyled elements, broken images)? (non-vision models: `vision_analyze` the printed path with that question). It catches layout bugs that pure source inspection misses (e.g. a font import that silently failed, a flex container that collapsed). Fix and reload (`goto_url(...)` with the same URL) until each variant looks right.
|
||||
|
||||
**Default CSS reset + system font stack** for fast starts:
|
||||
|
||||
@@ -229,8 +233,8 @@ Propose 2-4 named candidates. Let the user pick.
|
||||
terminal("mkdir -p sketches/001-calm-editorial")
|
||||
write_file("sketches/001-calm-editorial/index.html", "<!doctype html>...")
|
||||
write_file("sketches/001-calm-editorial/README.md", "## Variant: Calm editorial\n...")
|
||||
browser_navigate(url="file://$(pwd)/sketches/001-calm-editorial/index.html")
|
||||
browser_vision(question="How does this look? Any obvious layout issues?")
|
||||
browser_exec(code='new_tab("file:///abs/path/sketches/001-calm-editorial/index.html"); wait_for_load(); capture_screenshot()')
|
||||
# then check the screenshot: any obvious layout issues?
|
||||
```
|
||||
|
||||
Repeat for each variant, then present the comparison table.
|
||||
|
||||
@@ -442,7 +442,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -673,7 +673,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -1279,7 +1279,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ Many passages below say "use the S&P Kensho MCP / Daloopa MCP / FactSet MCP". Th
|
||||
- **Otherwise**, fall back to:
|
||||
- `web_search` / `web_extract` against SEC EDGAR (`https://www.sec.gov/cgi-bin/browse-edgar`) for US filings
|
||||
- Company IR pages for press releases, earnings decks
|
||||
- `browser_navigate` for interactive data portals
|
||||
- `browser_exec` for interactive data portals
|
||||
- User-provided data (explicitly ask when the context doesn't have it)
|
||||
- **Never fabricate**. If a multiple, precedent, or filing number can't be sourced, flag the cell as `[UNSOURCED]` and surface it to the user.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ Don't use for: one-off status questions (answer directly), price/availability th
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- At least one source connector the dashboard will read from: email/calendar via `himalaya` or `google-workspace`, websites via `web_extract` or `browser_navigate`, local files via `read_file`. If none is configured, renegotiate the sources in step 1 before writing any artifact.
|
||||
- At least one source connector the dashboard will read from: email/calendar via `himalaya` or `google-workspace`, websites via `web_extract` or `browser_exec`, local files via `read_file`. If none is configured, renegotiate the sources in step 1 before writing any artifact.
|
||||
- `cronjob` for the recurring tick.
|
||||
- Optional: the `desktop_preview` tool (Hermes desktop app sessions). When it is in the toolset, dashboards render in the in-app preview pane; otherwise the user is given the file path.
|
||||
|
||||
@@ -59,7 +59,7 @@ From the user's sentence, pin down: the dashboard's purpose in one line, the ent
|
||||
|
||||
### 2. Verify each source with one live read
|
||||
|
||||
For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_navigate`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user.
|
||||
For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_exec`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user.
|
||||
|
||||
### 3. Build the dashboard artifact
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ person controls.
|
||||
The Python CLI (`scripts/pdd.py`) owns the deterministic state - config, dossiers + consent, the
|
||||
broker database, tier planning, the ledger, drafts, reports, **email sending (SMTP), verification-link
|
||||
polling (IMAP), and the autonomous action queue (`next`)**. You (the agent) do the scanning and
|
||||
form-driving with native tools: `web_extract` and `browser_navigate` for searching and web forms, and
|
||||
form-driving with native tools: `web_extract` and `browser_exec` for searching and web forms, and
|
||||
`cronjob` for recurring re-scans.
|
||||
|
||||
## Autonomy contract
|
||||
@@ -90,13 +90,13 @@ verifying re-scan.
|
||||
- Email automation, two credential-free-or-not options:
|
||||
- **Browser mode (no password): `setup --email-mode browser`.** The agent sends opt-out/CCPA
|
||||
emails and opens verification links through the operator's **logged-in webmail** using
|
||||
`browser_*` tools. Nothing is stored. This requires Hermes to be pointed at the operator's own
|
||||
`browser_exec`. Nothing is stored. This requires Hermes to be pointed at the operator's own
|
||||
logged-in browser, **NOT** a cloud browser: a headless cloud browser (Browserbase) holds no
|
||||
webmail session and is itself Cloudflare/DataDome-gated on webmail and on session-bound broker
|
||||
gates (e.g. PeopleConnect guided-mode). Drive the operator's real Chrome over CDP - launch
|
||||
`chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.hermes/chrome-debug"` (a dedicated
|
||||
debug profile signed into the webmail once, not the Default profile) and connect the browser
|
||||
tools to `127.0.0.1:9222`. **`$PDD cdp` launches this for you** (finds Chrome/Chromium/Brave/Edge,
|
||||
debug profile signed into the webmail once, not the Default profile) and attach Hermes's browser
|
||||
to `127.0.0.1:9222` with `/browser connect`. **`$PDD cdp` launches this for you** (finds Chrome/Chromium/Brave/Edge,
|
||||
starts it detached on the dedicated profile, prints the CDP endpoint; `--check` to test, `--print`
|
||||
for the command). See `references/methods.md` -> "Browser backends: scan vs execute".
|
||||
Falls back to drafts for an email if the inbox isn't reachable.
|
||||
@@ -155,7 +155,7 @@ For anything past a couple of brokers, run this as **map → reduce → act**, n
|
||||
effects, so it is safe to parallelize and retry. Getting the full exposure map *before* acting is
|
||||
what unlocks cluster dedup and prioritization below. **Default: the parent drives `web_extract`
|
||||
probes directly** - most people-search sites render name/phone/address results as static HTML that
|
||||
`web_extract` reads in seconds. Escalate to `browser_*` only for the few JS-only sites, and to
|
||||
`web_extract` reads in seconds. Escalate to `browser_exec` only for the few JS-only sites, and to
|
||||
`delegate_task` subagents only for genuinely *reasoning*-heavy work (large-scale namesake/relative
|
||||
disambiguation). **Do NOT hand a browser-toolset subagent a big list of brokers to crawl** - in the
|
||||
field this timed out repeatedly (600s, ~5-6 brokers each, no summary) because browser navigation is
|
||||
@@ -237,7 +237,7 @@ recording `found` and before any deletion.
|
||||
`delegate_task` subagent per `batch`, in parallel, passing that batch's ready-made `brief`** - do
|
||||
not scan all brokers yourself sequentially. For `scan_inline`: scan the few brokers yourself.
|
||||
Either way, each broker gets **every** `search_vectors` entry via the `references/methods.md`
|
||||
ladder (`web_extract` → `site:` probe → `browser_navigate` → `scrapling`), a 404 is INCONCLUSIVE
|
||||
ladder (`web_extract` → `site:` probe → `browser_exec` → `scrapling`), a 404 is INCONCLUSIVE
|
||||
(not `not_found`), `blocked` is recorded when `antibot` is set and no stealth browser is available,
|
||||
and subject vs namesake/relative is confirmed before recording:
|
||||
`$PDD record <subject> <broker> <found|not_found|indirect_exposure|blocked> --found <bool> --evidence '{"listing_urls":[...]}'`.
|
||||
@@ -249,8 +249,8 @@ recording `found` and before any deletion.
|
||||
just the hide-my-listing flow. When it carries `prefer_suppression` instead (**PeopleConnect** -
|
||||
deleting removes your suppressions and does not stop re-listing), do the suppression flow and keep
|
||||
it maintained; use their Delete button only for a deliberate data-purge. Per method:
|
||||
- **web_form** → drive `optout_url` with `browser_navigate`/`browser_type`/`browser_click`, submit
|
||||
only `disclosure_fields`, screenshot the confirmation, then the action's `after` record command.
|
||||
- **web_form** → drive `optout_url` with `browser_exec` (`new_tab` + `wait_for_load`, `fill_input`,
|
||||
a `js()` click to submit), submit only `disclosure_fields`, `capture_screenshot()` the confirmation, then the action's `after` record command.
|
||||
Playbooks may end with a right-to-delete `send-email` follow-up - do it (full erasure, not just
|
||||
listing suppression).
|
||||
- **email** → `$PDD send-email <subject> <broker> --kind <ccpa|gdpr|generic> --to <addr>
|
||||
@@ -258,7 +258,7 @@ recording `found` and before any deletion.
|
||||
record declares; `next` picks the kind from residency - never claim CCPA/GDPR for someone who
|
||||
can't). In **browser** mode it returns a recipient-locked `compose` payload: compose a new
|
||||
message to `compose.to` with `compose.subject`/`compose.body` exactly in the operator's webmail
|
||||
via `browser_*` and send (no password); in **programmatic** mode it SMTP-sends. `next` also
|
||||
via `browser_exec` and send (no password); in **programmatic** mode it SMTP-sends. `next` also
|
||||
routes human-gated forms (phone-callback/gov-ID) through a broker's deletion email when one
|
||||
exists - the **rescue lane** (verified Whitepages pattern). Draft-only falls back to
|
||||
`render-email` + a digest entry.
|
||||
|
||||
@@ -184,11 +184,11 @@ payloads yet. Still scope-bounded.
|
||||
```
|
||||
|
||||
4. **Endpoint discovery:**
|
||||
- Crawl the app with the browser tool (`browser_navigate`,
|
||||
`browser_get_images`, follow links).
|
||||
- Crawl the app with `browser_exec` (`new_tab`/`goto_url`, collect
|
||||
links and images via `js()`, follow links).
|
||||
- Inspect `robots.txt`, `sitemap.xml`, `.well-known/*`.
|
||||
- Use the developer tools network panel via browser tool to capture
|
||||
XHR/fetch calls.
|
||||
- Capture XHR/fetch calls with `browser_exec`:
|
||||
`js("performance.getEntriesByType('resource').map(e => [e.initiatorType, e.name])")`.
|
||||
|
||||
5. **Auth surface:** Identify login, registration, password reset,
|
||||
session cookie names, token formats. Do NOT send credentials yet —
|
||||
|
||||
@@ -40,7 +40,7 @@ backend routing in [Agent Reach](https://github.com/Panniantong/Agent-Reach).
|
||||
|
||||
- "What is r/LocalLLaMA saying about X", "find Reddit threads on Y", "summarise this
|
||||
Reddit thread", "what has u/someone posted lately".
|
||||
- Any `reddit.com` URL the user shares. `web_extract`, `browser_navigate` and the
|
||||
- Any `reddit.com` URL the user shares. `web_extract`, `browser_exec` and the
|
||||
`.json` endpoints all fail from server IPs (403 or a "Prove your humanity" wall);
|
||||
this skill is the working path.
|
||||
- Not for posting, voting, messaging, or anything needing a user login.
|
||||
@@ -126,7 +126,7 @@ account. Never ask for a Reddit password or browser cookies.
|
||||
- `www.reddit.com/…/.json`, `api.reddit.com` and `old.reddit.com` return 403 or an
|
||||
empty "Welcome to Reddit" shell for datacentre IPs. Do not fall back to them; do not
|
||||
spoof a browser User-Agent (also 403).
|
||||
- `r.jina.ai` and the `browser_navigate` tool hit the same block ("blocked by network
|
||||
- `r.jina.ai` and the `browser_exec` tool hit the same block ("blocked by network
|
||||
security" / humanity check). `blocked-page-recovery`'s Wayback route can still recover
|
||||
an **old** thread that was archived; it cannot fetch fresh ones.
|
||||
- Anonymous thread feeds only contain the post plus top-level comments (Reddit caps the
|
||||
|
||||
+7
-8
@@ -41,8 +41,8 @@ session, you carry its headers/cookies forward, you don't forge them.
|
||||
The scripts are stdlib-plus-Playwright: capture needs Playwright, derivation
|
||||
is pure stdlib, replay needs only `requests`/`httpx` (or `curl`).
|
||||
|
||||
Covers **every Hermes browser pathway**: the default local `browser_navigate`
|
||||
backend, plus the cloud/remote backends (Browserbase, Browser-Use, Firecrawl)
|
||||
Covers **every Hermes browser pathway**: the default local Chromium that
|
||||
`browser_exec` drives, plus the cloud/remote backends (Browserbase, Browser-Use, Firecrawl)
|
||||
and any `/browser connect` CDP endpoint. There are two capture scripts — one
|
||||
for a browser you launch, one for a browser you attach to over CDP — because
|
||||
HAR recording works differently in each case (see How to Run).
|
||||
@@ -51,7 +51,7 @@ HAR recording works differently in each case (see How to Run).
|
||||
|
||||
- "Build a CLI/client for <website>" — derive its API instead of scripting clicks.
|
||||
- "This site has no public API but the page clearly fetches JSON."
|
||||
- You're about to loop `browser_navigate` for the same query repeatedly — stop and derive the endpoint once.
|
||||
- You're about to loop `browser_exec` navigations for the same query repeatedly — stop and derive the endpoint once.
|
||||
- Reverse-engineering an autocomplete, search, feed, or checkout XHR.
|
||||
- You captured a session on a cloud backend (Browserbase / Browser-Use / Firecrawl) or via `/browser connect` and want the API without re-renting the browser.
|
||||
|
||||
@@ -73,8 +73,7 @@ Scripts under this skill's `scripts/`, invoked through the `terminal` tool.
|
||||
|
||||
| Browser pathway | How Hermes reaches it | Capturer |
|
||||
|---|---|---|
|
||||
| Local `browser_navigate` (default, agent-browser/Playwright) | launched locally | `har_capture.py` |
|
||||
| Camofox (`CAMOFOX_URL` set) | local REST/CDP | `har_capture_cdp.py` if it exposes CDP, else drive it yourself |
|
||||
| Local Chromium (default `browser_exec` browser) | launched locally | `har_capture.py` |
|
||||
| Browserbase / Browser-Use / Firecrawl (cloud) | **CDP** (`cdpUrl`) | `har_capture_cdp.py` |
|
||||
| `/browser connect <url>` / `BROWSER_CDP_URL` | **CDP** | `har_capture_cdp.py` |
|
||||
|
||||
@@ -114,7 +113,7 @@ python3 scripts/har_to_client.py out.har --host SITE --max-body 400
|
||||
har_capture.py <url> <out.har> [--wait S] [--headed] [--action SPEC ...]
|
||||
action SPEC: fill:SELECTOR:TEXT | press:SELECTOR:KEY | click:SELECTOR
|
||||
goto:URL | sleep:SECONDS (run in order after page load)
|
||||
use when Hermes LAUNCHED the browser (local browser_navigate default)
|
||||
use when Hermes LAUNCHED the browser (local browser_exec default)
|
||||
|
||||
har_capture_cdp.py <cdp_url> <out.har> [--goto URL] [--wait S] [--action SPEC ...]
|
||||
same action SPEC; attaches to an existing CDP browser and does NOT close it
|
||||
@@ -130,7 +129,7 @@ har_to_client.py <in.har> [--host SUBSTR] [--include-static] [--max-body N]
|
||||
## Procedure
|
||||
|
||||
0. **Pick the capturer by pathway** (see How to Run table). Launched-locally → `har_capture.py`; reached over CDP → `har_capture_cdp.py`. On Hermes, `/browser connect` tells you the CDP endpoint when a cloud/remote backend is active.
|
||||
1. **Find the interaction.** Open the site with `browser_navigate` (or `--headed` capture) to see which selector to type into / click, and confirm a JSON XHR fires in devtools/network.
|
||||
1. **Find the interaction.** Open the site with `browser_exec` (`new_tab(url)` + `wait_for_load()`, or a `--headed` capture) to see which selector to type into / click via `js()`, and confirm a JSON XHR fires: `js("performance.getEntriesByType('resource').filter(e => ['xmlhttprequest','fetch'].includes(e.initiatorType)).map(e => e.name)")`.
|
||||
2. **Capture the HAR** via the `terminal` tool. Order `--action` to reach the request: `fill` the box, then `sleep` long enough for the debounced XHR, and always leave `--wait` at the end so late responses flush. Both capturers embed response bodies, so the derived client sees real payload shapes.
|
||||
3. **Derive** with `har_to_client.py --host <domain>`. Read off: the method, the URL/path template (numeric/UUID segments collapse to `{id}`), query params, request-body JSON, and the `### Replay hints` block.
|
||||
4. **Write the client.** Recreate the request exactly — same method, path, query params, body. Send the headers the site actually needs: at minimum copy the **User-Agent** from the replay hints. If hints report cookies or an auth/token header, resend those too.
|
||||
@@ -162,7 +161,7 @@ for p in r.json()["pages"]:
|
||||
- **`record_har_content="embed"` makes big HARs.** Use `--max-body` to cap what's printed; the file itself can be large for media-heavy pages.
|
||||
- **Endpoints shift.** Sites change private APIs without notice. Re-run the capture→derive loop when a client breaks rather than patching URLs by hand.
|
||||
- **Wrong capturer = empty/no HAR.** `har_capture.py` on a cloud/CDP backend records nothing (it launches its own local browser instead of the one you meant). `har_capture_cdp.py` needs the endpoint; on Hermes get it from `/browser connect` or `BROWSER_CDP_URL`. Match the capturer to the pathway (How to Run table).
|
||||
- **Headless-Chrome UA is a weak tell.** Local/agent-browser capture yields a `HeadlessChrome/...` User-Agent; some sites sniff the "Headless" token. Cloud backends (Browserbase/Browser-Use) send a real desktop-Chrome UA, so a client derived from a cloud capture replays more reliably. If a headless-derived client 403s where the browser didn't, swap the "Headless" UA for a normal Chrome UA string before assuming the endpoint changed.
|
||||
- **Headless-Chrome UA is a weak tell.** Local headless capture yields a `HeadlessChrome/...` User-Agent; some sites sniff the "Headless" token. Cloud backends (Browserbase/Browser-Use) send a real desktop-Chrome UA, so a client derived from a cloud capture replays more reliably. If a headless-derived client 403s where the browser didn't, swap the "Headless" UA for a normal Chrome UA string before assuming the endpoint changed.
|
||||
- **CDP capture doesn't close the browser.** `har_capture_cdp.py` attaches to a browser it doesn't own and leaves it running — correct for cloud/remote sessions Hermes manages. Don't add a close; let the owning backend tear it down.
|
||||
|
||||
## Verification
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ Load this skill when a user wants to:
|
||||
|
||||
## When NOT to use this skill
|
||||
|
||||
- User wants **Hermes itself to drive a browser** → use Hermes' built-in browser tool (Browserbase / Camofox). page-agent is the *opposite* direction.
|
||||
- User wants **Hermes itself to drive a browser** → use Hermes' built-in `browser_exec` tool (local Chromium or a cloud browser). page-agent is the *opposite* direction.
|
||||
- User wants **cross-tab automation without embedding** → use Playwright, browser-use, or the page-agent Chrome extension
|
||||
- User needs **visual grounding / screenshots** → page-agent is text-DOM only; use a multimodal browser agent instead
|
||||
|
||||
|
||||
Reference in new issue
Block a user