Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tidy-browsers-render.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/computer": minor
---

Add Worker JavaScript plugins and an opt-in Puppeteer plugin backed by a Browser Run binding.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ PLAN.md
# packages/computer/src/backends/worker-shell/script/build-bundle.mjs on
# prepare / pretest / pretypecheck.
packages/computer/src/backends/worker-shell/generated/
packages/computer/src/plugins/puppeteer/generated.ts

# SEA binary destinations populated at publish time from
# artifacts/computerd/ via the build-bin step. The @cloudflare/computer
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public surface. Each is a Worker workspace with its own README.
- [`examples/worker-javascript`](examples/worker-javascript) — mirrors
`worker-shell`, but `exec` evaluates an ECMAScript module in a Dynamic
Worker instead of running a shell command.
- [`examples/browser-rendering`](examples/browser-rendering) — installs the
Puppeteer plugin in the Worker JavaScript backend, then scrapes pages and
writes Markdown, JSON, and screenshot bundles to the durable workspace from
one isolated execution.
- [`examples/egress`](examples/egress) — sends one URL through the container,
Worker shell, and Worker JavaScript backends with matching `none`, `all`, or
custom egress policies.
Expand Down
40 changes: 37 additions & 3 deletions docs/17_isolate_javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ await workspace.runtime.exec(
);
```

Workspace parses the graph before loading the Worker, confines every durable path, rejects symlink traversal, and enforces aggregate source, module-count, and import-depth limits. Dynamic imports must use string literals.
Workspace parses the graph before loading the Worker, confines every durable path, rejects symlink traversal, and enforces aggregate source, module-count, and import-depth limits. Dynamic imports must use string literals. Configured and plugin modules are stored once under an internal canonical name; small directory-local aliases provide bare-import resolution without duplicating bundled package source throughout a nested caller graph.

## Execution limits and retention

The backend admits up to twenty-four executions at a time by default. A concurrent start past that ceiling fails with `EEXEC_BUSY` instead of creating an unbounded number of Dynamic Workers. Adjust `maxConcurrentExecutions` after measuring the Durable Object and Worker Loader limits for the deployment.

Each execution also bounds combined stdout and stderr output, active event subscribers, directory entries per read, concurrent and total capability calls, and cumulative capability request and response bytes. The corresponding `maxStdioBytes`, `maxExecutionSubscribers`, `maxDirectoryEntries`, and `max*Capability*` options may be lowered for public workloads. Directory reads apply their limit in SQLite before materializing rows. Requests are checked inside the isolate before Workers RPC and again by the host.
Each execution also bounds combined stdout and stderr output, active event subscribers, directory entries per read, concurrent and total capability calls, and cumulative capability request and response bytes. `maxSourceBytes` applies to caller-owned entry and relative module source, while `maxLoaderSourceBytes` separately bounds the complete generated Loader graph, including configured plugin bundles. The corresponding `maxStdioBytes`, `maxExecutionSubscribers`, `maxDirectoryEntries`, and `max*Capability*` options may be lowered for public workloads. Directory reads apply their limit in SQLite before materializing rows. Requests are checked inside the isolate before Workers RPC and again by the host.

Completed execution records remain available for replay for sixty minutes by default. The backend also keeps at most 100 completed records. Configure these bounds with `retentionMs` and `maxRetainedExecutions`. Completed records leave the in-memory active set immediately; replay reads them from SQLite.

Expand Down Expand Up @@ -134,6 +134,40 @@ new WorkerJavaScriptBackend({

Unknown bare imports fail before Worker creation. `node:fs` and `node:fs/promises` are host-installed exceptions backed by the durable Workspace. Configured modules are code, not host authority, and may not use the reserved `ws:` namespace or shadow either filesystem specifier.

## Plugins and host bindings

Plugins install a prebuilt module and the host bindings it needs. The bindings are fixed at backend construction, stay separate from `process.env`, and are not available to caller or ordinary configured modules. Plugins installed on one backend are mutually trusted.

The Puppeteer plugin bundles Cloudflare's Worker-compatible client and passes a Browser Run binding into the Dynamic Worker:

```ts
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { puppeteer } from "@cloudflare/computer/plugins/puppeteer";

new WorkerJavaScriptBackend({
loader: env.LOADER,
plugins: [puppeteer({ browser: env.BROWSER })],
});
```

Execution source imports the configured package name. Browser objects remain inside that execution; Chromium runs in Browser Run:

```js
import { withBrowser } from "@cloudflare/puppeteer";

