Skip to content

`harmos-sidecar`

harmos-sidecar owns the complete native process boundary: static declarations, executable attachment, Tonic/prost gRPC over stdio, route dispatch, publication batching, and the author-side serve loop.

The root harmos crate exposes runtime supervision only behind its sidecar feature.

A sidecar is one trait and catalogs of types.

impl harmos::sidecar::Sidecar is what an author writes: Config, Error and Resources as associated types, initialize(config, &Context) and resources, and the optional interrupt and cleanup endings. Nothing typed appears on an attribute — a type belongs in an associated type, so a sidecar's error is written down exactly once and every contribution answers it.

#[harmos::sidecar] carries what a host reads before anything runs: identity, one semantic version from Cargo, a description, the restart policy, the rows the sidecar publishes by hand, and the catalogs naming what it contributes. It implements Declared, which is the generated half and is implementable by hand.

Each contribution is a struct, an attribute over its impl that is sugar for a trait, and an enum listing the types:

  • Sampler / #[harmos::sampler(interval_ms = …)] / #[harmos::samplers] — a stream's producer. An instance starts on the first lease of its stream, samples once per tick on its own task, and stops after the last release. The row it answers declares the stream, so a sampler names neither an id nor a label. Cadence belongs to the sampler: every leaseholder shares one running instance and one tick, and a call that overruns skips only its own ticks.
  • Listener / #[harmos::listener(record = "…")] / #[harmos::listeners] — reacting to a narrated record without capturing it.
  • Route / #[harmos::route(id = "…")] / #[harmos::routes] — one call this sidecar answers. One block emits two implementations on the route type: the transport-neutral Call declaration a caller and a host name, and the served handler. A route may carry an explicit bound.

Every trait is the API and every attribute is the derive path; a hand-written implementation behaves identically, which is what the derive cargo feature makes testable. It is on by default, the way serde ships its derives, and a build with it off links no proc-macro chain at all.

Declarations are static. A host inventories what a sidecar contributes by reading its embedded bytes, so nothing may be registered in initialize — initialization opens handles and nothing else.

  • Declaration::inspect reads executable bytes without starting the process. It requires exactly one complete route inventory for this sidecar id; linked descriptors and other owners' inventories do not advertise served routes.
  • Plugin::register contributes typed static registrations before initialization.
  • Executable attaches a resolved absolute program with private stdio pipes. It clears the environment except for SystemRoot on Windows, needed by Winsock provider loading. A missing or empty host value is resolved through GetSystemWindowsDirectoryW. Explicit .variable(name, value) entries override this baseline. The working directory is inherited unless .directory(path) is set; .argument(value) adds a command-line argument. Initialization configuration arrives separately through the private protocol.
  • Supervisor::publish(&sample) serializes any declared Published value into the fixed protobuf envelope.
  • Three reserved pushes travel host to sidecar: harmos.lease and harmos.release carry a Leasing and are the transitions a sampler's lifecycle runs on, and harmos.record carries a Narrated for the listener that declared it. The host drives the first two off the consumer count of every row it ingests, so a stream nothing ingests is a stream nothing leases.

gRPC supplies framing, multiplexing, limits, and batches. Route request/reply values and arbitrary samples are protobuf messages derived directly on their Rust types. Typed configuration and registrations cross the private boundary as JSON; authors maintain no parallel schema files.

Publication is non-blocking. A bounded queue protects device collection and monotonic sequence numbers expose dropped samples to the host.

A bundle host supplies its verified catalog through Supervised::expected_registrations(&registrations). Every attempt compares all namespace/schema/payload entries before initialization, including unknown namespaces. Namespace order is ignored; payload bytes are compared exactly. A mismatch refuses initialization and configuration delivery.

Supervised::registrations() observes the accepted catalog before initialization; it is not a readiness signal. Supervised::ready() watches a boolean that becomes true only after initialization succeeds and the call channel opens. It resets on shutdown, failed attempts, restarts and cancelled or panicked attempts. Watch updates can coalesce, and a true snapshot cannot guarantee that a process will survive until a later call. Hosts still handle ordinary call refusals.

Use a native process to test this admission boundary. Declared::local(config) opens deferred exactly as a process does — initialization runs inside the serve loop, on the frame that carries the configuration, because that is the only place the Supervisor a Context is built from exists.

A native binary whose main propagates harmos::sidecar::run::<Bridge>().await (or Bridge::run().await) supports the sole argument --harmos-export-catalog-v1. Author build tools execute their selected binary with that argument to collect exactly the registrations from Plugin::register and Registrar::finish, before routes, initialization, or resource factories are constructed. The lower-level serve and serve_with APIs do not read process arguments.

Success writes one compact JSON array of Registration envelopes, sorted by namespace, with no trailing newline. Each envelope has namespace, schema, and payload; the payload is a JSON byte array containing consumer-owned JSON. CATALOG_EXPORT_FLAG names this versioned protocol and CATALOG_EXPORT_LIMIT bounds the encoded bytes. Registration and size errors return before any export bytes are written; output errors also propagate to main. Main must return the error so the build tool sees a failed exit status. Stdout is reserved for the export, and author diagnostics must use stderr.

This mode executes arbitrary author registration code; it is not inert static inspection. Hooks and code before the runner can have side effects, hang, write to stdout, or produce nondeterministic payloads. Namespace order and encoding are deterministic for identical registrations. Build tools must enforce child timeouts and output limits, require successful exit, and validate the complete JSON output. Installation must consume the packaged build result and must never execute this mode. Declaration::inspect remains the separate byte-only inspection API.

mise run run:minibench builds a probe executable, inspects it statically, registers it directly, samples a simulated native device interface, observes a published reading, and stops the process.

Run mise run bench:sidecar for the ignored release-mode throughput, latency, payload-size, and overload measurements.

Custom SDK composition emits the same complete manifest through harmos::sidecar::inventory::{RouteEntry, encoded_len, encode} using the final Declared::ID and facility sidecar. Build the static descriptor list and runtime dispatch from the same composition. Inspection refuses artifacts with no matching manifest, including older binaries carrying only unowned route fragments. Static bytes declare the author's inventory; they do not prove that arbitrary executable code implements it honestly.

Stokker Technologies markDesigned and built by Stokker Technologies