Skip to content

Load Guests and Sidecars

Harmos installs the file that executes: a native executable is a sidecar and a WebAssembly component is a guest. They are parallel artifact kinds, not children of another runtime abstraction.

SidecarGuest
FileNative executableWasm component
BoundarySupervised processWasmtime Component Model
Declaration#[harmos::sidecar]#[harmos::guest]
CallsTyped routes over gRPCHost-mediated capabilities
ConfigurationTyped Rust value; JSON only at IPCTyped Rust value; bytes only at WIT
RegistrationPlugin::registerPlugin::register
Failure policyEmbedded, host-bounded restart policyTrapped instance is discarded; no operation retry
VersionOne artifact semverOne artifact semver

Enable only what the application ships:

harmos = { version = "0.2", features = ["sidecar"] }
# or features = ["guest"]
# or features = ["sidecar", "guest"]

Both authoring macros embed bounded JSON declaration fragments into the final artifact. Inspection scans the bytes and validates the declaration; it does not start the executable, compile the component, run constructors, or instantiate Wasm.

let bytes = std::fs::read(path)?;
let sidecar = harmos::sidecar::Declaration::inspect(&bytes)?;
// or harmos::guest::Declaration::inspect(&bytes)?

Inspection proves what the artifact declares, not that native code is benign. Enforce provenance and file integrity at installation, then enforce the declaration again at the transport boundary. Unknown routes, undeclared guest capabilities, oversized frames, and undeclared publications are refused.

#[harmos::requires] names only an artifact id and a semantic-version range:

#[harmos::requires(id = "probe-bridge", version = "^1")]
#[harmos::guest(
id = "calibrator",
description = "Calibrates probe readings.",
)]
struct Calibrator;

It applies to both artifact kinds and intentionally names no route. The author knows which compatible artifact version supplies the behavior they need.

Convert inspected declarations to harmos::load::Artifact values and call harmos::load::Plan::resolve. Planning refuses duplicate ids, missing dependencies, incompatible versions, and cycles before anything executes. Its answer is dependency-first initialization order.

The complete executable is Minibench. Its host reads the declaration, resolves an absolute Executable, installs a resident Supervised using the file's validated restart policy, receives static registrations, and calls a typed route. Tonic/prost owns the fixed gRPC envelope over stdio and the Prost derives on request, reply, and sample types own those payloads. Typed configuration and registrations cross as JSON.

Publications use a bounded non-blocking queue. A device producer never waits on the runtime; sequence gaps expose overload. Run mise run bench:sidecar to record current throughput and drop accounting.

The Autoroute example builds a component and passes that .wasm file directly to the application:

let host = harmos::guest::Host::new(&runtime)?
.transaction::<RenameNet>()
.survey::<Voltage>();
let loaded = host.load(&std::fs::read(component)?)?;
let registered = loaded.register().await?;
apply_registrations(registered.registrations())?;
let active = registered
.initialize(&AutorouteSettings::default())
.await?;

Static validation precedes Wasmtime compilation. A restricted pre-initialization instance returns registrations, then initialization receives typed config while the instance remains under a fuel budget. Accepted transactions enter ordinary application history under guest:<id>, so recovery neither loads nor executes the guest. Guest facts enter harmos:guest/<id>/… and recall can only page that same namespace. Survey and subscription edges require the guest to have declared them — an undeclared edge is a wiring bug, not a refused permission — and egress additionally requires an application-owned adapter, so Harmos and the guest never acquire sockets or credentials.

If a guest traps, its instance is discarded. A later operation may instantiate a fresh one, but Harmos does not retry the failed operation: replaying an effect would violate at-most-once behavior.

Delivery hands an entry down and takes no reply. To ask a guest something and be answered, call a route — a Rust type carrying the method both ends speak, the payload, the reply, and the bound one call is held to:

let answered = active.call(&StartRecording { output: path }).await?;

The same declaration serves a sidecar, so moving a tenant between facilities changes how it is declared and installed rather than the call site. A guest serving no routes — including one built before routes existed — refuses by name rather than failing, and Active::answers says which kind you loaded.

Each call refills its own fuel budget and arms an epoch deadline from the route's declared bound. Fuel stops a guest that loops; the epoch stops one that spent its bound suspended in a host call, which fuel cannot see. A call that ends badly says which of the three ended it — the bound, the fuel, or a trap — and the instance is discarded either way, so a later call is refused without running.

Both artifact kinds return the same Registration { namespace, schema, payload } envelope before initialization. A consumer decodes only namespaces it owns and atomically replaces the set keyed by artifact id. A crashing sidecar therefore does not unregister its catalogs while it backs off; replacing or unloading the artifact replaces or removes that source as one operation.

Settings, request types, replies, and stream rows live with the sidecar or guest that owns them. The macros contribute their declarations from those same Rust types. There is no manifest to synchronize and no archive-building xtask.

Native and guest declarations derive their version from the consuming Cargo package on every build. Omit version from #[harmos::sidecar] and #[harmos::guest]; explicit versions are rejected. Dependency requirements such as #[harmos::requires(version = "^1")] remain explicit version constraints.

Descriptions default to Cargo's package.description. Set a nonblank package description, or provide description = "…" when one executable needs a more specific purpose than its package. The explicit owner id remains independent of the Cargo package name. The generated constants and inert declaration bytes use the same tracked Cargo values; inspection still executes no plugin code.

Stokker Technologies markDesigned and built by Stokker Technologies