Skip to content

Add in-browser pg_shell demo running PostgreSQL in WebAssembly - #86

Merged
seanwevans merged 2 commits into
mainfrom
claude/pg-shell-github-pages-qwku01
Sep 4, 2026
Merged

seanwevans merged 2 commits into
mainfrom
claude/pg-shell-github-pages-qwku01

Conversation

@seanwevans

Copy link
Copy Markdown
Owner

This PR adds a complete, fully functional pg_shell instance that runs entirely in the browser using PostgreSQL compiled to WebAssembly (PGlite). The demo is published to GitHub Pages and demonstrates that pg_shell's architecture is independent of its deployment environment.

Summary

The browser-based demo replicates the full pg_shell stack by substituting only the components that cannot run in a tab:

  • Database: PostgreSQL (real, via PGlite WebAssembly build)
  • Schema & RPCs: Identical SQL from sql/ directory, installed at page load
  • HTTP gateway: Browser-based PostgREST stand-in (postgrest.js)
  • Frontend: Unmodified htmx with the same hx-* attributes from html/index.html
  • Executor: Browser-based command executor with an allowlist and in-memory filesystem

Key Changes

  • web/assets/executor.js: Browser stand-in for workers/executor_agent.py that claims commands, executes them against an allowlist, and manages the claim/execute/complete cycle with the database
  • web/assets/commands.js: Allowlist of 20+ shell commands (echo, cat, ls, grep, sed, etc.) with implementations matching coreutils behavior
  • web/assets/vfs.js: In-memory filesystem providing path confinement under SHELL_ROOT, mirroring the isolation a deployed executor gets from OS permissions
  • web/assets/shlex.js: POSIX-style tokenizer matching Python's shlex.split() for command parsing
  • web/assets/postgrest.js: HTTP request handler that routes RPC calls to PGlite and returns database-rendered HTML
  • web/assets/db.js: Boots PGlite, installs the schema from SQL files, and manages IndexedDB persistence
  • web/assets/inspector.js: Database inspector panel for viewing commands, sessions, and running arbitrary SQL
  • web/assets/app.js: Orchestrates startup, wires together all components, and manages the UI
  • web/index.html: Single-page application with terminal panel, database inspector, and boot overlay
  • web/assets/app.css: Terminal-themed dark UI with responsive layout
  • web/build.mjs: Build script that assembles the GitHub Pages site by copying SQL files and htmx from the repository
  • web/package.json: Node.js dependencies (PGlite, htmx)
  • tests/test_web_demo.py: End-to-end browser tests using Playwright that exercise the real schema, RPCs, and HTML fragments
  • web/test/executor.test.mjs: Unit tests for the browser executor against a real PGlite database

Notable Implementation Details

  • The executor maintains the same claim/execute/complete cycle and lease management as the production worker, ensuring the demo exercises the real SQL
  • Commands are executed against an allowlist (ALLOWED_COMMANDS) rather than being interpreted, matching the security model of EXECUTOR_ALLOWED_COMMANDS
  • The in-memory filesystem confines all paths under SHELL_ROOT and refuses escapes, providing the same isolation boundary as a correctly configured deployment
  • The demo persists the database to IndexedDB so the transcript survives page reloads
  • GitHub Actions workflow publishes the built site to GitHub Pages on pushes to main
  • The build process copies SQL and vendor files at build time, ensuring the published demo always runs the SQL from the branch it was built from

https://claude.ai/code/session_01H8iHRyutFShXsSLQ2wSQe5

GitHub Pages serves static files, and pg_shell needs a PostgreSQL server, an
HTTP gateway and a process executor. Rather than publishing a dead copy of the
UI pointed at an RPC endpoint that is not there, web/ moves the backend into
the page: PGlite (PostgreSQL compiled to WebAssembly) runs in the tab and this
repository's sql/ is installed into it on load, so the schema, the RPCs and the
HTML fragments are the real ones and htmx swaps exactly what it swaps in a
deployment.

Two layers cannot be real in a browser and are stand-ins:

- assets/postgrest.js answers the two RPC calls the UI makes from XHR, so the
  page's hx-* attributes match html/index.html rather than being rewritten
  around a JavaScript API.
- assets/executor.js keeps executor_agent.py's contract -- claim under a lease
  with the session's cwd/env copied into the row's snapshot columns, interpret
  cd itself and write it back to environments, rebuild the command environment
  and reject the same reserved names, cap output at 64 KiB, time out at 30s
  with exit 124, refuse anything off the allowlist with the worker's own error
  -- but runs commands against an in-memory filesystem confined to SHELL_ROOT.
  Its tokenizer is a port of shlex.split(posix=True), differentially tested
  against Python's over several thousand fuzzed inputs.

build.mjs copies sql/ and the vendored htmx into the artifact instead of
keeping duplicates under web/, so the published demo cannot drift from the
source it claims to run.

Testing: web/test/ unit-tests the executor against real PGlite in Node, and
tests/test_web_demo.py drives the built site in Chromium (skipping until it is
built). Both run in CI; pages.yml builds and deploys on pushes to main.

One bug this surfaced in the shared UI markup: the prompt's
hx-trigger="submit from:form" resolves against every form in the document, so
a second form anywhere on the page posts an empty command. The inspector's SQL
panel is deliberately not a form, with a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8iHRyutFShXsSLQ2wSQe5
Playwright compiles wait_for_function predicates in the page's main world, so
they are refused by the demo's own script-src 'self' 'wasm-unsafe-eval'. The
local Chromium build did not enforce this and CI's did, which is why it passed
here and failed there.

The policy is still covered, and better than it was: a new test boots the page
in a context without the bypass and asserts it comes up clean under its real
CSP with no unsafe-inline or unsafe-eval. Bypassing it for the interaction
tests also strengthens the escaping test, which now fails if a fragment is
unescaped instead of being saved by the policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H8iHRyutFShXsSLQ2wSQe5
@seanwevans
seanwevans merged commit a846420 into main Sep 4, 2026
2 checks passed
@seanwevans
seanwevans deleted the claude/pg-shell-github-pages-qwku01 branch September 4, 2026 19:32
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.

2 participants