Skip to content

feat: add nemoguardrails plugin contract#131

Open
afourniernv wants to merge 4 commits into
NVIDIA:mainfrom
afourniernv:afournier/nmf-72-design-plugin-fresh
Open

feat: add nemoguardrails plugin contract#131
afourniernv wants to merge 4 commits into
NVIDIA:mainfrom
afourniernv:afournier/nmf-72-design-plugin-fresh

Conversation

@afourniernv
Copy link
Copy Markdown
Contributor

@afourniernv afourniernv commented May 19, 2026

Overview

Adds the first core-owned nemoguardrails plugin contract for NeMo Flow.

This PR intentionally stays at the contract layer:

  • adds a public typed Rust config surface for the planned first-party nemoguardrails plugin

  • models remote and local backend modes

  • adds validation, schema support, and editor metadata

  • does not auto-register the plugin yet

  • does not implement backend runtime behavior yet

  • I confirm this contribution is my own work, or I have the right to submit it under this project's license.

  • I searched existing issues and open pull requests, and this does not duplicate existing work.

Details

  • add crates/core/src/nemoguardrails/ as the public core module for the planned Guardrails plugin surface
  • add ComponentSpec, NeMoGuardrailsConfig, backend config sections, and manual registration helpers
  • model the backend split explicitly:
    • mode = remote | local
    • local config-source pass-through:
      • config_path
      • config_yaml
      • colang_content
    • remote config identity:
      • remote.endpoint
      • remote.config_id
      • remote.config_ids
  • keep NeMo Flow-owned interception toggles in the contract:
    • input
    • output
    • tool_input
    • tool_output
  • widen the contract to include backend request defaults without claiming full backend parity:
    • context
    • request-time rail selection
    • llm_params
    • output_vars
    • llm_output
    • log
  • add focused unit coverage for:
    • schema/editor metadata
    • singleton behavior
    • explicit registration vs auto-registration
    • invalid config shapes and values
    • nested unknown-field policy
    • fail-fast initialization until backend implementation lands

Where should the reviewer start?

Start in crates/core/src/nemoguardrails/plugin_component.rs.

The most important design decision in this PR is that it stops at the public contract layer: it makes the Guardrails surface broad enough to avoid repeating the old narrow example-plugin shape, but it does not auto-register or implement the backend yet.

Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to)

  • Relates to #NMF-72
  • Relates to #NMF-131

Summary by CodeRabbit

New Features

  • Introduced NeMo Guardrails plugin component with comprehensive configuration support
  • Added plugin registration and deregistration capabilities
  • Implemented configuration validation with detailed error reporting for invalid inputs
  • Added JSON schema generation support for plugin configuration settings

Tests

  • Added extensive unit test coverage for plugin component contract validation, configuration validation rules, and schema generation behavior

Review Change Stack

Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv afourniernv requested a review from a team as a code owner May 19, 2026 17:06
@copy-pr-bot
Copy link
Copy Markdown

copy-pr-bot Bot commented May 19, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added size:XL PR is extra large Feature a new feature labels May 19, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 19, 2026

Walkthrough

This PR introduces a complete NeMo Guardrails plugin component for the core framework. It exports a new public module, defines configuration schema with remote/local backend options, implements plugin registration, adds comprehensive validation with semantic constraint checking, and includes extensive test coverage for configuration handling and error scenarios.

Changes

NeMo Guardrails Plugin Component

Layer / File(s) Summary
Public API Surface and Module Exports
crates/core/src/lib.rs, crates/core/src/nemoguardrails/mod.rs
Adds public nemoguardrails module export at crate root and establishes module surface with documentation and test helper for runtime-owned mutex synchronization.
Configuration Data Model
crates/core/src/nemoguardrails/plugin_component.rs (lines 1–239)
Defines ComponentSpec wrapper, root NeMoGuardrailsConfig with version/mode/codec/surface toggles, RemoteBackendConfig and LocalBackendConfig with nested sub-structures, RequestDefaultsConfig, RequestRailsConfig, and RailSelector enum with default initializers.
Editor Metadata and Plugin Registration
crates/core/src/nemoguardrails/plugin_component.rs (lines 240–388)
Wires editor_config! metadata for all config types including enum constraints and optional field sections; implements Plugin trait with validation delegation and registration error; exposes public registration/deregistration functions and feature-gated nemoguardrails_config_schema() for JSON schema generation.
Schema Generation Infrastructure
crates/core/src/nemoguardrails/plugin_component.rs (lines 389–421)
Provides enum-to-schema conversion and default value metadata helpers for feature-gated schema generation.
Configuration Parsing and Semantic Validation
crates/core/src/nemoguardrails/plugin_component.rs (lines 423–1105)
JSON parsing entry point with error handling; orchestrates unknown-field and policy validation; enforces mode/backend/codec/surface/request-defaults constraints with detailed diagnostics; implements validation helpers for field shapes, named rail entries, and nested unknown fields; provides diagnostic construction and configuration defaults.
Comprehensive Test Suite
crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
Covers editor schema metadata, default config conversions, JSON schema introspection, registration lifecycle, disabled component initialization, singleton enforcement, comprehensive invalid configuration scenarios with specific error codes and field paths, policy-driven unknown field handling, and initialization error propagation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format with type 'feat' and a concise imperative summary, staying well under 72 characters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description follows the template with all required sections completed: Overview with confirmation checkboxes, Details with comprehensive changes, reviewer guidance, and related issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the lang:rust PR changes/introduces Rust code label May 19, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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/nemoguardrails/plugin_component.rs`:
- Around line 365-370: Current code treats a duplicate-registration as success
by matching the error message text "already registered" from register_plugin;
instead, make the check state-based: first call
lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND) and if it returns Some(_) immediately
return Ok(()); otherwise call register_plugin(Arc::new(NeMoGuardrailsPlugin))
and propagate any error. If altering call order is undesirable, update error
handling to match a structured PluginError variant (e.g.,
PluginError::AlreadyRegistered) instead of inspecting the message string and use
that variant in the match for idempotent behavior.
🪄 Autofix (Beta)

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: 0a6c1c82-1ed6-45a5-9332-098d843644c1

📥 Commits

Reviewing files that changed from the base of the PR and between 91ca10c and 67db881.

📒 Files selected for processing (4)
  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (11)
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/test-ffi-surface/SKILL.md)

**/*.rs: Run cargo fmt --all for FFI work as it is Rust work
Run just test-rust for FFI validation
Run cargo clippy --workspace --all-targets -- -D warnings to enforce warnings-as-errors linting

**/*.rs: Run cargo fmt --all for Rust code formatting
Run cargo clippy --workspace --all-targets -- -D warnings to enforce Rust linting with no warnings
Run just test-rust as the shared-runtime build/test wrapper for Rust changes

Use Rust snake_case naming convention for Rust code

**/*.rs: Any Rust change must run just test-rust
Any Rust change must run cargo fmt --all
Any Rust change must run cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all for Rust code formatting when Node changes touch Rust files
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting when Rust files changed as part of Node work

**/*.rs: Always run just test-rust when any Rust code changes
Always run cargo fmt --all when any Rust code changes
Always run cargo clippy --workspace --all-targets -- -D warnings when any Rust code changes

If any Rust files changed as part of the Python work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
**/*.{rs,go,js,ts,tsx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include SPDX license headers in all Rust, Go, JavaScript, and TypeScript source files using C-style comment syntax

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
**/*.{rs,py,go,js,ts,tsx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use SONAR_IGNORE_START / SONAR_IGNORE_END markers only for documented false positives that cannot be resolved in code; keep ignored blocks small, add explanatory comments, and require reviewer sign-off

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
**/*.{js,ts,tsx,jsx,py,rs,go,java,c,cpp,h,cc,cxx,cs,rb,php,swift,kt}

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

Changed files must be formatted with the language-native formatter

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
crates/{core,adaptive}/**

📄 CodeRabbit inference engine (.agents/skills/prepare-pr/SKILL.md)

Changes to crates/core or crates/adaptive must run the full language matrix

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
{crates/core,crates/adaptive}/**/*.rs

📄 CodeRabbit inference engine (.agents/skills/test-wasm-binding/SKILL.md)

If the change touched shared runtime semantics in crates/core or crates/adaptive, also use validate-change

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
crates/core/**/*.rs

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

When crates/core changes, run the full validation matrix across Rust, Python, Go, Node.js, and WebAssembly

crates/core/**/*.rs: Use Json = serde_json::Value in Rust-facing runtime APIs where the existing code expects JSON payloads.
Use Result<T> with FlowError in core runtime paths. Keep errors explicit and binding-appropriate at the wrapper layer.

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
**/*.{py,js,ts,tsx,go,rs,md}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

Format changed files with the language-native formatter before the final lint/test pass

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
**/*.{rs,py,js,ts,tsx,go}

📄 CodeRabbit inference engine (.agents/skills/validate-change/SKILL.md)

During iteration, prefer uv run pre-commit run --files <changed files...> for targeted validation

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
crates/{core,adaptive}/**/*.rs

⚙️ CodeRabbit configuration file

crates/{core,adaptive}/**/*.rs: Review the Rust runtime for async correctness, scope isolation, middleware ordering, and event lifecycle regressions.
Pay close attention to task-local/thread-local scope propagation, callback lifetimes, stream finalization, and root_uuid isolation.
Public API changes should preserve existing behavior unless tests and docs show the intended migration path.

Files:

  • crates/core/src/lib.rs
  • crates/core/src/nemoguardrails/mod.rs
  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
  • crates/core/src/nemoguardrails/plugin_component.rs
{crates/**/tests/**,python/tests/**,go/nemo_flow/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_flow/**/*_test.go}: Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant.
Prefer assertions on lifecycle events, scope stacks, middleware ordering, and binding parity over shallow smoke tests.

Files:

  • crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs
🔇 Additional comments (4)
crates/core/src/lib.rs (1)

64-64: LGTM!

crates/core/src/nemoguardrails/mod.rs (1)

1-15: LGTM!

crates/core/src/nemoguardrails/plugin_component.rs (1)

1-364: LGTM!

Also applies to: 371-1105

crates/core/tests/unit/nemoguardrails/plugin_component_tests.rs (1)

1-638: LGTM!

Comment on lines +365 to +370
match register_plugin(Arc::new(NeMoGuardrailsPlugin)) {
Ok(()) => Ok(()),
Err(PluginError::RegistrationFailed(message))
if message.contains("already registered")
&& lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND).is_some() =>
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Avoid brittle duplicate-registration detection.

This path depends on matching error message text ("already registered"), which is fragile across internal error-message changes. Prefer state-based idempotency checks.

Proposed refactor
 pub fn register_nemoguardrails_component() -> PluginResult<()> {
+    if lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND).is_some() {
+        return Ok(());
+    }
+
     match register_plugin(Arc::new(NeMoGuardrailsPlugin)) {
         Ok(()) => Ok(()),
-        Err(PluginError::RegistrationFailed(message))
-            if message.contains("already registered")
-                && lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND).is_some() =>
+        Err(PluginError::RegistrationFailed(_))
+            if lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND).is_some() =>
         {
             Ok(())
         }
         Err(err) => Err(err),
     }
 }
🤖 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/core/src/nemoguardrails/plugin_component.rs` around lines 365 - 370,
Current code treats a duplicate-registration as success by matching the error
message text "already registered" from register_plugin; instead, make the check
state-based: first call lookup_plugin(NEMO_GUARDRAILS_PLUGIN_KIND) and if it
returns Some(_) immediately return Ok(()); otherwise call
register_plugin(Arc::new(NeMoGuardrailsPlugin)) and propagate any error. If
altering call order is undesirable, update error handling to match a structured
PluginError variant (e.g., PluginError::AlreadyRegistered) instead of inspecting
the message string and use that variant in the match for idempotent behavior.

@willkill07
Copy link
Copy Markdown
Member

From a design choice -- do we want this to live in the core crate or be part of a new crate?

If it's part of core, we should be forward thinking and have a src/plugins module

@willkill07 willkill07 added this to the 0.3 milestone May 19, 2026
@afourniernv
Copy link
Copy Markdown
Contributor Author

@willkill07 I think a separate crate makes sense if this starts adding deps to core.

Right now PR 1 doesn’t. Remote mode (PR2) doesn’t either. Local mode (PR3) as far as I researched is really the Python SDK/runtime path, not just another transport aka “call a localhost server.” If we want faithful local behavior, I’d prefer a Python helper/subprocess over in-process Python embedding from Rust.

So I don’t think there’s a strong dependency reason to split into a separate crate yet.

If you just want it totally modularized out of core I can design it that way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature a new feature lang:rust PR changes/introduces Rust code size:XL PR is extra large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants