Repository navigation
Add in-browser pg_shell demo running PostgreSQL in WebAssembly - #86
Merged
Merged
Conversation
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
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.
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:
sql/directory, installed at page loadpostgrest.js)hx-*attributes fromhtml/index.htmlKey Changes
web/assets/executor.js: Browser stand-in forworkers/executor_agent.pythat claims commands, executes them against an allowlist, and manages the claim/execute/complete cycle with the databaseweb/assets/commands.js: Allowlist of 20+ shell commands (echo, cat, ls, grep, sed, etc.) with implementations matching coreutils behaviorweb/assets/vfs.js: In-memory filesystem providing path confinement underSHELL_ROOT, mirroring the isolation a deployed executor gets from OS permissionsweb/assets/shlex.js: POSIX-style tokenizer matching Python'sshlex.split()for command parsingweb/assets/postgrest.js: HTTP request handler that routes RPC calls to PGlite and returns database-rendered HTMLweb/assets/db.js: Boots PGlite, installs the schema from SQL files, and manages IndexedDB persistenceweb/assets/inspector.js: Database inspector panel for viewing commands, sessions, and running arbitrary SQLweb/assets/app.js: Orchestrates startup, wires together all components, and manages the UIweb/index.html: Single-page application with terminal panel, database inspector, and boot overlayweb/assets/app.css: Terminal-themed dark UI with responsive layoutweb/build.mjs: Build script that assembles the GitHub Pages site by copying SQL files and htmx from the repositoryweb/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 fragmentsweb/test/executor.test.mjs: Unit tests for the browser executor against a real PGlite databaseNotable Implementation Details
ALLOWED_COMMANDS) rather than being interpreted, matching the security model ofEXECUTOR_ALLOWED_COMMANDSSHELL_ROOTand refuses escapes, providing the same isolation boundary as a correctly configured deploymenthttps://claude.ai/code/session_01H8iHRyutFShXsSLQ2wSQe5