export default function main(input) {
return withBrowser(async (browser) => {
const page = await browser.newPage();
await page.goto(input.url);
return { title: await page.title() };
}, {
guardrails: { allowedDomains: [input.hostname] },
});
}
```

See [Browser automation in Worker JavaScript](./20_browser_automation.md) for the complete setup and [`examples/browser-rendering`](../examples/browser-rendering) for a working application.

## Trusted Workspace modules

Filesystem access uses the familiar asynchronous Node API, but is backed by the durable Workspace rather than an isolate-local filesystem. Both forms are installed automatically:
Expand Down Expand Up @@ -184,7 +218,7 @@ Each execution receives a fresh Dynamic Worker with:
- a host wall-clock deadline;
- `globalOutbound: null` by default;
- finite, acyclic JSON-compatible input and structured result validation;
- configurable source/module graph, input, result, stdin, stdio, file/capability request, and response byte limits (`maxSourceBytes`, `maxInputBytes`, `maxResultBytes`, `maxStdinBytes`, `maxStdioBytes`, and `maxCapabilityBytes`);
- configurable caller source, complete Loader graph, input, result, stdin, stdio, file/capability request, and response byte limits (`maxSourceBytes`, `maxLoaderSourceBytes`, `maxInputBytes`, `maxResultBytes`, `maxStdinBytes`, `maxStdioBytes`, and `maxCapabilityBytes`);
- explicit entrypoint and Worker disposal;
- host-owned cancellation;
- retained events and result rows in the Workspace database.
Expand Down
127 changes: 127 additions & 0 deletions docs/20_browser_automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Browser automation in Worker JavaScript

`@cloudflare/computer/plugins/puppeteer` lets code running in `WorkerJavaScriptBackend` use Cloudflare Browser Run. Puppeteer and its `Browser` and `Page` objects stay inside the isolated Dynamic Worker; Chromium runs in Browser Run.

## Configure the plugin

The host Worker needs Worker Loader and Browser Run bindings:

```jsonc
{
"compatibility_flags": ["nodejs_compat", "experimental"],
"worker_loaders": [{ "binding": "LOADER" }],
"browser": { "binding": "BROWSER" }
}
```

Pass both bindings to the backend:

```ts
import { DurableObject } from "cloudflare:workers";
import { type DurableObjectStorageLike, Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { puppeteer } from "@cloudflare/computer/plugins/puppeteer";

export class BrowserWorkspace extends DurableObject<Env> {
readonly workspace: Workspace;

constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.workspace = new Workspace({
storage: ctx.storage as unknown as DurableObjectStorageLike,
backends: [
new WorkerJavaScriptBackend({
loader: env.LOADER,
plugins: [puppeteer({ browser: env.BROWSER })],
}),
],
});
}
}
```

The plugin bundles the Worker-compatible Puppeteer client, so the application does not need a separate runtime dependency on `@cloudflare/puppeteer`.

## Run a browser task

Code passed to `workspace.runtime.exec()` imports the configured module normally:

```ts
using execution = await workspace.runtime.exec(
`
import { withBrowser } from "@cloudflare/puppeteer";

export default (input) => withBrowser(async (browser) => {
const page = await browser.newPage();
await page.goto(input.url, { waitUntil: "domcontentloaded" });
return { title: await page.title(), finalUrl: page.url() };
}, {
guardrails: { allowedDomains: [input.hostname] },
});
`,
{
input: {
url: "https://developers.cloudflare.com/agents/",
hostname: "developers.cloudflare.com",
},
},
);

const result = await execution.result();
```

`withBrowser(callback, options?)` launches a connection-bound browser, runs the callback, and closes the browser afterward. Use `launch(options?)` when code needs to manage the browser itself:

```js
import { launch } from "@cloudflare/puppeteer";

const browser = await launch();
try {
// Use Puppeteer normally.
} finally {
await browser.close();
}
```

The module also exports `browserBinding`, the unchanged upstream default export, and upstream runtime exports. `browserBinding` is useful for APIs such as `puppeteer.sessions()` that take the Browser Run binding directly.

Do not return Puppeteer objects from an execution. Return structured data or write larger output to the Workspace.

## Save browser output

Worker JavaScript provides Workspace-backed `node:fs` and `node:fs/promises`. A screenshot can be written without returning its bytes through the structured result:

```js
import { withBrowser } from "@cloudflare/puppeteer";
import fs from "node:fs/promises";

