Skip to content

feat(playground): make baml playground usable over SSH tunnels - #3949

Merged
rossirpaulo merged 7 commits into
canaryfrom
paulo/playground-over-ssh
Jul 8, 2026
Merged

rossirpaulo merged 7 commits into
canaryfrom
paulo/playground-over-ssh

Conversation

@rossirpaulo

@rossirpaulo rossirpaulo commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

baml playground was unusable on a remote machine over ssh -L, and risky even locally:

  1. Origin guard blocked remapped tunnels. The /api guard allowlisted localhost:<exact server port> only, so a tunnel like ssh -L 8000:localhost:4265 loaded the UI but every WS/fetch got 403.
  2. No way to pin the port, nothing printed on startup. The server scanned silently from a base port; over SSH you couldn't tell what to forward.
  3. Headless hang. On a display-less host webbrowser falls back to text browsers (lynx/w3m) and the open call sat on the startup path — banner printed, server never served.
  4. Env exfiltration. Any local process (or any loopback-origin page, after fix 1) could open /api/ws with no credentials and receive the entire process environment in the first frame.

Changes

Reachability + UX

  • Origin guard now accepts any loopback-host origin (localhost, 127.0.0.0/8, [::1], http/https, any port). The host — not the port — is the DNS-rebinding trust signal; hostile remote origins are still denied. VS Code webview origins unchanged.
  • --port <PORT> (exact bind, loud actionable failure) and --no-open; headless sessions (SSH, or no display on Linux) auto-suppress the browser open.
  • Plain-text stdout banner the moment the listener binds: URL, project root, copy-pasteable ssh -L recipe. Independent of RUST_LOG.
  • webbrowser::open moved to a detached thread — a text-mode browser can never block startup.
  • Default port standardized on 4265 ("BML"), scan 4265..4364.

