feat(webui): built-in browser panel — sandboxed local preview (webui-parity slice 04) - #51
Merged
Merged
Conversation
…raw (webui-parity slice 04)
Adds the right-panel Built-in Browser surface (the globe icon from
refs/ui/02-workspace-shell.jpg), wired against the slice 02 /api/fs/raw
route. The panel is intentionally NOT mounted by this slice — the
right-panel registry is owned by slice 03 (feat/git-panel); the
wiring slice imports the exported BrowserPanel component and adds
a 'browser' enum value alongside the existing kinds.
Threat model (see webapp/lib/browser-nav.ts#IFRAME_SANDBOX for the
full rationale):
- iframe sandbox='allow-scripts' ONLY. The five escape-hatch tokens
(allow-same-origin, allow-top-navigation, allow-popups,
allow-forms, allow-modals) are explicitly omitted; their absence
is pinned by browser-nav.test.ts.
- iframe src is ALWAYS the same-origin /api/fs/raw?path=… URL,
constructed in one place (buildSandboxUrl). A grep for fsRawUrl
in components/browser-panel.tsx is the tripwire that catches
any future 'open in iframe via filesystem' regression.
- address-bar input is rejected at the input layer for any
http(s)://, file://, or other scheme-prefixed input — before
the path ever reaches the iframe. coerceAddress returns
{ok:false, reason:'absolute'} so the panel renders the right
bilingual error.
Acceptance (pinned by tests):
- browser-nav.test.ts pins the IFRAME_SANDBOX literal, the
REJECTED_SCHEMES set, the input-layer rejections, the iframe
src construction, the back/forward history, and the iframeKey
remount discipline.
- i18n-browser.test.ts pins the bilingual symmetry of every key
in the slice-04 i18n file (no orphan keys, every visible label
differs en/zh, fallback to key name on missing entry).
- fs-raw-browser-panel.test.js pins the server-side counterpart:
workspace-local HTML → 200/text/html, 21 MiB → 413, directory
→ 400, out-of-root → 403 — the four failure modes the panel
depends on.
Self-check (isolated instance on 18166/18167, then torn down):
- The HTML page served through the panel's iframe src
(/api/fs/raw?path=public/index.html) renders correctly with
sandbox='allow-scripts'. The counter script inside the sandbox
runs (ticks every 250ms); the parent document cannot read the
iframe's contentDocument (returns null) and contentWindow
.location.href throws SecurityError as expected.
- /api/fs/raw?path=/etc/passwd → 403 '工作区越界' (gate enforced)
- /api/fs/raw?path=../../etc/passwd → 403 (gate enforced)
Gates: pnpm --filter @mavis/webui webapp:typecheck (clean),
pnpm typecheck (clean), pnpm --filter @mavis/webui test:webapp
(503 pass), pnpm check:source (4627 files, clean). The
packages/webui test/lib router-auth-gate pre-existing failure is
unrelated to this slice (fails identically on the base commit).
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.
What
Slice 04 of the webui-parity program: the built-in browser surface from the desktop target (the globe icon in the right-panel toolbar), previewing local pages from the workspace in a sandboxed iframe.
Delivered as an exported, unmounted component plus its pure logic —
components/panels.tsxis owned by the in-flight slice 03, so mounting is a separate slice with a documented one-line contract (<BrowserPanel locale t currentPath onNavigate />) and nine test-ids.components/browser-panel.tsx— controlled panel: address bar, back/forward/refresh, empty/error states.lib/browser-nav.ts— pure logic:IFRAME_SANDBOX, address coercion, sandbox URL construction, history helpers.lib/i18n-browser.ts— 18 bilingual keys, genuinely imported by the component.GET /api/fs/raw; no second file-serving path was introduced.Security (the core of this slice)
sandboxis literallyallow-scriptsand nothing more. Five escape hatches are omitted with rationale in the constant's JSDoc:allow-same-origin(a preview could read the app's cookies/sessionStorage),allow-top-navigation(could replace the host document), plusallow-popups/allow-forms/allow-modals. The constant is pinned by literal tests, and the panel root carriesdata-sandboxso the pin is meaningful even before mounting.Acceptance (independent agent) — PASS-WITH-CONCERNS
contentDocumentisnull;contentWindow.location.hrefthrows SecurityError; inside the preview the origin is opaque,document.cookiethrows ("sandboxed… lacks allow-same-origin"), parent-document read is blocked, top-navigation is blocked,window.openis null, alerts suppressed.http,https,FILE://,file://,javascript:,vbscript:,data:, and whitespace-prefixed variants are all rejected at the address bar with no iframe created;../is forwarded unrewritten and refused (403);buildSandboxUrlcan emit only an encoded/api/fs/raw?path=./etc/passwdand../../etc/passwd→ 403; directory → 400; 21 MiB → 413 (exactly 20 MiB → 200).f25ee22baseline, identifying it as a parallel-run port flake.Defects the acceptance surfaced — must be fixed in the mounting slice
workspaceDirprop that does not exist. Fix in the wiring slice: resolve relative → absolute against the active workspace before building the src.Gates
test:webapp503/503 · typecheck ✓ · build ✓ ·check:source✓ (4627 files) · full server suite green (1792 pass / 0 fail)