项目文件夹

文件
ComBba 84e7a2c0ae feat(domain-skills): add browser-use-cloud (REST + cleanup-zombies)
Adds an operator-facing skill for cloud.browser-use.com / api.browser-use.com,
covering the same REST surface that the harness's own admin.py already
uses (X-Browser-Use-API-Key header, /browsers, /profiles), plus a
companion script for the most common automation -- stopping zombie
sessions older than N minutes.

Live-tested on 2026-05-05 with a real BROWSER_USE_API_KEY:

  POST   /browsers              201 - shape verified incl. liveUrl on live.browser-use.com
  PATCH  /browsers/{id} stop    200 - returns final cost
  GET    /browsers              200 - paginated {items, totalItems, ...}
  GET    /profiles              200 - same envelope
  GET    /profiles/{id}         200 - cookieDomains=None on fresh profiles
  GET    /usage                 404 - no public endpoint, doc'd accordingly
  GET    /                      404 - no root metadata

The cleanup-zombies.py companion is the regression artefact; running it
in dry-run mode is the cheapest smoke test, and a full E2E loop
(spawn -> list -> stop -> re-list) was confirmed end-to-end during
authoring.

Notable wire gotchas surfaced and documented:

- Cost / proxy fields (proxyCost, browserCost, proxyUsedMb) are returned
  as JSON strings, not numbers; cast to float before arithmetic.
- liveUrl host is live.browser-use.com (different from cloud.browser-use.com),
  with the cdp WebSocket encoded as a ?wss= query parameter.
- cookieDomains can be null on a freshly-created profile despite
  list_cloud_profiles' docstring describing it as an array.
- GET /usage returns 404 -- per-session cost lives on each browser
  record; aggregate billing only on the dashboard.

Style follows the claude-ai/share-export (#267) pattern of
markdown-skill-with-companion-py, sized at 222 + 161 LOC. Discovery
under helpers.py:163's current logic would resolve cloud.browser-use.com
to "cloud/" rather than "browser-use-cloud/" -- the skill folder name
mirrors the convention used by claude-ai/, vercel/, and tasksquad-ai/
which are also not auto-discoverable today; PR #165 is the broader fix
for that.

Refs: PR #300 (run.py precedence fix in the same area), PR #267
(claude-ai companion-script pattern), PR #288 (vercel dashboard skill
header style).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:45:53 +09:00

223 行
8.7 KiB
Markdown

