Skip to content

feat(webui): built-in browser panel — sandboxed local preview (webui-parity slice 04) - #51

Merged
fengzhi09 merged 1 commit into
mainfrom
feat/builtin-browser
Sep 27, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
feat/builtin-browser

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

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.tsx is 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.
  • Builds on the already-merged GET /api/fs/raw; no second file-serving path was introduced.

Security (the core of this slice)

sandbox is literally allow-scripts and 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), plus allow-popups / allow-forms / allow-modals. The constant is pinned by literal tests, and the panel root carries data-sandbox so the pin is meaningful even before mounting.

Acceptance (independent agent) — PASS-WITH-CONCERNS

  • Isolation reproduced live, not asserted: parent contentDocument is null; contentWindow.location.href throws SecurityError; inside the preview the origin is opaque, document.cookie throws ("sandboxed… lacks allow-same-origin"), parent-document read is blocked, top-navigation is blocked, window.open is null, alerts suppressed.
  • Escape routes closed: 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); buildSandboxUrl can emit only an encoded /api/fs/raw?path=.
  • Containment intact: /etc/passwd and ../../etc/passwd → 403; directory → 400; 21 MiB → 413 (exactly 20 MiB → 200).
  • Mutation-tested the sandbox pin: widening the constant fails two pinned tests (then reverted, tree pristine).
  • The dev's "pre-existing failure" claim was refuted — the agent reproduced the full server suite green on the branch and on a clean f25ee22 baseline, identifying it as a parallel-run port flake.

Defects the acceptance surfaced — must be fixed in the mounting slice

  • F1 (history): the controlled-path effect rebuilds history on every prop change, so under the natural round-trip wiring back/forward never enable, and under decoupled wiring the address bar goes stale after back. Fix: skip the history reset when the controlled path already equals the internal current, and sync the draft from history on back/forward.
  • F2 (path resolution): "workspace-relative" addresses resolve against the server CWD and 403 with ENOENT; only absolute in-root paths render. The doc comment promises a workspaceDir prop that does not exist. Fix in the wiring slice: resolve relative → absolute against the active workspace before building the src.
  • F3 (multi-file sites): linked relative assets 404 because the raw route has no path-suffix resolution, so multi-file static sites render unstyled. Optional route-level follow-up.

Gates

test:webapp 503/503 · typecheck ✓ · build ✓ · check:source ✓ (4627 files) · full server suite green (1792 pass / 0 fail)

…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).
@fengzhi09
fengzhi09 merged commit 3fa2ed7 into main Sep 27, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the feat/builtin-browser branch September 27, 2026 12:59
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