Security (browser mode only)

  • A per-session uuid token is minted and carried as ?token= on the printed/opened URL. The existing /api guard middleware rejects requests without it (401, actionable message). CORS preflights stay unauthenticated by design; the origin check still runs first. This closes the Origin-less /api/ws hole.
  • The first-frame env push now sends only project-referenced env vars (names from BAML source via the salsa-cached all_env_var_names, values from the server env). Wire format unchanged. Lazy per-key resolution for dynamic keys is untouched.
  • LSP/editor mode mints no token — guard behavior there is byte-for-byte today's; the VS Code extension is untouched. Token for editor mode is a noted follow-up.
  • Frontend: the page token rides both WS URLs and the source-files fetch (browsers can't set WS headers). A thin banner surfaces the previously-silent reconnect loop when the socket can't connect (e.g. URL opened without the token).

Documentation: baml describe playground

Docs here are agent-native — the repo convention is that the CLI is the documentation (fern/ covers the deprecated legacy engine/ stack and is deliberately untouched). The feature gets three self-documenting surfaces, each pinned by tests:

  • baml playground --help — clap doc comments on the new flags, asserted by the CLI help tests.
  • baml describe playground — a new data-only YAML topic in baml_builtins2/keyword_docs/baml_keywords.yaml (embedded at compile time; zero Rust changes to add). It documents the port scan, headless behavior, the ssh -L recipe, and the token semantics. Following the check precedent, topic name == subcommand name, so it's guessable from baml --help despite the flat exact-name topic keyspace. Pinned by an insta snapshot of the rendered output plus a dispatch test asserting playground resolves to a keyword topic rather than falling through to "No symbol found".
  • The startup banner — teaches the tunnel recipe at the moment of need.

Ports that look related but are deliberately NOT changed

  • engine/language_server stays on 3700 — legacy stack; keeping it there separates the old and new servers' scan ranges (removes a wrong-instance collision mode).
  • BAML_PLAYGROUND_DEV_PORT=4000 examples — that's the Vite dev server the playground proxies to, an upstream target, not a listen port.
  • fern/ port mentions — legacy docs tree, out of scope.

Verification

  • Unit: origin matrix, token matrix (incl. constant-time compare, query parsing), env-var collection, headless detection (closure-injected, env-mutation-free), banner formatting, bind_exact_port conflict, help text, describe snapshot.
  • Live, local: full auth matrix over curl (401/403/200/204 as designed); a tokenless WS handshake is rejected where it previously dumped the environment; with the token the first env frame contained only the project-referenced key while a canary secret in the process env never crossed the wire.
  • Live, remote (x86_64 Linux VPS over real ssh -L with a remapped local port): UI loads, both WS endpoints connect, function runs end-to-end; banner/headless/lynx behavior confirmed (server serves while a forced text browser is open).

Out of scope (follow-ups noted in the design docs)

--host/non-loopback bind; token for LSP/editor mode + getWebviewHtml injection; run-event broadcast hardening (lossy 64-slot channel / full re-broadcast per profile event) — gated on tunnel measurements; multi-tab correctness; anything under fern/ or engine/.

Summary by CodeRabbit

  • New Features

    • Added a token-protected playground experience with support for custom ports and optional browser auto-open behavior.
    • Improved webview connectivity handling, including retry status feedback and clearer guidance when the server is unreachable.
    • Added support for preserving access tokens across playground and API/WebSocket requests.
  • Documentation

    • Expanded playground help and keyword documentation with usage, remote access, and connection details.
  • Bug Fixes

    • Refined remote access and browser-launch behavior for headless or SSH-based sessions.

…ess guard

The guard's port-derived origin allowlist blocked ssh -L remapped tunnels:
through a tunnel the browser's origin is the local endpoint — loopback host,
arbitrary port. Check host-is-loopback (localhost, 127.0.0.0/8, [::1], http
or https, any port) instead of exact strings. The host, not the port, is the
DNS-rebinding trust signal; a hostile page's fetch/WS still carries its real
remote origin and stays denied. VS Code webview origins are unchanged.
…p banner

- --port binds exactly that port via a new bind_exact_port (fails loudly
  with an actionable message); default stays the auto-scan.
- --no-open suppresses the browser; headless sessions (SSH_CONNECTION/
  SSH_TTY anywhere, no DISPLAY/WAYLAND_DISPLAY on non-macOS unix) are
  auto-detected in the CLI and suppress it too. The server crate just
  obeys a bool on the new PlaygroundServerOptions.
- A plain-text stdout banner (URL, project root, ssh -L hint, Ctrl-C note)
  prints the moment the listener is bound, before project load and
  independent of RUST_LOG. Browser mode only; stdout is unused there.
- webbrowser::open moves onto a detached thread so a text-mode browser
  (lynx/w3m) can never block server startup.
- In browser mode a port-bind failure now propagates as an error instead
  of limping on to the generic bail; LSP mode keeps serving stdio.
4265 ("BML") is memorable, unregistered, and below the OS ephemeral
ranges; auto-scan covers 4265..4364. Moving off 3700 also separates the
new server's scan range from the legacy engine/language_server (which
deliberately stays on 3700), removing a wrong-instance collision mode.
Doc-comment and test-literal references follow. BAML_PLAYGROUND_DEV_PORT
examples (4000) are the Vite upstream target, not a listen port, and are
unchanged, as is everything under fern/ and engine/.
Data-only YAML topic following the `check` precedent (topic name ==
subcommand name); covers --from/--file/--port/--no-open, the 4265..4364
scan, headless auto-suppression, and the ssh -L recipe. Snapshot via
cargo insta accept, plus the keyword-dispatch regression assertion.
`baml playground` mints a uuid-v4 session token, prints it on the banner
URL (?token=...), and the guard middleware rejects /api requests without
it (401 with an actionable message). The check runs after the OPTIONS
branch so CORS preflights stay unauthenticated; the origin check is
unchanged and still wins. This closes the Origin-less exfiltration hole:
any local process could previously open /api/ws and receive the env dump.

LSP/editor mode mints no token — guard behavior there is byte-for-byte
today's, and the VS Code extension is untouched.

Frontend: a `withPlaygroundToken` helper in pkg-editor carries the page
URL's token onto both WS URLs and the source-files fetch (browsers can't
set WS headers, so it rides the query string). The VS Code-injected WS
URL is left alone. `WebSocketRuntimePort` now exposes a connection
status, and the standalone app shows a thin banner when the socket is
stuck retrying — the new user-error path (opening the bare URL without
the token) previously rendered a fully dead UI with no explanation.

