Skip to main content

browser-harness

MUST load BEFORE running any browser-use command. This is the only bridge between the agent and the browser โ€” browser-use 0.13 has NO subcommands (open/click/state are gone); you drive the browser by piping Python helper code via a heredoc. Contains the helper API, wrapper rules, session management, and store-task restrictions. Without this skill, browser commands will fail.

Jump to install

Source facts

Repository
zpoint/vibe-seller
Last source activity
August 12, 2026 at 03:15
Detected SKILL.md language
English
Stars
68
Forks
14

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
browser-harness
description
MUST load BEFORE running any browser-use command. This is the only bridge between the agent and the browser โ€” browser-use 0.13 has NO subcommands (open/click/state are gone); you drive the browser by piping Python helper code via a heredoc. Contains the helper API, wrapper rules, session management, and store-task restrictions. Without this skill, browser commands will fail.
allowed-tools
Bash(browser-use:*)
<!-- VIBE-SELLER CUSTOMIZATIONS: adapted from the upstream 0.13 skill (browser_use/skills/browser-use/SKILL.md in the wheel). If re-syncing from upstream, re-apply: (1) the Store/No-store task banners, (2) the wrapper env-injection contract (BU_NAME/BU_CDP_WS auto-injected, agent overrides blocked), (3) removal of cloud/remote-daemon and local-profile sections we don't use, (4) the "js() does NOT parse JSON" bullet, (5) the "a screenshot is not a data source" rule. See docs/browser-use-0.13-migration.md. --> > **browser-use 0.13 changed everything.** There are **no subcommands**. You > no longer run `browser-use open <url>`. Instead you pipe Python helper code > to `browser-use` via a heredoc; helpers are pre-imported and a background > daemon is attached automatically. > **Store Tasks:** `browser-use` is a per-store wrapper script that > auto-injects `BU_NAME` (the store session) and `BU_CDP_WS` (the store's CDP > proxy). You **cannot** set `BU_NAME`, `BU_CDP_URL`, `BU_CDP_WS`, or `--mcp` > yourself โ€” the wrapper blocks them. Use the default session for the store's > seller center, or `--session <slug>-aux` for non-seller-center sites (the > wrapper maps this to the aux session; no other `--session` value is > allowed). > > **No-store (orchestrator) Tasks:** `browser-use` is the store-less `web` > wrapper (`bin/_web`). Use it only for neutral public web work (search, > tracking/logistics, research) โ€” NEVER for a store's seller center or to log > into store/platform accounts (create a store sub-task for those). # Browser Automation with browser-use (0.13, heredoc interface) Drive the browser by piping Python to `browser-use`. Helpers are pre-imported; the harness calls `ensure_daemon()` before running your code, so the browser attaches automatically to the store's CDP endpoint. ```bash browser-use <<'PY' new_tab("https://example.com") # first navigation is new_tab(), NOT goto wait_for_load() print(page_info()) PY ``` The wrapper takes the heredoc form **only** โ€” there is no `-c` flag (passing one just prints usage). Put every statement inside the heredoc. ## Prerequisites ```bash browser-use --doctor # verify installation / CDP connectivity ``` ## Budget every invocation: the wrapper kills it at 120 s The store wrapper runs the real `browser-use` under a **120-second alarm**, and a timeout is treated as evidence the *browser* is wedged: the wrapper bumps a strike counter, force-`--reload`s the daemon, and on the **second consecutive** timeout prints `UNRECOVERABLE`, exits 75, and tells you to stop retrying and report a gap. Any other outcome โ€” even an ordinary Python error โ€” resets the counter. **A slow-but-healthy call is indistinguishable from a wedged browser.** So a legitimately long heredoc does not merely get cut off: two of them in a row will convince the wrapper (and then you) that a perfectly good browser is dead, and the honest-reporting rule then makes you abandon real work. Keep each invocation comfortably under the limit: - **One unit of work per invocation** โ€” one page, one SKU, one export. Drive the loop from the **shell**, not inside the heredoc, and append results to a file under `/tmp/<task>/` so progress survives. - **Count your polls.** A render-wait of `range(15)` with `sleep(3)` is 45 s on its own; two of those plus navigation overruns 120 s. - If you genuinely need a long single operation (a slow export), poll it across **separate** invocations rather than sleeping inside one. A timeout also leaves the tab mid-operation, so re-read state at the start of the next invocation instead of assuming where you left off. ## Core Workflow 1. **Navigate**: `new_tab(url)` โ€” for the first page **and every later navigation**. There is **no `page` object** in the heredoc scope, so `page.goto(url)` raises `NameError`; use `new_tab(url)` (or click a link) to move around. 2. **Understand state โ€” via the DOM (PREFERRED, no vision needed):** `page_info()` for page-level facts (url/title/size), and **`js(...)` to read the DOM** โ€” text content AND every element's on-screen coordinates via `getBoundingClientRect` (see "Locate & click without vision"). This is the primary way to drive the browser; a non-vision model completes the whole flow this way. 3. **See the layout (OPTIONAL โ€” vision models only):** `capture_screenshot()` returns a PNG path (`~/.vibe-seller/bh-tmp/shot.png`, overwritten each call); `print()` it and **Read that PNG** to view it. Use it only to disambiguate a crowded layout โ€” never *depend* on it. If your model can't view images, skip screenshots entirely and use step 2. **A screenshot is not a data source.** Never take a value you will act on or report โ€” an ID, a number, a row, a column that exists, a count โ€” from an image. Read it from the DOM with `js(...)`. A screenshot answers "where is it on screen", nothing else. This is not caution about edge cases; it is the observed default. On a page holding exactly two rows, models on two different providers each described a table that did not exist โ€” invented columns (`ACOS`, `CPC`, `Bid`), invented campaigns, invented statuses โ€” then spent a dozen turns hunting for the data they had "seen", and one named a nonexistent campaign in its final report to the user. Nothing was wrong with the PNG. **When the DOM and the picture disagree, the DOM is right and the picture is a hallucination** โ€” do not go looking for the difference, and never reconcile by trusting the image. 4. **Interact**: get an element's centre coords from step 2, then `click_at_xy(x, y)`; set input values with `js(...)`. Re-read with `page_info()` / `js(...)` after to confirm. 5. **After navigation**: `wait_for_load()`; if the tab is stale/internal, `ensure_real_tab()`. ## Helper API Helpers are pre-imported into the heredoc namespace: ```python new_tab(url) # open a new tab and navigate (use for EVERY navigation) page_info() # structured summary of the current page capture_screenshot() # โ†’ PNG path (~/.vibe-seller/bh-tmp/shot.png); LAYOUT only, never data click_at_xy(x, y) # click at pixel coordinates wait_for_load() # wait for navigation/network to settle ensure_real_tab() # switch off a stale/internal (chrome://) tab js('<javascript>') # run JS; returns the SERIALIZABLE result only cdp('Domain.method', **params) # raw CDP โ€” params are KEYWORDS, not a dict # e.g. cdp('Page.navigate', url='...') ``` - **`js()` returns serializable values only.** `js("document.title")` and `js("return 1+1")` work; but `js("document.querySelector(...)")` returns a useless `{}` (a DOM node can't serialize). Return **numbers, strings, or plain objects/arrays** โ€” e.g. an element's coordinates (below), not the element itself. For an element *reference* (to set a file input) use `cdp('Runtime.evaluate', expression=..., returnByValue=False)` โ†’ `objectId` (note: `cdp()` params are **keyword args**, never a positional dict โ€” see "Uploading a file"). - **`js()` does NOT parse JSON โ€” so do not `JSON.stringify` your result.** The value comes straight from CDP `returnByValue`, so the JS type maps to the Python type: `return [{a:1}]` gives you a **list of dicts** already, while `return JSON.stringify([{a:1}])` gives you a **`str`** that you must then `json.loads` yourself. Both directions bite: stringifying and *not* parsing makes `for row in data` iterate **characters**; not stringifying and *then* parsing raises `TypeError: the JSON object must be str, bytes or bytearray, not list`. Return the object directly and use it as-is. If you inherit code whose shape you can't be sure of, normalise once rather than guessing: ```python import json # stdlib modules: import them, don't rely on the namespace def jsjson(expr): d = js(expr) return json.loads(d) if isinstance(d, str) else d ``` Only the **helpers** above are guaranteed pre-imported. `json`, `time` and friends do happen to be reachable in the heredoc namespace today (they leak in through the harness's own `import *`), but that is an implementation detail of the wheel, not a contract โ€” one `__all__` upstream and it stops. Import the stdlib you use. ## Locate & click an element WITHOUT vision (the preferred path) You do not need to see the page. Read the DOM and compute click coordinates from `getBoundingClientRect`, then `click_at_xy`. This drives any page โ€” buttons, links, shadow-DOM `kat-*` components โ€” with no screenshot: ```bash browser-use <<'PY' # one element by selector โ†’ its centre coords + text (None if not found): box = js(""" var el = document.querySelector('button.submit'); // any CSS selector if(!el) return null; var r = el.getBoundingClientRect(); return {text:(el.innerText||el.value||'').slice(0,40), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)}; """) print("target:", box) if box: click_at_xy(box["x"], box["y"]) # OR enumerate all clickables to find the right one by its text: els = js(""" return [].slice.call(document.querySelectorAll('a,button,input,[role=button],kat-button')) .map(function(el){var r=el.getBoundingClientRect(); return {text:(el.innerText||el.value||'').slice(0,40), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)};}) .filter(function(e){return e.x>0 && e.y>0;}); """) print(els) # pick the one whose text matches, then click_at_xy(it.x, it.y) PY ``` **A plain `<a href>` is a navigation, not a click.** Read the href and `new_tab(href)`. Clicking one means landing inside the *anchor's* own rect, and an anchor is usually a small target inside a much larger parent: the centre of the table row or cell holding it is typically not on the anchor at all, so `click_at_xy` hits dead space and silently does nothing โ€” no error, no navigation, and `page_info()` still shows the old page. Reserve clicking for controls that have no href (buttons, `kat-*`, JS handlers). ### A control BELOW the fold that won't scroll into view Some pages (e.g. Amazon's "Generate Spreadsheet" popover) put the button you need **below the viewport**, inside a `kat-popover`/panel that `scrollTo`/`scrollIntoView` can't bring up โ€” the window is short (e.g. 839px tall) and `click_at_xy` can't hit an off-screen y. Don't fight the scroll: **grow the layout viewport with CDP so the whole panel fits**, then click the (now on-screen) coordinate, then restore: ```bash browser-use <<'PY' import time cdp("Emulation.setDeviceMetricsOverride", width=1920, height=2400, deviceScaleFactor=1, mobile=False) # tall viewport time.sleep(1) box = js(""" var els=document.querySelectorAll('kat-button,button,[role=button]'); for(var i=0;i<els.length;i++){var t=(els[i].innerText||els[i].getAttribute('label')||'').trim(); if(/generate spreadsheet/i.test(t)){var r=els[i].getBoundingClientRect(); return {x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)};}} return null; """) if box: click_at_xy(box["x"], box["y"]) # trusted click, now on-screen time.sleep(3) cdp("Emulation.clearDeviceMetricsOverride") # restore the real viewport PY ``` (Verified: the override raises `window.innerHeight` to the set value, so a below-fold button becomes reachable; `clearDeviceMetricsOverride` undoes it. A plain JS `.click()` still no-ops on `kat-*` โ€” you need the trusted `click_at_xy`.) For an element inside an **open shadow root** (Amazon `kat-*`), pierce it in the selector: `document.querySelector('kat-file-upload').shadowRoot.querySelector('input')`. Set an input's value with `js("document.querySelector('#q').value='socks'")` (then click its search icon โ€” some inputs need the click to fire events). Only the helpers above (plus Python builtins) are in scope โ€” the heredoc runs as a plain Python script. **`time`, `json`, `re`, etc. are NOT pre-imported; `import` them yourself.** Bare `sleep 3` is a `SyntaxError` and `time.sleep(3)` without `import time` is a `NameError`. **Prefer `wait_for_load()` over sleeping** โ€” reach for `import time; time.sleep(n)` only when you must wait on something `wait_for_load()` can't observe (e.g. an async in-page render after a click). Multiple statements run in one heredoc (this replaces `&&` chaining): ```bash browser-use <<'PY' new_tab("https://example.com/login") wait_for_load() js("document.querySelector('#email').value = 'user@example.com'") js("document.querySelector('#password').value = 'secret'") click_at_xy(640, 480) wait_for_load() print(page_info()) PY ``` ## Uploading a file to a web `<input type=file>` > **The file must be in a path the BROWSER PROCESS can read, passed in > that process's path form โ€” NOT `/tmp`.** `setFileInputFiles` reads the > file in the browser, not the agent. Put it in the store's downloads dir > (`~/.vibe-seller/downloads/<slug>/`) โ€” the one location guaranteed > readable on every backend. Otherwise it **silently no-ops**: > `files.length` stays 0, Submit never enables, no error. Per backend: > macOS Ziniao's Chrome is sandboxed and can't read `/tmp` (so a `/tmp` > file fails โ€” this was the whole "the widget won't accept my file" > bug, not the widget); native-Windows Chrome has no such sandbox but the > winchrome case (WSL agent โ†’ native Windows Chrome) must pass the > **Windows-form** path (`C:\โ€ฆ\downloads\<slug>\file`, via `wslpath -w`) โ€” > a `/mnt/c` or WSL path is unreadable by native Chrome. There is **no native upload helper**. Never coordinate-click the visible "Browse"/"Upload file" button expecting to then drive the OS file picker
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub