Forward-merge release/0.7 into main - #711
Conversation
#### Overview Remove the Node-launcher-based npm distribution of the NeMo Relay CLI. Native binaries, shell and PowerShell installers, Cargo installation, and PyPI `nemo-relay-cli-bin` wheels remain supported; Node.js bindings and OpenClaw npm packages are unchanged. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Simplify `scripts/package-cli-bin.py` and its tests to produce only PyPI wheels. - Remove npm CLI workspaces, lockfile entries, version-bump logic, CI artifacts, publication jobs, and path filters. - Remove npm CLI installation guidance and update release documentation to describe the retired npm packages. - Preserve native CLI release binaries and wheels, plus the existing Node.js binding and OpenClaw npm release paths. This intentionally removes the supported npm installation path for the CLI. Deprecating the already-published npm packages remains a post-merge registry operation so package history is retained. Validation: - Focused CLI wheel packaging unit tests: 3 passed. - `npm ci --ignore-scripts --audit=false --fund=false`. - `just test-python` in isolated user configuration: 614 passed, 45 skipped. - `just test-node` in isolated user configuration: 349 passed. - `just docs` and `just docs-linkcheck` (zero errors; existing external redirect check returned HTTP 403 warning). - `uv run pre-commit run --all-files`. #### Where should the reviewer start? Start with `scripts/package-cli-bin.py` and `.github/workflows/ci_rust.yml`, which show the packaging and release-automation boundary after npm CLI removal. Then review `RELEASING.md` for the maintainer-facing retirement guidance. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-665 ## Summary by CodeRabbit * **Chores** * Discontinued npm CLI package distribution. CI/CD workflows updated to remove npm artifact validation, publishing, and uploads; now publish only Python wheel and Node.js packages. * Refactored CLI packaging script and configuration to support PyPI wheel distribution exclusively. * Simplified package workspace by removing npm CLI package module. * **Documentation** * Updated installation documentation, release guides, and README to remove npm CLI installation instructions. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#690
#### Overview Canonicalize OpenTelemetry destinations for duplicate detection while leaving exporter request URLs and public configuration unchanged. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Compare structured destinations after existing bare OTLP/HTTP trace-path resolution. - Realize default HTTP and HTTPS ports, normalize repeated and trailing path slashes, and retain query ordering and values. - Canonicalize standardized localhost names, IPv4 `127.0.0.0/8` forms, IPv4 shorthand, and IPv6 `::1` without DNS lookups. - Preserve transport and scheme distinctions, repeated projection support, parse-failure fallback behavior, and exporter routing. - Add validation and activation-time regression coverage and document endpoint equivalence rules. Validation included the Rust, Python, Node.js, and Go suites; documentation and link checks; and `uv run pre-commit run --all-files`. #### Where should the reviewer start? Start with `canonicalize_opentelemetry_destination` in `crates/core/src/observability/plugin_component.rs`, then review the table-driven collision cases in `crates/core/tests/unit/observability/plugin_component_tests.rs`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-630 ## Summary by CodeRabbit * **Bug Fixes** * Improved OpenTelemetry endpoint collision detection by recognizing equivalent URL formats, including default ports, normalized paths, host casing, and loopback addresses. * Preserved meaningful differences such as schemes, ports, hosts, paths, query strings, and transport types. * Updated collision messages to display normalized destinations more clearly. * **Documentation** * Expanded observability configuration guidance with details about endpoint normalization and duplicate detection behavior. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#693
#### Overview Stamp the intended package version into every prebuilt CLI binary before compilation and prevent corrected 0.7.0 binaries from reusing gateways started by incorrectly identified RC artifacts. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Add a Cargo-only version recipe with SemVer build metadata support. - Derive one CLI package version in CI and reuse it for Cargo, wheel, npm, and binary verification. - Assert exact `nemo-relay <version>` output across native, manylinux, musllinux, macOS, and Windows packaging paths. - Increment the bootstrap protocol from 2 to 3 and reject protocol-2 gateways. - Update bootstrap and CLI compatibility fixtures for protocol 3. - No public API changes. Existing protocol-2 gateways are intentionally incompatible and must stop or idle-shutdown before replacement. Validation: - Cargo version recipe with `0.7.0-rc.6` and `0.7.0+deadbeef` - Exact prerelease CLI `--version` assertion - CLI packaging-script tests - Focused bootstrap compatibility test - All 98 CLI integration tests - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets -- -D warnings` - `uv run pre-commit run --all-files` - `just test-rust` passed the affected workspace and CLI suites; its final FFI unit target encountered an ambient `/Users/wkillian/.nemo-relay/plugins.toml`, causing one unexpected discovery warning and cascading poisoned-lock failures unrelated to this change. #### Where should the reviewer start? Start with `.github/workflows/ci_rust.yml` for the single-source version derivation and exact verification, then `crates/cli/src/bootstrap/mod.rs` and the bootstrap compatibility test for the protocol bump. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-664 ## Summary by CodeRabbit * **New Features** * Added support for the updated bootstrap protocol, improving compatibility with current gateway connections. * Added a dedicated command to update the Cargo package version independently. * **Bug Fixes** * Improved version validation for SemVer build metadata and invalid leading-zero identifiers. * Added checks to ensure CLI binaries and packages report the expected version. * **Tests** * Expanded compatibility coverage for unsupported protocol and package-version combinations. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#689
#### Overview Deprecates the experimental `nemo-relay-switchyard` plugin ahead of its removal in NeMo Relay 0.8 and replacement by a Switchyard-owned native plugin. Clarifies that the existing integration is supported by NeMo Relay 0.6.0 and 0.7.0. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Mark the Switchyard docs landing page and navigation entry as deprecated. - Render the deprecation message as a native warning callout on the MDX documentation pages. - Add removal and replacement notices to the configuration guide, crate README, and example README. - Document support for the existing integration in NeMo Relay 0.6.0 and 0.7.0 across the landing page, architecture description, prerequisite table, configuration guide, crate README, and examples. - Tell readers that the NeMo Relay 0.8 documentation will provide updated configuration guidance and a migration plan when the replacement is available. - Keep the notice independent of any specific upstream pull request or provisional migration commands. Validation: - Targeted pre-commit checks passed for all six changed files, including YAML validation and docs link checking. - `git diff --check` passed. - The repository-wide pre-commit run completed all applicable formatting, lint, type, Cargo, Go, Node, attribution, and docs-link checks successfully. The generated Python worker-protocol check could not run because `just` is not installed. - `just docs` could not run because `just` is not installed in the environment. #### Where should the reviewer start? Start with `docs/configure-plugins/switchyard/about.mdx`, which contains the canonical deprecation warning and the supported-version language reused by the other entry points. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to: none ## Summary by CodeRabbit * **Documentation** * Marked the Switchyard integration as deprecated across guides, navigation, and examples. * Documented compatibility with NeMo Relay 0.6.0 and 0.7.0. * Added notice that the integration will be removed in Relay 0.8 and replaced by a native Switchyard plugin. * Updated setup guidance and example instructions for supported Relay versions. Authors: - Bryan Bednarski (https://github.com/bbednarski9) Approvers: - Will Killian (https://github.com/willkill07) URL: NVIDIA#700
#### Overview Make generated coding-agent enforcement hooks fail closed while keeping observational and after-the-fact hooks explicitly fail open. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Add mutually exclusive `hook-forward --fail-open` and `--fail-closed` modes while preserving the existing unqualified command and `NEMO_RELAY_FAIL_CLOSED=1` behavior. - Generate event-specific POSIX and PowerShell commands for transparent runs, persistent installs, and packaged Claude Code and Codex hooks. - Fail closed for Claude/Codex `PreToolUse` and `PermissionRequest` plus Hermes `pre_tool_call`; explicitly fail open for all other generated events. - Update Codex and Hermes hook validation, trust, uninstall, and migration handling to recognize both policy-specific commands and legacy single-command installations without removing user-owned entries. - Add regression coverage for policy precedence, conflicting flags, generated event mappings, Windows envelopes, packaged hooks, trust state, and legacy migration. - Document the policy split and required reinstall in the CLI guide, coding-agent integration guide, and v0.7 release notes. Validation: - `uv run pre-commit run --all-files` - `just docs` - `cargo fmt --all -- --check` - `cargo clippy --workspace --all-targets -- -D warnings` - Workspace and CLI portions of `just test-rust`, including all 100 CLI integration tests - Complete 88-test FFI binary from an isolated working directory. The normal Cargo FFI invocation inherited `/Users/wkillian/.nemo-relay/plugins.toml` and violated an existing empty-diagnostics test assumption. This change does not introduce a breaking API change. Existing unqualified manual `hook-forward` commands retain their current compatibility behavior. #### Where should the reviewer start? Start with `crates/cli/src/hooks/encoding.rs`, which defines the per-event policy mapping and generates the paired commands. Then review `crates/cli/src/agents/codex/host.rs` and `crates/cli/src/agents/hermes/integration.rs` for trust and legacy migration behavior. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-645 ## Summary by CodeRabbit * **New Features** * Hook forwarding now supports explicit fail-open and fail-closed policies. * Permission-related hooks fail closed, while lifecycle and reporting hooks fail open. * Hook behavior is selected per event across supported coding-agent integrations. * Added options to override the default failure policy. * **Bug Fixes** * Improved hook installation, validation, trust reporting, and cleanup for event-specific policies. * **Documentation** * Added upgrade guidance, policy details, and instructions for reinstalling generated hooks. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#692
#### Overview Preserve structured callback failure details through OpenTelemetry projection so leaf and turn-level spans report the same error classification and expose the originating binding exception type without parsing status descriptions. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Add a structured `CallbackException` flow error that retains Python and JavaScript exception class names while preserving the existing `internal_error` classification. - Emit standard `exception` span events with `exception.type` for the `full` and `gen_ai` projections. - Propagate descendant `error.type` and exception type to errored parent spans, including across scopes suppressed by the GenAI projection. - Match suppressed parent contexts by both trace ID and span ID, with a regression covering colliding span IDs across concurrent traces. - Preserve caller-provided `exception.type` metadata and capture JavaScript exception names even when the exception's message getter throws. - Add Rust projection regressions and Python and Node callback metadata coverage. - Document callback exception projection, parent-span inheritance, and the Rust migration requirement. Breaking change for Rust source compatibility: `FlowError` is exhaustive, so downstream exhaustive matches must handle the new `CallbackException` variant (or add a wildcard) when upgrading. The variant maps to the existing internal failure classification while retaining the binding exception type for observability. Validation: - `cargo fmt --all` - `cargo clippy --workspace --all-targets -- -D warnings` - focused Rust and Node regression tests - `just test-go` - `just docs` - `uv run pre-commit run --all-files` (all hooks passed; the docs link hook was rerun successfully after an initial race with concurrent API reference generation) - `just test-rust`: 1,164/1,165 passed; the single worker-reader timeout passed on isolated rerun - `just test-python`: 603 passed; 11 unrelated plugin tests discovered `/Users/wkillian/.nemo-relay/plugins.toml` - `just test-node`: 339 passed; 10 unrelated plugin tests discovered the same ancestor configuration #### Where should the reviewer start? Start with `crates/core/src/observability/otel.rs`, particularly end-span projection and propagation to the nearest exported parent, then review `failed_descendant_classification_and_exception_propagate_to_agent_span` and `suppressed_parent_error_propagation_isolated_by_trace_id` in the corresponding test module. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-603 ## Summary by CodeRabbit * **New Features** * Callback failures now preserve their original exception type and message across JavaScript and Python integrations. * OpenTelemetry traces include exception types, exception events, and propagated descendant error metadata. * Failed operations consistently report internal errors while retaining detailed exception classifications. * **Documentation** * Expanded OpenTelemetry documentation covering callback exceptions, error inheritance, and metadata behavior. * **Tests** * Added coverage for exception metadata in callback, LLM, tool, streaming, and span error scenarios. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#688
#### Overview Install a default process-lifetime `LoggingRuntime` when the Python, Node.js, and Go bindings load, with environment control matching the CLI. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Add an idempotent core initializer that retains the default logging runtime for the process lifetime. - Initialize operational logging from `NEMO_RELAY_LOG`, `NEMO_RELAY_LOG_STDERR_FORMAT`, and `NEMO_RELAY_LOG_CONFIG_PATH` in Python, Node.js, and Go. - Fail binding startup when logging environment configuration is invalid. - Add subprocess coverage for successful and invalid environment configuration in every binding. - Document automatic binding initialization and environment precedence. - Breaking changes: none. Validation: - Python: 616 passed, 45 skipped. - Node.js: 351 passed. - Go: full suite passed. - FFI: 88 passed in an isolated configuration environment. - `cargo clippy --workspace --all-targets -- -D warnings` passed. - `uv run pre-commit run --all-files` passed. - `just docs` passed. #### Where should the reviewer start? Start with `initialize_default_logging` in `crates/core/src/logging/mod.rs`, then review the Python, Node.js, and Go binding initialization entry points and their subprocess tests. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes RELAY-602 ## Summary by CodeRabbit * **New Features** * Python, Node.js, and Go bindings now automatically initialize operational logging using environment variables or built-in defaults. * Added graceful shutdown APIs that flush pending file-sink records. * Node.js logging remains active across worker environments and shuts down after the final environment closes. * Python logging now shuts down automatically when the interpreter exits. * Existing application loggers are preserved when no explicit logging configuration is provided. * **Bug Fixes** * Invalid logging configuration now reports clear initialization errors. * **Tests** * Added coverage for initialization, shutdown, validation, and file-sink flushing. * **Documentation** * Clarified logging configuration and shutdown requirements for language bindings. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) URL: NVIDIA#691
#### Overview Reject non-string ATIF filename metadata instead of silently routing trajectories through a fallback, and make remote-delivery teardown failures consistently observable and drainable. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Reject present non-string metadata values even when a filename fallback is configured, while retaining fallback behavior for missing and null values. - Preserve remote-upload diagnostics in `plugin.report()` after subscriber delivery is flushed. - Remove the misleading registration-removal wrapper from ATIF delivery errors. - Reset completed Python asynchronous clear state so a failed clear is drainable and a later clear or initialization succeeds. - Add Rust and Python regression coverage and document the resulting filename, reporting, and teardown contract. Validation: - Focused Rust filename-rendering, runtime-diagnostic, HTTP retry, and Python-binding teardown tests passed. - The Python end-to-end non-string metadata and drainable-clear regression passed against `release/0.7`. - `cargo fmt`, workspace clippy, cargo check, changed-file pre-commit checks, Go tests, and documentation checks passed. - Full Rust, Python, and Node runs were also exercised; local failures were limited to assertions receiving the inherited-configuration warning from `/Users/wkillian/.nemo-relay/plugins.toml`, followed by global-state cascades where those tests aborted before cleanup. #### Where should the reviewer start? Start with `render_atif_filename` in `crates/core/src/observability/plugin_component.rs`, then the end-to-end regression in `python/tests/test_observability_plugin.py`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Closes: RELAY-638 ## Summary by CodeRabbit - **Bug Fixes** - Improved observability plugin error reporting for invalid, non-string, or nested metadata in destination filenames. - Preserved delivery diagnostics through teardown, recovery, and failed reconfiguration. - Prevented overlapping asynchronous cleanup operations from overwriting newer plugin state. - Distinguished delivery errors from registration leaks, incomplete removal, and fatal shutdown failures. - **Documentation** - Clarified filename fallback behavior, diagnostic persistence, teardown reporting, and recovery outcomes. - **Tests** - Expanded coverage for invalid metadata, failed delivery cleanup, runtime diagnostics, and asynchronous configuration clearing. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Maryam Najafian (https://github.com/mnajafian-nv) - Eric Evans II (https://github.com/ericevans-nv) URL: NVIDIA#687
#### Overview
Adds a minimal Python compatibility API that converts standard `[[plugins.dynamic]]` records from one explicit `plugins.toml` into the existing `DynamicPluginActivationSpec` objects accepted by `initialize_with_dynamic_plugins()`.
This unblocks Python applications that embed Relay without introducing the larger file-backed activation, lifecycle reconciliation, dynamic layering, or initialization redesign proposed for a later release. The new API is intentionally a temporary 0.7 surface:
```python
plugin_config_path = os.environ["NEMO_RELAY_PLUGINS_TOML"]
dynamic_plugins = plugin.load_dynamic_plugin_activation_specs(plugin_config_path)
activation = await plugin.initialize_with_dynamic_plugins({}, dynamic_plugins)
```
`NEMO_RELAY_PLUGINS_TOML` is an optional host-side convention in this example. Relay does not read the environment variable automatically; the embedding application resolves a path through its environment, command-line, or configuration system and passes that path to the helper.
- [X] I confirm this contribution is my own work, or I have the right to submit it under this project's license.
- [X] I searched existing issues and open pull requests, and this does not duplicate existing work.
A broader implementation exists in NVIDIA#684. This PR is a deliberately scoped 0.7 alternative that reuses the existing activation owner instead of introducing shared lifecycle and host-configuration infrastructure.
#### Details
##### Public API
Adds:
```python
def load_dynamic_plugin_activation_specs(
plugin_config_path: str | os.PathLike[str],
) -> list[DynamicPluginActivationSpec]: ...
```
The helper:
* Reads one explicitly selected `plugins.toml`.
* Parses every `[[plugins.dynamic]]` record in declaration order.
* Resolves relative manifest paths against the selected file.
* Reads `plugin.id` and `plugin.kind` from each manifest.
* Preserves the record's JSON-compatible `config`.
* Rejects malformed TOML, invalid record shapes, unsupported fields, invalid plugin identities, duplicate plugin IDs, and non-JSON configuration.
* Returns the existing activation-spec type without loading code.
The existing dynamic initializer now accepts a `Sequence` rather than only a `list`. This reflects its existing behavior and allows parser results, lists, and tuples to compose without casts.
##### Developer flow
```mermaid
flowchart LR
User["User selects a plugins.toml"] -->
Host["Embedding host resolves the path"]
Env["Optional NEMO_RELAY_PLUGINS_TOML"] --> Host
Host --> Helper["load_dynamic_plugin_activation_specs(path)"]
Helper --> Config["Read one explicit plugins.toml"]
Config --> Records["Parse [[plugins.dynamic]] records"]
Records --> Manifests["Resolve and read relay-plugin.toml manifests"]
Manifests --> Specs["Build DynamicPluginActivationSpec list"]
Specs --> Initialize["initialize_with_dynamic_plugins(config, specs)"]
Initialize --> Activation["Owned PluginHostActivation"]
Activation --> Runtime["Host retains activation while work is admitted"]
Runtime --> Close["await activation.close() during shutdown"]
```
##### Configuration behavior
The temporary dynamic path and existing static configuration path remain separate:
```mermaid
flowchart TB
subgraph Static["Existing static component resolution"]
UserConfig["User plugins.toml"] --> StaticLayering["User → project → system → programmatic overlay"]
ProjectConfig["Project .nemo-relay/plugins.toml"] --> StaticLayering
SystemConfig["System /etc/nemo-relay/plugins.toml"] --> StaticLayering
end
subgraph Dynamic["New 0.7 compatibility path"]
ExplicitPath["One explicit plugins.toml path"] --> DynamicParser["Parse [[plugins.dynamic]] only"]
DynamicParser --> DynamicSpecs["Explicit activation specs"]
end
StaticLayering --> HostInitializer["Existing dynamic host initializer"]
DynamicSpecs --> HostInitializer
HostInitializer --> OwnedHost["PluginHostActivation"]
```
The helper does not perform dynamic-plugin layering. It reads only the explicitly supplied file. Static `[[components]]` from that file are inherited only when the same file is also selected by Relay's normal static discovery.
Every dynamic declaration in the selected file becomes an activation spec. Passing those specs to `initialize_with_dynamic_plugins()` is explicit consent to load the referenced trusted native libraries or worker processes.
Python workers that require a lifecycle-managed `environment_ref` still require the existing explicit activation or CLI lifecycle path.
##### Intentional non-goals
This PR does not:
* Consolidate `initialize()` and `initialize_with_dynamic_plugins()`.
* Add a unified `initialize_from_plugins_toml()` API.
* Discover or merge dynamic records across user, project, and system layers.
* Read or reconcile `.dynamic-plugins.json`.
* Consult CLI enablement or tombstone state.
* Provision or attest Python worker environments.
* Change plugin enablement, install plugins, or execute package managers.
* Change Rust, Node.js, Go, FFI, or CLI behavior.
The helper is documented as a 0.7 compatibility surface and is expected to be deprecated after the unified file-backed initializer lands. Keeping the conversion behind one Relay API lets embedded hosts remove their TOML and manifest parsing now while keeping the future migration localized to one call site.
##### Documentation and validation
Updates the Python type stub, plugin-configuration guide, and 0.7 release notes. Tests cover relative and absolute manifest resolution, native and worker spec construction, config preservation, malformed records and TOML, missing manifests, duplicate IDs, and end-to-end native activation from a real `[[plugins.dynamic]]` record.
Validation completed:
* Focused parser and native-activation tests: `16 passed`.
* Ruff formatting and linting.
* `ty` type checking.
* Changed-file and repository-wide pre-commit suites.
* Cargo formatting, clippy, check, and dependency-policy checks.
* Python worker protobuf compatibility.
* Go formatting and vet.
* Node formatting and public docstring checks.
* Fern structure and strict broken-link validation.
The complete dynamic-host Python module was also attempted locally. Pre-existing tests inherited an invalid machine-level `/etc/nemo-relay/plugins.toml`, and sandboxed worker tests could not bind Unix sockets. The tests directly covering this change passed independently.
Breaking changes: none.
#### Where should the reviewer start?
Start with `python/nemo_relay/plugin.py`, specifically `load_dynamic_plugin_activation_specs()`.
The central design decision is that this helper performs only the missing file-to-activation-spec conversion. It deliberately reuses the existing dynamic initializer and owned activation lifetime rather than introducing another activation owner or pulling CLI lifecycle behavior into the Python binding.
Then review `python/tests/test_dynamic_plugin_host.py` for the standard TOML parsing, failure behavior, and end-to-end native activation coverage.
#### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)
* Relates to NVIDIA#673
* Relates to NVIDIA#684
* Relates to [NousResearch/hermes-agent#77915](<NousResearch/hermes-agent#77915>)
## Summary by CodeRabbit
* **New Features**
* Added a Python compatibility helper for loading dynamic plugin activation specifications from a selected `plugins.toml` file.
* Supports manifest path resolution, ordered activation specifications, nested configuration, duplicate detection, and validation of plugin records and JSON values.
* Dynamic plugin initialization now accepts any ordered collection of activation specifications.
* **Documentation**
* Added guidance covering configuration resolution, explicit loading consent, supported behavior, limitations, and planned deprecation.
* **Tests**
* Expanded coverage for valid configurations, absolute and nested manifest paths, malformed files, duplicate IDs, and missing manifests.
## Summary by CodeRabbit
* **New Features**
* Added support for loading dynamic plugin activation settings from a selected `plugins.toml` file.
* Added validation for manifests, duplicate identifiers, malformed configuration, and invalid JSON values.
* Dynamic plugin initialization now accepts any ordered collection of activation specifications.
* **Documentation**
* Added configuration guidance, behavior details, limitations, compatibility notes, and planned deprecation information.
* **Tests**
* Added coverage for valid configurations, path resolution, nested settings, and common loading errors.
Authors:
- Bryan Bednarski (https://github.com/bbednarski9)
Approvers:
- Will Killian (https://github.com/willkill07)
- Maryam Najafian (https://github.com/mnajafian-nv)
URL: NVIDIA#694
Signed-off-by: Will Killian <wkillian@nvidia.com>
WalkthroughThis PR retires CLI npm packaging, adds event-specific coding-agent hook policies, improves callback error and OpenTelemetry metadata, adds default logging lifecycle APIs across bindings, introduces Python dynamic plugin loading, and updates plugin cleanup and documentation. ChangesRelease and packaging
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Comment |
License DiffCompared against Lockfile license changesLockfile License ChangesRustAdded
Removed
Updated/Changed
NodeAdded
Removed
Updated/Changed
PythonAdded
Removed
Updated/Changed
Status output |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/cli/tests/coverage/agents/plugin_host_tests.rs (1)
111-131: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the canonical Codex event mapping in the fallback.
When
hooks.jsonhas no matching entry,eventcan bepre_tool_use.for_event("pre_tool_use")selects the fail-open command because the policy classifier requiresPreToolUse. The helper can then mask a fail-closed policy error in malformed or incomplete hook-file tests.Proposed fix
- .unwrap_or_else(|| expected_plugin_command().for_event(event).to_owned()) + .unwrap_or_else(|| { + let commands = expected_plugin_command(); + expected_codex_hook_command(&commands, event) + .expect("supported Codex hook event") + .to_owned() + })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/cli/tests/coverage/agents/plugin_host_tests.rs` around lines 111 - 131, Update the fallback in expected_plugin_command_for_hooks to use the canonical event mapping before calling expected_plugin_command().for_event, converting normalized hook names such as "pre_tool_use" to the corresponding policy event "PreToolUse" so fail-closed behavior is preserved when hooks.json has no matching entry.crates/node/tests/llm_tests.mjs (1)
291-305: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAdd
exception.typecoverage for rejected LLM callbacks. The existing assertion covers only synchronousllmCallExecute; the asynchronous test checks only the rejection message. Add coverage for anllmCallExecuteAsynccallback rejection, then runjust test-node.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/node/tests/llm_tests.mjs` around lines 291 - 305, Extend the Node LLM error tests near the existing synchronous llmCallExecute assertion to cover a rejected callback from llmCallExecuteAsync, asserting both the rejection message and exception.type. Keep the existing synchronous coverage intact, and run just test-node to verify the tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/core/src/error.rs`:
- Around line 132-140: Mark the public FlowError enum as non-exhaustive and
update existing exhaustive FlowError matches, including integration-test
matches, with a fallback arm. Preserve the existing CallbackException handling
in FFI and native status mappings while keeping Python and Node string
conversion behavior unchanged.
In `@crates/core/src/observability/plugin_component.rs`:
- Around line 2548-2563: The non-loopback branch of canonical_opentelemetry_host
must normalize trailing dots by returning the computed domain rather than the
raw host. Update crates/core/src/observability/plugin_component.rs lines
2548-2563 accordingly, and extend the collision table in
crates/core/tests/unit/observability/plugin_component_tests.rs lines 2782-2783
with equivalent collector.example. and collector.example URLs.
In `@crates/core/tests/unit/observability/plugin_component_tests.rs`:
- Around line 2782-2783: Update canonical_opentelemetry_host in the non-loopback
hostname handling to remove a trailing dot before returning the canonical host,
while preserving loopback normalization. Add table coverage for
collector.example. and collector.example resolving to the same endpoint.
In `@crates/core/tests/unit/shared_tests.rs`:
- Around line 145-168: Add a negative assertion in the shared metadata tests
using FlowError::Internal to verify metadata_with_otel_error omits the
"exception.type" attribute for non-callback errors, while preserving the
existing callback-derived and caller-override assertions.
In `@crates/ffi/nemo_relay.h`:
- Around line 448-455: Update nemo_relay_initialize_default_logging and its Rust
FFI implementation so default logging initialization does not hard-code the
runtime identity as “go”; accept the caller’s binding name or introduce a
Go-specific wrapper while keeping the generic API identity-neutral. Update the
associated Rust documentation and regenerate crates/ffi/nemo_relay.h to match
the revised FFI surface.
In `@crates/ffi/tests/unit/api_tests.rs`:
- Around line 246-257: The logging lifecycle tests currently cover repeated
shutdown but not the documented idempotent initialization behavior. Add a test
alongside default_logging_shutdown_is_idempotent that acquires TEST_MUTEX, calls
api::nemo_relay_initialize_default_logging() twice expecting
NemoRelayStatus::Ok, then calls api::nemo_relay_shutdown_default_logging() for
cleanup; isolate the test in a child process like
configure_rejects_preinstalled_foreign_logger if the process-global logger could
affect other tests.
In `@crates/node/src/callable.rs`:
- Around line 212-221: Update safe_middleware_callback to preserve synchronous
callback exception metadata by safely extracting error.name and including it as
exceptionType in the returned failure envelope. Ensure the resulting path
supplies a non-empty exception type so it selects FlowError::CallbackException
rather than FlowError::Internal, while retaining the existing error message.
In `@crates/node/src/callback_factory.rs`:
- Around line 201-214: Update the synchronous callback failure handling in the
wrapper around the errorMessage/errorName extraction to preserve thrown
primitive values, matching the behavior used by callPromise. For strings,
numbers, and null, use the thrown value in the returned error field before
attempting object-property access, while retaining the existing message and
exceptionType handling for objects.
In `@go/nemo_relay/nemo_relay.go`:
- Around line 303-307: Document the Go binding’s fail-fast logging behavior next
to ShutdownLogging in go/nemo_relay/README.md, stating that invalid
NEMO_RELAY_LOG values cause init to panic during module loading. Keep the
existing init behavior unless implementing the alternative InitializeLogging()
error API is explicitly required.
In `@RELEASING.md`:
- Line 36: Update both GitHub Release asset descriptions in RELEASING.md,
including the entries around the Node npm tarballs, to explicitly list the
retained nemo-relay-openclaw tarball alongside the existing release artifacts.
---
Outside diff comments:
In `@crates/cli/tests/coverage/agents/plugin_host_tests.rs`:
- Around line 111-131: Update the fallback in expected_plugin_command_for_hooks
to use the canonical event mapping before calling
expected_plugin_command().for_event, converting normalized hook names such as
"pre_tool_use" to the corresponding policy event "PreToolUse" so fail-closed
behavior is preserved when hooks.json has no matching entry.
In `@crates/node/tests/llm_tests.mjs`:
- Around line 291-305: Extend the Node LLM error tests near the existing
synchronous llmCallExecute assertion to cover a rejected callback from
llmCallExecuteAsync, asserting both the rejection message and exception.type.
Keep the existing synchronous coverage intact, and run just test-node to verify
the tests.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Enterprise
Run ID: 779254db-b3a6-40d5-a83c-6fbd3ede4697
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (92)
.github/ci-path-filters.yml.github/workflows/ci.yaml.github/workflows/ci_rust.yml.gitlab-ci.ymlREADME.mdRELEASING.mdcrates/cli/README.mdcrates/cli/src/agents/claude/launch.rscrates/cli/src/agents/codex/host.rscrates/cli/src/agents/codex/launch.rscrates/cli/src/agents/hermes/config.rscrates/cli/src/agents/hermes/integration.rscrates/cli/src/agents/hermes/trust.rscrates/cli/src/agents/mod.rscrates/cli/src/bootstrap/mod.rscrates/cli/src/commands/hook_forward.rscrates/cli/src/hooks/delivery.rscrates/cli/src/hooks/encoding.rscrates/cli/src/hooks/mod.rscrates/cli/src/hooks/types.rscrates/cli/tests/cli_tests.rscrates/cli/tests/coverage/agents/hermes_tests.rscrates/cli/tests/coverage/agents/plugin_host_tests.rscrates/cli/tests/coverage/agents/plugin_install_tests.rscrates/cli/tests/coverage/shared/bootstrap_tests.rscrates/cli/tests/coverage/shared/installer_tests.rscrates/cli/tests/fixtures/windows_hook_relay.rscrates/core/src/api/shared.rscrates/core/src/error.rscrates/core/src/logging/mod.rscrates/core/src/observability/otel.rscrates/core/src/observability/plugin_component.rscrates/core/src/plugin.rscrates/core/src/plugin/dynamic/native.rscrates/core/tests/coverage/error_tests.rscrates/core/tests/coverage/logging_tests.rscrates/core/tests/integration/atif_storage_tests.rscrates/core/tests/unit/observability/otel_tests.rscrates/core/tests/unit/observability/plugin_component_tests.rscrates/core/tests/unit/plugin_tests.rscrates/core/tests/unit/shared_tests.rscrates/ffi/nemo_relay.hcrates/ffi/src/api/mod.rscrates/ffi/src/error.rscrates/ffi/tests/coverage/error_tests.rscrates/ffi/tests/unit/api_tests.rscrates/node/src/api/mod.rscrates/node/src/callable.rscrates/node/src/callback_factory.rscrates/node/src/promise_call.rscrates/node/tests/llm_tests.mjscrates/node/tests/logging_tests.mjscrates/node/tests/tools_tests.mjscrates/python/src/lib.rscrates/python/src/py_api/mod.rscrates/python/src/py_callable.rscrates/python/src/py_plugin.rscrates/python/tests/coverage/py_plugin_coverage_tests.rscrates/switchyard/README.mddocs/about-nemo-relay/release-notes/index.mdxdocs/configure-plugins/about.mdxdocs/configure-plugins/observability/atif.mdxdocs/configure-plugins/observability/configuration.mdxdocs/configure-plugins/observability/opentelemetry.mdxdocs/configure-plugins/plugin-configuration-files.mdxdocs/configure-plugins/switchyard/about.mdxdocs/configure-plugins/switchyard/configuration.mdxdocs/getting-started/installation.mdxdocs/index.ymldocs/nemo-relay-cli/basic-usage.mdxdocs/reference/operational-logging.mdxexamples/switchyard/README.mdgo/nemo_relay/README.mdgo/nemo_relay/logging_test.gogo/nemo_relay/nemo_relay.gointegrations/coding-agents/README.mdintegrations/coding-agents/claude-code/hooks/hooks.jsonintegrations/coding-agents/codex/hooks/hooks.jsonjustfilepackage.jsonpackages/cli-bin/package.jsonpython/nemo_relay/__init__.pypython/nemo_relay/_native.pyipython/nemo_relay/plugin.pypython/nemo_relay/plugin.pyipython/tests/test_dynamic_plugin_host.pypython/tests/test_llm.pypython/tests/test_logging.pypython/tests/test_observability_plugin.pypython/tests/test_tools.pyscripts/package-cli-bin.pyscripts/tests/test_package_cli_bin.py
💤 Files with no reviewable changes (5)
- packages/cli-bin/package.json
- crates/cli/README.md
- README.md
- .github/workflows/ci.yaml
- .github/ci-path-filters.yml
Overview
Forward-merge
release/0.7intomainafter the automated forward-merger PR encountered conflicts.Details
mainandrelease/0.7histories in a merge commit.Merge requirement: An admin must merge this PR with GitHub's Create a Merge Commit strategy. Do not use the auto-merger or a
/mergecomment; squashing or rebasing would lose the release branch history.Validation:
cargo fmt --allpassed.cargo clippy --workspace --all-targets -- -D warningspassed.just test-rust: 1,177 passed and one parallel global-registration test failed; that test passed on an immediate isolated rerun.just test-python: 628 passed; 11 failures resulted from the pre-existing/Users/wkillian/.nemo-relay/plugins.tomlbeing inherited, with the first warning leaving static state active and cascading into the dynamic-host tests.just test-gowas interrupted at the request to proceed directly with publishing.just test-nodeand the all-files pre-commit suite were not run.Where should the reviewer start?
Start with the merge commit's resolution of
package-lock.jsonand deletion ofpackages/cli-bin/package.json, then verify that both parents of the merge commit are preserved.Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)
Summary by CodeRabbit
New Features
--fail-openand--fail-closedoptions for hook forwarding.plugins.toml.Documentation