Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skill-src/webcmd-browser/SKILL.src.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ return { title: await page.title(), url: page.url() };
JS
```

For sandbox boundaries, artifacts, errors, snapshots, and timings, read [`references/browser-run-playwright.md`](references/browser-run-playwright.md).
**`browser run` executes in QuickJS — not in Node, and not in the page.** `document` and `window` are not in scope (use `page.evaluate`), and `Buffer`, `require`, and `fs` do not exist. Read [`references/browser-run-playwright.md`](references/browser-run-playwright.md) before writing your first program; it lists what is available, what is blocked, and what to use instead.

---

Expand Down
70 changes: 64 additions & 6 deletions skill-src/webcmd-browser/references/browser-run-playwright.src.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
# Browser Run Details

## Sandbox boundaries
## The runtime is QuickJS, not Node and not the page

`run` evaluates the supplied JavaScript in a fresh sandbox. Browser state in the bound session persists, but JavaScript variables and handles do not. `page`, `context`, `browser`, and `console` are normal Playwright globals; use the vendored Playwright client as the API reference. Return only JSON-compatible data. `page.snapshotForAI()` is not available.
`browser run` executes your program in a QuickJS sandbox. `page`, `context`, and `browser`
are Playwright handles that drive a browser running somewhere else — they are not evidence
that you are inside that browser, and not evidence that you are inside Node.

`context.newPage()` is not available inside `run`; create or bind Session tabs through Webcmd commands so page ownership stays deterministic.
Two consequences produce almost every `browser run` failure:

## Artifact paths
- **Anything DOM-shaped must go inside `page.evaluate()`.** `document`, `window`, and
`localStorage` are not in your scope. `document.querySelector(...)` at the top level
throws `'document' is not defined`; `await page.evaluate(() => document.querySelector(...))`
works, because that callback is serialized and run in the page.
- **Anything Node-shaped does not exist.** No `require`, no `import` of host modules, no
`fs`, no `Buffer`, no `process`.

Artifacts written by Playwright must use a relative logical filename. Webcmd returns an artifact receipt with its locator; it does not grant host-path write access.
Browser state in the bound session persists between runs. JavaScript variables and handles
do not — each run starts with a fresh scope.

## What is available

| Need | Use |
|---|---|
| Drive the page | `page`, `context`, `browser` (Playwright) |
| Read or manipulate the DOM | `page.evaluate(() => …)` |
| Find elements | `page.locator(selector)`, `page.getByRole(...)`, and the other `getBy*` locators |
| Log | `console` |
| Return data | `return` any JSON-compatible value |

`page.$` and `page.$$` work, but prefer `page.locator()` — it retries and auto-waits.

`context.newPage()` works and creates a tab the Webcmd session tracks. You cannot close it
from inside `run` (see below); list tabs with `webcmd --session <session-id> browser tabs`.

`page.snapshotForAI()` is not available; use `webcmd browser snapshot` instead.

## What is blocked, and what to use instead

These throw `BROWSER_RUN_API_UNSUPPORTED` because page and context ownership belongs to the
Webcmd session, not to your program:

| Blocked | Instead |
|---|---|
| `page.close()` | Leave the tab open, or `webcmd session close <session-id>` |
| `context.close()`, `browser.close()` | `webcmd session close <session-id>` |
| `browser.newContext()` | `webcmd session create` — one run is scoped to one context |
| `browser.newBrowserCDPSession()`, `context.newCDPSession()` | Not exposed inside `run` |
| `playwright.request` (`newRequest`) | `page.request` for calls in the page's context |

## Files and binary data

There is no host filesystem. Passing a host path to `setInputFiles` fails with
`File paths are unavailable in the QuickJS sandbox; use in-memory file payloads` — supply
`{ name, mimeType, buffer }` with a `Uint8Array` instead.

Artifacts written from the sandbox must use a relative logical filename. Webcmd returns an
artifact receipt with its locator; it does not grant host-path write access.

## Errors

`BROWSER_RUN_*` errors name invalid input, unsupported Playwright calls, timeouts, output limits, or serialization failures. A timeout can include `BROWSER_RUN_SIDE_EFFECTS_MAY_HAVE_OCCURRED`; inspect the page state before retrying a write.
`BROWSER_RUN_*` errors name invalid input, unsupported Playwright calls, timeouts, output
limits, or serialization failures. A timeout can include
`BROWSER_RUN_SIDE_EFFECTS_MAY_HAVE_OCCURRED`; inspect the page state before retrying a write.

A rejection phrased `QuickJS promise rejected: 'X' is not defined` means `X` is a Node or DOM
global that the sandbox does not provide — check the two tables above before retrying.

## Snapshot behavior

Expand All @@ -21,3 +73,9 @@ Use `webcmd --session <session-id> browser snapshot --snapshot-mode act` to insp
## Timing

Run results include timing fields such as `quickjs_boot_ms`, `client_bundle_init_ms`, `program_ms`, `browser_wait_ms`, and `snapshot_ms`. `--timeout <seconds>` limits the complete run; `--max-output <characters>` bounds returned data and logs.

## Hosted mode

Hosted `browser run` uses the same QuickJS sandbox and the same rules. Only the browser on
the far end differs — hosted runs drive a Browser Use browser over CDP rather than local
Cloak. Programs that work locally work hosted; the tables above apply in both modes.
2 changes: 1 addition & 1 deletion skills/webcmd-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ return { title: await page.title(), url: page.url() };
JS
```

For sandbox boundaries, artifacts, errors, snapshots, and timings, read [`references/browser-run-playwright.md`](references/browser-run-playwright.md).
**`browser run` executes in QuickJS — not in Node, and not in the page.** `document` and `window` are not in scope (use `page.evaluate`), and `Buffer`, `require`, and `fs` do not exist. Read [`references/browser-run-playwright.md`](references/browser-run-playwright.md) before writing your first program; it lists what is available, what is blocked, and what to use instead.

