Skip to content

Add a Browser workspace surface - #436

Draft
cfal wants to merge 6 commits into
mainfrom
iframe-browser
Draft

Add a Browser workspace surface#436
cfal wants to merge 6 commits into
mainfrom
iframe-browser

Conversation

@cfal

@cfal cfal commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Previewing a local dev server or reading documentation meant leaving Garcon entirely, and the page could not be kept beside a chat, terminal, or diff. This adds a Browser surface that embeds http(s) pages in a sandboxed iframe behind an address bar, opened from the taskbar menu, the command menu, or on mobile, and behaving like the other portable singletons: it docks in either host, persists in the workspace layout, and restores its last URL. A local setting, off by default, additionally routes plain left-clicks on external markdown links into the surface so following a link keeps the conversation on screen. Because embedded content is untrusted, the surface accepts only http/https, refuses the app's own origin, and every app-origin document is now served with X-Frame-Options: DENY and frame-ancestors 'none' — a framed page can navigate itself to an app URL, so the client-side check alone would not keep app-origin documents out of the frame. Host navigation remounts the iframe instead of reassigning src, keeping address-bar and back/forward actions out of the tab's session history. Sites that refuse framing render blank with no way for the embedder to tell, so an advisory server-side probe of X-Frame-Options/frame-ancestors lets the surface explain the blank frame and offer to open the page in a real tab.

cfal added 6 commits August 1, 2026 02:23
The Browser surface embeds third-party pages with allow-same-origin, which is
only safe while no app-origin document can render inside it. A framed page can
navigate itself, or be redirected, to an app URL, so refusing such URLs on the
client is not sufficient on its own.

Adds applyAppDocumentSecurityHeaders() and applies it to every HTML-serving
route. /shared/:token needed it explicitly because it builds its own Response
and boots the full SPA. The raw file endpoint also gains CSP: sandbox so
workspace HTML can never execute with app-origin authority.
Sites that refuse framing render as a blank iframe, and that refusal is not
detectable from the embedding page. This adds a cookieless probe of
X-Frame-Options and CSP frame-ancestors so the Browser surface can explain a
blank frame instead of leaving the user guessing.

The verdict follows CSP3 enforcement: policies are evaluated separately, only
the first frame-ancestors per policy counts, results intersect, and
X-Frame-Options applies only when no policy declares frame-ancestors. The
response carries the verdict alone, and the route requires an
X-Garcon-Embed-Check header that only same-origin script can set so framed
content cannot drive the server-side fetch.
Previewing a local dev server or reading docs meant leaving the app, and the
page could not be kept beside a chat, terminal, or diff. This adds a Browser
surface that embeds http(s) pages in a sandboxed iframe behind an address bar,
opened from the taskbar menu, the command menu, or on mobile.

It is a portable singleton, so it docks in either host, persists in the
workspace layout, and restores its last URL. Only http and https are accepted
and the app's own origin is refused, so framed content is always cross-origin.
Host navigation remounts the iframe rather than reassigning src, which keeps
address-bar and back/forward actions out of the tab's session history; back and
forward step through URLs the host committed, since an embedder cannot observe
navigation inside a cross-origin frame. On mobile it presents like Commit, with
frame chrome to leave the surface.
Adds a local setting, off by default, that routes plain left-clicks on external
markdown links into the Browser surface instead of a new tab, so following a
link from a chat keeps the conversation on screen.

The policy is provided through optional context and consumed in one place in
the markdown renderer, so every consumer picks it up without change. Modifier
and middle clicks, non-http(s) targets, and same-origin URLs all fall through
to the anchor's default behavior, and capture is skipped where the workspace is
not on screen, such as the public shared-transcript page.
Captures the design behind the Browser surface, including how other IDEs handle
embedding and why proxying around anti-framing headers is rejected. Adds the
standing rules to docs/security.md: every app-origin document must deny
framing, nothing remote or workspace-derived may render from the app origin,
and message listeners must validate their event origin.
The unknown-token case only returns HTML when web/build exists, so it served a
plain 404 in CI, where server tests run without building the SPA. The
known-token path renders standalone when no build is present and goes through
the same response helper, so asserting there covers the same wiring in every
environment.
@cfal
cfal force-pushed the iframe-browser branch from 1b9c036 to 22c2347 Compare July 31, 2026 19:23
@cfal
cfal marked this pull request as draft July 31, 2026 23:29
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.

1 participant