Skip to content

Show pages and Tools in Pocket over the encrypted session - #1515

Merged
nedtwigg merged 51 commits into
mainfrom
pocket-browser
Nov 2, 2026
Merged

nedtwigg merged 51 commits into
mainfrom
pocket-browser

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Lets Pocket show Tools and browser Surfaces in real iframes on the phone, and close them, with every page request tunneled over the existing encrypted Noise session.

What it does

  • Frame tunnel: a framed page's HTTP and WebSocket traffic rides protocol-v1 (frame.open, http.*, ws.*, flow.ack, flow.pause) to the Burrow, through the same iframe proxy a local pane uses. Nothing passes through Hosted but three static files.
  • Direct path only: frame.open is refused on a relayed session, and the phone says why, naming a VPN such as Tailscale. Only pages on the Burrow's own machine are listed or opened.
  • Frame origins: each Surface's page gets its own origin under the new dormouseusercontent.com, salted per tab, with a stateless service worker and a sandboxed bootstrap page. The dormouse-frames Worker serves only those files, framed only by Hosted's relay origin, which serves Pocket and the one-time page. Self-hosted Pocket stays terminal-only.
  • Closing: surface.close follows the desktop's contract and wording, and leaves a reopen record. The phone's own copy of a Tool is asked about first, and its page takes no input while the close runs.
  • One-time page: it frames pages as Pocket does. Its salt lives in memory for the one session, its CSP names the frame domain, and its own sandbox grants what a frame needs. A framed page may leave a worker and its own data behind, at an origin nothing can reach again.
  • Mobile UI: a Tool opens on its page, with Browser and Terminal mode buttons at the header's far left and no bottom bar. The wall keeps one header row, and the way back to the Burrows moves to the Sessions screen. This also moves the one-time page's End button there.

Specs: docs/specs/remote-api.md -> "Frames" and "Closing a surface", docs/specs/pocket-app.md -> "Frame origin", docs/specs/mobile-terminal-ui.md -> "Browser", plus S296, S297, and S298 in the security specs.

Testing

  • pnpm test and Hosted's suite pass, and the Pocket/Browser stories pass in Chromium and WebKit.
  • The socket shim's reload and close order were checked in real Chromium with the shipped worker and bootstrap page.
  • A real iPhone on a tailnet Pocket, with the real frame domain, showed a Storybook iframe and a Markdown Tool, and dragging on the Tool's page. That was before the later review fixes.
  • Reviewed over repeated Codex rounds, including blank-session full-diff reviews, and a fresh Claude Opus /code-review. The last blank-session review found no P0 or P1. Its four P2s are fixed, and a Codex review of those fixes found one more, also fixed.

Not verified:

  • The one-time page framing a real page end to end: its sandbox was checked in Chromium on a test page, not the real page, and not on a phone.
  • On a real phone since those fixes: the page freezing during a close, ExternalLinkModal at phone width, recovery after the phone wakes, the shim's later changes, and the keyboard in a frame.
  • Hosted's STUN path and the real relay.dormouse.sh origin, together with frames.
  • A close in a real desktop Wall.
  • The dormouse-frames deploy from CI. The zone's wildcard record exists and dormouse-hosted-production-deploy now covers the zone, but this Worker deploys first, so a token mismatch would stop the Hosted release.

Known limits

  • A Surface's owner staying silent past OWNER_SILENCE_LIMIT_MS closes its frames, which loses a phone's unsaved edits; Try again reopens the page.
  • While one window is hung, a Surface closed in a healthy window can stay listed until the hung one answers.
  • A socket past MAX_WEBSOCKETS_PER_SESSION fails rather than queues, so hidden kept pages can use up the slots.
  • Raw-deflate bodies fail.
  • A browser's Duplicate Tab copies a tab's salt, the one way two tabs can still share a frame origin and worker.

🤖 Generated with Claude Code

nedtwigg and others added 30 commits November 1, 2026 13:40
A paired phone or one-time Client can now reach the pages the laptop's
iframe browser panes and Tools show. The Client frames the page at an
origin of its own and carries every request over the authorized session;
the Burrow answers each through a lease on the same iframe proxy a local
pane frames, so the proxy's pinning, rewriting, shim, and error pages
apply unchanged.

