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
1 change: 1 addition & 0 deletions examples/next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Experimental use APIs that may change between releases.
| [`harnesses/think`](./harnesses/think) | Available | Experimental: Think's agent loop on a plain `DurableObject` with `agents/harness/think`, served to `useAgentChat` by `ThinkChat` |
| [`harnesses/container`](./harnesses/container) | Available | Experimental: Claude Code or Codex in a Cloudflare Container, driven from a Durable Object by `agents/harness/container`, resumed across containers |
| [`harnesses/self-modifying`](./harnesses/self-modifying) | Available | A Lifecycle capability runs editable harness revisions in fresh Dynamic Workers with trusted System tools and auto-discovered Custom tools |
| [`tools/browser`](./tools/browser) | Available | Experimental: a pi agent with a container workspace and a persistent browser that opens its dev server, build, and docs on internal hostnames |
| [`models`](./models) | Available | One `createAI` per framework (AI SDK and pi-ai): `@cf/` ids for Workers AI, vendor models routed through AI Gateway |
| [`channels`](./channels) | Available | Agents on different harnesses served through `Channels.forHarness` and one `ChannelGateway`, to a browser client and terminal clients |

Expand Down
32 changes: 32 additions & 0 deletions examples/next/tools/browser/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# The agent's workspace container: computerd (the Computer workspace daemon)
# on a slim Debian image with Node.js, npm, and git, so the agent can install
# packages and run a dev server.
#
# computerd mounts the workspace at /workspace over FUSE and listens on :8080,
# so the agent's dev servers use any other port.
FROM ghcr.io/cloudflare/computer-computerd-linux-x64:0.5.1 AS computerd

FROM debian:stable-slim

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
fuse3 libfuse2t64 ca-certificates curl gnupg git procps \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

COPY --from=computerd /usr/local/bin/computerd /usr/local/bin/computerd

# FUSE_MOUNT=auto uses real FUSE on Cloudflare Containers and a userspace
# shim under local dev, where /dev/fuse isn't available.
ENV PORT=8080
ENV MOUNT_POINT=/workspace
ENV FUSE_MOUNT=auto
EXPOSE 8080

ENTRYPOINT ["/usr/local/bin/computerd"]
134 changes: 134 additions & 0 deletions examples/next/tools/browser/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Browser tool

A pi agent with a container workspace and a browser that stays open between
turns. The browser opens three sites nobody else can reach, on Browser Run
internal hostnames:

| Host | Serves | Model assigns |
| ------------------ | ---------------------------------------------------- | ----------------------------------- |
| `preview.internal` | A dev server in the workspace container | `{ port }`, 1024 to 65535, not 8080 |
| `files.internal` | Static files from a workspace folder, such as `dist` | `{ folder }`, under `/workspace/` |
| `docs.internal` | `DocsSite`, a Worker entrypoint with build docs | Nothing: it always serves the same |

The system prompt only points the model at `http://docs.internal/`. The docs
tell it how to scaffold a Vite app, configure its dev server, what site to
build, and how to preview it.

The example composes:

- `PiHarness` from `agents/harness/pi`, as in [`harnesses/pi`](../../harnesses/pi);
- a `Workspace` from `@cloudflare/computer` with a `ContainerBackend`:
computerd mounts the workspace at `/workspace` in a Container, and `exec`
is a Linux shell there;
- a `Browser` from `agents/browser` on Browser Run, with
`guardrails: { allowedDomains: [] }` and the three internal hosts;
- `browserTool` from `agents/browser/pi`: the model writes CDP code and
calls `browser.assignHost()` to point a host somewhere;
- a Browser panel in the UI: Live View of the browser's tabs and each
host's current target.

The model is Workers AI `@cf/zai-org/glm-5.3-flash`, set in
`src/shared.ts`. It accepts images, so it sees the screenshots the browser
tool returns.

## Run locally

```sh
pnpm install
pnpm run start
```

You need Docker running: the workspace container builds from the
`Dockerfile`. The `AI` and `BROWSER` bindings are remote and may incur
usage. No API keys. If your Wrangler login has more than one account, set
`CLOUDFLARE_ACCOUNT_ID`.

Internal hostnames only work deployed. Locally the browser starts without
them and logs a warning, so the agent can build but not open its sites.
Deploy with:

```sh
pnpm run deploy
```

## What to try

- `Build the site the docs describe, run it on the dev server, and show it to me in your browser.`
- `Now make a production build and show me the built site on files.internal.`
- `In /workspace, run npm create vinext-app@latest vinext-app -- --yes --use-npm --platform node --disable-git. Set up its dev server for preview.internal the way the docs say, start it, and show me the home page.`

The Browser panel beside the chat shows the browser live while the agent
works (on narrow screens, open it from the header). Screenshots the agent
takes show up in the chat.

## Core pattern

