0002. The wasmkeep:actor WIT world, v0.1.0
- Status: accepted; extended to v0.2 by ADR 0003 (0.x may still break).
Deferred items since:
envadded 2026-11-04 (deploys and secrets in ADRs 0013 and 0016); guest stdout and stderr reach the node’s log since 2026-11-06, not in M1 as planned (until then the host discarded them). - Date: 2026-11-02
- Milestone: M1
- Source:
crates/wasmkeep-wit/wit/actor.wit
Context
The initial design sketched the world (storage, alarms, actors.call, exports
on-init, on-alarm, on-websocket-message, and the wasi:http handler)
but left several types undefined (value, row, request, response, the
error types) and imported wasi:logging, which Wasmtime does not provide.
This ADR records how v0.1.0 fills those gaps. The interface is the product, so
every choice below favours “small now, additive later”.
Decisions
Shape: two worlds
actor-core: WasmKeep’s own imports (storage,alarms,actors) and exports (on-init,on-alarm). Host bindings (wasmkeep-wit, wasmtimebindgen!) and guest bindings (wasmkeep-sdk, wit-bindgen) are generated from this world only.actor:actor-core+wasi:http/proxy@0.2.12+ thewasi:cliimports that Rust’s standard library always pulls in (environment, exit, terminal-*), served empty by the host. This is the world components are validated against (wasm-tools component targets, in CI and later inwasmkeep deploy), so it doubles as the capability list: filesystem and sockets are not in it.
Why: generating bindings for actor would duplicate the WASI types already
provided by wasmtime-wasi-http (host) and the wasip2 crate (guest). With the
split, each side reuses the standard WASI bindings and adds only WasmKeep’s.
Storage
valueis a variant over SQLite’s storage classes:null,integer(s64),real(f64),text(string),blob(list<u8>).queryreturnsrows { columns, values }: column names once, then rows as lists of values. Smaller on the wire than a list of records with names.- One statement per call (
multiple-statementserror otherwise): no accidental batches, no stacked-query injection. - Autocommit per statement; explicit
BEGIN…COMMITacross calls inside one event is allowed, and a transaction left open when the event ends is rolled back. (No implicit per-event transaction: it would hold the write lock across outbound calls and surprise anyone who catches an error.) sql-erroris a record of anenumkind plus a message, so guests can match on the kind (constraint,quota-exceeded, …) without parsing.exec-durableis in v0.1 even though replication arrives in M2, so guest code written now does not change then. On a node without a bucket it means “committed and fsynced locally”. This settles the open question (per-call opt-in vs always durable) as per-call opt-in.
Alarms
set-alarm(at), clear-alarm() as first sketched, plus get-alarm()
(Durable Objects users expect it; a rate limiter needs it to avoid
re-scheduling). At-least-once delivery is stated in the interface docs.
Actor-to-actor calls
call(address, request) -> result<response, call-error> with fully buffered
request / response records (method, path, headers, body). The callee
receives it through its normal wasi:http/incoming-handler, so there is no
second export to implement and every language can handle it. address is
class/name inside the caller’s tenant; cross-tenant calls are impossible by
construction. call-error includes reentrant: with strict one-event-at-a-time
execution, calling yourself or any actor already waiting in the call chain
would deadlock, so the host refuses it up front.
Lifecycle exports return results
on-init and on-alarm return result<_, string>. A failing migration must
fail the event instead of trapping; a failing alarm is retried.
Deferred, with reasons
- WebSockets (
on-websocket-message, accept/send/close): without a send API an exported message handler is dead weight, and adding an export later is a breaking change anyway. They arrive together in a later minor version (ADR 0003). wasi:logging: Wasmtime has no host implementation. Guests write to stderr; the host will capture and tag it per actor (M1). Revisit if a standard logging interface lands in WASI.env(config and secrets): no example needs it yet.- Actor identity (
own address): the request path carries the name; add an import when an example needs it.
Consequences
- Guests implement two traits in Rust:
wasmkeep_sdk::Actor(hooks, default no-ops) andwasmkeep_sdk::http::Guest, and callwasmkeep_sdk::export!. - Every example is checked in CI against the
actorworld, so the WIT, the SDK and the examples cannot drift apart. - The host must link only what the
actorworld lists. The server links all of WASI 0.2; restricting the linker to the world was planned for M1 and is still open. - Any change to
actor.witneeds a superseding ADR once 0.1.0 is published, and an RFC issue once the project takes outside proposals (see CONTRIBUTING.md).