The app's vite config aliases @b/pkg-editor to its source dir, which
bypasses the package `exports` map — the new `/token` subpath therefore
gets an explicit alias entry so the helper can be imported without
pulling the Monaco re-exports into the main bundle.
The first WS frame used to carry the entire process environment. Now the
push computes the project-referenced names (salsa-cached
`all_env_var_names`) and sends only those that are set. Wire format is
unchanged — `processEnvVars` with fewer entries — so no frontend changes;
the env panel simply shows only relevant vars. Dynamically-computed keys
still resolve lazily through the EnvVarRequest round-trip, which reads
the server process env per key and is untouched.
@vercel

vercel Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jul 8, 2026 8:41pm
promptfiddle Ready Ready Preview, Comment Jul 8, 2026 8:41pm
promptfiddle2 Ready Ready Preview, Comment Jul 8, 2026 8:41pm

Request Review

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@vercel
vercel Bot temporarily deployed to Preview – beps July 8, 2026 19:55 Inactive
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds session-token authorization to the baml playground server, plus configurable port selection (--port) and browser-open control (--no-open, headless-session detection). Changes span CLI args, LSP server orchestration, HTTP guard/origin rules, environment-variable filtering, and VS Code webview/editor client wiring including WebSocket connection-status tracking.

Changes

Playground session-token auth and port/browser controls

Layer / File(s) Summary
CLI flags, headless detection, docs
baml_language/crates/baml_cli/src/playground_command.rs, baml_language/crates/baml_cli/src/commands.rs, baml_language/crates/baml_builtins2/keyword_docs/baml_keywords.yaml, baml_language/crates/baml_cli/src/describe_command_tests.rs
Adds --port/--no-open args, is_headless_session detection, playground keyword doc entry, and describe/dispatch test coverage.
LSP server options, token minting, banner
baml_language/crates/baml_lsp_server/src/lib.rs, baml_language/crates/baml_lsp_server/Cargo.toml
Introduces PlaygroundServerOptions, mints per-session token, selects port, conditionally opens browser, prints token-aware banner.
Sender token-aware browser open
baml_language/crates/baml_lsp_server/src/playground_sender.rs
NativePlaygroundSender builds token-bearing URLs and opens browser asynchronously.
HTTP guard, origin rules, env filtering
baml_language/crates/baml_lsp_server/src/playground_server.rs
Adds PlaygroundAccessGuard token authorization, loopback/vscode origin acceptance, bind_exact_port, and referenced-env-var filtering.
Shared client token utility and wiring
typescript2/pkg-editor/src/playground-token.ts, typescript2/pkg-editor/src/index.ts, typescript2/pkg-editor/package.json, typescript2/app-vscode-webview/vite.config.ts, typescript2/app-vscode-webview/src/RemoteEditorView.tsx, typescript2/pkg-editor/src/remote-backend.ts, typescript2/app-vscode-ext/src/panels/__tests__/getWebviewHtml.test.ts
Adds getPlaygroundToken/withPlaygroundToken helpers and wires them into fetch/WS URL construction.
WebSocket status tracking and retry UI
typescript2/pkg-playground/src/ports/WebSocketRuntimePort.ts, typescript2/pkg-playground/src/index.ts, typescript2/app-vscode-webview/src/App.tsx
Adds WebSocketRuntimePortStatus lifecycle with onStatusChange, and a retry banner in the webview UI.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3551: Extends the same keyword-docs/ResolvedTarget::Keyword describe rendering path with a new playground keyword topic.
  • BoundaryML/baml#3786: Both change the same playground server protocol surface in playground_server.rs and baml_lsp_server.
  • BoundaryML/baml#3745: Both modify the OpenPlayground notification plumbing across LSP/sender/webview.

Poem

A token hops along the URL trail,
No stranger's fetch shall now prevail,
Ports pick free, or one you chose,
Browsers wake unless it's headless woes.
🐇 Hop, connect, retry, and cheer—
The playground's locked but warm in here!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: making baml playground work over SSH tunnels.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch paulo/playground-over-ssh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 8, 2026 20:04 Inactive
@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Binary size checks passed