- Wire: hello `capabilities.frames`, `browser` on directory entries and
  `type: 'browser'` entries (listed only to a frames-capable Client), and
  the frame.open / http.* / ws.* / flow.ack methods in frame-wire.ts.
- Burrow: FrameTunnel opens a frame only at the page its surface embeds,
  holds the grant under a session-owned lease, closes it when the page
  goes, bounds frames/exchanges/sockets/headers, and keeps Burrow-to-Client
  body bytes inside a flow window the Client acknowledges.
- Node: createBurrowFrameHost replays requests and WebSockets against the
  grant as its own loopback caller, translating the frame origin, dropping
  cookies and Sec- fields, and decoding compressed bodies.
- Webview: browserOp answers a surface's embedded page from the browser
  chrome registry; page changes invalidate the directory.
- Specs: remote-api.md "Frames", bounds, S292/S293, and the pocket-frames
  scope for the Client half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- createBurrowFrameHost takes the host's own iframe proxy (main.js's
  iframe-proxy.cjs, VS Code's iframe-proxy-host) instead of importing a
  second copy into burrow.cjs, so remote and window leases share one
  bound. Remote leases carry REMOTE_LEASE_OWNER_PREFIX, which a window
  sweep (retainIframeProxyOwners) never ends.
- rewriteOrigin moves to iframe-proxy-rewrite.ts and serves the proxy and
  the tunnel's Referer and Location translation alike.
- FrameTunnel: revalidates every frame from one directory collect, encodes
  a body chunk only when the window sends it, derives frame membership and
  socket state instead of mirroring them, and reuses concatBytes,
  utf8Encode, and base64UrlLength.
- FrameClosedEvent drops the never-sent 'closed' reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FrameClient speaks the frame methods over a session as ReadableStream
responses and sockets. No UI: Pocket's hello still declares no frames, so
its directory is unchanged.

- flow.pause holds one exchange. A Client now acknowledges body bytes on
  receipt rather than when its page reads them, so a page that stops
  reading (a paused video) holds its own exchange, never the session's
  window.
- A subscription outlives a cancel until the answer, so bytes the Burrow
  already sent are still acknowledged and the window never shrinks.
- FrameClient queues behind MAX_HTTP_EXCHANGES_PER_SESSION: an unbundled
  page's hundreds of modules meet no browser connection limit in front of
  a service worker.
- toBase64Url encodes into bytes then decodes once: 172 MiB/s, up from
  30 MiB/s, for a body chunk's 192 KiB; same output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The non-UI remainder of the Client half, short of serving it. A framed
page's requests reach the Client window through a stateless service worker
and a hidden bootstrap page, and FrameClient answers them; a tunneled
grant's pages get a WebSocket shim, since no worker sees a socket. Nothing
in Pocket's bundle changes: it neither declares frames nor mounts one.

Driven in Chromium through the real chain, same-site and cross-site: POST
with Origin and Referer translated, a 6 MiB body, 120 parallel requests, a
redirect, a 204, a subprotocol socket with a 400 KB message, the built-in
Markdown and Monaco editors, and a Monaco save reaching disk. A top-level
visit is refused and a worker with no Client window answers 503.

pocket-app.md's "Frame origin" holds the contract; pocket-frames keeps
serving it per deployment and the UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- frameOriginFor names one origin per Surface and page, from a device salt
  the host never sees: a reload keeps the page's storage, a phone does not
  gather a new origin per open, and the hostname names no page.
- frameOriginResponse answers a frame origin's three files, framed only by
  the Client, and 404 for anything else; it holds no credential.
- pocket-frames records the decisions: Hosted on a registrable domain of
  its own so a framed page cannot set a cookie on dormouse.sh, a real iPhone
  check before the UI, and a self-host Relay staying terminal-only for now.

