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
1 change: 1 addition & 0 deletions pkgs/id/nix-common.nix
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ let
# Web development tools
bun # JavaScript bundler and runtime (required for web builds)
typescript # TypeScript for type checking # nodepackages remoed 2026-04-03
wrangler # Cloudflare Worker CLI (worker-poc dev, #2271)

# E2E testing (Playwright) — use playwright-driver.browsers for
# pre-patched browser binaries that work with Playwright's CDP protocol.
Expand Down
170 changes: 170 additions & 0 deletions pkgs/id/tests/worker_poc_reference.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//! Native reference runner for the #2271 Worker-isolate PoC.
//!
//! Drives the checked-in example guests through the native wasmtime host
//! (`crate::sandbox` via the public `id::sandbox` API) over the exact
//! scripted sequences the isolate side runs in
//! `pkgs/id/worker-poc/compare/isolate-runner.mjs`, and emits one JSON line
//! per step: `{world, step, event, view, records, snapshot_hex}`. Step 0 is
//! the freshly instantiated state (event `null`); step N is the state after
//! the Nth scripted event.
//!
//! The comparison harness (`worker-poc/compare/compare.mjs`) diffs these
//! lines against the isolate host's, per step — that diff is the #2271
//! journal-equality evidence. This test also asserts the native host agrees
//! with itself across a forced cold start (fresh instance + `restore` of
//! the mid-sequence snapshot) for both worlds.
//!
//! Run standalone to regenerate the reference file:
//! WORKER_POC_REFERENCE_OUT=worker-poc/compare/native-reference.jsonl \
//! cargo test --test worker_poc_reference -- --nocapture

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]

use id::sandbox::{Sandbox, SandboxLimits};

const COUNTER_WASM: &[u8] = include_bytes!("../examples/roc-counter/counter.wasm");
const TIC_TAC_TOE_WASM: &[u8] = include_bytes!("../examples/tic-tac-toe/tic-tac-toe.wasm");

/// The #2271 scripted sequences, shared verbatim with the isolate runner.
/// Tic-tac-toe: X plays 0, O plays 4, X plays 1, O retries the occupied 4
/// (ignored), 9 is not a move (ignored), O plays 8, X plays 2 and wins,
/// 3 after the win is ignored.
fn sequences() -> Vec<(&'static str, &'static [u8], Vec<&'static [u8]>, usize)> {
vec![
("roc-counter", COUNTER_WASM, vec![b"inc", b"inc", b"inc"], 2),
(
"tic-tac-toe",
TIC_TAC_TOE_WASM,
vec![b"0", b"4", b"1", b"4", b"9", b"8", b"2", b"3"],
5,
),
]
}

fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push_str(&format!("{byte:02x}"));
}
out
}

fn json_escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if (ch as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", ch as u32)),
ch => out.push(ch),
}
}
out
}

fn step_line(
world: &str,
step: usize,
event: Option<&[u8]>,
instance: &mut id::sandbox::WorldInstance,
) -> String {
let view = String::from_utf8(instance.view().unwrap()).unwrap();
let records = instance
.records()
.unwrap()
.map(|bytes| String::from_utf8(bytes).unwrap());
let snapshot = instance.snapshot().unwrap();
let event_json = match event {
Some(event) => format!(
"\"{}\"",
json_escape(&String::from_utf8(event.to_vec()).unwrap())
),
None => "null".to_string(),
};
let records_json = match &records {
Some(records) => format!("\"{}\"", json_escape(records)),
None => "null".to_string(),
};
let snapshot_json = match &snapshot {
Some(snapshot) => format!("\"{}\"", hex(snapshot)),
None => "null".to_string(),
};
format!(
"{{\"world\":\"{world}\",\"step\":{step},\"event\":{event_json},\"view\":\"{}\",\"records\":{records_json},\"snapshot_hex\":{snapshot_json}}}",
json_escape(&view)
)
}

/// Run one sequence; when `cold_start_after` is `Some(step)`, restart from
/// that step's snapshot in a fresh instance before continuing.
fn run_sequence(
world: &str,
wasm: &[u8],
events: &[&[u8]],
cold_start_after: Option<usize>,
) -> Vec<String> {
let runner = Sandbox::compile(wasm, SandboxLimits::default()).unwrap();
let mut instance = runner.instantiate(0).unwrap();
let mut lines = vec![step_line(world, 0, None, &mut instance)];
for (index, event) in events.iter().enumerate() {
let step = index + 1;
if cold_start_after == Some(step - 1) {
let snapshot = instance.snapshot().unwrap().unwrap();
instance = runner.instantiate(0).unwrap();
instance.restore(&snapshot).unwrap();
}
instance.update(1, event).unwrap();
lines.push(step_line(world, step, Some(event), &mut instance));
}
lines
}

#[test]
fn worker_poc_reference_sequences() {
let mut all_lines: Vec<String> = Vec::new();
for (world, wasm, events, cold_start_after) in sequences() {
let uninterrupted = run_sequence(world, wasm, &events, None);
let cold_started = run_sequence(world, wasm, &events, Some(cold_start_after));
assert_eq!(
uninterrupted, cold_started,
"native host diverged across a forced cold start after step {cold_start_after} in {world}"
);
for line in &uninterrupted {
println!("{line}");
}
all_lines.extend(uninterrupted);
}

// The counter must actually count; guards against a vacuous equality
// pass over a guest that never mutated.
let counter_final = all_lines
.iter()
.filter(|line| line.contains("\"world\":\"roc-counter\""))
.last()
.unwrap();
assert!(
counter_final.contains("count=3"),
"counter final step does not show count=3: {counter_final}"
);
// Tic-tac-toe: X completes the top row (0,1,2); the final records must
// name X the winner with five plays on the board.
let ttt_final = all_lines
.iter()
.filter(|line| line.contains("\"world\":\"tic-tac-toe\""))
.last()
.unwrap();
assert!(
ttt_final.contains("\\\"winner\\\":\\\"X\\\""),
"tic-tac-toe final step does not record X as winner: {ttt_final}"
);

if let Ok(path) = std::env::var("WORKER_POC_REFERENCE_OUT") {
let mut text = all_lines.join("\n");
text.push('\n');
std::fs::write(&path, text).unwrap();
eprintln!("wrote {} reference lines to {path}", all_lines.len());
}
}
1 change: 1 addition & 0 deletions pkgs/id/worker-poc/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.wrangler/
89 changes: 89 additions & 0 deletions pkgs/id/worker-poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# worker-poc — plaza worlds in a Worker isolate (#2271)

PoC for developing-today/code#2271 (from the #2057 design): run one example
world **unmodified** in a Worker isolate (workerd / Durable Object with a
SQLite journal) and prove **journal equality** against the native wasmtime
host (`pkgs/id/src/sandbox.rs`): per-step `(view, records, snapshot)` +
snapshot hash over an identical scripted input sequence, including a forced
cold start mid-sequence.

Pre-registered semantic differences (findings note on #2271, posted before
implementation): no fuel metering in Workers (CPU-time bound instead), and
the hosted loader refuses over-ceiling / `portability=native` manifests up
front where the native host traps at run time. Equality is claimed on state
evolution only, never on execution-budget behaviour.

## Layout

- `src/guest.ts` — `PlazaGuest`: drives only the `plaza_*` exports, with the
native host's alloc/free and output discipline (see the ABI table in the
file header). Bytes path (Node) and module path (workerd) both supported.
- `src/manifest.ts` — hosted loader: refuses `portability=native` and
declared memory over the 64MB hosted ceiling (#2057), naming the reason.
- `src/worker.ts` — one Durable Object per world; its SQLite `journal` table
is the source of truth. `POST /step` applies + journals (snapshot, view,
records, hash); `POST /cold-start` drops the in-memory guest so the next
request restores from the journal via `plaza_restore`.
- `compare/` — the journal-equality harness. `isolate-runner.mjs` runs
both scripted sequences through `PlazaGuest` (each also with a forced
cold start mid-sequence; the two runs must agree), and `compare.mjs`
diffs the per-step lines against the native reference
(`../../tests/worker_poc_reference.rs`, which drives the same sequences
through the native wasmtime host and likewise asserts its own
cold-start equality). Regenerate the reference and compare:

```sh
cd pkgs/id
WORKER_POC_REFERENCE_OUT=worker-poc/compare/native-reference.jsonl \
cargo test --test worker_poc_reference -- --nocapture
cd worker-poc && npm run compare
```

- `smoke/counter-smoke.mjs` — Node smoke over the prebuilt
`../examples/roc-counter/counter.wasm`: seed + `inc`×3 with per-step
view/records/snapshot-hash, a guest-level cold start (restore the step-2
snapshot into a fresh instance, hash must match the uninterrupted run),
and both loader-refusal assertions. Run: `npm run smoke`
(Node ≥ 22, no install needed for the smoke).

## Status vs #2271 acceptance

- [x] Scaffold: TS isolate host instantiating the prebuilt counter guest,
driving only the `plaza_*` ABI (this change).
- [x] Counter pipeline smoke green on the isolate side (2026-11-01, laptop
Node 24): seed 0 + `inc`×3 → `count=0..3` in view and records, per-step
snapshot hashes recorded in the run; guest-level cold start (restore
step-2 snapshot, apply `inc`) reproduces the step-3 snapshot hash
`4e07408562…` exactly.
- [x] DO host hardened per Codex review on this PR (2026-11-01): SQLite
BLOB snapshots normalized to Uint8Array before plaza_restore, all
request handling serialized (shared lazy-init promise; update -> hash
-> journal insert is one unit), step inputs validated before any guest
mutation with the guest discarded on journal failure, guest output
pointers normalized unsigned, wrangler declared in nix-common.nix.
Smoke PASS + tsc clean after the fixes.
- [x] Counter sequence compared against the native reference runner
(2026-11-01): `compare/compare.mjs` diffs the isolate host against
`tests/worker_poc_reference.rs` (native `Sandbox` over the same
scripted sequence) — all steps equal in (view, records, snapshot
sha256).
- [x] tic-tac-toe scripted sequence (0,4,1,4-occupied,9-not-a-move,8,
2-X-wins,3-after-win) equal on isolate and native hosts per step
(2026-11-01, same harness): final records
`{"board":"XXX-O---O","plays":5,"winner":"X"}` on both hosts; each
host also asserts its own forced cold start mid-sequence (native
after step 5, isolate after step 5) changes nothing.
- [x] Durable Object forced cold start mid-sequence under workerd
(2026-11-01): `compare/do-cold-start.mjs` drives the counter
through the real DO host — `/cold-start` after step 2 restores
step 2 from the SQLite journal, a full `wrangler dev` restart
(fresh DO instance) restores it again, and step 3's snapshot hash
equals the native reference hash. Full harness run:
`compare: PASS (13 steps equal)`, `do-cold-start: PASS`.
- [x] Hosted-loader refusals: `portability=native` and >64MB declared
memory, each with the reason named (asserted in the smoke).
- [x] Findings note with the pre-registered differences posted on #2271.

Nothing here changes the native sandbox, the guest examples, or the plaza
ABI, and the native `WorldManifest` portability field is a separate Rust
follow-up that does not gate this PoC.
58 changes: 58 additions & 0 deletions pkgs/id/worker-poc/compare/compare.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// #2271 journal-equality harness: native wasmtime host vs Worker isolate.
//
// Compares, per step, (view, records, snapshot) over the scripted
// sequences for roc-counter and tic-tac-toe. The snapshot is compared by
// sha256 of the raw bytes (the native side emits hex; both sides are
// hashed here). Equality must hold after every step, and each side must
// already have proven equality across its own forced cold start
// mid-sequence (asserted inside both runners).
//
// Native reference first (from pkgs/id):
// WORKER_POC_REFERENCE_OUT=worker-poc/compare/native-reference.jsonl \
// cargo test --test worker_poc_reference -- --nocapture
// Then:
// node compare/compare.mjs [path-to-native-reference.jsonl]
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { runIsolateSequences } from "./isolate-runner.mjs";

const nativePath = process.argv[2] ?? new URL("./native-reference.jsonl", import.meta.url).pathname;
const sha256hex = (hexStr) =>
hexStr === null ? null : createHash("sha256").update(Buffer.from(hexStr, "hex")).digest("hex");

const nativeLines = readFileSync(nativePath, "utf8")
.trim()
.split("\n")
.map((l) => JSON.parse(l));
const isolateLines = await runIsolateSequences();

let failures = 0;
if (nativeLines.length !== isolateLines.length) {
console.error(`FAIL: step count differs (native ${nativeLines.length}, isolate ${isolateLines.length})`);
failures++;
}
for (let i = 0; i < Math.min(nativeLines.length, isolateLines.length); i++) {
const n = nativeLines[i];
const s = isolateLines[i];
const diffs = [];
if (n.world !== s.world || n.step !== s.step || n.event !== s.event)
diffs.push(`key native=${n.world}/${n.step}/${n.event} isolate=${s.world}/${s.step}/${s.event}`);
if (n.view !== s.view) diffs.push(`view native=${JSON.stringify(n.view)} isolate=${JSON.stringify(s.view)}`);
if (n.records !== s.records)
diffs.push(`records native=${JSON.stringify(n.records)} isolate=${JSON.stringify(s.records)}`);
if (sha256hex(n.snapshot_hex) !== sha256hex(s.snapshot_hex))
diffs.push(`snapshot sha256 native=${sha256hex(n.snapshot_hex)} isolate=${sha256hex(s.snapshot_hex)}`);
if (diffs.length) {
failures++;
console.error(`DIVERGENCE at ${n.world} step ${n.step}: ${diffs.join("; ")}`);
} else {
console.log(
`equal ${n.world} step=${n.step} event=${JSON.stringify(n.event)} view=${JSON.stringify(n.view)} records=${JSON.stringify(n.records)} snapshot_sha256=${sha256hex(n.snapshot_hex)}`,
);
}
}
if (failures) {
console.error(`compare: FAIL (${failures} divergence(s))`);
process.exit(1);
}
console.log(`compare: PASS (${nativeLines.length} steps equal on native and isolate hosts, cold starts included)`);
Loading