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

Add Worker JavaScript plugins and an opt-in Puppeteer plugin backed by a Browser Run binding, plus a `browser` shell command group that runs a Workspace task module against that plugin.

Separate caller-source limits from complete Loader-graph limits, and include Worker code and compatibility settings in the shell Loader cache identity.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ jobs:
- name: assets
workspace: "@example/computer-assets"
path: examples/assets
- name: browser-rendering
workspace: "@example/computer-browser-rendering"
path: examples/browser-rendering
- name: container
workspace: "@example/computer-container"
path: examples/container
Expand Down
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Source imports require generation first

The plugin imports ignored generated.ts. npm hooks create it, but source-based tooling that skips scripts cannot load this entrypoint.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.


# 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) — stores one
browser task in the workspace and runs it two ways, as a JavaScript module
through the Puppeteer plugin and as a `browser` shell command, writing the
same Markdown, JSON, and screenshot bundle either way.
- [`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
45 changes: 41 additions & 4 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` and `maxLoaderModules` separately bound 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 @@ -132,7 +132,44 @@ 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.
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. On a backend with plugins, their dynamic imports must use string literals so the backend can keep the private plugin binding bridge out of ordinary configured modules.

## 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, "*." + input.hostname],
allowedDomainSets: ["common-cdns"],
},
});
}
```

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

## Trusted Workspace modules

Expand Down Expand Up @@ -184,7 +221,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 limits (`maxSourceBytes`, `maxLoaderSourceBytes`, `maxLoaderModules`, `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
174 changes: 174 additions & 0 deletions docs/20_browser_automation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Browser automation

Computer reaches Cloudflare Browser Run from both execution backends. `@cloudflare/computer/plugins/puppeteer` gives `WorkerJavaScriptBackend` a bundled Puppeteer client, and `@cloudflare/computer/shell/browser` gives `WorkerShellBackend` a `browser` command that runs a task module from the Workspace.

Both paths end in the same place. Puppeteer, its `Browser`, and its `Page` objects stay inside an isolated JavaScript Dynamic Worker, and Chromium runs in Browser Run. The shell command does not drive a browser itself; it dispatches the task into the JavaScript backend and reports the structured result.

Reach for the plugin when your own code decides what to browse, since it hands you the browser directly and returns a structured value. Reach for the command when something working in the shell decides, such as an agent composing a pipeline, because a task stored in the Workspace can be listed, edited, and rerun by name. Installing both costs one extra backend registration, and the example does exactly that.

## 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.hostname],
allowedDomainSets: ["common-cdns"],
},
});
`,
{
input: {
url: "https://developers.cloudflare.com/agents/",
hostname: "developers.cloudflare.com",
},
},
);

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

Confining a session to the single requested host is usually too tight. Pages pull fonts, images, and scripts from subdomains and shared CDNs, so a page rendered under that policy comes out half-loaded. The shell `browser` command applies the wider policy above by default when it is given a `--url`.

`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.

## Run a browser task from the shell

The Worker shell reaches the same capability through a `browser` command. Import the command group and pass it alongside any other shell commands:

```ts
import browser from "@cloudflare/computer/shell/browser";
import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell";

new WorkerShellBackend({
loader: env.LOADER,
workspace: { binding: "BrowserWorkspace", id: ctx.id.toString() },
ctx,
commands: [browser],
});
```

The command needs a JavaScript backend carrying the Puppeteer plugin in the same Workspace, because that is where the task actually runs. It dispatches to the backend named `worker-javascript`, and reads `BROWSER_BACKEND` from the shell environment to pick another. That target has to accept structured input, which in practice means a JavaScript backend; anything else fails with a message saying the backend is not callable. Since the variable is shell-settable, a script can aim the command at any such backend in the same Workspace, which is the same authority the shell already has through the Workspace runtime and worth knowing when deciding which backends share one.

A task module exports a default function that receives the browser alongside the caller's input:

```js
// /workspace/tasks/title.js
export default async ({ url, browser }) => {
const page = await browser.newPage();
await page.goto(url);
return { title: await page.title() };
};
```

```sh
browser puppeteer --url https://developers.cloudflare.com/agents/ tasks/title.js
browser puppeteer --url https://developers.cloudflare.com/agents/ --stdin < tasks/title.js
```

The command prints the returned value as JSON and exits with the task's exit code. `--timeout <ms>` sets the execution budget, and `--input <json>` merges a JSON object into the task input. The task runs in the JavaScript backend, so it can write to the Workspace through `node:fs/promises` exactly as an inline module does.

The browser is opened and closed around the task by the generated entry, so a task never manages the session itself. Scripts stored in the Workspace survive the execution that wrote them, which is what makes a task worth naming: the shell can rerun it, and the JavaScript backend can import it.

## 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 runnable example that stores one task and runs it both ways, writing the same Markdown, JSON, and screenshot output to a durable Workspace from either path.
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) | Reach Cloudflare Browser Run from isolated JavaScript through the Puppeteer plugin or from the shell through the `browser` command, and persist browser artifacts. |

## 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
Loading
Loading