# Browser Use Cloud — Programmatic Automation
`https://api.browser-use.com/api/v3` (REST). All five endpoints below were
exercised end-to-end on 2026-05-05 with a real `BROWSER_USE_API_KEY`; the
companion script `cleanup-zombies.py` next to this file *is* the
field-test — running it lists active browsers and stops zombies via the
same wire calls the harness uses internally.
This skill is for users who already start cloud browsers via
`start_remote_daemon()` and want to manage the surrounding lifecycle —
provisioning fleets, cleaning up zombies, listing what's running, sharing
liveUrls — without clicking through `cloud.browser-use.com`.
## Auth
REST uses a custom header (not `Authorization: Bearer` — that path
returns a generic 401 silently):
```python
import os
HEADERS = {
"X-Browser-Use-API-Key": os.environ["BROWSER_USE_API_KEY"],
"Content-Type": "application/json",
}
```
The key only authorises actions on browsers and profiles created under
it — there are no organisation-level admin endpoints on the public API.
## Endpoint reference
All paths are under `https://api.browser-use.com/api/v3`. Verified status
codes and shapes from 2026-05-05 below.
### `POST /browsers` — provision a cloud browser
Body (camelCase):
| Key | Type | Notes |
|---|---|---|
| `profileId` | UUID | optional; logged-in cloud profile |
| `profileName` | str | optional; resolved client-side |
| `proxyCountryCode` | ISO2 | default `"us"`; pass `null` to disable BU proxy |
| `timeout` | int | minutes, 1..240 |
| `customProxy` | obj | `{host, port, username, password, ignoreCertErrors}` |
| `browserScreenWidth` / `browserScreenHeight` | int | viewport |
| `allowResizing` | bool | viewport user-resizable |
| `enableRecording` | bool | session recording |
Returns `201` with this shape (also returned by `GET /browsers/{id}`,
`GET /browsers` items, and `PATCH /browsers/{id}`):
```python
{
"id": str,
"status": str, # e.g. "active"
"liveUrl": str, # host: live.browser-use.com (different from cloud.browser-use.com)
"cdpUrl": str, # https:// — daemon converts to ws via /json/version
"timeoutAt": str, # ISO 8601 UTC
"startedAt": str,
"finishedAt": None, # populated only after stop
"proxyUsedMb": str, # STRING — cast to float before arithmetic
"proxyCost": str, # STRING
"browserCost": str, # STRING
"agentSessionId": None,
"recordingUrl": None, # str only when enableRecording=True at create
}
```
The `liveUrl` carries the cdp WebSocket as a `?wss=...` query param, so
sharing the URL alone hands off a viewable session — no extra setup.
### `PATCH /browsers/{id}` — stop (end billing)
Body `{"action": "stop"}`. Returns `200` with the same browser object,
but `liveUrl` and `cdpUrl` come back as `null` and `finishedAt` is
populated. Use the returned `proxyCost` + `browserCost` for final cost.
Always wrap caller code in `try/finally`; every billed minute counts.
### `GET /browsers` — list active sessions
Returns `200` and the standard envelope
`{items: [...], totalItems, pageNumber, pageSize}`. `items[*]` matches
the `POST /browsers` response shape. Already-finished browsers appear in
the listing for a window with `finishedAt` populated — filter them out
when computing age.
### `GET /profiles?pageSize=N&pageNumber=N` — list cloud profiles
`pageSize` caps at 100. Same envelope as `/browsers`.
### `GET /profiles/{id}` — profile detail
Returns the same shape as the listing items:
```python
{
"id": str,
"userId": None, # null in observed responses
"name": str,
"lastUsedAt": str | None, # null until first use
"createdAt": str,
"updatedAt": str,
"cookieDomains": list[str] | None, # null on freshly-created profiles
}
```
`browser_harness.admin.list_cloud_profiles()` already wraps the listing
+ per-id GET; prefer it unless you need raw access.
## Companion script: `cleanup-zombies.py`
A self-contained operator script next to this file. Run it with:
```bash
BROWSER_USE_API_KEY=... python agent-workspace/domain-skills/browser-use-cloud/cleanup-zombies.py
# stops every active browser older than 30 minutes (default)
BROWSER_USE_API_KEY=... python .../cleanup-zombies.py --older-than 5 --dry-run
# preview only; no PATCH /stop sent
```
The script is the practical residue of the API verification — running it
exercises four of the five endpoints (`GET /browsers`, plus
`PATCH .../stop` per zombie). Use it as the live regression check
whenever this skill is updated.
## Dashboard navigation (when API isn't enough)
The dashboard at `cloud.browser-use.com` requires a logged-in session;
the unauthenticated root redirects to `/signup` (verified 2026-05-05).
Beyond `/signup` the slugs below are *inferred from typical SaaS layout*
— confirm in your own browser before relying on the literal paths:
```
/signup (verified)
/dashboard [verify]
/browsers [verify] — likely the dashboard mirror of GET /browsers
/browsers/<id> [verify]
/profiles [verify]
/api-keys [verify]
```
There is no `/usage` page mirror — `GET /usage` on the API returns 404,
so per-session cost has to come from each browser record (`proxyCost` +
`browserCost`). The dashboard surfaces aggregate billing somewhere, but
that's outside the API surface and not useful from inside `bh`.
For dashboard scraping, attach to your real Chrome and read cookies:
```python
cookies = cdp("Network.getCookies", urls=["https://cloud.browser-use.com"])
parts = [c["name"] + "=" + c["value"] for c in cookies.get("cookies", [])]
dash_headers = {"Cookie": "; ".join(parts), "Accept": "text/html,application/json"}
```
Empty cookie jar = not logged in; open `cloud.browser-use.com` in your
real Chrome once, then retry.
## Traps to avoid
- **Auth header name** is `X-Browser-Use-API-Key`. `Authorization:
Bearer ...` silently fails with a generic 401.
- **Cost fields are strings**, not numbers. `proxyCost`, `browserCost`,
`proxyUsedMb` come back as quoted strings (`"0.0123"`); cast to
`float` before arithmetic.
- **`cookieDomains` can be `None`** on freshly-created profiles, despite
what `admin.py:list_cloud_profiles`'s docstring says. Guard with
`c or []`.
- **`liveUrl` host is `live.browser-use.com`**, not
`cloud.browser-use.com`. They're separate surfaces.
- **`start_remote_daemon` overwrites `BU_CDP_WS`** in the daemon env;
re-read from `browser["cdpUrl"]` if you need the value afterwards.
(PR #300 stops `run.py` from clobbering an explicit `BU_CDP_URL`, but
the daemon env still gets set.)
- **`liveUrl` is single-session** — after stop, the URL no longer
resolves; don't cache across calls.
- **`_browser_use` has a 60s timeout** in `admin.py`; long-running ops
(large profile sync) need their own polling.
- **`profile-use` CLI is a separate install**:
`curl -fsSL https://browser-use.com/profile.sh | sh`.
- **`pageSize` caps at 100** silently — paginate via `pageNumber`.
`totalItems` in the envelope lets you size loops up front.
- **`proxyCountryCode` defaults to `"us"`** when omitted; pass `None` to
disable BU proxy entirely. Wrong country = wrong egress IP = breaks
geo-locked auth.
## What this skill does NOT cover
- **Billing / payment methods** — dashboard only, intentionally
sensitive.
- **Organisation / team admin** — outside the per-API-key surface.
- **SDK features** — Browser Use ships official SDKs separately; this
skill is the raw-HTTP path for power users inside `bh`.
- **Cross-API-key reads** — every endpoint is scoped to the calling key.
## Provenance
Live-tested 2026-05-05 against `https://api.browser-use.com/api/v3`:
| Endpoint | Method | Status | Notes |
|---|---|---|---|
| `/profiles?pageSize=100&pageNumber=1` | GET | 200 | shape verified |
| `/profiles/{id}` | GET | 200 | `cookieDomains=None` observed on a fresh profile |
| `/browsers` | POST | 201 | `liveUrl` host is `live.browser-use.com` |
| `/browsers/{id}` (`{action:"stop"}`) | PATCH | 200 | returns final cost |
| `/browsers` | GET | 200 | paginated `{items,totalItems,pageNumber,pageSize}` |
| `/usage` | GET | 404 | **no public endpoint** |
| `/` | GET | 404 | no root metadata |
Companion script `cleanup-zombies.py` re-runs the listing + stop subset
end-to-end and is the regression artefact for this skill. A full E2E
loop (spawn → list → stop → re-list) was executed on 2026-05-05 against
the production API and printed:
```
[STOP] 3ac4c964-...-d3d3e1ad7508 age= 0.0min cost=$0.0020
summary: 1 active session(s), stopped 1
```
Re-running the script in `--dry-run` mode against an empty pool is the
cheapest smoke test (no `PATCH /stop` calls, ~$0).