---

Expand Down
70 changes: 64 additions & 6 deletions skills/webcmd-browser/references/browser-run-playwright.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,70 @@
# Browser Run Details

## Sandbox boundaries
## The runtime is QuickJS, not Node and not the page

`run` evaluates the supplied JavaScript in a fresh sandbox. Browser state in the bound session persists, but JavaScript variables and handles do not. `page`, `context`, `browser`, and `console` are normal Playwright globals; use the vendored Playwright client as the API reference. Return only JSON-compatible data. `page.snapshotForAI()` is not available.
`browser run` executes your program in a QuickJS sandbox. `page`, `context`, and `browser`
are Playwright handles that drive a browser running somewhere else — they are not evidence
that you are inside that browser, and not evidence that you are inside Node.

`context.newPage()` is not available inside `run`; create or bind Session tabs through Webcmd commands so page ownership stays deterministic.
Two consequences produce almost every `browser run` failure:

## Artifact paths
- **Anything DOM-shaped must go inside `page.evaluate()`.** `document`, `window`, and
`localStorage` are not in your scope. `document.querySelector(...)` at the top level
throws `'document' is not defined`; `await page.evaluate(() => document.querySelector(...))`
works, because that callback is serialized and run in the page.
- **Anything Node-shaped does not exist.** No `require`, no `import` of host modules, no
`fs`, no `Buffer`, no `process`.

Artifacts written by Playwright must use a relative logical filename. Webcmd returns an artifact receipt with its locator; it does not grant host-path write access.
Browser state in the bound session persists between runs. JavaScript variables and handles
do not — each run starts with a fresh scope.

## What is available

| Need | Use |
|---|---|
| Drive the page | `page`, `context`, `browser` (Playwright) |
| Read or manipulate the DOM | `page.evaluate(() => …)` |
| Find elements | `page.locator(selector)`, `page.getByRole(...)`, and the other `getBy*` locators |
| Log | `console` |
| Return data | `return` any JSON-compatible value |

`page.$` and `page.$$` work, but prefer `page.locator()` — it retries and auto-waits.

`context.newPage()` works and creates a tab the Webcmd session tracks. You cannot close it
from inside `run` (see below); list tabs with `webcmd --session <session-id> browser tabs`.

`page.snapshotForAI()` is not available; use `webcmd browser snapshot` instead.

## What is blocked, and what to use instead

These throw `BROWSER_RUN_API_UNSUPPORTED` because page and context ownership belongs to the
Webcmd session, not to your program:

| Blocked | Instead |
|---|---|
| `page.close()` | Leave the tab open, or `webcmd session close <session-id>` |
| `context.close()`, `browser.close()` | `webcmd session close <session-id>` |
| `browser.newContext()` | `webcmd session create` — one run is scoped to one context |
| `browser.newBrowserCDPSession()`, `context.newCDPSession()` | Not exposed inside `run` |
| `playwright.request` (`newRequest`) | `page.request` for calls in the page's context |

## Files and binary data

There is no host filesystem. Passing a host path to `setInputFiles` fails with
`File paths are unavailable in the QuickJS sandbox; use in-memory file payloads` — supply
`{ name, mimeType, buffer }` with a `Uint8Array` instead.

Artifacts written from the sandbox must use a relative logical filename. Webcmd returns an
artifact receipt with its locator; it does not grant host-path write access.

## Errors

`BROWSER_RUN_*` errors name invalid input, unsupported Playwright calls, timeouts, output limits, or serialization failures. A timeout can include `BROWSER_RUN_SIDE_EFFECTS_MAY_HAVE_OCCURRED`; inspect the page state before retrying a write.
`BROWSER_RUN_*` errors name invalid input, unsupported Playwright calls, timeouts, output
limits, or serialization failures. A timeout can include
`BROWSER_RUN_SIDE_EFFECTS_MAY_HAVE_OCCURRED`; inspect the page state before retrying a write.

A rejection phrased `QuickJS promise rejected: 'X' is not defined` means `X` is a Node or DOM
global that the sandbox does not provide — check the two tables above before retrying.

## Snapshot behavior

Expand All @@ -21,3 +73,9 @@ Use `webcmd --session <session-id> browser snapshot --snapshot-mode act` to insp
## Timing

Run results include timing fields such as `quickjs_boot_ms`, `client_bundle_init_ms`, `program_ms`, `browser_wait_ms`, and `snapshot_ms`. `--timeout <seconds>` limits the complete run; `--max-output <characters>` bounds returned data and logs.

## Hosted mode

Hosted `browser run` uses the same QuickJS sandbox and the same rules. Only the browser on
the far end differs — hosted runs drive a Browser Use browser over CDP rather than local
Cloak. Programs that work locally work hosted; the tables above apply in both modes.
5 changes: 3 additions & 2 deletions src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,9 @@ describe('webcmd skills content', () => {
expect(browser).toContain('--no-snapshot-diff');
expect(browser).not.toContain('page.snapshotForAI()');
expect(browser).not.toContain('--snapshot-diff');
expect(browserRunReference).toMatch(/sandbox boundaries/i);
expect(browserRunReference).toMatch(/artifact paths/i);
expect(browserRunReference).toMatch(/QuickJS/);
expect(browserRunReference).toMatch(/page\.evaluate/);
expect(browserRunReference).toMatch(/artifact/i);
expect(browserRunReference).toMatch(/errors/i);
expect(browserRunReference).toMatch(/snapshot behavior/i);
expect(browserRunReference).toContain('--snapshot-mode act|tree');
Expand Down
Loading