The rule nothing in the browser can change
Keeps 2 promises
A document's cookies and address belong to the origin it was navigated to,
not to whoever fetched its bytes. That is the same-origin policy, and no
proxy changes it: a page has a site's session only when it is at that
site. The prototype tried four ways round it (framing through a tunnel at a
second hostname, a worker in between, rewriting addresses, a service
worker keeping cookies) and each failed there; a cookie jar kept on a
server fixes half of it and leaves the page's own script writing the other
half to the wrong place (docs/REMOTE-BROWSER.md in the prototype has the
whole argument). So the page renders on a node, in a browser whose own
profile is the jar, and RFC 6265, the public suffix list, SameSite and
HttpOnly are right with nothing of ours involved.
That also removes the tunnel's second origin. The prototype served framed sites
at <label>.notez.beer, and D-6 has to keep every key-holding origin off
any site like that; with no framed sites there is nothing to keep away
from.
A copy of the document, and a picture when there isn't one
Keeps 6 promises
- Scrolling, selecting and find are instant
- Input lands on the element it was aimed at
- The copy draws each element in the face the node used
- A page that is one big picture is shown as a picture, without asking
- A pane's comments, picking and screenshots work on a site
- There is one way to open a site, and the pane says nothing about engines
The node records the page's DOM (a snapshot, then its changes) from a
script in an isolated world the page can't see, and the pane replays it
into a frame on the shell's origin that is sandboxed with no
allow-scripts. The copy is real markup, so scrolling, selecting, find,
zoom, comments and screenshots are the browser's own and instant. Images,
fonts and styles cross as ids the node minted while building the snapshot
it is serving, never as addresses: an id from another document names
nothing. The frame's policy is default-src blob: data:, so the copy makes
no request of its own, and a stylesheet in it can't send what is on screen
anywhere. It is the document frame your own pages are drawn in
(build/document-frame): one frame, two sources of DOM.
Fonts are matched by face, not by name. The prototype found that a viewer
holding a family of the same name draws with its own copy, so lines break
differently. The node resolves the face each element was drawn with, sends
those faces, and the copy declares them under family names of its own
minting and points each element at its face, so nothing on the viewer
can match. Not built, and not yet tested (web/same-faces).
Live <canvas> and <video> elements are filled with a track captured
from that element on the node and drawn into the copy's own element,
never laid over it. Without a track a canvas loses its size, and the page
under it moves (measured in the prototype: 240×120 became 0×20), so this is
part of layout, not decoration.
Input names the element it landed on, by the id the recorder gave it, with an offset inside it; coordinates go only where a point is the input (drags, the inside of a canvas). The node makes the clicks, double clicks and scrolls from the presses it is sent, as a real mouse would.
When the page is mostly one canvas, or the copy has diverged past what the node measures as tolerable, the node sends the tab as video instead and the pane says so in its bar. The prototype had a switch for this and no way to fall back by itself, because a framed page can't report that it failed; a browser on the node can.
Pictures come first. Video of the tab is right on every site, so the first release sends pictures only, and the box, profiles, the gate, downloads and dialogs are proven against something that works. The copy replaces it once it passes a fixed list of ordinary sites, compared pixel by pixel against the node's own rendering.
Frames ride the session
Keeps 1 promise
Video is encoded on the node and sent as datagrams of the session, sealed like every other message, and the pane decodes them with WebCodecs. It crosses the relay or a direct pipe the same way, and DTLS is never what keeps it private (D-9). A browser's own media stack is not used for it, so there is no second path to trust and nothing for ICE to do.
Its own browser UI
Keeps 2 promises
Alerts, confirms and prompts are shown by the pane and answered there;
permissions (camera, location, notifications) are refused unless the
profile allows them; a file chooser asks the pane for a file and sends it;
a download is saved into the profile's downloads folder, reachable through
the files service. A new window or target=_blank opens as a sibling
pane. Back, forward and reload are on the bar. A tab outlives its pane and
its pipe: a pane that reattaches asks for the tab back, and the node sends
a fresh snapshot.
Reach and audit
Keeps 4 promises
Each profile is a box made by the sandbox service, and the box's only way
out is the node's gate, TCP and UDP alike. A profile names its reach:
internet (no addresses of the node or its networks), local (the node
and its networks, never the internet) or both, and the gate checks the
address after DNS, at every dial, so a name that resolves to a private
address is refused too. The prototype could only bind Chrome's HTTP to its
policy; a page's own WebRTC dialled around it, and closing that would have
broken the video tracks. With the box's network the gate's, UDP is checked
like TCP, and STUN and TURN to the internet are allowed while a LAN
address is not.
An agent's grant names one profile, usually a throwaway made for it, and the audit says what it opened.
Where it breaks:
- Passkeys. A site's WebAuthn call happens on the node, not where the phone or the security key is. The only way that works is an authenticator held by the node for that profile, which moves those passkeys onto the node. Until then such sites sign in by password and a second factor, or not at all.
- Camera, microphone and screen share. A video call in the pane would need your camera fed to a fake device on the node. Calls stay in a browser of your own.
- A node has to be on. A person with only a phone and a browser has no node to browse from, unless they add one in the cloud.
- Robot checks. Some sites refuse anything that looks driven: a
headless browser,
navigator.webdriver, an emulated screen, no GPU, globals the recorder left behind. The prototype met each of them; each is fixed by being an ordinary browser on an ordinary display, and the list doesn't end. - Everything in one place. A node that is taken over has every session in its profiles. A box keeps them from each other and from the node's key, not from the node.
- Cost. About 2 s from cold and 350 MB a profile while it runs, a browser to ship, and a GPU wanted for WebGL.
Not this
Not your own pages and apps: they are docs, open offline, edit with
nothing in between and are reviewed code (D-13), so they stay in the
shell. Not a browser extension in your own browser: it works only where it
is installed, never on a phone, and it would be a second authority
crossing the origin boundary. And not a reader, a proxy or a tunnel beside
this: a readable version of a page is a view of the same tab, and a dev
server on the node is a local profile.