Skip to content

feat(browser): open local files from the session's checkout - #981

Merged
omartelo merged 2 commits into
mainfrom
feat/browser-local-preview
Oct 29, 2026
Merged

omartelo merged 2 commits into
mainfrom
feat/browser-local-preview

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Summary

A session's browser could not open a local file: file: is blocked, and previewing an index.html meant the agent starting python -m http.server or npx http-server on whatever port it picked.

file: stays blocked. Its page follows its own links to any path on the machine, and the Chromium loading it runs outside a confined session's sandbox; its null origin also breaks module scripts and fetch. Instead, a path or file: URL inside the session's checkout loads over http:

  • internal/preview: one static server per checkout, rooted in an os.Root (refuses .. and symlinks leading out), bound on first use and closed when lich exits. A path outside the checkout is refused.
  • Port: the checkout's LICH_WORKTREE_PORT plus 1000 (25000-25999), so each worktree is its own origin and never takes the dev server's port. A taken port moves to the next free one in the window, wrapping at its end.
  • browser.SetLocal resolves a local URL for browser_open, browser_navigate and the address field; terminal.SessionCheckout names the session's spawn directory and reserved port. A blocked URL still fails before the browser is looked up.
  • Native address field passes an absolute path through instead of prefixing https://.
  • docs/ceilings.md: the server is static (no reload, no SPA fallback), serves everything under the spawn directory (all of home for a session started there), roots at the checkout so a Vite build with the default base loads blank from dist/index.html, and a moved port changes the origin and its storage.
  • docs/browser-tab.md, MCP tool descriptions and CHANGELOG.md updated.

Provider-independent: the browser belongs to the session, so all eight providers get it the same way.

Test plan

  • internal/preview: serves a path and a file: URL, reuses one listener per checkout, refuses outside paths, other hosts and a symlink out, picks the port above the dev-server window, moves past a taken port and wraps, fails only with the whole window taken.
  • internal/browser: local resolution goes to the owner's checkout; a blocked URL is refused before the browser lookup in Navigate and Nav.
  • internal/terminal: SessionCheckout reads the spawn and its reservation.
  • native: typedURL passes an absolute path through.
  • Local gate: gofmt, vet and go test ./... for the backend and native/; GOOS=darwin|windows go vet on the touched packages.

A file: URL stays blocked: its page follows links anywhere on the machine,
outside a confined session's sandbox, and its null origin breaks module
scripts and fetch. A path or file: URL inside the session's checkout now
loads from a loopback static server rooted in an os.Root, one per checkout,
on its LICH_WORKTREE_PORT plus 1000 (25000-25999), moving to the next free
port when that one is taken.
A root held for the server's life keeps the directory open, and Windows
refuses to remove an open directory: a worktree could not be deleted while
its preview ran, which is also what failed the suite's TempDir cleanup on
the Windows runner. SessionCheckout's test moves beside the Unix-only store
stub it uses, so the package vets on GOOS=windows.
@omartelo
omartelo merged commit 00b46a2 into main Oct 29, 2026
7 checks passed
@omartelo
omartelo deleted the feat/browser-local-preview branch October 29, 2026 15:56
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