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
Route
Turn it on Nothing opened yet.
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; and a vendored font, fetched from this origin when a page on the phone uses a family the host has no face for (Roboto, or a stand-in for one of Safari's). A PerformanceObserver counts every request and shows each of those apart, under its own name; the number is in the footer, and it is a tool the agent can call. The sealed route's carriage is a frame of ByteTraverse's connect page on its own origin, which resource timing on this origin 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 ciphertext leaves by a ByteTraverse exit (your own box , or the demo box while it lasts), so the site can be read and tapped and the exit sees the hostname, not the page; never signed in. When no exit can be reached it opens through the relay on this origin, which fetches the site and sees the traffic, and the lamp says so. 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. On a machine without Safari's fonts, an iPhone frame's Helvetica Neue is stood in by TeX Gyre Heros, an open face with Helvetica's metrics, and its San Francisco by Inter, the nearest open face; Arial, Times New Roman and Courier New by the Liberation faces (Arial's bold is real; a bold Times or Courier is synthesised from the regular); a Mac uses the real ones, and the stand-ins are fetched from this origin only when a page uses the family.
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, under the relay's address on this origin: readable and tappable, the TLS done in this page and the ciphertext leaving by a ByteTraverse exit, so neither the relay nor the exit sees 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 only place anything is fetched on your behalf by a server. The sealed route below does not run on Cloudflare at all. 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 nothing
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 stream to the site through a ByteTraverse exit (a machine reached over a WebRTC DataChannel through ByteTraverse's connect page, with nothing between the browser and it), 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 exit 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. Cloudflare sees none of it: the ciphertext never touches its compute. A login made through the sealed route is held in this browser only. WebSockets from the site are not carried; only https sites, on port 443. Nothing to install while the demo exit lasts.
The exits
A box of your own is preferred when its invite is here and it answers; else the demo box we run, while the demo lasts. The lamp under the phone names the one that carried what is on the screen. When no exit can be reached, or the page cannot take the sealed route, the site goes through the relay above and the phone says so, with the reason; a box that fails while a request is in flight is given that request once more if it is still connected, else the request goes through the relay and the lamp says which box failed.
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
The relay fetches from Cloudflare's network, 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. Through the relay, the URL and the traffic pass through it in plaintext for the duration of a request, and it keeps nothing. Sealed, the relay carries nothing and learns nothing; the ByteTraverse exit learns which host you opened and when, and the shape of the traffic, not what. Nothing through a relay or an exit 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.
Your own exit
A live site opens sealed: this page does the TLS, and a raw TCP socket on a ByteTraverse box carries the ciphertext; while the demo lasts, that box is the one we run. It can be a machine you own instead. Install ByteTraverse on it, run the home box with --browse, mint an invite, and paste it here: the page reaches the box over a WebRTC DataChannel through ByteTraverse's connect page, and the phone's traffic exits from that machine with nothing in between. The invite stays in this browser; it travels only in the fragment of the connect page's address, which never leaves the browser.
btv-homebox --browse # on the machine you own
btv-homebox --mint-ticket # prints the invite URL to paste below
Use it
Forget
Honest limits
The exit, yours or the demo, sees which hostnames you open and how much traffic goes by, when; it does not see the URL, the headers, the cookies or the page, because the TLS session is this page's own. The demo exit is a machine we run. When neither box is reachable, the site opens through the relay on this origin, which sees the traffic, and the phone's lamp says so; a box that fails while a request is in flight gets that request once more if it is still connected, else that request goes through the relay too.
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.