Native Dynamic Plugins (Rust)

View as Markdown

Native dynamic plugins are trusted in-process shared libraries. Use them when plugin behavior needs host-process access while still following the Relay plugin contract: a stable kind, JSON component configuration, validation diagnostics, and registration through a component-scoped context.

Native plugins are not sandboxed. They run in the gateway process, must not unwind across ABI callbacks, and remain loaded until Relay removes their registered callbacks.

Manifest

Use the following manifest:

1manifest_version = 1
2
3[plugin]
4id = "acme.native_policy"
5kind = "rust_dynamic"
6
7[compat]
8relay = ">=0.5,<1.0"
9native_api = "1"
10
11[defaults]
12enabled = false
13
14[capabilities]
15items = ["plugin_native"]
16
17[source]
18artifact = "target/release/libacme_native_policy.dylib"
19
20[integrity]
21sha256 = "sha256:<artifact-sha256>"
22
23[load]
24library = "target/release/libacme_native_policy.dylib"
25symbol = "nemo_relay_register_plugin"

Set plugin.kind to rust_dynamic, compat.native_api to "1", and capabilities.items to include plugin_native. Relay resolves relative paths from the manifest. The exported symbol must return a descriptor whose plugin_kind matches plugin.id exactly.

Create a Native Plugin

Create a Rust library project with the following Cargo.toml file:

1[package]
2name = "acme-native-policy"
3version = "0.1.0"
4edition = "2024"
5
6[lib]
7crate-type = ["cdylib"]
8
9[dependencies]
10nemo-relay-plugin = "0.5.0"
11serde_json = "1"

Add the following implementation to src/lib.rs:

1use nemo_relay_plugin::{Json, NativePlugin, PluginContext, Result};
2use serde_json::Map;
3
4struct NativePolicy;
5
6impl NativePlugin for NativePolicy {
7 fn plugin_kind(&self) -> &str {
8 "acme.native_policy"
9 }
10
11 fn register(
12 &mut self,
13 _config: &Map<String, Json>,
14 context: &mut PluginContext<'_>,
15 ) -> Result<()> {
16 context.register_subscriber("audit", |_| {})?;
17 Ok(())
18 }
19}
20
21nemo_relay_plugin::nemo_relay_plugin!(nemo_relay_register_plugin, || NativePolicy);

Build the library with the following command:

$cargo build --release

Update source.artifact and load.library with the resulting platform library path, then replace <artifact-sha256> with that library’s SHA-256 digest. Use .dylib on macOS, .so on Linux, or .dll on Windows. Refer to Build a Rust Native Plugin for a complete example with validation, middleware, scopes, and configuration schema support.

Native ABI v1

The host passes a NemoRelayNativeHostApiV1 table to the entry symbol. The plugin returns a NemoRelayNativePluginV1 descriptor:

1extern "C" fn nemo_relay_register_plugin(
2 host: *const NemoRelayNativeHostApiV1,
3 out: *mut NemoRelayNativePluginV1,
4) -> NemoRelayStatus

Text and JSON data cross this boundary as host-owned NemoRelayNativeString handles. ABI structs also carry scalars, opaque handles, callback pointers, and plugin-owned user_data. Do not pass Rust runtime types, trait objects, futures, serde_json::Value, or allocator-owned strings across the ABI.

ABI callbacks can register these runtime surfaces:

  • Subscribers
  • Tool and LLM guardrails or intercepts, including stream intercepts
  • Marks, scopes, and isolated scope stacks

Relay keeps the library alive while those registrations exist and deregisters them before unloading it.

Use the nemo-relay-plugin crate rather than the host nemo-relay runtime crate. Refer to Build a Rust Native Plugin for the SDK-backed example.

Register the Plugin

After you package the manifest and library, register and validate the plugin:

$nemo-relay plugins add ./relay-plugin.toml
$nemo-relay plugins enable acme.native_policy
$nemo-relay plugins validate acme.native_policy

Refer to Configure Discoverable Plugins for lifecycle and trust-policy configuration.