Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b935781
docs: rewrite plugin authoring guides
willkill07 Aug 12, 2026
4f44f7a
docs: address plugin guide review feedback
willkill07 Aug 12, 2026
84b88e8
docs: address verified plugin review findings
willkill07 Aug 12, 2026
0046e9c
docs: link native example to rendered guide
willkill07 Aug 12, 2026
5fcd6f6
docs: remove unpublished native guide link
willkill07 Aug 12, 2026
4ccfd00
docs: resolve plugin review findings
willkill07 Aug 12, 2026
9b90954
fix: declare worker interceptor compatibility
willkill07 Aug 13, 2026
7d4bc04
docs: expand plugin authoring examples
willkill07 Aug 13, 2026
97f617a
docs: make plugin examples independently verifiable
willkill07 Aug 13, 2026
418a8ea
test: exercise Python worker runtime in managed calls
willkill07 Aug 13, 2026
ebb7e69
docs: rename plugin overviews to about pages
willkill07 Aug 13, 2026
d31fa5d
test: escape native lifecycle manifest paths
willkill07 Aug 13, 2026
34d4334
test: escape worker lifecycle manifest paths
willkill07 Aug 13, 2026
78c2c9b
docs: align plugin guides with canonical tool results
willkill07 Aug 14, 2026
39f043d
test: preserve worker request intercept coverage
willkill07 Aug 14, 2026
6b9c0a7
fix: preserve Python worker tool annotations
willkill07 Aug 14, 2026
f7f348d
docs: align plugin examples with current contracts
willkill07 Aug 14, 2026
01629ee
revert: keep bootstrap key fix on the release branch
willkill07 Aug 14, 2026
ad36095
docs: preserve current plugin observability guidance
willkill07 Aug 18, 2026
15ab68b
docs: preserve current plugin observability guidance
willkill07 Aug 18, 2026
720139c
Apply suggestions from code review
willkill07 Aug 18, 2026
21240d0
docs: correct language-binding terminology
willkill07 Aug 18, 2026
5bdc3d2
docs: group worker examples by language
willkill07 Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions crates/cli/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,9 +787,6 @@ fn load_or_create_bootstrap_hmac_key_at(
path: &Path,
) -> Result<[u8; BOOTSTRAP_HMAC_KEY_BYTES], CliError> {
if bootstrap_hmac_key_permissions_are_private(path)?
&& fs::metadata(path)
.map(|metadata| metadata.len() == BOOTSTRAP_HMAC_KEY_BYTES as u64)
.unwrap_or(false)
&& let Some(key) = load_existing_bootstrap_hmac_key_at(path)?
{
return Ok(key);
Expand Down
19 changes: 0 additions & 19 deletions crates/cli/tests/coverage/shared/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2639,25 +2639,6 @@ fn bootstrap_hmac_key_creation_is_concurrency_safe_and_stable() {
assert_eq!(std::fs::metadata(path).unwrap().len(), 32);
}

#[cfg(unix)]
#[test]
fn bootstrap_hmac_key_completes_empty_private_state() {
use std::os::unix::fs::PermissionsExt;

let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("state/fingerprint-hmac.key");
let parent = path.parent().unwrap();
std::fs::create_dir_all(parent).unwrap();
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)).unwrap();
std::fs::File::create(&path).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();

let key = load_or_create_bootstrap_hmac_key_at(&path).unwrap();

assert_eq!(std::fs::metadata(&path).unwrap().len(), 32);
assert_eq!(load_or_create_bootstrap_hmac_key_at(&path).unwrap(), key);
}

