The problem

To get AI help with a spreadsheet today, you upload it. Payroll, patient lists, customer records, unreleased financials — all of it goes to a third party. Anyone working under a data-egress policy, a client NDA, or basic caution simply can't do that, so they get no help at all.

The tension is that an agent is only useful if it has real power over the data, and the data is only safe if the agent never sees it. Cleanroom is built on the resolution: the data cannot go to the agent, so the tools go to the data.

What it does

Cleanroom is a capability firewall for AI agents. The spreadsheet is the worked example.

Every firewall in history has worked the same way: you do not ask the traffic politely to stay out, you close the port. Agent permission systems have not caught up — they let the tool exist and make it refuse. A refusal is a conversation, and a conversation can be argued with.

Cleanroom closes the port. A three-position trust dial decides which WebMCP tools are registered with the browser at all. Move it and tools are unregistered live, toolchange fires, and an agent holding a handle it discovered thirty seconds ago gets this from the interface itself:

NotFoundError: No tool named "sample_rows" is registered.

Not a policy error from our code. The browser saying the capability does not exist. A firewall you cannot argue with, because there is nothing to argue with.

Drop a CSV into the page and it is parsed and held only in your browser tab, on a page served with connect-src 'none' so it cannot make a network request of any kind. Up to eleven WebMCP tools let an agent profile, query, chart and audit that data — while the file itself never leaves your device and never reaches the agent.

A running egress ledger keeps the balance in one line — 955 B kept local · 583 B released — and itemises every call beneath it.

Why WebMCP is the right fit

In one sentence: a server-side MCP integration is physically incapable of doing this, because the data is never on a server. WebMCP is the only mechanism by which an agent can compute over data that stays on the user's device.

Four things make the WebMCP usage structural rather than decorative:

1. The tool surface is the privacy boundary, and the person sets it. query_dataset cannot return individual rows — its schema requires an aggregate and the engine has no raw projection path. The one tool that can reveal a record, sample_rows, is not registered at all until the person turns the dial to Raw, and even then it suspends inside execute until a human answers a dialog. At Sealed, every tool that computes from the data is withdrawn and the agent is left with three. No amount of persuasive text inside a dataset can talk past it.

2. Capabilities are registered and withdrawn live, driving toolchange. An empty page offers two tools. Loading a dataset makes six more appear. Turning the dial to Sealed takes five away; Raw adds one. Adding a report block reveals the block-editing tools. The agent's menu always describes what the app can actually do right now — and what the person is currently willing to let it do.

3. The boundary cannot be moved by an argument. The trust level and the k-anonymity threshold are read from the workspace on every call; an agent that passes minGroupSize or trustLevel is rejected for an unexpected property. The threshold ships at 5 rather than off, and applies to any answer computed from fewer than five records however the set got small — by grouping, by filtering, or by asking how many rows matched. A call that arrives after the dial moved but before the tool was withdrawn is refused by the runner.

4. Errors are designed for a model to recover from. A misspelled column returns the real column names; a numeric aggregate on a text column returns the list of numeric columns; a withdrawn tool tells the agent to call list_datasets, which reports the level in force.

What people and agents can do together that was hard before

The person supplies data they were never willing to upload, and decides with one control how much power the agent has over it. The agent explores it, runs statistics it could never do reliably by reading a rendered table, and builds a report.

The canvas is genuinely two-sided. Charts and notes the agent creates land on the same surface the person writes on, each block badged with who made it. The person can write their own notes, edit the agent's wording in place, and remove either — and editing does not launder the authorship, so the badge stays honest. An agent's update_report_block and a person's Edit button are the same operation on the same store.

When the agent needs something genuinely sensitive, the application stops it and asks — quoting the agent's own stated reason back to the person — and the person can change their mind mid-session by turning the dial, which the agent sees as its own tools changing.

Neither could do this alone: the agent cannot see the file, and the person does not want to write the queries.

How it is built

TypeScript, React 19, Vite 8. No backend, no database, no API key, no telemetry. The entire application is static files.

The containment guarantee is architectural, not a promise. The app is served with connect-src 'none', removing the page's ability to make any network request — fetch, XHR, WebSocket, EventSource, sendBeacon. It is delivered both as an HTTP header and embedded in the built markup, so it travels with the artifact. An end-to-end test fires a real cross-origin fetch and asserts the browser blocks it.

That policy has teeth: it cost us a dependency. Ajv compiles JSON Schemas with new Function, which script-src 'self' forbids, and every tool call failed in the production build. Rather than weaken the policy we replaced Ajv with a hand-written validator covering exactly the schema subset the tools use. Cleanroom now has no runtime dependencies beyond React.

529 automated tests — 460 unit and integration, 69 end-to-end. The E2E suite drives document.modelContext.getTools() and executeTool() from page context without importing our source, so it verifies what an agent actually receives — including that a tool handle captured at Raw is dead once the dial comes down, and that the browser reports a securitypolicyviolation rather than merely throwing when the page attempts to reach the network.

Who it helps

Anyone with a spreadsheet they cannot upload: finance and HR analysts, clinicians, teachers, small-business owners, journalists working with confidential material, and everyone at a company with a data-egress policy. Today their options are to break policy or to go without. This is a third option.

More broadly, it is a worked example of a pattern the agentic web needs: a page that gives an agent real capability while keeping the authority — over what is released, how much, and what cannot be undone — with the person, and expresses that authority as the set of tools the agent is offered.

What is new here

The trust dial treats WebMCP registration as a policy surface. Instead of a tool that consults a setting and refuses, the setting decides whether the tool exists. An agent cannot ask for what it has not been offered, and the person can watch the offer change.

The egress ledger is a live account of every byte an agent has received from your data, summed against the bytes that never left — kept local · released — and itemised beneath, including the requests it made and was refused. It turns a privacy claim into an audit log.

Alongside them: k-anonymity as a user-facing control the agent provably cannot override; a profiler that refuses to report min/max on a text column because the minimum of a name column is a real person's name; and a sample_rows tool that must state its reason in writing, shown to the person verbatim.

Honest limitations

The file never leaves your browser, but the aggregates the agent computes do. That is exactly why the ledger exists. Repeated narrow queries leak more than one broad one, and a determined attacker can reconstruct values by combining aggregate answers — a Dinur–Nissim style reconstruction. The k-anonymity floor, the caps and the visible ledger raise the cost and make probing observable; they do not make it impossible. Cleanroom does not claim perfect privacy. This is stated in the README and documented in docs/SECURITY.md rather than glossed over.

A note on the demo video

Every call in the video goes through the real document.modelContext on the live site — real getTools(), real executeTool(), real toolchange — and the NotFoundError is raised by the WebMCP implementation, not by Cleanroom's code. But the browser used for the recording has no native WebMCP agent, so the calls are made by hand through the in-page Tool Inspector rather than by a language model. Nothing is simulated, and the protocol behaviour is identical either way.

Built With

  • content-security-policy
  • github-actions
  • json-schema
  • netlify
  • playwright
  • react
  • svg
  • typescript
  • vite
  • vitest
  • webmcp
Share this project:

Updates

Submission history