Subprocess plugins
Approved executable plugins can add resource types, service and package providers, deterministic functions, and shared prerequisite actions. Rivet uses persistent local RPC sessions. Built-in resources keep their in-process implementations; the compiler and engine retain evaluation, ordering, safety checks, host locking, durable intent, verification, and refresh delivery.
Installation and requirements
Build Rivet and the complete SDK example:
Inspect an explicitly trusted binary to produce a proposed installation lock:
Inspection executes that binary. Review the source, version, descriptor digest, executable digest, and platform path before using the lock. The lock and binary must be beneath protected directories. Rivet checks ownership, group/world write permissions, symlinks, executable permission, and checksums before launch. Root execution requires root-owned installations. A lock beneath /tmp is rejected because its parent is writable by other users.
Use the global --plugin-lock /absolute/path/to/plugins.lock.json option. Without it, Rivet reads /etc/rivet/plugins.lock.json when present. Local runner children receive an explicit selection. SSH runners, source-mode agents, and compile servers use their own locally approved installation policy. Bundles cannot supply executable paths or authorize installation.
Requirements use literal source and version strings:
Save the configuration as site/main.hcl, replacing the target paths with paths beneath existing private directories. On a disposable Linux host as root, with the binary and lock installed in protected root-owned locations:
Compilation needs target facts that advertise the selected resource/provider capabilities. Noop checks the same catalog without managed writes or journal creation. A second real apply reports unchanged resources when the desired state is already present.
The example marker manages file content. The example service is a file-backed model for protocol conformance, not an operating-system daemon provider. Its shared reload action and persisted restart count demonstrate convergence, verification, and notification handling. The marker’s crash_after_apply option deliberately exits after publishing content, for recovery tests.
Versions accept an exact selection, = VERSION, or numeric ~> MAJOR.MINOR / ~> MAJOR.MINOR.PATCH constraints. Constraints check the approved selection; there is no registry resolver, downloader, automatic upgrade, or hot reload. Duplicate requirements or registrations fail. Plugins cannot replace built-in resources or systemd through registration.
Manifest tests with plugins
rivet test uses the same approved definition processes as compilation. Pass the global installation policy explicitly:
External resources support attribute, content, relationship, count, and output assertions. Plugin functions run while compiling the manifest. Service assertions expose provider and the selected provider’s normalized options under its child-block name, such as example_service = { state_path = "/fixture/service.json" }.
Implement the required ResourceDefinition::test_view hook in each plugin resource. Return canonical declaration attributes and set ResourceTestView::content to a bundled asset digest for content matchers. Rivet calls this hook over definition RPC; it does not infer test attributes from the plugin’s desired-state serialization. Service providers use the common service test view.
Use saved facts with the capabilities required by the external resources and service providers. Tests do not probe plugin runtime capabilities or collect host facts. Only definition-role plugin processes run: no runtime checks, actions, mutations, or recovery calls are sent. Approved plugins remain trusted executable code, so their definition hooks must remain pure.
A plugin crash, timeout, or malformed RPC reply is a setup error (exit 2), even when expect_error matches its generic message. An assertion mismatch is exit 1; authored configuration diagnostics can satisfy expect_error. Projection failures are reported as inspection errors in both text and JSON results.
Platforms and offline application
An identity contains source, release version, RPC protocol, portable descriptor digest, and platform executable digests. Local paths belong only to installation policy. Inspect each platform build on its own platform, compare descriptor digests, and merge platform digests into the same identity.artifacts map on both installations. Local paths can differ. Platform keys include macos-aarch64 and linux-aarch64.
Plugin compositions retain exact compilation selections in manifest.plugins. External resource payloads carry implementation identities. Service selections retain provider name, option schema version, normalized options, and implementation identity. These enter desired-state digests, binding interrupted operations and refresh causes to their implementation.
Install required runtime binaries on the managed host before applying a bundle. Offline application uses installed code and verified bundled assets. A function-only plugin’s evaluated results are already compiled, so the target does not need that executable. SSH retains its controller-to-runner transport; plugin RPC stays local to the runner. Its handshake advertises approved identities, payload/provider schemas, and actual host capabilities. A missing/different plugin is an installation mismatch, rather than a reason to silently replace code. Receipt validation checks identities again before mutation.
Rust SDK
The rivet-plugin crate supplies the SDK and wire payload types. See the complete example executable (crates/rivet-plugin/examples/example_plugin.rs), service backend (crates/rivet-plugin/examples/example_service.rs), and package backend (crates/rivet-plugin/examples/example_package.rs).
Register portable contracts on every platform artifact with register_function, register_resource_definition, register_service_provider_definition, and register_package_provider_definition. Register target implementations with register_resource_runtime, register_service_provider_runtime, register_package_provider_runtime, and register_action. Resources use the existing ResourceDefinition, ResourceRuntime, or StateRuntime contracts. Finish with serve_plugin().
Use the resource authoring contracts for desired models, deferred assets, state comparison, full lifecycle hooks, and safety declarations. The SDK adapts those same contracts to RPC; authors do not implement a second convergence engine.
Action metadata is also part of the portable descriptor. Register the same action keys, labels, and capabilities on every artifact; an action’s available() returns false where its native backend is unavailable.
The host launches --rivet-plugin definition or --rivet-plugin runtime; RPC hello establishes the role. stdout is reserved for frames. stderr is drained during RPC exchanges and discarded. The child receives a fixed system PATH and internal parent identity, rather than the controller environment; credentials/configuration must be explicit inputs.
Definition hooks and functions must be pure. Runtime hooks inspect and converge the target. These are trusted executable contracts, not filesystem/network sandboxes. Plugins run with the calling process’s execution identity.
Resource lifecycle and recovery
Receipt validation recomputes capabilities, physical claims, affected paths, assets, and required actions through approved definition code. Serialized annotations cannot replace these checks. Existing safety gates still protect state, locks, caches, and plugin executable paths.
Exports use a bounded request/evaluate/resume exchange: the plugin asks for individual canonical parameter names, and the host retains every read’s dependencies and sensitivity. Unrelated fields remain lazy. Deferred assets use temporary assignment slots; Rivet acquires and verifies bytes before the matching pure assignment. Slots never enter catalogs.
Checks return work handles bound to payload digest, reason, and process session. Apply consumes a work handle and produces a verification handle; verify consumes it and performs fresh checks. Discarded plans release handles. Neither handle kind enters a bundle or journal. Verified assets are transferred in chunks and verified again by the SDK.
The engine commits intent before dispatching mutation. Crashes, malformed responses, lost replies, and mutation deadlines leave uncertain outcomes. A broken session is never restarted/replayed automatically. Recovery of compatible interrupted work uses the same pinned implementation, recorded staging identity, and explicit recovery before continuing. Missing resource recovery support refuses continuation. A new catalog selecting a changed implementation supersedes obsolete records and inspects current host state; it does not replay old work through the new plugin or claim that the historical operation succeeded. Catalogs still require matching approved plugin installations. Refresh recovery is at-least-once delivery. Shared action keys include plugin source and descriptor identity, and obsolete obligations are superseded rather than delivered through a changed implementation.
Service and package providers
Plugin backends implement the same traits as built-in systemd, apt and dpkg, and run inside the built-in ServiceRuntime or PackageRuntime. Rivet owns planning, property comparisons, refresh policy, verification, notification coalescing, and reporting; RPC carries only provider primitives. Both runtime traits have typed Options decoded strictly from the catalog, an optional validate hook for runtime-only checks, and an optional begin_run.
ServiceProviderDefinition declares typed option attributes, capabilities, ownership domain, supported properties, refresh_modes (a set of ServiceRefresh values that must include None), optional definition reload action/path prefixes, and optional auto-selection priority. It owns pure name normalization, option compilation/validation, and additional static ownership/affected-path declarations. ServiceProvider supplies canonical identity, observation, checked native operations, refresh and optional recovery; inspect_safety adds fresh dynamic claims/paths without reading managed content or planning changes. Unsupported enabled, masked, refresh modes, and explicit automatic reload fail validation. Unmanaged properties remain unmanaged.
PackageProviderDefinition declares typed options, NAME, SCHEMA_VERSION, CAPABILITY, option attributes, auto_priority, architecture normalization and validation. PackageProvider implements read_state and apply_state; recovery refuses replay unless overridden. A plugin cannot reuse a built-in provider name. Archive sources are uploaded to the plugin as verified assets before the first operation that reads them.
provider="auto" resolves at compile time to the unique highest-priority provider whose capabilities the target advertises. Ties/no applicable backend fail. Plugin-specific child blocks cannot be supplied to a different selected provider. Catalogs record the provider schema version and plugin identity, and runners refuse catalogs whose provider is missing or different.
Functions and wire protocol
Functions receive evaluated typed values and return values. The wire format distinguishes lists, sets, maps, resource handles, numbers, and null. The host retains all argument dependencies and conservatively propagates nested sensitivity. Plugins can add sensitivity but cannot declassify arguments. Deterministic handlers must not read ambient files, environment, clock, randomness, or network state.
RPC uses four-byte big-endian lengths and JSON frames capped at 24 MiB. Calls have monotonic IDs, a remaining-time budget, and tagged method payloads. Replies have matching IDs and tagged success/failure outcomes. Failures carry partial-change/uncertainty flags, without desired values. Unknown envelope and method fields, bad identities, malformed frames, and oversized messages fail closed. Asset chunks are at most 256 KiB before base64 encoding; total retained assets remain bounded by the bundle limit.
Requests serialize within a session, with deadline-aware connection waiting. A session is reused for its composition lifetime. Definition/runtime roles can use separate processes, and discovery/probes may create short-lived sessions. Shutdown attempts a bounded close, then terminates the owned process group and reaps its leader. Linux SDK servers establish a parent-death guard before handshake. A launcher that starts the SDK server as a child must set RIVET_PLUGIN_PARENT_PID to the launcher PID; an exec wrapper preserves the original parent identity. Authors must bound native work and own its children; detached descendants and uninterruptible kernel calls do not acquire a hard termination guarantee.
Checks
See testing for focused checks and the disposable Linux CLI/SSH workflows.
Run cargo test -p rivet-plugin -p rivet-process --locked for subprocess reuse, lock/checksum rejection, annotations, lazy exports, bundles/assets, noop, idempotence, lost replies, implementation mismatch refusal, superseding obsolete work, recovery, service actions/notifications, ownership conflicts, malformed protocol, timeouts, and child cleanup. Workspace and disposable Linux suites also cover the existing built-ins and SSH/bootstrap paths.