> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://rivet.chrisboulton.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://rivet.chrisboulton.com/_mcp/server.

# Subprocess plugins

> Install and author resource, service and package provider, function, and shared action 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:

```sh
cargo build --bin rivet --bin rivet-plugin-example --locked
```

Inspect an explicitly trusted binary to produce a proposed installation lock:

```sh
rivet plugin-inspect example /absolute/path/to/rivet-plugin-example > /absolute/path/to/plugins.lock.json
```

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:

```text
plugin "example" {
  source  = "example.com/rivet/example"
  version = "~> 1.0"
}
example_marker "message" {
  path    = "/absolute/private/example/message"
  content = plugin::example::slug("Web API")
  meta {
    notify = [service.web]
  }
}
service "web" {
  provider = "example_service"
  ensure   = "running"
  example_service {
    state_path = "/absolute/private/example/service.json"
  }
}
```

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:

```sh
rivet --plugin-lock /absolute/path/to/plugins.lock.json facts --local > facts.json
rivet --plugin-lock /absolute/path/to/plugins.lock.json compile ./site --facts facts.json --out example.bundle
rivet --plugin-lock /absolute/path/to/plugins.lock.json apply example.bundle --local --noop --format json
rivet --plugin-lock /absolute/path/to/plugins.lock.json apply example.bundle --local --format json
```

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:

```sh
rivet --plugin-lock /absolute/path/to/plugins.lock.json test ./site --format json
```

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()`.

| SDK registration                       | Contract                                                                                           |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `register_function`                    | Typed parameters/result and a deterministic handler; called during compilation.                    |
| `register_resource_definition`         | Portable `ResourceDefinition` schema, compilation, validation, assets, exports, and relationships. |
| `register_resource_runtime`            | Target `ResourceRuntime` or `StateRuntime` lifecycle.                                              |
| `register_service_provider_definition` | Portable provider options, supported properties, identity, and safety.                             |
| `register_service_provider_runtime`    | Target `ServiceProvider` identity, observation, native operations, refresh, and recovery.          |
| `register_package_provider_definition` | Portable `PackageProviderDefinition` options, auto priority, architecture, and validation.         |
| `register_package_provider_runtime`    | Target `PackageProvider` state reads, native application, and recovery.                            |
| `register_action`                      | Portable action metadata and a target `ActionRuntime` implementation.                              |

Use the [resource authoring contracts](/contributing/resource-authoring) 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](/contributing/testing#plugin-checks) 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.