Not wired for deploy: the Worker's Wrangler config, WORKERS entry, and
release checks wait for the domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/frame-origin-check serves a page standing in for Pocket and a frame
origin from the shipped worker, bootstrap page, bridge, socket shim and
Hosted handler, on two ports for any HTTPS front. A fake OpenFrame in the
page answers the frame's traffic, so it needs no session or pairing, and each
run's results post back to the server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A worker in a cross-site frame works on an iPhone, in a tab and from the Home
Screen, and recovers after a force-quit. A frame's localStorage did not survive
that relaunch, so a built-in Tool must keep no state in browser storage; they
keep none today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote Client frames a page only where every address its grant pins is
loopback, unspecified, or held by one of this host's interfaces, judged on
those addresses and never on a name. A local pane keeps its remote pages; the
directory lists none of them to a Client. Frames exist for Dor Tools and local
dev servers, and a remote page's stripped protections would reach a phone with
no consent the Burrow can read. Future item 1a records what lifting this needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Page traffic is far heavier than a terminal's: over the Relay it would look broken and put every page's volume on it. The Burrow answers frame.open only once the direct path carries both directions. The switch is one-way, so a frame never outlives its path. A relayed session still lists its pages, so a Client can say why they do not open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the dormouse-frames Worker on a wildcard route: three static files and a 404, no binding but its assets, and HSTS since .com does not force HTTPS. It deploys first and passes a live smoke, so a Pocket that frames never outruns it. Previews skip it, as workers.dev has no wildcard. Audited as S296 and S297. Nothing declares frames yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Tool opens on its page under the header, with a button to its terminal and back, and a browser Surface shows its page alone. The bottom bar and composer go, and touches and keys are the page's while it shows. MobilePagePane says what a page is waiting on, including the relayed session's note about a VPN on both devices. Storybook only: nothing in Pocket uses these yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Browser and Terminal buttons lead the mobile header, in the desktop's glyphs, with the one showing marked. The model says browser rather than page, and a Session names its browserDisplay, so agent-browser and Playwright streams can land in the same layout: a story shows one wearing the robot glyph, and scope mobile-browser-streams holds the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A row's path fills the space beside its title, as the header's does, rather than a second line under it. The path gives way before the title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mobile header offers a close on a Tool or browser in either mode, through onBrowserClose, even where the consumer hides the terminal's kill. Pocket does not pass it yet: closing a Surface from a phone needs a new surface.close, recorded in scope pocket-frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
surface.close runs the desktop's kill gesture decisions in the owning Wall: a transfer or a helper's work refuses, a close that must confirm answers confirm, and unsaved edits answer unsaved, with consent skipping no refusal. A terminal never closes this way, and a session that declared no frames closes nothing. A peer op answers within a budget, so the Burrow polls for the outcome. Logged as closed, and audited as S298.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hosted's production Pocket declares frames, and shows a Tool on its browser and an embedded page alone: its frame on the session, the bootstrap page joining the worker, and the page sandboxed as a pane's. A browser showing holds no terminal, up to four frames stay loaded with unsaved edits spared, and a relayed session says what would help. Close asks what the desktop asks, the phone's own unsaved edits first. Nothing is deployed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Peer ops may now answer asynchronously, with a longer budget for a close, which removes its polling. The tunnel takes the session's directory collect rather than running its own, acks coalesce on a short timer, and flow control has hysteresis. WORKERS is the application Workers plus the frame Worker, so nothing has to opt out of an origin or a preview. Removes a wire field and a provider field nothing read, and reuses embedderOrigins, ModalFrame, messageOf, the JSON store, DEFAULT_RELAY_ORIGIN, and the hop-by-hop list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bootstrap page is sandboxed too, since the page it sits beside can script it and step out of its own sandbox through it. A close addresses the one owner it resolved, so a duplicate in another window stays. A frame whose page went while its grant was minted closes. A cancelled exchange or a closed frame drops what it left queued, which no Client would ack and would hold the flow window for good.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
frame.open names the page origin its frame origin is for, and the Burrow refuses another, so a page that moved never inherits the old page's storage. A close finds its one owner with a read-only probe that answers for browsers too, closing nothing when no owner answers, which also ends the browser-only broadcast. A close left asking about the phone's own edits is cancelled with the wall, so its answer cannot reach the next Burrow. A socket paused while connecting stays paused once open. Request bodies now go only through http.write, and every body chunk shares a session-wide send window, so parallel uploads cannot overflow the direct sender. A socket's close follows its sends, and a page going away closes the sockets it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The close's owner probe moves into VS Code's existing attach mechanism, the one host with duplicate windows, so the shared provider and standalone go back to one ask. The phone's own unsaved edits use the close hook's dialog rather than the Wall's module-wide prompt, so nothing outlives the wall to need cancelling. One Gate replaces the exchange slots and the send window, one body sender and shared frameChunks replace three chunking loops, and normalizeOrigin, messageOf and the exact base64url guard replace local copies. Removes the queue drops and the stripped request the fixes left redundant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only covering it left xterm's z-indexed link layer above the iframe, so a drag on the page selected in the terminal. As the desktop's ToolPanel does, the terminal is now hidden and inert while its size holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What a connection is and how to leave it, Pocket's way back to the Burrows and the one-time page's End, moves from a row above the wall to the head of the Sessions screen. The wall host now clears the status bar, and a wall with no Session offers the screen directly, since it has no header to tap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A remote close consents only to the edits it was asked about, so edits made after a save or a clean read refuse it, and the Surface's kind is judged again at the close itself. The socket shim takes a send's bytes at the call, hands a Blob over as it is for the Client to read in order, and installs ahead of the page's own scripts. A directory listing now only raises the question of whether a frame's page went, and the surface's owner answers it, so a stale or empty listing cannot revoke a live frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tunnel no longer collects a directory of its own: its trigger and a new frame ask the surface's owner directly, while a session's listing stays a cheap filter. A frame closed as gone can be tried again, since a slow owner reads as gone too. closeForClient alone holds the terminal rule, one serial queue orders a socket's messages at both ends, and PocketWall owns its host with one ConnectionHeader for both apps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The socket shim's insertion point searched the first kilobyte for tags, so a tag spelled in a script's text put the shim inside that script. It now walks the document from the start and stops at the first script. Its send recognizes a buffer from another window as binary rather than stringifying it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nedtwigg and others added 11 commits November 2, 2026 11:34
Re-reading the phone's edits before each ask still lost anything typed during the last one, so the page now takes no input from the start of a close to its end, and the check goes back to one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A quoted `>` in an attribute no longer puts the shim inside that attribute. A page the upstream compressed anyway is decoded before instrumenting on a tunneled grant, since its sockets need the shim. Frame staging reads Windows path separators as the slashes its list spells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The proxy decoded HTML and the frame host everything else, which took a shared module and left a stray decoder on compressed non-HTML. "Answer every body identity-encoded" is a property of the tunneled grant, so the proxy now owns all of it, and the frame host only translates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page stays listed through a slow lookup, on the last answer for its origin, since dropping it from the directory loses its unsaved edits on the phone. A request the worker gave up on is abandoned rather than run after its 504, and a worker that stopped running no longer strands an exchange. A tunneled grant decodes every coding it can and an empty body, and refuses one it cannot rather than handing on the bytes as plain. A close finishing after the Client went is still logged, a shimmed socket refuses a close a native one refuses, and the shim stays behind a charset that a script stands ahead of. The spec now says only HTTP exchanges queue behind their bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
decodedBody answers a body or null rather than three states, and the lookup cache uses settleAllWithin and holds nothing for a URL with no origin. A page's own abort now reaches the abandon path, and a failed relay no longer holds the worker open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed name lookup keeps a listed page listed, as a slow one does. The flow window charges each message as well as its bytes, so a burst of tiny ones pauses its sources long before the direct sender's queue fills. A page's abort before its head now cancels the upload, and a shimmed socket's bufferedAmount falls as sends leave the phone, for pages that throttle by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The locality helper turned a resolver failure into false, so a transient DNS failure still dropped a listed page. It now rejects, which the lookup cache already reads as no answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2eebdc3
Status: ✅  Deploy successful!
Preview URL: https://a7f8c452.mouseterm.pages.dev
Branch Preview URL: https://pocket-browser.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A framed page's requests can be served by the wrong Pocket tab. Details are inline on bootstrapOf in lib/src/remote/frame-origin/worker.ts.

