hecks

A native host crate drives the sandboxed WASM module; Postgres persistence is rehydrate-and-replay, not incremental

Status: Accepted — implemented. rust/host/ (new crate: wasm_runner.rs, journal.rs, dispatch.rs, main.rs), branch feat/rust-projection. Answers the deployment question 0012 raised but didn’t build: “the immediate goal driving this pass wasn’t a browser embedding — it was making the compiled Rust artifact runnable in non-browser WASM hosts… the kind relevant to ‘run domain logic in workers’ on a cloud platform.”

Context

The compiled .wasm artifact (0012) and the compiled native binary both dispatch commands against an in-memory-only store — proven correct, never persisted anywhere real. Deploying it to AWS Lambda needs two things 0012 explicitly didn’t build: a real Postgres-backed store, and an actual host process to run inside Lambda.

Two real constraints shaped the design:

Decision

A new crate, rust/host/, is the actual Lambda entry point (provided.al2023 custom runtime, PackageType: Zip, no container — see the bin/project_deploy-generated deploy/ config). It embeds wasmtime/wasmtime-wasi to run the .wasm module and tokio-postgres for persistence — the crate’s first-ever real Cargo dependencies, but scoped entirely to this new, narrow, host-only crate, the same boundary 0015 already established for rust/web. Unlike rust/web, rust/host does NOT depend on rust as a path dependency at all — it never links the kernel in-process; the .wasm file is loaded and run as an opaque artifact through wasmtime, which is the entire point of keeping it sandboxed.

wasm_runner.rs is the only file that touches wasmtime: one Engine/Store/instance per call, stdin/stdout wired to wasmtime_wasi::p2::pipe::{MemoryInputPipe,MemoryOutputPipe} via two thin StdinStream/StdoutStream wrapper impls (neither pipe type implements those traits directly in wasmtime-wasi 47.0.3, despite what its own doc comment suggests). Because wasmtime-wasi’s p1-sync bridge internally tries to start its own tokio runtime to drive those async pipes, calling wasm_runner::run directly from async code panics (“cannot start a runtime from within a runtime”) — dispatch.rs calls it through tokio::task::spawn_blocking, moving it onto a thread with no runtime of its own.

Persistence is rehydrate-and-replay against ONE flat, whole-domain journal table (hecks_lambda_journal: ordinal, verb, args, recorded_at) — not per-aggregate, not incremental find/save. On each invocation (dispatch.rs#handle): load every prior successfully-dispatched command in order, append the new one, replay the WHOLE sequence through wasm_runner::run, and persist the new command only if the replay’s refusals array came back empty. Determinism is the entire correctness argument: every prior row was, by construction, a command that succeeded the first time — replaying the identical steps against the identical empty-start kernel must produce the identical result, so the only step that can legitimately end up in refusals is the newest one, appended last.

This needs zero changes to the sandboxed kernel/dispatch code — it reuses the exact unmodified {"steps": [...]} -> {"instances","events","refusals"} contract bin/rust_conformance’s own WASM mode already speaks.

Consequences

Rejected alternatives