export default (input) => withBrowser(async (browser) => {
const page = await browser.newPage();
await page.goto(input.url);
await fs.writeFile(input.outputPath, await page.screenshot({ type: "png" }));
return { title: await page.title(), outputPath: input.outputPath };
});
```

The Dynamic Worker is disposable, but files written to the Workspace remain available to later executions.

## Authority and limits

Installing the plugin grants every execution on that backend access to its public browser API. Put browser-enabled work on a separate named backend when only some callers should have that authority. Plugins installed on one backend are mutually trusted and share the plugin binding authority domain; caller modules and ordinary configured modules cannot import the internal binding bridge.

Browser navigation happens through Browser Run, not through the backend's `globalOutbound` policy. Validate user input and set Browser Run guardrails when the application accepts URLs from other users.

Computer execution timeouts and Puppeteer navigation timeouts are separate. Set both for the workload. `withBrowser()` closes the browser after normal completion or an error. Cancellation and timeout dispose the Dynamic Worker and its client connection, so application cleanup code may not finish in those paths.

Screenshots are encoded when they cross the Workspace filesystem bridge. For larger screenshots, raise the capability limits deliberately:

```ts
new WorkerJavaScriptBackend({
loader: env.LOADER,
plugins: [puppeteer({ browser: env.BROWSER })],
maxCapabilityBytes: 8 * 1024 * 1024,
maxCapabilityRequestBytes: 16 * 1024 * 1024,
});
```

See [`examples/browser-rendering`](../examples/browser-rendering) for a complete self-hosted example that scrapes pages and writes Markdown, JSON, and PNG output to a durable Workspace.
4 changes: 3 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ It provides:
- R2-backed mounts for pre-filling read-only data into the workspace tree.
- Durability over DO restarts for all file operations.
- Pluggable execution backends selected through `workspace.runtime`: a Cloudflare Container shell, a just-bash Dynamic Worker, or an isolated ECMAScript-module Dynamic Worker.
- Isolated JavaScript with structured input/results, durable relative imports, configured libraries, durable `node:fs/promises`, trusted `ws:git` / `ws:artifacts`, and managed execution records.
- Isolated JavaScript with structured input/results, durable relative imports, configured libraries, plugin bindings, durable `node:fs/promises`, trusted `ws:git` / `ws:artifacts`, and managed execution records.
- Workspace constructable without a backend, for filesystem-only use cases.
- Out-of-the-box AI SDK tools for `@cloudflare/agents` through `@cloudflare/computer/tools`.

Expand All @@ -46,6 +46,7 @@ The package ships several entrypoints:
| `@cloudflare/computer/backends/container` | `CloudflareContainerBackend` and `withWorkspaceContainer`. Pulls in the computerd / capnweb sync plumbing. |
| `@cloudflare/computer/backends/worker-shell` | `WorkerShellBackend` and the bundled just-bash command runtime. |
| `@cloudflare/computer/backends/worker-javascript` | `WorkerJavaScriptBackend`, configured libraries, durable relative imports, `node:fs/promises`, and trusted `ws:git` / `ws:artifacts`. |
| `@cloudflare/computer/plugins/puppeteer` | Opt-in Cloudflare Puppeteer module and Browser Run binding for isolated JavaScript. |
| `@cloudflare/computer/git` | Opt-in isomorphic-git glue for working with checkouts inside the workspace. Bundled lazily, with `pako` replaced by Workers `node:zlib`, and kept out of the default `@cloudflare/computer` graph. |
| `@cloudflare/computer/artifacts` | `createArtifact`, an optionally session-scoped wrapper over the Cloudflare Artifacts Workers binding, plus its argv CLI. |
| `@cloudflare/computer/tools` | AI SDK tools for agents: read, write, edit, ls, optional exec, and optional publish. |
Expand Down Expand Up @@ -243,6 +244,7 @@ above, then dive into the area you're working on.
| [17. Isolate JavaScript runtime](./17_isolate_javascript.md) | ECMAScript modules, durable imports, configured libraries, durable `node:fs/promises`, trusted `ws:git` / `ws:artifacts`, and managed lifecycle. |
| [18. Runtime migration](./18_runtime_migration.md) | Breaking preview-API mappings from public shell and script-execution surfaces to `workspace.runtime`. |
| [19. Performance](./19_performance.md) | Filesystem benchmarks: `fs-bench` numbers, an `npm install` comparison, and how to reproduce them. |
| [20. Browser automation](./20_browser_automation.md) | Install Cloudflare Puppeteer and Browser Run in isolated JavaScript, persist browser artifacts, and apply production lifecycle, security, and resource limits. |

## High-level API

Expand Down
4 changes: 4 additions & 0 deletions examples/browser-rendering/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
node_modules/
.wrangler/
worker-configuration.d.ts
92 changes: 92 additions & 0 deletions examples/browser-rendering/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Computer browser rendering example

This is a self-hosted demonstration of the public `@cloudflare/computer/plugins/puppeteer` integration. A generated ECMAScript module gets the normal Puppeteer `Browser` and `Page` APIs, while Cloudflare Browser Run supplies the managed Chromium session.

## Plugin usage

The reusable integration has two pieces. First, register the plugin on a Worker JavaScript backend:

```ts
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { puppeteer } from "@cloudflare/computer/plugins/puppeteer";

const workspace = new Workspace({
storage: ctx.storage,
backends: [
new WorkerJavaScriptBackend({
loader: env.LOADER,
plugins: [puppeteer({ browser: env.BROWSER })],
}),
],
});
```

Then use the bound lifecycle helper inside an ordinary `workspace.runtime.exec()` module:

```js
import { withBrowser } from "@cloudflare/puppeteer";

export default (input) => withBrowser(async (browser) => {
const page = await browser.newPage();
await page.goto(input.url);
return { title: await page.title() };
}, {
guardrails: { allowedDomains: [input.hostname] },
});
```

That is the complete plugin boundary. `withBrowser()` uses the configured Browser Run binding and closes the connection-bound browser after the callback. `Browser`, `Page`, selectors, and page evaluation stay inside the Dynamic Worker; Chromium runs in Browser Run.

## What the example adds

The rest of this directory is an example application, not code required by the plugin. Its web interface accepts a user-provided HTTP(S) URL and adds:

- text, heading, metadata, and link scraping;
- a full-page screenshot written to the durable Workspace;
- response, document, viewport, and navigation timing data;
- a research workflow that writes `report.md`, `page.json`, and `screenshot.png` to one durable Workspace directory;
- artifact routes, result components, and a Workspace file tree.

Those pieces show ways to combine Browser Run with Computer's durable filesystem. Applications can instead execute a module as small as the one above.

## Run it

From the repository root:

```sh
npm install
npm run build --workspace @cloudflare/computer
npm run dev --workspace @example/computer-browser-rendering
```

Open the URL printed by Wrangler. Local development uses the Browser Run binding, so requests consume Browser Run quota and need a Cloudflare account with Browser Run access.

Local development does not require authentication. Before deploying, set a token and deploy from the example directory:

```sh
cd examples/browser-rendering
npx wrangler secret put DEMO_TOKEN
npm run deploy
```

The deployed site uses HTTP Basic authentication. Enter `demo` as the username and the secret as the password.

The Worker needs both bindings shown in `wrangler.jsonc`:

```jsonc
{
"compatibility_flags": ["nodejs_compat", "experimental"],
"worker_loaders": [{ "binding": "LOADER" }],
"browser": { "binding": "BROWSER" }
}
```

## Files

- `src/index.ts` configures Computer and exposes the API and durable artifact routes.
- `src/execution-source.ts` defines the browser task that runs inside the Dynamic Worker and writes the research bundle through Computer's built-in `node:fs/promises` module.
- `src/ui.ts` contains the dependency-free demonstration interface.
- `wrangler.jsonc` declares the Worker Loader, Browser Run, and Durable Object bindings.

The example requires authentication when deployed, applies Browser Run guardrails for the requested host, and uses connection-bound browser sessions. Add workload-specific URL policy and quota handling if you adapt it for a shared service.
23 changes: 23 additions & 0 deletions examples/browser-rendering/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@example/computer-browser-rendering",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Browser Run demo that executes Cloudflare Puppeteer inside the Computer Worker JavaScript backend.",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"build:types": "wrangler types",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
Comment thread
scuffi marked this conversation as resolved.
"dependencies": {
"@cloudflare/computer": "*"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260616.1",
"typescript": "^6.0.3",
"vitest": "^4.1.11",
"wrangler": "^4.130.0"
}
}
Loading
Loading