Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"generate:cloudflare-canonical-mdi-seed": "npm run generate:cloudflare-mdi-runtime-bundle && php scripts/build-cloudflare-canonical-mdi-seed.php",
"smoke": "tsx scripts/run-smoke.ts",
"test:redaction": "tsx tests/redaction.test.ts",
"test:cloudflare-runtime": "tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
"test:cloudflare-runtime": "tsx tests/cloudflare-site-context.test.ts && tsx tests/cloudflare-coordinator-site-partitioning.test.ts && tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
"test:cloudflare-wordpress-auth": "tsx tests/cloudflare-wordpress-auth.test.ts",
"test:cloudflare-wordpress-archive-corpus": "tsx tests/cloudflare-wordpress-archive-corpus.test.ts",
"test:agent-task-contracts": "tsx tests/agent-task-contracts.test.ts && tsx tests/agent-task-canonical-evidence.test.ts && npm run test:agent-task-workflow-interface && npm run test:runtime-sources-materialization && tsx tests/agent-task-reusable-workflow.test.ts",
Expand Down
16 changes: 11 additions & 5 deletions packages/runtime-cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ This candidate integration for [wp-codebox#1838](https://github.com/Automattic/w

## Runtime Architecture

The entry Worker executes PHP-WASM and WordPress. Runtime behavior depends on the typed `RevisionCoordinator` contract rather than a Cloudflare storage product. The standard `worker-do.ts` entrypoint injects the existing `WordPressStateCoordinator` Durable Object adapter. The ChatGPT Sites-compatible `worker-d1.ts` entrypoint injects a D1 adapter that stores only the current pointer, version, lease token, lease base, and expiry in one conditional-update row. Both adapters serialize the same bounded lease and CAS promotion semantics; neither imports or instantiates PHP-WASM. MDI, disposable SQLite, and canonical R2 storage are unchanged.
The entry Worker executes PHP-WASM and WordPress. Runtime behavior depends on the typed `RevisionCoordinator` contract rather than a Cloudflare storage product. The standard `worker-do.ts` entrypoint injects the existing `WordPressStateCoordinator` Durable Object adapter. The ChatGPT Sites-compatible `worker-d1.ts` entrypoint injects a D1 adapter that stores only the current pointer, version, lease token, lease base, and expiry in one conditional-update row per site. Both adapters serialize the same bounded lease and CAS promotion semantics; neither imports or instantiates PHP-WASM. MDI, disposable SQLite, and canonical R2 storage are unchanged.

On cold start, the entry Worker uses the acquired pointer to rebuild PHP-WASM's disposable SQLite index from canonical MDI Markdown and JSON files. A missing pointer materializes the packaged canonical MDI seed and boots one PHP-WASM primary runtime. The build-time PHP CLI generator creates that archive from `wordpress-install-seed.sqlite` through MDI's public `bootstrap_existing_cache()` API, validates its pinned MDI revision and input digest, and never packages SQLite. The runtime updates `siteurl` and `home` through WordPress APIs using the request origin and sets the admin password from `WORDPRESS_ADMIN_PASSWORD`; only WordPress's password hash is canonical. Bootstrap persists and CAS-promotes this mutation before serving the next request.

Canonical browser, health, and mutation boots require the separately managed `WORDPRESS_AUTH_SECRET` Worker secret. The entry Worker derives the eight WordPress auth keys and salts from that secret with a versioned, site-scoped (`default`) SHA-256 domain separator before `wp-load.php`. It never logs, persists, or returns the secret or derived values. Configure it independently from the bootstrap password with `wrangler secret put WORDPRESS_AUTH_SECRET --config packages/runtime-cloudflare/wrangler.jsonc`; rotating `WORDPRESS_ADMIN_PASSWORD` does not rotate authentication salts or invalidate sessions.
Canonical browser, health, and mutation boots require the separately managed `WORDPRESS_AUTH_SECRET` Worker secret. The entry Worker derives the eight WordPress auth keys and salts from that secret with a versioned, site-scoped SHA-256 domain separator before `wp-load.php`. It never logs, persists, or returns the secret or derived values. Configure it independently from the bootstrap password with `wrangler secret put WORDPRESS_AUTH_SECRET --config packages/runtime-cloudflare/wrangler.jsonc`; rotating `WORDPRESS_ADMIN_PASSWORD` does not rotate authentication salts or invalidate sessions.

After each mutating HTTP request, the entry runtime invokes MDI's explicit request-boundary flush, collects canonical files, stores immutable content-addressed R2 objects and a revision manifest, then commits the new pointer through the held lease. GET, HEAD, and asset requests release without promotion. Failed requests abort their leases; stale leases recover by token/version/expiry checks. The entry isolate can cache one runtime only for the exact acquired pointer revision and exits it after promotion or when another isolate advances the pointer. It does not persist SQLite. Existing manifests are reused when canonical file hashes have not changed. Canonical mutations such as login session-token writes do not rebuild a publication unless WordPress reports explicit affected routes or a global publication change.

Anonymous HTML responses are stored as create-once, host-independent R2 artifacts under their canonical revision. An authorized `POST ?phase=operator-publish` establishes or replaces a bounded route set. Once that publication exists, WordPress hooks report the public routes affected by post, term, menu, theme, plugin, and relevant option mutations. The Worker renders only those routes in the already-mutated PHP runtime, stages immutable page artifacts and a version 2 publication descriptor, commits canonical state through the coordinator, then conditionally promotes `sites/default/publications/current.json` against its prior R2 ETag. Unchanged route entries retain their older canonical artifacts. A promotion conflict or post-commit R2 failure leaves the prior publication readable and is exposed through `x-wp-codebox-publication`; it never rolls back already-committed WordPress state. Published GET and HEAD requests check a 60-second edge entry and then R2 before coordinator construction, while authenticated, preview, admin, REST, and unpublished routes continue through WordPress. The version 2 reader accepts existing version 1 descriptors during migration. This provides bounded automatic route compilation and local edge invalidation, not a global CDN purge pipeline.
Anonymous HTML responses are stored as create-once, host-independent R2 artifacts under their canonical revision. An authorized `POST ?phase=operator-publish` establishes or replaces a bounded route set. Once that publication exists, WordPress hooks report the public routes affected by post, term, menu, theme, plugin, and relevant option mutations. The Worker renders only those routes in the already-mutated PHP runtime, stages immutable page artifacts and a version 2 publication descriptor, commits canonical state through the coordinator, then conditionally promotes `sites/{siteId}/publications/current.json` against its prior R2 ETag. Unchanged route entries retain their older canonical artifacts. A promotion conflict or post-commit R2 failure leaves the prior publication readable and is exposed through `x-wp-codebox-publication`; it never rolls back already-committed WordPress state. Published GET and HEAD requests check a 60-second site-scoped edge entry and then R2 before coordinator construction, while authenticated, preview, admin, REST, and unpublished routes continue through WordPress. The version 2 reader accepts existing version 1 descriptors during migration. This provides bounded automatic route compilation and local edge invalidation, not a global CDN purge pipeline.

The same revision transaction persists bounded user-managed files under `wp-content/plugins`, `themes`, `languages`, and `mu-plugins`. Runtime-owned MDI, SQLite integration, and Codebox adapter files remain reconstructable artifacts and are excluded. Unchanged bundled-theme files are omitted by release hash, while modified files become canonical overrides. Public canonical plugin and theme assets serve directly from revision-addressed R2 cache entries before the immutable release corpus. Nonce-protected `wp-admin` GET actions such as plugin activation are classified as mutations so their filesystem and MDI option changes commit atomically. Existing revisions without `wpContent` remain valid.

The Worker forwards browser cookies directly to Playground and disables Playground's internal cookie store, preventing an empty per-isolate store from replacing a valid browser session after cold restart.

## Site Identity

The Worker resolves a validated `SiteContext` from the exact request hostname before reading a cache or constructing a coordinator. `WORDPRESS_SITE_CONTEXTS` is a JSON array of `{ "id", "hostname", "origin" }` records. Site IDs use lowercase letters, digits, and single hyphens; hostnames must be canonical lowercase DNS names or loopback addresses; origins must match the configured hostname and contain no path, query, or credentials. HTTPS is required except for loopback and `*.localhost` development origins. Unknown hostnames fail with `421` and never fall back to another site.

Every mutable R2 key is rooted below `sites/{siteId}/`, D1 tables partition rows by site ID, Durable Object names include the site ID, and runtime and publication caches include it in their identity. The existing production hostname remains explicitly mapped to `default`, preserving its `sites/default/...` objects and credentials. Non-default sites derive distinct operator tokens and bootstrap admin passwords from the corresponding root Worker secrets with a versioned HMAC domain separator; the mapping itself contains no credentials. WordPress auth keys and salts are independently site-derived from `WORDPRESS_AUTH_SECRET`.

A minute Cloudflare Cron Trigger first drains at most one publication route, or otherwise drains at most five due WordPress events or 25 seconds of work. Canonical mutations persist immutable state, enqueue a revision- and coordinator-version-bound publication job, and return `x-wp-codebox-publication: queued` when public routes need work; they never render routes in the mutation request. A publication job boots only its committed canonical revision, writes one immutable page snapshot per scheduled invocation, then conditionally promotes a complete descriptor. A stale job cannot overwrite a later canonical version, and failed work leaves the prior publication readable for retry. Direct `/wp-cron.php` requests are disabled so callbacks cannot bypass that transaction boundary.

The bundled MDI source is pinned to immutable commit `bf6d434d1673fdd86d777501f7eaec292d32ad1f`, including [MDI PR #141](https://github.com/Automattic/markdown-database-integration/pull/141). The bundle generator, worker provenance, and source-contract test use the same revision.
Expand All @@ -34,7 +40,7 @@ Use authenticated `GET ?phase=operator-fence-status` after acquisition to export

## Static Artifact Import

An authenticated `POST ?phase=operator-static-artifact-import` materializes a verified website artifact into the existing canonical site. The request is limited to 16 KiB and references an immutable JSON object at `sites/default/import-artifacts/<sha256>.json`; the Worker requires the declared R2 key, size, and SHA-256 to agree before acquiring a coordinator lease. The canonical `blocks-engine/php-transformer/site-artifact/v1` payload is limited to 4 MiB serialized, 500 safe unique files, 8 MiB per decoded file, and 32 MiB decoded in aggregate.
An authenticated `POST ?phase=operator-static-artifact-import` materializes a verified website artifact into the existing canonical site. The request is limited to 16 KiB and references an immutable JSON object at `sites/{siteId}/import-artifacts/<sha256>.json`; the Worker requires the declared R2 key, size, and SHA-256 to agree before acquiring a coordinator lease. The canonical `blocks-engine/php-transformer/site-artifact/v1` payload is limited to 4 MiB serialized, 500 safe unique files, 8 MiB per decoded file, and 32 MiB decoded in aggregate.

The import transaction boots a dedicated runtime with the pinned Static Site Importer v1.3.4 archive (`8d27286021d7c6141609def40a97591322a14340b23a17d9405f7919ea145a29`) from R2, invokes its public `static-site-importer/import-website-artifact` ability as the operator-authorized administrator, and requires the canonical quality gate plus zero fallback, core HTML, freeform, and invalid blocks. SSI and its MU loader are runtime-owned and excluded from canonical mutable `wp-content`; generated themes, pages, options, and assets persist normally through MDI/R2. Failed or partial imports discard the PHP runtime without committing. Successful imports return compact provenance and canonical revision/version headers, enqueue affected publication routes, and store at most 20 idempotency receipts in canonical options. Exact replay returns the existing receipt without a new revision; reuse with different input returns a conflict.

Expand All @@ -46,6 +52,6 @@ SSI is extracted only for import requests, so normal browser, mutation, publicat
2. Run `npm run provision:cloudflare-wordpress-runtime-corpus -- --local --persist-to <directory>` to verify and upload all exact content-addressed artifacts into isolated local R2 storage. For an authorized deployment, run the provisioner with `--remote` and require every upload to succeed before deploying the Worker that imports their manifests.
3. Run `npm run test:cloudflare-runtime` for routing, coordinator composition, canonical-state, artifact validation, source contract, and TypeScript coverage.
4. Run `npm run cloudflare:dry-run` and `npm run cloudflare:dry-run:d1` to compile the Durable Object and D1 profiles without creating Cloudflare resources. The placeholder D1 database ID is for local/dry-run verification; an implementation supplies its provisioned binding at deployment.
5. Run `npm run cloudflare:local-gate` and `npm run cloudflare:local-gate:d1` for the same isolated workerd workflow through both coordinator implementations. Each gate generates and provisions all artifacts, verifies the selected backend through the state envelope, injects stable test-only admin-password, auth-secret, and operator-token values, uploads and activates a real plugin ZIP, establishes an anonymous homepage and canonical-permalink publication, updates a published post through authenticated REST without immediate rendering, restarts with pending publication work, and proves bounded scheduled draining eventually exposes the updated permalink through coordinator-free R2. The cron gate also proves that a scheduled post becomes readable from the publication path without an operator call. Restart verification requires the immutable R2 publication, while the remaining coverage includes login, concurrent canonical writes, media, representative frontend/admin/editor assets, PHP diagnostics, session recovery, and a fresh login after restart.
5. Run `npm run cloudflare:local-gate` and `npm run cloudflare:local-gate:d1` for the same isolated workerd workflow through both coordinator implementations. Each gate generates and provisions all artifacts, verifies the selected backend through the state envelope, injects stable test-only admin-password, auth-secret, and operator-token values, uploads and activates a real plugin ZIP, establishes an anonymous homepage and canonical-permalink publication, updates a published post through authenticated REST without immediate rendering, restarts with pending publication work, and proves bounded scheduled draining eventually exposes the updated permalink through coordinator-free R2. The cron gate also proves that a scheduled post becomes readable from the publication path without an operator call. Restart verification requires the immutable R2 publication. A final two-host phase proves isolated mutations, coordinator versions, REST collections, credentials, publications, and caches plus fail-closed unknown-host routing. The remaining coverage includes login, concurrent canonical writes, media, representative frontend/admin/editor assets, PHP diagnostics, session recovery, and a fresh login after restart.

This document describes local candidate verification only. It does not claim remote deployment.
Loading
Loading