Add a Browser workspace surface - #436
Draft
cfal wants to merge 6 commits into
Draft
Conversation
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
marked this pull request as draft
July 31, 2026 23:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 onlyhttp/https, refuses the app's own origin, and every app-origin document is now served withX-Frame-Options: DENYandframe-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 reassigningsrc, 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 ofX-Frame-Options/frame-ancestorslets the surface explain the blank frame and offer to open the page in a real tab.