Comment thread lib/src/remote/frame-origin/worker.ts Resolved
nedtwigg and others added 4 commits November 2, 2026 14:21
… tab

The one-time page now frames pages as Pocket does, with its salt held in memory for the one session, so a frame origin it leaves behind cannot be reached again. Its CSP names the frame domain and its sandbox grants what a frame needs, since a frame's is at most its parent's. "Ephemeral" now means ephemeral to the person using it: the page keeps nothing of its own, and what a framed page stores stays at its own frame origin.

Pocket's salt moves to sessionStorage, one per tab. Two tabs on one Surface shared an origin and one worker, which cannot tell whose page asked, so a frozen background tab could hang a visible page's requests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A frame's sandbox is at most its parent's, so a flag added to the shared constant now reaches frames on the one-time page rather than being silently capped. Also names the one way two tabs can still share a frame origin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg marked this pull request as ready for review November 2, 2026 22:45

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

S95 in docs/specs/security-hosted.md now contradicts the one-time page's policy, so the nightly audit (a release gate) will fail it. The rule still says the /connect/ policy must not admit "a network source outside the relay APP_ORIGIN's /connect/" or permit "framing, forms, or popups". The policy now adds frame-src https://*.dormouseusercontent.com, and its sandbox grants allow-forms, allow-popups, and allow-popups-to-escape-sandbox. S296 already owns the frame source, so S95 can carve that out and name what it still forbids, for example: "…permits inline or off-origin script, being framed, a form submission (form-action), or a top navigation…". That line is outside the diff, so it isn't an inline suggestion.

