The embedder gets a say
Everything a browser is, the engine already decides
Servo renders the page, and for most of what a browser does that is the whole answer. It is not the whole answer for the parts a user thinks of as the browser rather than the page: whether a request is allowed to leave at all, which stylesheet the page is dressed in, what a private tab means, who answers when a site asks for a passkey. Those belong to the shell, and until this autumn the shell had no way to say anything about them.
Eight commits in tramuntana changed that, and they share a shape. Each one takes a decision Servo was making on its own and hands it to the embedder — not as a configuration flag read once at startup, but as a question asked at the moment the answer matters.
A filter before every fetch
A content blocker has to veto a request before it reaches the network. Servo already had RequestInterceptor, which asks the embedder over a channel and can substitute a whole response. That is the right tool for a handful of requests and the wrong one for all of them: a page with two hundred subresources would pay two hundred channel round trips to learn that one hundred and ninety of them were fine.
RequestFilter is the cheap counterpart. It is synchronous, it answers allow or block and nothing else, and it is held by the FetchContext rather than reached across a channel. It is consulted in main_fetch after HSTS and before interception, which puts it in the one place where it sees every request on its way out — including each redirect hop, because a redirect goes back through main_fetch and so gets asked again. A request the filter blocks never touches the network.
Positioning it after HSTS rather than before it matters more than it looks. The filter sees the URL the request will actually use, upgraded to HTTPS if the host is on the list, so a blocklist written against https:// does not quietly miss the same host on http://.
One thing the filter could not see on its own. By the time a request reaches it, a WebSocket handshake has an HTTP scheme and an empty destination, exactly like a request from fetch(). The two are indistinguishable from the fields a filter is given, and a shell that wants to allow a site's sockets while blocking its trackers needs to tell them apart. FilteredRequest::is_websocket carries the one field that does: the request mode.
Stylesheets that can be swapped mid-life
Cosmetic ad filtering needs a different stylesheet per page. UserContentManager could add and remove sheets one at a time, applied when a pipeline is created, which is wrong twice over: there is a window during which the outgoing page's rules and the incoming page's rules are both live, and a document that already exists never hears about the change at all.
SetUserStyleSheets replaces a manager's whole set in one action, so there is no such window. And the change now reaches live documents: the script thread passes it on rather than leaving it for the next pipeline.
Then the panic. A Layout keeps its WebView's user stylesheets but only hands them to the Stylist during the first reflow, so before that reflow the Stylist holds none of them. Window::replace_user_stylesheets removed the previous sheets and added the new ones, which on a WebView that had not yet reflowed asked the Stylist to drop a sheet it had never seen:
stylesheet_set.rs:238: called Option::unwrap() on a None value
An embedder doing the obvious thing — set the stylesheet for this site, then navigate — hit it every time. The fix is to let the removal be a no-op before the first reflow rather than an index into a list that is still empty.
Scripts chosen by the page being loaded
User scripts needed the same two things as stylesheets, and one more. UserContentManager::set_scripts replaces the whole set at once, and documents created afterwards run the new set.
The extra piece is knowing *when* to choose. An embedder that picks scripts per site has to make the choice at the one moment the destination is known and the document does not exist yet, which is request_navigation. But that callback fires for nested frames too, and swapping the whole script set because an advertising iframe navigated would be wrong. NavigationRequest::is_for_main_frame separates a navigation of the WebView's main frame from one of a frame inside it.
A private tab, defined precisely
WebViewBuilder::temporary_session puts a webview in a session of its own, named by an id the embedder picks. Cookies, HTTP cache, HSTS list, credentials and site storage all live in memory, shared only with the webviews of the same session, and they are gone once Servo::close_temporary_session is called and the last of its webviews has closed.
The detail that makes it usable is inheritance: a webview opened by a page is in the session of its opener. Without that rule, a private tab that follows a target=_blank link leaks the next page into the public jar, which is the one thing a private tab exists to prevent.
The shell as authenticator
navigator.credentials.create() and get() with a publicKey member now reach the embedder as a PasskeyRequest through WebViewDelegate::request_passkey. The embedder is the authenticator: it decides whether the page may speak for the relying party it names, asks the user, and answers.
The security property is in which origin it is handed. The embedder is given the origin Servo knows for the document, not one the page states, so a page cannot ask for a credential belonging to someone else by claiming to be them. A request that is dropped, or that no delegate takes, is refused rather than left hanging.
And one the embedder was getting wrong about itself
What a global inherits about being a secure context comes from its creator: the parent of a frame, the owner of a worker. For a top-level document the inherited value was that of the page whose link was followed to reach it, so an HTTPS page opened from an HTTP one — or from a page belonging to the embedder — was not treated as a secure context, and everything gated on that quietly refused to work.
A top-level window is now judged by its own top-level creation URL alone, as the specification has it. The shell's own start page was one of the creators that could poison the judgement, which is a good argument for the embedder not being a special case.
What this does not settle
The filter is synchronous, which is what makes it cheap and also what bounds it: an embedder cannot go and ask a remote service whether to allow a request without blocking fetch while it does. For that, the interceptor and its channel are still the only option, and still too expensive per subresource.
Temporary sessions have resource limits of their own, and nothing in rumb uses them yet — the shell has no private tab to put one behind. The same is true of the passkey delegate: the plumbing reaches the embedder, and the embedder currently refuses every request by not implementing it.
And none of this is site isolation. Every one of these decisions is still made in one process with the renderer, which is the trigger the roadmap names and which nothing here moves.