#[cfg(unix)]
#[test]
fn bootstrap_hmac_key_reuses_private_state_without_changing_permissions() {
Expand Down
12 changes: 2 additions & 10 deletions crates/core/tests/integration/worker_plugin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1333,10 +1333,6 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() {
.await
.expect("Python tool execution intercept should call ToolNext and return its outcome");
assert_eq!(tool_result.result["provider_result"], true);
assert_eq!(
tool_result.result["_nemo_relay_plugin"]["tag"],
"managed-environment"
);
assert_eq!(
tool_result.result["args"]["_nemo_relay_plugin"]["tag"],
"managed-environment"
Expand All @@ -1354,14 +1350,10 @@ async fn python_worker_host_runtime_mark_and_mutated_request_round_trip() {

flush_subscribers().expect("Python callback mark should flush");
let captured_events = events.lock().unwrap();
find_event(
&captured_events,
"examples.python_grpc_worker.tool_request",
None,
);
find_event(&captured_events, "example.python_worker.tool_request", None);
let tool_mark = find_event(
&captured_events,
"examples.python_grpc_worker.tool_execution",
"example.python_worker.tool_execution",
None,
);
assert_eq!(
Expand Down
76 changes: 23 additions & 53 deletions crates/plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,41 +24,20 @@ Native plugins run in the Relay process and are not sandboxed. They should
depend on this crate rather than the host `nemo-relay` runtime crate, keeping
the dynamic-library boundary on the stable C-compatible ABI.

## Why Use It?

- **Author native plugins safely**: Implement `NativePlugin` with typed Rust
callbacks instead of constructing ABI tables directly.
- **Register real runtime behavior**: Use `PluginContext` for subscribers,
guardrails, and intercepts.
- **Keep a stable boundary**: Export one versioned native entry point through
the `nemo_relay_plugin!` macro.
- **Use host runtime helpers**: Emit events and manage scope state through the
high-level `PluginRuntime` wrapper.

## What You Get

- **`NativePlugin`**: Plugin kind, configuration validation, and registration
lifecycle contract.
- **`PluginContext`**: Component-scoped registration APIs for middleware and
subscribers.
- **`PluginRuntime`**: Typed helpers for Relay-owned scopes and marks.
- **Stable native ABI v4**: C-compatible host and plugin tables behind the
safe Rust authoring interface. Relay negotiates frozen v3 and v2 tables for
Relay 0.8-built plugins that target those layouts.
- **Typed async middleware**: Every typed guardrail, sanitizer, and intercept
returns a future driven by a per-plugin SDK-owned Tokio runtime. Subscribers
and raw synchronous ABI registrations remain synchronous.
- **Async continuations and streams**: Cloneable `ToolNext`, `LlmNext`, and
`LlmStreamNext` handles support repeated or concurrent calls. Streaming LLM
continuations use a pull-based host handle.
- **Canonical tool results**: `ToolNext` returns `ToolExecutionResult`, keeping
application results and opaque annotations adjacent across native API 1.
- **Typed telemetry marks**: `PluginRuntime::emit_mark_with_options` carries a
`DataSchema` and `LogSeverity`; `emit_metric` validates and emits typed
`MetricMeasurement` values through the reserved Relay metric schema.
- **Runtime diagnostics**: `PluginRuntime::runtime_diagnostics()` reads the
active host-level `RuntimeDiagnostics` snapshot, with ordered entries and
`get(code)` lookup.
## Authoring Surface

| Surface | Role |
|---|---|
| `NativePlugin` | Defines plugin identity, configuration validation, registration, and multiple-component behavior without requiring an author to construct ABI tables. |
| `PluginContext` | Installs component-owned subscribers, guardrails, intercepts, continuations, and streams. |
| `PluginRuntime` | Emits marks and manages Relay-owned scopes and scope stacks through typed host helpers. |
| `nemo_relay_plugin!` | Exports the one versioned native entry point used by the loader. |
| Native ABI v4 | Keeps C-compatible host and plugin tables behind the safe Rust interface while the host retains frozen v3 and v2 tables for previously compiled plugins. |
| Typed async middleware | Drives guardrails, sanitizers, and intercepts on a per-component SDK-owned Tokio executor. Subscribers and raw ABI registrations remain synchronous. |
| Async continuations and streams | `ToolNext`, `LlmNext`, and `LlmStreamNext` support repeated or concurrent downstream calls. Streaming LLM continuations use a pull-based host handle. |
| Tool results | `ToolNext` returns `ToolExecutionResult`, which keeps an application result and optional annotation together. |
| Typed telemetry marks | `emit_mark_with_options` adds `DataSchema` and `LogSeverity`; `emit_metric` validates `MetricMeasurement` values before it emits the reserved Relay metric schema. |
| Runtime diagnostics | `PluginRuntime::runtime_diagnostics()` returns the active host-level `RuntimeDiagnostics` snapshot. Entries are ordered and available through `get(code)`. |

## Installation

Expand Down Expand Up @@ -102,27 +81,24 @@ nemo_relay_plugin::nemo_relay_plugin!(nemo_relay_register_plugin, || ExamplePlug
```

Build the `cdylib`, describe its entry symbol and compatibility in a
`relay-plugin.toml` manifest, then register it through the Relay CLI. See the
`relay-plugin.toml` manifest, then register it through the Relay CLI. Refer to the
complete example for platform-specific artifact and manifest setup.
Comment thread
willkill07 marked this conversation as resolved.

Use `compat.native_api = "1"`. Relay 0.8 establishes the canonical
`ToolExecutionResult` JSON contract as the native API 1 baseline. Every native
plugin must be rebuilt for Relay 0.8 and declare a `compat.relay` range that
excludes earlier versions. The recommended range is `>=0.8.0,<1.0`; an
open-ended range such as `>=0.8.0` is also valid. The manifest is the plugin
author's compatibility assertion, not proof that the artifact was rebuilt.

The JSON contract is independent of the native host-table layout. This SDK
continues to export ABI v4, whose C-compatible layouts and callback signatures
are unchanged by the tool-result cutover. Future incompatible native JSON
contract changes must increment `compat.native_api`. Relay creates one
Typed async plugins require `compat.relay = ">=0.8.0,<1.0"`. Relay creates one
SDK-owned Tokio executor for each configured plugin component. It defaults to
two workers: enough for modest concurrent async I/O without broadly
oversubscribing the host. Increase the count only when measured I/O concurrency
leaves callbacks queued; lower it when the host runs many components or has a
tight CPU budget. Do not block these workers; use async I/O or
`tokio::task::spawn_blocking`.

Relay 0.8 establishes canonical tool results as the native API 1 baseline. Tool
callbacks and `ToolNext` return `ToolExecutionResult`, preserving an application result
and optional opaque annotation. Tool execution intercepts return the same pair plus
Relay-owned pending marks. The manifest contract remains `compat.native_api = "1"` and
the C host-table ABI remains v4, but plugins must rebuild and exclude pre-0.8 Relay
versions because the JSON result boundary changed.

Set a plugin-wide default in Rust, then let the component's TOML configuration
override it:

Expand Down Expand Up @@ -170,9 +146,3 @@ ABI-v2 compatibility tables cannot use runtime diagnostics.
Relay scope context is restored around every poll of a registered middleware
future. Child tasks created with `tokio::spawn` do not automatically inherit
that scope context.

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Rust native plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/rust-native-plugin/README.md)
75 changes: 16 additions & 59 deletions crates/worker-proto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,59 +25,22 @@ Use `nemo-relay-worker` to author Rust workers. Depend on this crate directly
only when implementing another worker SDK, a custom host, or protocol-level
tooling.

Relay 0.8 establishes the canonical tool-result contract as the `grpc-v1`
baseline. Workers built for an earlier Relay release must be rebuilt, and their
manifests must declare a `compat.relay` range beginning at `0.8.0` or later.
The protocol identifier and protobuf package remain `grpc-v1` and
`nemo.relay.worker.v1`, respectively. However, the generated protobuf API
changes at the tool-result boundary: `ToolNext` returns
`ToolExecutionResultResponse`, and `ToolExecutionInterceptResult.outcome` is a
typed `ToolExecutionInterceptOutcome`. Rebuild every worker against the Relay
0.8 protocol definitions.

## Why Use It?

- **Share the stable transport contract**: Use the `grpc-v1` service and
message definitions accepted by Relay worker manifests.
- **Use generated Tonic bindings**: Access versioned client and server types
from `v1` without generating protobuf code in a consumer project.
- **Keep data ownership clear**: Use structural protobuf wrappers for tool
results while preserving open application payloads as lossless JSON bytes.
Other Relay DTOs continue to use JSON envelopes backed by
`nemo-relay-types`.

## What You Get

- **`WORKER_PROTOCOL_GRPC_V1`**: The stable `grpc-v1` protocol identifier.
- **`v1` module**: Generated `PluginWorker` and `RelayHostRuntime` gRPC
clients, servers, services, and messages.
- **JSON envelope helpers**: `json_envelope` and `decode_json_envelope` for
serializing Relay DTOs into protocol payloads.
- **JSON value helpers**: `json_value` and `decode_json_value` for the opaque
application values inside structural tool-result messages.

## Structural Tool Result Contract

The `grpc-v1` tool-result boundary uses these generated message types:

| Protocol Location | Protobuf Type |
| --- | --- |
| Successful `RelayHostRuntime.ToolNext` response | `ToolExecutionResultResponse.value` containing `ToolExecutionResult` |
| `ToolExecutionInterceptResult.outcome` | `ToolExecutionInterceptOutcome` |

Both messages define `result` and optional `annotation` fields. Intercept
outcomes also carry their ordered `pending_marks` as one JSON array. Arbitrary
JSON values use `JsonValue`, whose bytes contain exactly one JSON value; this
preserves JSON integers and other application data without the numeric coercion
of `google.protobuf.Value`. Hosts and SDKs reject a missing required `result`
or invalid JSON bytes. JSON null annotations normalize to absence.
- **Additive mark options**: `EmitMarkRequest.data_schema` carries a
`nemo.relay.DataSchema@1` envelope and `severity` carries the canonical log
severity string. Omitting both is wire-compatible with legacy workers.
- **Runtime diagnostics**: Authenticated `GetRuntimeDiagnostics` returns the
bounded active-host `{ code, message, count }` snapshot. Older hosts return
gRPC `UNIMPLEMENTED`; current SDKs surface that as an explicit unsupported
runtime-diagnostics error.
Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline. Workers built
for earlier releases must regenerate their bindings, rebuild, and declare
`compat.relay` beginning at `0.8.0`. `ToolNext` returns `ToolExecutionResultResponse`,
and tool execution intercepts use structural `ToolExecutionInterceptOutcome` messages.

## Protocol Surface

| Surface | Role |
|---|---|
| `WORKER_PROTOCOL_GRPC_V1` | Identifies the stable protocol accepted by Relay worker manifests. |
| `v1` module | Exposes generated `PluginWorker` and `RelayHostRuntime` Tonic clients, servers, services, and messages without regenerating protobuf in a consumer. |
| JSON envelope helpers | Serialize Relay DTOs through `json_envelope` and `decode_json_envelope`, keeping protobuf responsible for transport flow rather than runtime data modeling. |
| JSON value helpers | Serialize application-owned fields inside structural tool-result messages through `json_value` and `decode_json_value`. |
| Tool results | `ToolNext` returns `ToolExecutionResultResponse`, and `ToolExecutionInterceptResult` returns `ToolExecutionInterceptOutcome`. Both preserve the application result and optional annotation. Intercept outcomes also include ordered pending marks. These fields use lossless protobuf `JsonValue` wrappers rather than `google.protobuf.Value`. |
| Mark options | `EmitMarkRequest.data_schema` carries a `nemo.relay.DataSchema@1` envelope and `severity` carries the log severity. Omitting both preserves legacy behavior. |
| Runtime diagnostics | Authenticated `GetRuntimeDiagnostics` returns a bounded active-host `{ code, message, count }` snapshot. Older hosts return gRPC `UNIMPLEMENTED`. |

## Installation

Expand All @@ -104,9 +67,3 @@ fn main() -> Result<(), serde_json::Error> {
Ok(())
}
```

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Rust worker SDK](https://github.com/NVIDIA/NeMo-Relay/blob/main/crates/worker/README.md)
53 changes: 19 additions & 34 deletions crates/worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,34 +20,25 @@ SPDX-License-Identifier: Apache-2.0
dynamic worker plugins. Use it when plugin code needs process isolation and
communicates with Relay through the versioned `grpc-v1` worker protocol.

Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline.
Workers built for an earlier Relay release must be rebuilt with this SDK and
declare a `compat.relay` range beginning at `0.8.0` or later. The protocol name
remains `grpc-v1`, but its generated `ToolNext` response and tool-execution
outcome fields now use structural protobuf messages.

## Why Use It?

- **Isolate plugin code**: Run custom runtime behavior outside the Relay host
process.
- **Use typed registration APIs**: Implement `WorkerPlugin` and register
subscribers, guardrails, or intercepts with `PluginContext`.
- **Call the host runtime**: Emit marks, manage scopes, and invoke middleware
continuations through `PluginRuntime`.
- **Keep lifecycle managed**: Let Relay provide authenticated endpoints and
start the worker with `serve_plugin`.

## What You Get

- **`WorkerPlugin`**: The plugin identity, validation, and registration
contract.
- **`PluginContext`**: Typed registrations for all supported worker surfaces.
- **`PluginRuntime` and continuations**: Host-runtime callbacks and tool/LLM
execution-chain helpers.
- **Canonical tool results**: `ToolNext` returns `ToolExecutionResult`, so
workers can preserve an opaque annotation independently of the tool result.
- **`serve_plugin`**: Tokio gRPC server startup using the Relay-provided worker
environment.
Relay 0.8 establishes canonical tool results as the `grpc-v1` baseline. Workers built
for an earlier release must rebuild with this SDK and declare `compat.relay` beginning at
`0.8.0`. The protocol identifier remains `grpc-v1`, but `ToolNext` now returns
`ToolExecutionResult`, which keeps an optional opaque annotation beside the application
result.

## Authoring Surface

| Surface | Role |
|---|---|
| `WorkerPlugin` | Defines plugin identity, validation, registration, and multiple-component behavior in the worker process. |
| `PluginContext` | Installs typed handlers for all 15 supported registration surfaces. |
| `PluginRuntime` and continuations | Emit marks, manage scopes, and call the remaining tool or LLM execution chain through the authenticated host service. |
| Canonical tool results | Preserve application results and opaque annotations across tool callbacks and continuations. |
| `serve_plugin` | Starts the Tokio gRPC server from the activation identity, local endpoints, and token supplied by Relay. |

This model keeps plugin dependencies and crashes outside the Relay process, while the
SDK retains the shared runtime contract and manages authentication, cancellation, and
shutdown.

## Installation

Expand Down Expand Up @@ -112,9 +103,3 @@ Relay sends `CancelInvocation` when a managed caller is cancelled, times out,
or stops consuming a stream, and the SDK aborts the matching async callback
task. An accepted cancellation confirms the task was found; it cannot prove
that arbitrary blocking work started by the callback has stopped.

## Documentation

- [NeMo Relay documentation](https://docs.nvidia.com/nemo/relay)
- [Build Plugins guide](https://docs.nvidia.com/nemo/relay/build-plugins/about)
- [Python gRPC worker plugin example](https://github.com/NVIDIA/NeMo-Relay/blob/main/examples/python-grpc-worker-plugin/README.md)
Loading
Loading