[wasm-posix-kernel] WordPress Playground Web under wasm-posix-kernel - Proof of concept#3635
Draft
mho22 wants to merge 18 commits into
Draft
Conversation
1fa02a3 to
50d9bf8
Compare
c0418c4 to
905f8ba
Compare
905f8ba to
11a61a2
Compare
50d9bf8 to
487916a
Compare
487916a to
50d9bf8
Compare
ecbde4c to
99027a1
Compare
2252902 to
0c51a67
Compare
99027a1 to
efb02a4
Compare
0c51a67 to
16470c7
Compare
efb02a4 to
897ff47
Compare
Build the SAB-backed VFS image consumed by the kernel worker: binaries, /etc files, nginx + php-fpm configs, fpm-router, wp-config.php, wasm-optimizations mu-plugin, dinit service tree, streaming zip extraction. Co-locates host-bridge re-exports from the wasm-posix-kernel submodule and the playground-defines.php mu-plugin that backs KernelLimitedPHPApi.defineConstant.
Download WordPress + SQLite zips through the playground CORS proxy. Broadens the proxy allowlist to wordpress.org, downloads.w.org and the GitHub release host so zip fetches succeed under the iframe's cross-origin isolation regime.
Construct BrowserKernel + HttpBridgeHost from the prebuilt VFS image, inject the Host header on every bridge request (nginx returns 400 without one), poll waitForNginx until any HTTP status comes back (dinit returns before its children bind their sockets), and bump nextPid past the kernel-reserved range to avoid an EEXIST on the first host-side spawn.
KernelSpawnAdapter wraps kernel.spawn(coreutils|php, ...) behind a natural FS facade, serializing spawns through inFlight because BrowserKernel's onStdout/onStderr are constructor-time singletons with no per-pid routing. KernelLimitedPHPApi mirrors the CLI's shape (mkdir/writeFile/run/request/defineConstant + cookie jar), backed by the spawn adapter and the bridge sendRequest.
KernelPlaygroundWorkerEndpoint orchestrates boot (prepareWordPressZips → buildVfsImage → bootKernelWordPress → ensureWordPressInstalled), forwards every iframe HTTP request to the in-kernel nginx via requestStreamed (scope strip → origin- form → Host + x-playground-absolute-url injection → COEP/COOP/ CORP injection), and exposes LimitedPHPApi over Comlink via the late-binding stubs pattern so the iframe sees methods the moment KernelLimitedPHPApi is ready.
bootPlaygroundRemote registers the existing service worker, spawns the Comlink worker endpoint and exposes playgroundApi with progress/nav/goTo and URL helpers — mirroring the classic remote's iframe-side surface. remote-posix-kernel.html is the sibling entry the vite middleware aliases /remote.html to.
vite.posix-kernel.config.ts (remote) aliases the kernel binary URLs, rewrites /remote.html to /remote-posix-kernel.html, and serves the iframe with COEP require-corp / COOP same-origin / DIP / Service-Worker-Allowed so SharedArrayBuffer is available to the worker. The website wrapper config overlays COEP credentialless + COOP same-origin on the parent document so the iframe can be cross-origin isolated. Wired through nx targets and a `dev:experimental-posix-kernel` script — additive only, the classic `npm run dev` path is untouched.
- Switch submodule re-exports in `host-bridge.ts` / `vfs-builder.ts` to a `@wasm-posix-kernel/*` alias so TS doesn't descend into the submodule source (strict tsconfig vs. submodule's looser config). - Vite alias in `vite.posix-kernel.config.ts` resolves the same specifiers at runtime against `WASM_POSIX_KERNEL_DIR` or the bundled submodule. - New `wasm-posix-kernel.d.ts` declares opaque `any` shims for the alias plus Vite asset-suffix wildcards (`?url`, `?raw`, `?worker&url`) and the growable `SharedArrayBuffer` 2-arg form. - Drop now-unneeded `@ts-ignore` / `eslint-disable` comments at the kernel-mode import sites. - Replace the `Function` cast in `boot-playground-remote.ts` with an `as any` indexer; the dynamically-bound Comlink methods aren't in `keyof KernelPlaygroundWorkerEndpoint`. - Annotate `BrowserKernelOptions` callback params locally in `boot.ts` since the alias surface is `any`.
897ff47 to
45b4dde
Compare
The kernel-mode `BrowserKernel` (re-exported via `posix-kernel/host-bridge.ts`) imports `@rootfs-vfs?url` to overlay `/etc/*` onto the SAB-backed VFS. The kernel's own `examples/browser/vite.config.ts` defines this alias, but the playground's `resolveKernelBinariesPlugin` only handled `@kernel-wasm` and `@kernel-binary/*` — Vite would fail the worker compile with "Failed to resolve import @rootfs-vfs", surfacing in the browser as a cryptic "WebWorker failed to load". Add a `@rootfs-vfs` branch that maps to `<kernelDir>/host/wasm/rootfs.vfs` and throws a build-hint error when the file is missing, mirroring the kernel's own resolver behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a worker-owned CookieJar shared between requestStreamed (iframe nav) and KernelLimitedPHPApi.request (blueprint playground.request). Chrome's response-header guard drops Set-Cookie from synthetic SW responses, so cookie state cannot ride the browser jar — the kernel worker injects the Cookie header outbound, ingests Set-Cookie inbound, and strips it from the response handed to the SW. Wires the auto-login mu-plugin into the kernel VFS (mirroring the CLI's ensureAutoLoginMuPlugin) so the `login` blueprint step's PLAYGROUND_AUTO_LOGIN_AS_USER constant turns into a real WordPress session on first request. Adds the kernel-mode Playwright suite: dedicated playwright config, nx target, CI job (chromium/firefox/webkit × 3 shards, marked continue-on-error while the suite stabilizes), and testIgnore in the classic playwright config so the two suites don't collide. Includes cookie-jar unit tests covering serialize, ingest (upsert, Max-Age=0, past/future Expires, attribute ignoring), and ingestAll (comma-joined, Expires-internal commas, \n-joined, mixed shapes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PHP's proc_open() shells out via `/bin/sh -c '<cmd>'`, which resolves
the program name through the child's PATH. The kernel boot env sets
PATH=/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin, but the VFS only
contained php-fpm + nginx + coreutils — no `php` or `less` binary on
disk anywhere on PATH. Tests calling `proc_open('less', ...)` and
`proc_open('php -r ...')` failed with exit=127 and stderr
"sh: 1: php: not found".
Fetches php.wasm and less.wasm in vfs-builder.ts and writes them at
/usr/local/bin/php and /usr/bin/less (upstream demo conventions in
wasm-posix-kernel/examples/browser/pages/php/main.ts and
shell-vfs-build.ts). The php.wasm fetch overlaps with the existing
worker-endpoint fetch for the host-side KernelSpawnAdapter; both hit
the same URL so the browser cache dedupes.
Unblocks blueprints.spec.ts:25 (spawning less) and blueprints.spec.ts:59
(proc_open(php) three times in a row) in the kernel-mode Playwright
suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ive URL Against the kernel-mode dev server (baseURL http://127.0.0.1:5400/website-server/), Playwright's goto('/?...') resolves to http://127.0.0.1:5400/?... and 404s, because the website Vite proxy regex ^[/]((?!website-server).) forwards any /<one-char>... path to the remote dev server which has no route for it. Bare goto('/') survives via Vite's base redirect (/ -> /website-server/), but adding a query string defeats that redirect. Classic CI hides the same bug by using playwright.ci.config.ts with baseURL http://127.0.0.1/ and a production preview server at root; kernel mode has no production preview, so it always hits the dev-config mismatch. The fix is to use the relative form ./?blueprint-url=... so the URL resolves against the /website-server/ baseURL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacked kernel-mode improvements across vfs-builder and the worker endpoint: - phpinfo preload, loaded via the FPM pool's auto_prepend_file (php-fpm runs with -c /dev/null, so php.ini-level prepend won't fire) - PHP-only mode triggered by `preferredVersions.wp: false`: VFS skips wp-config, mu-plugins, and the SQLite drop-in; boot/install drives gated by shouldBootWordPress / shouldInstallWordPress - WP version switching: getMinifiedWordPressVersions() Comlink method for the Site Manager dropdown - Default WP version normalization: 'nightly' aliases to 'trunk'; everything else not in the minified bundle (including 'latest') falls back to LatestMinifiedWordPressVersion - Default to pretty permalinks after install so WP 6.7+'s wp_redirect_xml_sitemap and rewrite-rule-driven URLs work - /sitemap.xml -> /wp-sitemap.xml redirect as a kernel preload (mirror of classic mode's mu-plugin) - fpm-router re-attaches the scope path to REQUEST_URI before any include so WP-internal URL builders (auth_redirect, redirect_canonical, ...) agree with home_url(); $file is resolved from the unscoped URI above so per-file resolution is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngPage
Two changes are needed together because `WP_Customize_Manager::setup_theme()`
runs `current_user_can('customize')` during the `setup_theme` action — before
`init` fires — so the auto-login hook (init priority 1) never gets a chance
to set the auth cookies when customize.php is the Blueprint landingPage.
- Add `playground_auto_login_redirect_target` to the kernel auto-login
mu-plugin (mirror of classic mode's helper): on
`/index.php?playground-redirection-handler&next=...`, redirect to the
`next` URL after `playground_auto_login` has run on the same request.
- Drop the `goTo` unwrap that bypassed the redirection-handler URL. The
workaround predated kernel-mode auto-login and was the only thing
preventing the intermediate hop the redirect target depends on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…backfill through getWordPressModuleDetails Replaces the kernel-mode WP download with the same Vite-bundled wp-X.Y.zip classic mode uses, removing the long upstream downloads.w.org fetch from the kernel hot path. The single source switch surfaced three latent assumptions in the VFS-build pipeline that each needed fixing for the bundled zip to land cleanly: - prepare-wordpress.ts: branch on MinifiedWordPressVersionsList.includes(wpVersionQuery). Numeric and beta versions go through getWordPressModuleDetails(...) for a same-origin Vite-resolved URL (direct fetch, no proxy). `trunk` still returns an https://github.com/.../master.zip and falls back to resolveWordPressRelease + maybeProxyUrl. The endpoint caller already coerces non-MinifiedList values to LatestMinifiedWordPressVersion, so the bundled branch fires for every numeric/latest test today. - vfs-builder.ts: the bundled wp-X.Y.zip is FLAT (entries start at the archive root: index.php, wp-includes/, wp-content/). The previous hard-coded stripLeadingDir: 'wordpress' dropped every entry. Plumb a wpZipStripLeadingDir? option through PrepareWordPressZipsResult and BuildVfsImageOptions; pass undefined for the bundled branch, 'wordpress' for the resolveWordPressRelease branch. - vfs-builder.ts: the bundled wp-X.Y.zip is "minified" (~1669 entries fewer than the companion wordpress-static.zip — admin CSS/JS, theme screenshots, editor styles). After fetching the WP zip, prepare-wordpress.ts also fetches /<wpVersionToStaticAssetsDirectory>/ wordpress-static.zip and returns it as wpStaticZipBytes. vfs-builder extracts it over /var/www/html with a new noOverwrite option on extractZipIntoVfs (gated by a pathExists helper that wraps fs.stat), matching classic's unzipFile(..., noOverwrite=true) semantics so the bundled archive wins on overlap. ~50 MB uncompressed; the 128 MB initial SAB auto-grows to the 256 MB max as needed. - vfs-builder.ts: the bundled wp-X.Y.zip ships its own placeholder wp-config.php at the archive root, which clobbered the kernel's tailored wp-config.php (written before the extract). The downloads.w.org release zip only ships wp-config-sample.php, so the order-of-ops was harmless pre-bundle. The SQLite drop-in masked the bad DB constants for admin paths, but front-end rendering needs the rest of WP_CONFIG_PHP — hence the empty-body iframe in smoke, sitemap, login-page, and blueprint-url tests. Fix: add wp-config.php to the WP extract's exclude predicate. No-op for the release-zip branch (no wp-config.php to exclude); load-bearing for the bundled branch. Net: 19/19 baseline 19/19 (2.4 min, down from 3.2 min with the proxy hop), and website-ui workers=3 at 25/3/7 (improving the prior band-aid's 23/5; the remaining 3 failures are out-of-scope React UI issues unrelated to the kernel). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt + wp-config marker + fpm-router rewrite) Three small changes wire subdirectory multisite end-to-end. Recovers blueprints.spec.ts:209 (enableMultisite) and :892 (subdir multisite wp-admin URL) — part of the §5.4 multisite cluster. - enable-multisite.ts: the port check fires for http://127.0.0.1:5400/<scope>/, but the port in the iframe URL never reaches PHP — the step itself forces HTTP_HOST=url.hostname (sans port) and writes siteurl/home through url.hostname. Relax the check for loopback hosts (localhost, 127.0.0.1, [::1]); keep it for public hosts where the port would actually leak into WP's URL semantics. Classic CI uses port 80 (url.port === '') and is unaffected. New unit tests cover both the loopback bypass and the public-host rejection. - vfs-builder.ts: WP_CONFIG_PHP now ships the canonical `/* That's all, stop editing! Happy publishing. */` marker. wp-cli's `core multisite-convert` inserts MULTISITE, SUBDOMAIN_INSTALL, DOMAIN_CURRENT_SITE etc. immediately above the marker; without it, wp-cli silently appends after `require_once wp-settings.php` and the constants never load. - vfs-builder.ts: teach fpm-router the subdir-multisite file-lookup rewrite. `/<slug>/wp-(admin|content|includes)/…` and `/<slug>/<file>.php` now resolve the file lookup against the unslugged path while REQUEST_URI keeps the /<slug>/ prefix so WP's ms-load.php dispatches to the right subsite. Mirrors the .htaccess rules WP emits for subdir multisite installs. Not in scope: blueprints.spec.ts:175 (re-activate plugins) still fails on a pre-existing installPlugin bug — the wordpress.org/plugins resource sets File.name to the human-readable string ("Hello dolly") and installAsset uses that as the destination folder when the upstream zip ships .php files at the archive root. Independent of multisite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 22, 2026
…ll.php Drop DISALLOW_FILE_MODS from the kernel-mode mu-plugin so admins reach plugin-install.php instead of "Sorry, you are not allowed to access this page", and wire a plugins_api_result filter that surfaces the "Network access is an experimental, opt-in feature" notice classic mode shows when WP_HTTP requests fail. Closes the two query-api kernel-mode failures (networking=no notice, networking=yes page load). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…S reload
§3.4 — close the website-ui kernel-mode suite:
- Focus `.cm-content` (the contenteditable) so keystrokes land on the
CodeMirror editor rather than `<body>`; align file-tree paths with
the kernel-mode docroot at `/var/www/html`.
- Skip Adminer (kernel-mode libsqlite3 lacks
SQLITE_ENABLE_COLUMN_METADATA) and phpMyAdmin (ZipArchive).
§3.6 — close the loose tail of website specs by citing the upstream
gap: `playground.cli()` (kernel boot skips cli() plumbing),
Gutenberg-driven client-side media + Document-Isolation-Policy tests
(both blocked by ZipArchive on Gutenberg install), and the
shutdown-loopback-prefetch test (kernel-mode prefetchUpdateChecks() is
a no-op stub).
§3.5 — implement OPFS reload (opfs-to-memfs) end-to-end:
- `mountOpfs` now dispatches on `initialSyncDirection`; the
`opfs-to-memfs` branch walks OPFS via `dir.entries()`, packs every
file into the same `uint32 LE`-framed wire stream the forward path
emits, and pipes it as `body:` (stdin) to a single `api.run({code,
body})` spawn. Single-spawn preserves the OOM ceiling — a stock WP
install is ~10k files and ~30k per-file spawns would crash the
kernel worker. The PHP unpacker uses a `read_exact` closure because
pipe `fread` can return partial buffers and silently misalign the
wire stream on a >8KB file.
- `KernelWorkerBootOptions.mounts` carries saved-site mounts from the
host through to the kernel endpoint; `doBoot` iterates them after
the API is bound and BEFORE the install probe so a fully-installed
saved site short-circuits `ensureWordPressInstalled`. Only
`opfs-to-memfs` mounts are processed at boot — `memfs-to-opfs` saves
are initiated later via `persistTemporarySite`.
- Boot-remote: expose explicit `mountOpfs` / `flushOpfs` /
`unmountOpfs` wrappers so the iframe-side proxy's `onProgress`
callback round-trips through Comlink's `FUNCTION` handler instead of
crashing with `DataCloneError`.
Constants fix — `defineConstant` now works for non-WP PHP entry
points: added `/internal/shared/preload/playground-defines.php` (loaded
via php-fpm's `auto_prepend_file`) so blueprints that combine
`constants: {…}` with a plain-PHP landing page see their constants.
The mu-plugin only fires from `wp-settings.php`; the new preload reads
the same JSON store and guards with `if (defined($name)) continue;`,
so both are harmless when they coexist on a WP request. Mirrors
classic mode's `consts.json` auto-prepend mechanism in `php.ts`.
Two follow-on fixes layered on top of the auto-prepend:
- `defineConstant` serializes its `regenerateDefinesPlugin` writes
via a private chain so back-to-back unawaited calls (the v1 `login`
step at `blueprints/src/lib/steps/login.ts:43` fires without
`await`) can't leave the older value on disk;
`flushPendingDefines()` exposes the chain so
`KernelPlaygroundWorkerEndpoint.requestStreamed` drains it at
entry. Iframe navs (the v1 runner's
`/index.php?playground-redirection-handler` redirect) reach the
bridge here — NOT through `KernelLimitedPHPApi.request` — so
without the flush the auto-login mu-plugin could read a stale
`PLAYGROUND_AUTO_LOGIN_AS_USER`. Unblocks
`blueprints.spec.ts:888 should login a non-admin user`.
- `WP_DEBUG` / `WP_DEBUG_LOG` / `WP_DEBUG_DISPLAY` in `WP_CONFIG_PHP`
are now wrapped in `if (!defined(...))` guards (mirroring the CLI's
wp-config template). Without them, a blueprint that overrides any
of these via `defineWpConfigConsts` triggers a PHP redefine
warning; with `WP_DEBUG_DISPLAY` also set to `true`, the warning
prints to the response body, sends headers early, and breaks the
auto-login redirect. Unblocks `blueprints.spec.ts:987 WordPress
homepage loads when mu-plugin prints a notice`.
withNetworking plumbing — forwarded `KernelWorkerBootOptions.with
Networking` through to the VFS image: `populatePhpFpmConfig` now
appends a php-fpm overrides block that sets `allow_url_fopen = 0` and
`disable_functions = curl_exec,curl_multi_exec`, mirroring the php.ini
surface classic mode flips off.
Vite dev-server plumbing — both the website and the remote configs
rewrite the kernel worker's hardcoded `/cors-proxy?url=<encoded>` dev
shape (baked into the wasm-posix-kernel worker entry) onto
`/cors-proxy.php?<encoded>` so outbound HTTPS from kernel-resident PHP
actually reaches the proxy. The remote config also serves the
`@wp-playground/client` source at `/client/index.js` so `<php-snippet>`
embeds load it without a built `dist/client/`; the matching test-side
shim in `php-code-snippet.spec.ts` was dropped.
Test surface — `opfs.spec.ts:110` (PHP constants round-trip via OPFS)
and `:174` (rename + reload) now pass in kernel mode. The two ZIP-
import tests (`:458`, `:552`) re-skip citing the ZipArchive cluster:
OPFS reload itself works, but `importWordPressFiles → unzip` fatals
and the overlay's catch path leaves the dialog open. Spec changes in
`blueprints.spec.ts` and `query-api.spec.ts` likewise close their
remaining failing tests by citing the kernel-mode ZipArchive / curl
gaps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
Adds an experimental
dev:experimental-posix-kernelmode to the Playground website that boots WordPress in an iframe under nginx + PHP-FPM running on wasm-posix-kernel, instead of the existing PHP.wasm Asyncify/JSPI runtime.This PR is stacked on top of #3604 (the CLI-side proof of concept). The first 15 commits in this PR belong to that PR; the last 7 are the new web work — see "Commits" below. Like #3604, this is for visibility / posterity, not ready to merge: it depends on the same five upstream-pending kernel fixes tracked in mho22/wasm-posix-kernel#50.
The constraint while implementing was additive only — every change is a new file under
packages/playground/remote/orpackages/playground/website/, plus three minimal sibling edits (package.jsonscript, twoproject.jsontargets, one new vite config per package). The classicnpm run devpath is untouched and continues to boot Playground exactly as before.What this adds
A new
npm run dev:experimental-posix-kernelscript that:http://127.0.0.1:5400/website-server/with COEPcredentialless+ COOPsame-originoverlaid on the parent document, so an embedded iframe can be cross-origin isolated and use SharedArrayBuffer.:4400with COEPrequire-corp, COOPsame-origin, DIPisolate-and-require-corp,Service-Worker-Allowed: /, and aliases/remote.htmlto a sibling/remote-posix-kernel.htmlentry.BrowserKernel+HttpBridgeHost, pollswaitForNginxuntil any HTTP status comes back,LimitedPHPApivia aKernelLimitedPHPApishim so existing v1 blueprint steps work against the kernel-resident WordPress.requestStreamed(scope strip → origin-form → Host +x-playground-absolute-urlinjection → COEP/COOP/CORP header injection on the response).Demo:
Commits (web port)
Only the last 7 commits are new in this PR — the first 15 are inherited from #3604:
993af91Experimental posix-kernel (web): nginx + PHP-FPM VFS imagebd5e063Experimental posix-kernel (web): WordPress preparation869845eExperimental posix-kernel (web): kernel boot orchestrator57edf38Experimental posix-kernel (web): KernelLimitedPHPApi shimad93171Experimental posix-kernel (web): Comlink worker endpointf2b71deExperimental posix-kernel (web): iframe boot + entry HTML5abc1c4Experimental posix-kernel (web): Vite configs + nx targetWhy this is experimental
wakeBlockedPollsnapshot, host chown error swallowing, node-kernel-worker-entry bundle, PHP wasm size optimization) tracked in mho22/wasm-posix-kernel#50.DO NOT MERGE - CI artifact branchinwasm-posix-kernelbecause release-tag artifacts aren't fetched by the in-tree workflow yet. The submodule needs its own upstreaming before this PR is mergeable.loginblueprint step relies on a cookie-setting mu-plugin inpackages/playground/wordpress/src/index.ts. Kernel mode hasKernelLimitedPHPApi.defineConstant+ aplayground-defines.phpmu-plugin to injectPLAYGROUND_AUTO_LOGIN_AS_USER, but the cookie-setting half isn't wired. A defensivegoTounwrap inboot-playground-remote.tsshort-circuits the V1 redirection-handler URL in the meantime; once auto-login lands the unwrap must be removed.downloads.w.org/ GitHub, outside our origin, so site-data clears don't touch it). DevTools → "Disable cache" boots cleanly. Incognito is the workaround for now.Notable design choices
credentiallessso cross-origin sub-resources (analytics, Octokit) keep loading without CORP. WordPress front-end responses don't ship COEP themselves (onlywp_set_up_cross_origin_isolationon wp-admin pages), sorequestStreamedinjects COEP/COOP/CORP on every bridge response before constructing thePHPResponse.x-playground-absolute-urlon every bridge request. wp-config reads the header and uses it asWP_HOME/WP_SITEURL— avoids a boot-time substitution pass. nginx doesn't auto-forward arbitrary headers to fastcgi, so bothlocationblocks in the in-VFS nginx config explicitly pass it through.requestStreamedstrips the scope and reduces to origin-form before posting to the bridge. Absolute-URI form hangs nginx — RFC 7230 §5.3.2 allows it on origin servers but the kernel-resident nginx doesn't./cors-proxy.php?instead of/cors-proxy/?.offline-mode-cache.ts:shouldCacheUrlreturns true for cors-proxy URLs (same-origin in dev), routing them throughcacheFirstFetchwhich truncates around 19 MiB on HTTP origins. Pathnames ending in.phpshort-circuit the cache, so the.php?form bypasses it; the vite cors-proxy prefix still forwards because the rewrite no-ops for.php?paths.LimitedPHPApiover Comlink. The worker endpoint installs throwing stubs for every name inLIMITED_PHP_API_METHODSat construction;doBootconstructs aKernelLimitedPHPApiand rebinds those names viabindApiMethods. Comlink resolves method paths at message-receive time, so post-boot rebinding is visible to the iframe immediately.waitForNginxpolls because dinit returns before services bind.kernel.boot()resolves as soon as the kernel itself is ready, not when dinit's child services have bound their sockets. The boot orchestrator pollsGET /until any HTTP status comes back (404 counts — nginx is up).BrowserKernel'sonStdout/onStderrare constructor-time singletons (no per-pid routing). The boot installs a single-slotactiveCapturehandler that the spawn adapter swaps in before eachkernel.spawnand clears infinally; the adapter'sinFlightpromise serializes spawns so the global slot is unambiguous.Open follow-ups (deliberately out of scope)
@wp-playground/wordpress, then remove thegoTounwrap inboot-playground-remote.ts).cache: 'no-store'onfetchZipBytes.wasm-posix-kernelsubmodule with an npm-installable package — same blocker as [wasm-posix-kernel] WordPress Playground CLI under wasm-posix-kernel - Proof of concept #3604.Test plan
The 25 existing
playground-remoteunit tests continue to pass:Manual boot in incognito Chrome:
The iframe should render the WordPress front page end-to-end.
🤖 Generated with Claude Code