✅ 7 passed

Artifact Platform File Gzip Gated on Baseline Delta Status
✅ baml-cli Linux 🔒 22.0 MB 9.4 MB file 21.5 MB +425.3 KB (+2.0%) OK
✅ packed-program Linux 🔒 15.9 MB 6.7 MB file 15.6 MB +280.5 KB (+1.8%) OK
✅ baml-cli macOS 🔒 16.9 MB 8.2 MB file 16.6 MB +348.0 KB (+2.1%) OK
✅ packed-program macOS 🔒 12.3 MB 5.9 MB file 12.1 MB +232.0 KB (+1.9%) OK
✅ baml-cli Windows 🔒 18.5 MB 8.4 MB file 18.1 MB +350.7 KB (+1.9%) OK
✅ packed-program Windows 🔒 13.2 MB 5.9 MB file 13.0 MB +222.6 KB (+1.7%) OK
✅ bridge_wasm WASM 14.7 MB 🔒 4.2 MB gzip 4.1 MB +112.8 KB (+2.8%) OK

🔒 = the size this artifact is GATED on (ceiling + delta). Binaries gate on file size (installed binary); WASM gates on gzip (download size). The other size is shown for information only.


Generated by cargo size-gate · workflow run

@rossirpaulo
rossirpaulo enabled auto-merge July 8, 2026 20:16
@vercel
vercel Bot temporarily deployed to Preview – beps July 8, 2026 20:18 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 July 8, 2026 20:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle July 8, 2026 20:41 Inactive
@rossirpaulo
rossirpaulo added this pull request to the merge queue Jul 8, 2026
Merged via the queue into canary with commit 777b537 Jul 8, 2026
55 checks passed
@rossirpaulo
rossirpaulo deleted the paulo/playground-over-ssh branch July 8, 2026 20:59
meefs pushed a commit to meefs/baml that referenced this pull request Jul 10, 2026
## Summary

- Remove the per-session playground token minting and API authorization
checks.
- Restore bare HTTP, WebSocket, and source-file URLs with no `?token=`
propagation.
- Delete the token helper, package export, Vite alias, UUID dependency,
and token-specific connection-status banner.
- Remove token documentation and update the playground describe
snapshot.
- Preserve the SSH tunnel support from BoundaryML#3949, including loopback-origin
handling, the 4265 default port, explicit port selection, headless
behavior, and filtered environment-variable delivery.

## Why

BoundaryML#3949 introduced the session token alongside the SSH playground changes.
We have decided not to use token-based authorization for the local
playground, so this removes the feature and all supporting structures
rather than leaving compatibility plumbing or dead code behind.

## Impact

`baml playground` now prints and opens a bare local URL. Playground API
and WebSocket requests no longer require a query parameter. The origin
guard remains in place: remapped loopback origins used by SSH tunnels
are accepted, while hostile remote origins are rejected.

## Validation

- `cargo fmt --all -- --check`
- `cargo test -p baml_lsp_server --lib` — 24 passed
- `cargo test -p baml_cli describe_command_tests` — 89 passed
- `pnpm --filter @b/pkg-playground typecheck`
- `pnpm --filter @b/pkg-proto generate && pnpm --filter
app-vscode-webview build`
- Live smoke test with an explicit port: bare URL printed, tokenless
remapped-loopback request returned 200, and a hostile origin returned
403

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Simplified playground access by removing session-token requirements
across browser, editor, API, and WebSocket connections.
* Playground links now open using plain base URLs, and WebSocket
endpoints are derived from the current page (no token query parameters).

* **Bug Fixes**
* Removed token-dependent banner/retry behavior and adjusted banner
output to match the token-free flow.

* **Refactor**
* Removed public WebSocket connection status reporting and related UI
hooks.

* **Documentation**
* Updated playground keyword documentation to reflect the shortened,
token-free URL behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — d11e8827 Deployed Jul 8, 2026 by vercel[bot]
Preview – promptfiddle2 — d11e8827 Deployed Jul 8, 2026 by vercel[bot]
Preview – beps — d11e8827 Deployed Jul 8, 2026 by vercel[bot]
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