`harmos-sidecar`
Purpose
Section titled “Purpose”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.
The shape
Section titled “The shape”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 instancestarts on the first lease of its stream,samples once per tick on its own task, andstops 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-neutralCalldeclaration 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.
Boundary
Section titled “Boundary”Declaration::inspectreads 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::registercontributes typed static registrations before initialization.Executableattaches a resolved absolute program with private stdio pipes. It clears the environment except forSystemRooton Windows, needed by Winsock provider loading. A missing or empty host value is resolved throughGetSystemWindowsDirectoryW. 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 declaredPublishedvalue into the fixed protobuf envelope.- Three reserved pushes travel host to sidecar:
harmos.leaseandharmos.releasecarry aLeasingand are the transitions a sampler's lifecycle runs on, andharmos.recordcarries aNarratedfor the listener that declared it. The host drives the first two off the consumer count of every row itingests, 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.
Frozen admission and readiness
Section titled “Frozen admission and readiness”A bundle host supplies its verified catalog through
Supervised::expected_registrations(®istrations). 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.
Build-time catalog export
Section titled “Build-time catalog export”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.
Example
Section titled “Example”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.