```ts
// Browser Run calls back into this Worker through InternalHostRouter.
export { InternalHostRouter } from "agents/browser";
export { DocsSite } from "./docs-site";

export class BrowserAgent extends withWorkspaceContainer(BrowserAgentBase) {
readonly browser = new Browser({
provider: browserRun(this.env.BROWSER, {
guardrails: { allowedDomains: [] }, // internal hosts only
internalHosts: {
router: this.ctx.exports.InternalHostRouter,
hosts: {
"preview.internal": {
description: "A dev server in your workspace container.",
target: z.object({ port: z.number().int().min(1024) }),
fetch: (request, { port }) =>
this.getWorkspaceContainer().fetchPort(port, request)
},
"files.internal": {
description: "Static files from a workspace folder.",
target: filesTarget,
fetch: (request, { folder }) =>
serveWorkspaceFolder(this.workspace, request, folder)
},
"docs.internal": {
description: "Your instructions for building things.",
worker: this.ctx.exports.DocsSite
}
}
}
})
});

readonly lifecycle = Lifecycle.install(this)
.use(this.webSockets)
.use(this.browser) // internal hosts need the Browser on the lifecycle
.use(this.harness);

// InternalHostRouter calls this. Agent has it built in.
fetchInternalHost(message: InternalHostMessage) {
return this.browser.fetchInternalHost(message);
}
}
```

The model points a host with `browser.assignHost()` in its browser code, then
navigates to the URL it returns. Targets are checked against the schema and
persist across turns, browser restarts, and redeploys.

## Details

- **Plain Durable Object.** `InternalHostRouter` delivers requests by
calling `fetchInternalHost()` on the owning object. `Agent` has it built
in; a plain `DurableObject` with `Lifecycle` adds it and passes the message
to its `Browser`, as `BrowserAgent` does.
- **Dev server sync.** Files the model writes with `write` and `edit` reach
the container on the next sync. `#fetchPreview` pushes the workspace on
page loads, so a reload shows the latest files.
- **files.internal needs no container.** `src/files-host.ts` reads the
Workspace's own files. The container backend ignores `node_modules` and
`.vite` but syncs `dist`, so a build keeps working after the container
sleeps.
- **Dev server config.** Internal hosts are plain http on port 80 with no
WebSockets, so the docs have the model turn off HMR, allow the
`preview.internal` Host header, listen on `0.0.0.0`, and start the server
with `setsid nohup` so `exec` returns.
- **Port 8080** is computerd's, so the `preview.internal` schema rejects it.

Pi is MIT licensed; see [`licenses/mit-earendil-pi.txt`](./licenses/mit-earendil-pi.txt).
16 changes: 16 additions & 0 deletions examples/next/tools/browser/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --include-runtime=false env.d.ts` (hash: a35ff1f2be5fa6615326c154d842fa7f)
interface __BaseEnv_Env {
LOADER: WorkerLoader;
BROWSER: BrowserRun;
AI: Ai;
BrowserAgent: DurableObjectNamespace<import("./src/server").BrowserAgent>;
}
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./src/server");
durableNamespaces: "BrowserAgent";
}
interface Env extends __BaseEnv_Env {}
}
interface Env extends __BaseEnv_Env {}
21 changes: 21 additions & 0 deletions examples/next/tools/browser/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<title>Browser tool</title>
<script>
(() => {
const stored = localStorage.getItem("theme");
const mode = stored || "light";
document.documentElement.setAttribute("data-mode", mode);
document.documentElement.style.colorScheme = mode;
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client.tsx"></script>
</body>
</html>
21 changes: 21 additions & 0 deletions examples/next/tools/browser/licenses/mit-earendil-pi.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 Mario Zechner

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
44 changes: 44 additions & 0 deletions examples/next/tools/browser/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@cloudflare/agents-next-browser-tool-example",
"description": "A pi agent with a container workspace and a persistent browser that opens its dev server, build output, and docs on Browser Run internal hostnames",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"start": "vite dev",
"build": "vite build",
"deploy": "vite build && wrangler deploy",
"typecheck": "tsc --noEmit",
"types": "wrangler types env.d.ts --include-runtime false"
},
"dependencies": {
"@cloudflare/codemode": "*",
"@cloudflare/computer": "^0.5.1",
"@cloudflare/kumo": "^2.6.0",
"@earendil-works/chord": "~1.4.0",
"@earendil-works/pi-ai": "~1.4.0",
"@earendil-works/pi-durable": "~1.4.0",
"@earendil-works/pi-telemetry": "~1.4.0",
"@phosphor-icons/react": "^2.1.10",
"@platformatic/vfs": "^0.4.0",
"@streamdown/code": "^1.1.1",
"agents": "*",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"streamdown": "^2.5.0",
"typebox": "1.3.27",
"zod": "^4.6.7"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.62.3",
"@cloudflare/workers-types": "^5.20261014.1",
"@tailwindcss/vite": "^4",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"tailwindcss": "^4.3.2",
"typescript": "^6.0.3",
"vite": "^8.1.6",
"wrangler": "^4.145.0"
}
}
Binary file added examples/next/tools/browser/public/favicon.ico
Binary file not shown.
Loading
Loading