The other findings are inline.

Comment thread hosted/server/headers.ts Resolved
Comment thread lib/src/host/remote/frame-host.ts Resolved
Comment thread lib/src/host/remote/frame-host.ts Resolved
Comment thread lib/src/components/Wall.tsx Resolved
Comment thread lib/src/remote/frame-origin/worker.ts Resolved
nedtwigg and others added 2 commits November 2, 2026 15:11
- Reword S95 for the policy the one-time page now has: frames are S296's,
  and the sandbox's forms and popups are not what it forbids.
- A tunneled grant sends no Clear-Site-Data, whose reason is a loopback
  port a tunneled page is not at, and the tunnel drops the hop-by-hop
  headers of its own connection to the proxy.
- A socket honors a pause and a resume while closing, so a held socket
  gets the upstream's close code rather than ws's 1006 after 30 s, and a
  full session window also holds one that is closing.
- A Surface the computer closed during a wait is no terminal: the phone
  hears it closed rather than "A terminal closes only on the computer."
- The worker posts nothing for a request the page aborted before it went.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Surface gone during a Client's close is one check in closeForClient,
which also keeps it from asking to confirm a Surface that is gone. A
tunneled grant is never fresh, rather than fresh and ignored. The Wall
test shares its helper and kill with its neighbours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nedtwigg
nedtwigg merged commit e7a246b into main Nov 2, 2026
25 checks passed
@nedtwigg
nedtwigg deleted the pocket-browser branch November 2, 2026 23:27
nedtwigg added a commit that referenced this pull request Nov 2, 2026
#1515 added tunneled phone grants to the same proxy, so the merge reconciles the two gates in mintGrant: one locality check, refusing a tunneled grant first, then an external one the user has not allowed.

- Pass allowExternal and tunneled to the VS Code host in an options object. Both were the fourth positional argument, so a plain merge let the webview's flag set tunneled.
- Keep main's isThisMachineAddress and drop this branch's copy.
- Skip the off-origin redirect page on a tunneled grant, whose Pocket frame cannot follow it.
- Renumber this branch's rule to S299, since main's S294 is the frame tunnel, and set next to 300.
- Ratchet docs/specs/dor-browser.md, since both sides added words.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nedtwigg added a commit that referenced this pull request Nov 2, 2026
…he phone

#1515 sends a phone's frames through the same proxy, on tunneled grants that
are refused off this machine whatever allowExternal says. The merge made the
code agree; this pins it and says so.

A tunneled grant now has a test that an off-origin redirect passes as sent:
PocketBrowserFrame handles no `navigate`, so the Dormouse page would be a dead
end there. burrow.test.ts pins the VS Code glue asking the real proxy for a
phone's frame as tunneled, through the argument shape that once collided with
allowExternal.

S299 and the Guarantee row name local panes and point at S294 for tunneled
grants; dor-browser.md defers its definition of local to remote-api.md's, which
also stops saying a local pane keeps every remote page. The phone's redirect is
recorded in remote-api.md's Future. pinAddresses' comment no longer claims every
private address is trusted, which stopped being true when the gate landed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants