Full access is on
Turn off
Warrant live
Revoke now
No agent is attached to this browser
Open this page in ChatGPT's browser, or in Chrome with WebMCP. Everything here works by hand in the meantime.
Nothing open yet. Type a path or a URL below, or ask your agent.
· reset zoom
Preview the fix
Turn it on
Open a site from Netlify
Sign out
collapse
tab bar
gesture nav 3-button nav
On the screen
What the agent reads with device_snapshot lands here, and is drawn on the glass.
Console
Activity 0
Every tool call, grant and revoke lands here.
Workbench
starting a machine…
Log
Files
Changes
loading emulator…
·
origin requests: —
Sever the origin
asking each time
tabbox · /dev/ttyS0 · one shell, shared with the agent
Pick a file. A ◐ marks a staged edit that is not on disk yet.
No staged edits. When the agent stages one it appears here as a diff you can read, before anything touches disk.
The machine's own VGA text console, drawn by the emulator: what a monitor plugged into the box would show. The log you share with the agent is the serial line. Why two consoles
How it works Close
A Linux 4.16 kernel boots on an emulated x86 (v86, WebAssembly) inside this page: a shell, a filesystem, processes, a VGA console. It holds the workspace your app is served from, runs the commands the agent is allowed to run, and keeps the log. No server. It has no network, and it is erased when the tab closes.
The catalogue is generated from the machine
When a machine attaches it reports a capability manifest, and the WebMCP tool list is built from that. This box has no tmux, so there is no pane tool; no Codex, so no subagents. The page says which tools it did not register and why.
Lifetime is an AbortSignal
Every registration scope (always, session, view, warrant) is one AbortController passed to document.modelContext.registerTool(). Open the Changes tab of the workbench and changes_review exists; close it and the tool is gone. Watch the count in the header.
The warrant is the registration
Writing to disk and running commands unattended are not gated handlers. Those tools are absent until you click Grant, and grant() requires transient user activation, which a tool call never has. The page ships the attack as a tool, warrant_force_grant, so you can watch it fail.
Two consoles
The workbench log is the serial console (ttyS0); you and the agent type into the same shell. Unattended commands run on a second, unwatched shell (ttyS1). The Screen tab shows the VGA text console, where the kernel's own init shell lives.
Nothing leaves the browser
After boot the page makes no request to its own origin. The exceptions are a website you open on the phone: through the relay it is fetched on this origin, and sealed it loads the TLS module and its WebAssembly from this origin once, at that first open. A PerformanceObserver counts every request and shows those apart; the number is in the footer, and it is a tool the agent can call. The sealed route's tunnel is a WebSocket, which resource timing does not record.
state booting
The phone is a real browsing context held at that phone's viewport, pixel ratio, safe areas and platform hints, drawn in its frame the way an installed web app is shown: the status bar overlays the page, as with viewport-fit=cover.
Not iOS or Android. No Apple or Google code runs here. What a web app can observe is what is simulated, because that is what a web app is tested against.
Apps load from the workbench's workspace with their CSS, scripts and SVGs inlined and env(safe-area-inset-*) replaced by the device's real insets, so a fix the agent commits on the Linux box is the fix on the phone. A live website opens sealed by default: this page does the TLS with the site itself, in WebAssembly, and the relay on this origin carries only ciphertext, so the site can be read and tapped and the relay sees the hostname, not the page; never signed in. Through the relay itself the relay fetches the site and sees the traffic. With the Bonnie Live extension it opens as the real site in your own browser, signed in as you; framed directly it is private and uninspectable.
Which phones, which browsers
Every iPhone that runs iOS 26, from the iPhone 11 and the SE (2nd generation) to the 17e, and the current Android references (Pixel 10, Galaxy S26, Z Flip 7, Z Fold 7): 39 models, generated from the vendored mobile-browser-chrome-devkit (2026-09-02) and cross-checked against its JSON by the suite. Any other phone: device_custom takes a width, height, pixel ratio, cutout archetype and navigation.
The phone is drawn from the kit's own vectors, composed in its layer order: the model's frame (svg/iphone-frames), the status bar, the browser chrome for the state (svg/browser-chrome, split at the separator the kit draws), the home indicator or Android navigation (svg/system-bars), the cutout archetype. Every height is the kit's; each state names the files it was drawn from in device_describe. The browser is state, as the kit describes it: an installed app; Safari 26 in its compact, bottom or top layout; Safari 18; Chrome with its address bar at the bottom or the top; controls expanded or collapsed; gesture or three-button navigation on Android. Each state changes the viewport and the safe areas the page gets. Layer sizes are measured from screenshots of real phones: an iPhone 16 Pro Max on iOS 26, an iPhone 17 for the compact bar, a Pixel 6 for Chrome on Android. The kit's own wireframe sizes remain available to the agent, for comparison, as device_set metrics: "kit".
device_geometry reads what the page measures for itself — viewport, pixel ratio, screen, display mode, pointer — and says whether it matches the fixture; device_matrix sweeps the page across every viewport size. Honest limits: resolution and colour-scheme media queries inside the page follow the host browser, not the phone; a real WebKit or Blink is not running.
Bonnie Live is a small companion extension. With it, a URL you open on the phone is the real, published site in your own browser, signed in as you : no proxy, no second browser, nothing leaving this machine. Without it, a site opens sealed through the relay on this origin: readable and tappable, the TLS done in this page so the relay sees the hostname and not the page, but never signed in. The relay can also be yours, on your own Cloudflare .
Turn it on
Open chrome://extensions (the same page works in ChatGPT Atlas).
Turn on Developer mode .
Download Bonnie Live and unzip it (or use the extension folder from the repository).
Click Load unpacked and choose that folder.
Come back here. The phone notices it and says Bonnie Live is on ; whatever was waiting to open, opens.
What it does, and only here
It acts inside this tab alone: it lets the site be framed, gives its requests the phone's user agent, keeps the site's cookies in a jar of its own (a cross-site frame can neither store nor send them), and injects the phone's shim and reading tools into every page the frame commits. Its rules are session rules and die with the tab.
Honest limits
The engine is your browser's, not a phone's. A site's own scripted requests in the instant after a login are not gated, only navigations are. Chromium only for now.
Live websites come to the phone through a relay: a small function on this page's own origin that fetches the site and hands it back readable. It has to be on the page's origin, because a browser lets a page read only its own documents; a relay anywhere else would show the site and let nothing be read or tapped. The one here runs on Cloudflare's edge as a Pages Function, in functions/frame.js, and it is the only server-side code on this page. The site's own scripts keep working: their calls to the site travel back through the relay with a cookie jar the frame holds for that site alone, and the frame's address mirrors the site's path so a client router hydrates the route it was rendered for. What does not carry is you: your own logins never pass, calls to other hosts stay bound by those hosts' rules, and WebSockets are not relayed.
Sealed: the relay carries ciphertext
By default a site opens sealed . The document still comes through the relay's address, but the relay does not fetch it: this page's service worker hands the request to the page, the page opens a raw TCP tunnel to the site through functions/wisp.js (Wisp, a WebSocket carrying TCP streams), does the TLS handshake with the site itself in WebAssembly, verifies the certificate against Mozilla's root store inside the page, speaks HTTP/1.1 over the result, and rewrites the document for the phone in the page with the same code the relay uses. What the relay sees: the hostname and the port it must connect to, and how many bytes went by, when. What it does not see: the URL, the headers, the cookies, the page. A login made through the sealed route is held in this browser only. Sites that refuse Cloudflare's network still refuse: the TCP connection still comes from Cloudflare, and so does the address the site sees. WebSockets from the site are not carried; only https sites, on port 443. Nothing to install. If the page cannot take the sealed route, the site goes through the relay itself and the phone says so, with the reason.
Run it on your own Cloudflare
Deploy the whole page to your own account and the relay is yours: your origin, your quota, your account's edge. Cloudflare's own command line signs you in, so no token is ever pasted here or held by anyone else.
git clone https://github.com/meshbergio/bonnie.codes
cd bonnie.codes
npx wrangler login # Cloudflare's own sign-in, in your browser
npx wrangler pages deploy . --project-name=bonnie
The address it prints is your Bonnie. Everything here works the same there, and the counter in the workbench bar still reads zero after boot.
Honest limits
A relay fetches from Cloudflare's network, sealed or not, so a site that refuses bots or Cloudflare's addresses refuses it whoever owns the account; the phone names such a wall when it meets one. Sealed, the relay still learns which host you opened and when, and the shape of the traffic; it does not learn what. One more limit is Cloudflare's: its network will not open a raw socket to a site that is itself on Cloudflare, so such a site cannot be sealed from a relay hosted there; it opens through the relay in the clear, and the lamp says so. Through the relay itself, the URL and the traffic pass through it in plaintext for the duration of a request, and it keeps nothing. Nothing through a relay is signed in. For a site as you see it, signed in, use Bonnie Live .
The workbench above is disposable and lives in this tab. Bonnie's host runs on your real computer and pairs it to this page over a direct peer connection, with no relay and no account. Your agent then gets the same catalogue, generated from what that machine has: tmux terminals that outlive the tab, the real filesystem, the screen, Codex subagents.
git clone https://github.com/meshbergio/bonnie
cd bonnie && ./install.sh
bonnie pair # prints a URL and a QR code — open it in this page
Same gate Warranted tools do not exist until you grant them here, with a real click. A tool call cannot produce one.
Same record Every call, warrant and command lands in an append-only ledger the agent can read back, including what happened while it was not looking.
Nothing in the middle The page is static. Once paired it makes no requests to its own origin; the counter in the footer is the measurement.
Ask your agent for bonnie_install_plan and it will walk you through it.
An archive is the whole machine in one file: its memory and disk as the emulator saved them, plus the staged edits, the record, and what the phone had open. Resume it later and you are where you left off, including anything typed into the shell.
Authority does not survive a file. A live warrant or standing access is never archived and never restored; the agent starts a resumed session with exactly the tools it had at boot.
Drop a .bonnie archive here, or click to choose one
Download
Archive this session now
The agent can prepare an archive with session_archive, but the download needs your click. It can read what a loaded file contains with session_describe.