Implementation status (by PRD milestone)
Engineering reference: what is built, measured, and deviated from, organized by the PRD's milestone structure (the PRD itself is an internal document; section references are kept for traceability). For user-facing documentation see the manual.
M1 status (PRD §16)
Done:
- Crate skeleton with the PRD §5.1 module shape;
rs2-serverbinary. - Message/Body model: stream-or-bytes payloads, mandatory media types,
schema-carrying
Content-Type(application/json; schema="…"), provenance (Materialized/Replayable/Ephemeral), capped materialization. - Router: single/multi tenancy (domain map + subdomain), longest-prefix mounts, path safety (traversal/encoding/null/drive-letter) for all services.
- Wrapper: per-tenant concurrency admission (fail-fast), wall-clock timeouts,
auth stub (
"access": "authenticated"), RFC 9457 problem+json errors with machine-readable codes everywhere. - Engine-neutral contract: WIT world,
HostApi/Enginetraits,GrantedHostwith capability default-deny + outbound budget; conformance suite pinning message semantics, capability denial, wall-clock/materialization limits, and state-capability persistence. - Wasmtime engine (
--features wasm): component host with epoch-based wall-clock interruption, per-store memory caps, content-hash component cache; passes conformance against a realwasm32-wasip2guest, including capability denial across the sandbox boundary. file+dataservices with documented PRD §10 semantics (streamed writes, Range/206, ETag from store versions, paginated listings, schema-validated CRUD with cached compiled validators, JSON merge PATCH, keyless POST,?confirm=dataset delete) — both implementing the store pattern, one normative conversation shape pinned bytests/store_conformance.rs(carried over from Restspace v1's pattern system for client polymorphism): trailing-slash dir+json listings at every container level, uniform PUT/POST/DELETE semantics, the 409 +?confirm=container guard, ETags on children. Mounts declarepattern+facetson the discovery surface; the generated OpenAPI$refs one shared store path-item shape.- Static-site mode on
file(no separate service — config):defaultResourceserves a default document for directory GETs,spaFallbackserves the root app shell for extension-less misses (asset misses stay 404),listings: falsedisables browsing; mounts with it declare thestatic-sitefacet. Pair withcachingfor CDN headers. - Host-enforced tenant scoping on every store capability; local-fs and in-memory adapters.
- OTel-compatible logging (PRD §14): a
LogSink/LogStorecapability with an OTLP-shapedLogRecordmodel. The host emits one boundary log per dispatch at the singleRuntime::handlechoke point (external requests and internal hops, severity from outcome, errors carry code/detail); prebuilt services emit application logs through a mount-boundServiceLogger; sandboxconsole.log/Wasmlog()land via the (now un-stubbed)HostApi::log— all stamped with tenant/mount/service/trace, the service identity fixed by the host. The defaultFileLogStorewrites per-tenant rotated NDJSON off the request path (bounded channel + background writer; a no-op sink is allocation-free on the hot path). Thelogreader service queries it back, tenant-scoped. Write-only OTLP/observability exporters are a documented follow-on (swap the sink; the reader degrades to "exported, not queryable").
Remaining M1 (tracked, not started):
- S3 file-store and database data-store adapters (traits are final; slots in
adapters/). - WIT streaming bodies: the v0.1 world materializes bodies at the component boundary (PRD open question 1); revisit when component-model streams land.
M2 status (PRD §16, "Composition & control")
Done:
- Pipeline executor (
pipeline/): typed spec (PRD §8.1) with serial / parallel / conditional / tee / teeWait modes,jsonSplit+jsonObject, conditions (config-time-checked grammar), JSONata transforms (jsonata-rs — the PRD's "evaluate Rust JSONata early in M2" risk, resolved; evaluated on the blocking pool, internally timeboxed, so a heavy expression can't stall a reactor worker),try,as:$varcapture,elevate(run a call as trusted internal composition — drops the caller principal so a caller-accessible pipeline can front a locked-down service; the v1 gateway pattern, opt-in),${...}interpolation,?$to-step, fan-out/depth/step limits + an aggregate fan-out footprint cap (branch count × body bytes →pipeline_fanout_bytes). - String DSL converter (
pipeline/dsl.rs): the Restspace terse form ("if (ok) GET /x :$y") is accepted as sugar; stored format is typed. - Segments (PRD §7.3): planned at materialization points; the segment is
the atomic retry unit; per-step keys derive from
H(invocation-id, step-path)so keyed effects dedupe across attempts (proven ing6_segment_retry_dedupes_keyed_effects).?$planreturns the plan with unsafe-mid-segment warnings. - Idempotency store + replay (PRD §7.2):
Idempotency-Keyscoped per tenant+mount+method+path; replay window withIdempotency-Replayed: true; in-flight duplicates 409; payload-hash mismatch 422; store is a capability (in-memory adapter; shared adapter slot for scale-out). - Retry policies (PRD §7.3): declarative, resolved per-call → mount → tenant → runtime; gated on effect classes (pure/idempotent/keyed/unsafe, method-inferred defaults).
authservice + RBAC (PRD §10.5): HS512 JWT (constant-time verify) inrs-authcookie or bearer; login/refresh (sliding past 50%)/logout; login lockout; argon2id hashes with bcrypt migration verify; per-mount role specs (read/write/delete/invoke, path-scoped grants like"U /user/{email}") enforced in the wrapper; tenantoperatorRolesgate who may change authorization.- Caching, host-applied (v1's universal
cachingconfig): defaultCache-Control: no-storeon every response; mounts opt in with{"mode": "noStore"|"revalidate"|"cache", "maxAgeSeconds", "public", "immutable"}—publicclamps toprivate+Varyon authenticated mounts;Set-Cookieand error responses are always uncacheable;fileanddataanswer matchingIf-None-Matchwith 304s, sorevalidatemode is always-fresh at near-zero bandwidth. - CORS, host-enforced (PRD §5.2; v1's
trustedDomainsposture): tenantcorsblock — trusted origins get credentialed CORS andSameSite=None; Securelogin cookies; allowed origins get plain CORS with bearer-only login (no cookie); preflights answered before routing; error responses decorated; an always-on CSRF guard rejects cookie-authenticated unsafe requests from untrusted cross-site origins;auth.allowedLoginOriginsoptionally allowlists login/refresh callers. servicesself-config (PRD §10.6):GET catalogue|services|raw,PUT rawvalidates the whole config (dry tenant build), persists through the config loader, and hot-swaps atomically;If-Matchoptimistic concurrency; invalid config never touches the running tenant.pipelineservice (PRD §10.3): mount a spec (typed or DSL);?$planintrospection; internal calls re-enter full dispatch (authz, limits, idempotency all apply).- Exit criteria: demo tenant e2e, G6 idempotency proof, and the G7 benchmark
live in
tests/m2_composition.rs(G7: ~0.08 ms per two-step pipeline invocation, release build).
M2 deviations (tracked):
- Lockout/session state is node-local (PRD wants the shared state store).
- TOTP MFA, impersonation, data-field authorization rules, zip/unzip and multipart split/join, and the audit log are deferred.
M3 status (PRD §16, "Surface & migration")
Done:
pipeline+queryare spec stores (v1's store-transform/store-view patterns): authoring is a full store-contract surface under the reserved dot-subtree (/<mount>/.pipelines/…,/<mount>/.queries/…— guard authoring withwrite); every other path on any HTTP verb executes the longest-prefix-matched spec, with a.rootspec governing the mount root (so a pipeline can transparently wrap another service — the reason verbs pass through; replaces v1's modal manage header). DRY by construction: theSpecStorefaçade owns a realFileServiceand delegates (validate → canonicalize → forward), withPrefixedFileStoreas the seam where per-store named infra (PRD §9.1) plugs in later ("store": {"root"}today). The instruction plane of a tenant is now exactly: tenant config +.rs2-code/+.rs2-pipelines/+.rs2-queries/— mechanically extractable for git change control.queryservice (PRD §10.4): stored queries authored like files (no tenant-config round trip), executable on any verb (query-string params coerce to the params schema's types), language-agnostic by design: JSON templates (Mongo aggregates, Elastic DSL, the reference adapter) substitute structurally —"${name}"nodes take the param's JSON value, so templates are valid JSON at rest and injection-safe by construction;"${name?}"optionals elide their enclosing clause (generalizing v1 Mongo'signoreEmptyVariables); string (SQL) templates pass to the adapter unsubstituted with validated params for real bind parameters. Param schemas applydefaults and validate before execution; positional params from trailing URL segments (longest stored prefix wins); missing params are 400s, never silent.QueryStorecapability (the v1IQueryAdapterequivalent, now bind-capable) + reference adapter scanning anyDataStore.- Agent surface (PRD §12), generated per tenant and filtered by the
caller's read permission and
?surface=againstx-expose:/.well-known/rs2/services— mount catalogue with x-agent/x-policy/.well-known/rs2/agent-surface— entities, actions (effect class + Idempotency-Key guidance advertised), stored queries with their schemas/.well-known/rs2/openapi— OpenAPI 3.1; the schemas referenced are the ones enforced at runtime (no drift by construction)
- Structured errors complete: pipeline failures carry the failing step and per-step statuses in the problem+json body.
- Custom code deployment (PRD §10.6):
PUT /services/code/<name>stores components content-addressed and immutable per version, with a compile smoke test when the engine is in the build; mounts referencecode:<name>@<version>; capabilitygrantsmap names to internal URL prefixes and re-enter full dispatch (authz/limits/idempotency apply). Proven end-to-end against the real conformance component (wasm feature). rs2CLI (cargo run -p rs2-cli --or thers2binary):rs2 new <name> [--js]— scaffold against the published WIT (the Rust scaffold compiles to a working component as-is)rs2 dev— run a local node (shares the rs2-server library)rs2 test— manifest consistency + component checks (engine compile check with--features wasm)rs2 deploy <wasm> --name n— upload via the self-config APIrs2 migrate <services.json>— Restspace config → RS2 tenant config: mounts, access roles, retry policies carried over; string-DSL pipelines converted to the typed spec; the result is validated by dry-building the tenant; unsupported services are skipped with explicit warnings
Exit criteria: G4 (core service parity) met — all six services reimplemented with documented semantics.
V8/JS engine (--features js)
engines/js.rs embeds rusty_v8 (v8 150) via deno_core. Shape: one isolate
per invocation on a blocking thread (PRD open question 2 resolved
conservatively — full isolation; warm pools are a later optimization
behind the same Engine trait).
- Bundle contract: a single-file ES module whose default export is
async (msg, ctx) => responseor{ handle }. JSON bodies arrive parsed; responses are{ status?, headers?, body?, mediaType? }(or any value as a 200 JSON body).ctx={ config, request, log, state }. - Host bridge:
ctx.requestis synchronous from the guest (the isolate thread parks on the async host future);async/awaitworks via microtask pumping to quiescence. No event loop: timers unavailable in v1. - Limits: wall clock via a watchdog thread +
terminate_execution; memory via isolate heap caps with a near-limit callback (an allocation bomb gets a structuredlimit_exceeded, not a process abort); outbound budget and materialization caps host-enforced as for every engine. - Conformance (G2): the JS engine passes the identical invariant suite
(message semantics, capability denial with preserved error identity,
wall-clock kill, memory containment, outbound budget, state capability,
no global leakage between invocations) —
cargo test --features js. - Deployment:
PUT /services/code/<name>withapplication/javascriptstores the bundle (compile smoke test when the engine is in the build);code:mounts dispatch by bundle type, so wasm and JS services share grants, limits, and the host contract. - Startup snapshot: per-invocation isolates boot from a committed V8
snapshot with the bootstrap + compat prelude baked in — currently
Windows-only; on Linux custom-snapshot isolate creation intermittently
aborts inside V8's shared-heap deserializer (upstream deno_core/rusty_v8
bug, family of denoland/deno#15590 — full investigation in commit
dbd6857), so Linux runs the identical-semantics source-eval fallback (~10 ms per isolate creation). Revisit at the deno_core upgrade.
npm-compat layer (G5)
Every JS bundle runs against an injected compat prelude
(engines/js_prelude.js) — the explicit supported-API list (PRD §17
risk mitigation), pinned by the corpus suite in tests/npm_compat.rs:
fetch/Headers/Request/Response— outbound HTTP through thefetchcapability, granted per mount with host allowlists:"grants": { "fetch": { "type": "httpOut", "hosts": ["api.stripe.com", "*.example.com"] } }. Default deny; disallowed hosts fail withcapability_deniedbefore any I/O. The server wires a ureq-backedHttpOutadapter (httpfeature); embedders supply their own.setTimeout/clearTimeout/setInterval/clearInterval— virtual time: when the handler is otherwise idle, pending timers fast-forward, so SDK retry backoffs (429 + Retry-After loops) complete without real waits.console,queueMicrotask,structuredClone,TextEncoder/TextDecoder,atob/btoa,Buffer(from/alloc/concat/toString utf8|base64|hex),URL/URLSearchParams,AbortController/AbortSignal,crypto.{getRandomValues, randomUUID},process.{env, nextTick, version}.
The corpus exercises the request/auth/retry patterns the popular
API-wrapper SDKs are built from (Stripe-style form POST + idempotency
keys, OpenAI-style JSON + 429 backoff, Slack-style query building +
base64 auth) — cargo test --features js --test npm_compat. Compat
additions require a corpus-driven case (PRD §17).
G5 measured against the real SDKs (corpus/ + tests/sdk_corpus.rs):
the official npm packages, bundled with esbuild (--platform=browser --conditions=worker, the same settings as rs2 deploy --bundle) and run
in the engine against mocked fetch:
| SDK | result |
|---|---|
| stripe (fetch http client) | ✅ unmodified |
| openai | ✅ unmodified |
| @anthropic-ai/sdk | ✅ unmodified |
| @octokit/core | ✅ unmodified |
| @supabase/supabase-js | ✅ unmodified |
| resend | ✅ unmodified |
| @google/generative-ai | ✅ unmodified |
| @mistralai/mistralai | ✅ unmodified |
| groq-sdk | ✅ unmodified |
| @slack/web-api | ❌ axios transport imports node:os/node:path |
9/10 = 90% — G5 met. Reproduce: cd corpus; npm install; ./build.ps1; cargo test --features js --test sdk_corpus -- --nocapture.
Out of scope for v1 (documented): WebSocket connections, response-body
streaming (ReadableStream is presence-only), binary multipart uploads,
real wall-clock timers.
Loadable adapters (G13, --features js)
Bring-your-own-backend: a tenant connects a custom data backend — including
non-HTTP wire protocols — without recompiling rs2-core.
- Phase 0/1 (done): the JS engine runs on
deno_core(guest contract byte-for-byte unchanged), with a host-gated TCP/TLS socket capability ({"type":"socket","hosts":[…]}, default-deny, allowlisted before connect) so custom JS can speak Postgres/Mongo/Redis.RS2Socket.connect(host, port, {tls}), synchronous from the guest. - Phase 2 (done): resident adapter runtimes + a loadable
DataStore. Adatamount with"store": {"adapter": "code:my-redis@v1", "grants": {…socket…}}backs its persistence with a deployed JS bundle, kept resident on its own thread (engines/resident.rs) so the adapter pools one connection across requests. The bundle implements the store pattern (GET/PUT/DELETE /{ds}/{key}, listings, schema);GuestDataStoremaps theDataStoretrait onto those messages, and the stockDataServiceruns unchanged on top (schema validation, ETags,.schemas, the store contract).engines/js.rs's per-invocation and resident paths sharebuild_runtime+dispatch_once. Proof:tests/guest_adapter.rsruns the store contract over a Redis (RESP) adapter against an in-process mock, asserting a single pooled connection. - Phase 3 (in progress):
GuestQueryStore— a second guest-backed capability on the sameResidentAdaptersubstrate. Aquerymount with"store": {"adapter": "code:…"}runs stored queries through a resident JS adapter (run_query→POST /query {query,params,take,skip}→{rows, total}); the query service still substitutes templates + validates params first, unchanged. Proven by a Redis-backed query adapter that scans + filters a dataset over its socket.- Resident N-pool + idle eviction —
ResidentAdapteris a small per-mount pool: it grows lazily under concurrent load tostore.maxRuntimes(default 4, least-busy dispatch; a serial workload stays at one) and a background sweeper evicts runtimes idle paststore.idleMs/idleSeconds(default 60 s), closing the isolate and its pooled sockets. GuestFileStore— afilemount with"store": {"adapter": "code:…"}serves files from a resident JS adapter. All eightFileStoremethods map to store-pattern messages (HEAD/GET/PUT/DELETE/MOVE, listings); content crosses base64-encoded andreadrebuilds aReplayableBodyso ETags/304 andRange→206 keep working. Proven by the full store contract over an in-memory guest file adapter.- MongoDB adapter — a real-wire-protocol
DataStoreadapter (OP_MSG + a hand-written BSON codec over the socket), purely a JS bundle on the existingGuestDataStore. Datasets = collections, key = string_id; CRUD →find/update/delete/count/drop/listCollections. Proven against an in-process mockmongod. No-auth (SCRAM-SHA-256 needs HMAC/PBKDF2 the JS prelude doesn't expose — acrypto.subtleaddition would unlock it).
- Deferred (Phase 3): a
GuestActormodel for long-lived push connections (Discord gateway), SCRAM auth for Mongo, multi-file ESM resolution.
G1 + G3 benchmarks (tests/g_benchmarks.rs)
cargo test -p rs2-core --release --features js --test g_benchmarks -- --ignored --nocapture
G1 — dispatch overhead (target: p99 added < 1 ms vs the native call path). Measured: direct service call p99 ~7–10 µs; the full dispatch path (router → tenancy → token verify → access → admission → breaker → idempotency) p99 ~7–17 µs — added p99 well under 10 µs, ~100× inside the target.
G3 — containment (target: a pathological service cannot push another tenant's p99 beyond 2× baseline). Two attacks from a hostile tenant — an infinite loop and an allocation bomb in sandboxed JS — under an 8-client flood while a neighbor tenant's data service is measured:
| attack | neighbor p99 baseline | under attack | evil outcomes |
|---|---|---|---|
| infinite loop | 14.0 µs | 20.8 µs (1.5×) | 108 requests, all structured 503 |
| allocation bomb | 14.0 µs | 78.8 µs | 200 requests, all structured 503 |
The mechanism stack: wall-clock/heap kill inside the isolate → per-tenant
concurrency admission → per-tenant breach circuit breaker (PRD §9.3,
added by this work: repeated limit breaches trip the tenant open for a
cooldown with 503 + Retry-After, so pathological code stops re-occupying
engine threads — without it the attack ran 27 000 isolate restarts and
pushed the neighbor to 3.1 ms). The assertion bound is
max(2× baseline, baseline + 2 ms): with microsecond-scale baselines,
scheduler noise alone exceeds a bare 2×; raw numbers are printed.
Bundling: rs2 deploy entry.ts --name x --bundle runs npx esbuild
(single-file ESM; npm deps resolve at build time; native addons fail at
build time). Timers are virtual and there is no event loop: code needing
real wall-clock waits or background work is out of scope for v1.