Skip to content

feat: add dynamic plugin control-plane foundation#279

Draft
afourniernv wants to merge 2 commits into
NVIDIA:mainfrom
afourniernv:afournier/relay-337-plugin-control-plane-model
Draft

feat: add dynamic plugin control-plane foundation#279
afourniernv wants to merge 2 commits into
NVIDIA:mainfrom
afourniernv:afournier/relay-337-plugin-control-plane-model

Conversation

@afourniernv

@afourniernv afourniernv commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Overview

Add the first Rust-side dynamic plugin control-plane slice for RELAY-337.

  • 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 crate::plugin::dynamic as the foundation for dynamic plugin control-plane types
  • introduce durable dynamic plugin record modeling with metadata, source, spec, and status
  • add authored relay-plugin.toml manifest types, parsing, path loading, strict validation, and conversion into registry records
  • add an in-memory dynamic plugin registry with add/get/list/enable/disable/remove semantics, tombstone revival, and status updates
  • enforce kind-specific manifest and raw-record shape validation for rust_dynamic and worker plugin lanes
  • add focused unit coverage for manifest validation, canonical path handling, registry lifecycle semantics, tombstones, generation behavior, and raw-record integrity checks
  • add PluginError::Conflict and propagate it through adaptive and FFI error mappings

Validation run:

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p nemo-relay plugin::dynamic::tests --lib

Known validation gap:

  • just test-rust is currently red on this branch because of the pre-existing failure in crates/core/tests/integration/pipeline_tests.rs for test_response_codec_failure_non_fatal, which appears unrelated to this change.

Where should the reviewer start?

Start in crates/core/src/plugin/dynamic.rs and then review:

  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs

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

  • Relates to RELAY-337

@afourniernv afourniernv requested review from a team and lvojtku as code owners June 16, 2026 18:27
@copy-pr-bot

copy-pr-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

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:XXL PR is very large Feature a new feature lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code labels Jun 16, 2026
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 39f538e1-eedb-4653-a722-ed64f245762e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds a dynamic plugin control-plane to crates/core: a new PluginError::Conflict variant, a full durable schema for DynamicPluginRecord (enums, structs, metadata helpers), relay-plugin.toml manifest parsing/validation, and an in-memory DynamicPluginRegistry with lifecycle and status-update operations. Error mappings in the adaptive and FFI crates are updated for the new variant.

Changes

Dynamic Plugin Control-Plane

Layer / File(s) Summary
PluginError::Conflict variant and module wiring
crates/core/src/plugin.rs, crates/adaptive/src/error.rs, crates/ffi/src/error.rs
Adds Conflict(String) to PluginError, preserves it in clone_cached_plugin_error, exposes the dynamic submodule via pub mod + pub use, and maps the variant to AdaptiveError::InvalidConfig and NemoRelayStatus::InvalidArg.
Durable schema: enums, structs, metadata helpers
crates/core/src/plugin/dynamic.rs
Defines all exported enums (DynamicPluginKind, WorkerRuntime, capabilities, startup/attestation modes, check/runtime/failure state axes), durable structs (DynamicPluginMetadata, Source, Spec, Compatibility, LoadContract, Failure, ValidationStatus, RuntimeStatus, Status, Record), is_reconciled/is_tombstoned helpers, and crate-scoped timestamp/generation mutation functions.
Manifest schema, parsing, validation, and record conversion
crates/core/src/plugin/dynamic/manifest.rs
Defines the relay-plugin.toml data model and implements parse_toml, load_from_path with canonicalization, comprehensive v1 validate (required fields, optional-string trimming, kind-specific capability/load/compat rules), into_record converting a validated manifest to a disabled DynamicPluginRecord, and validation_status.
DynamicPluginRegistry implementation
crates/core/src/plugin/dynamic/registry.rs
Implements DynamicPluginRegistry backed by BTreeMap: get/list, add/add_manifest (shape validation, conflict/tombstone lineage inheritance, metadata stamping), enable/disable/remove (tombstone guard, generation bump), status update methods, and validate_record_shape with kind-specific enforcement.
Unit tests
crates/core/tests/unit/plugin_dynamic_tests.rs
Tests DynamicPluginRecord defaults/reconcile/tombstone/JSON round-trip, registry add/list/conflict/revival/lineage/lifecycle/status-update/NotFound semantics, and manifest parse/validate/load/convert for both lanes with comprehensive rejection cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 73.49% 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 Title follows Conventional Commits format with 'feat' type and concise imperative summary 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 PR description is complete with all required sections: overview with confirmations, detailed changes, reviewer guidance, and related issue reference.

✏️ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 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/adaptive/src/error.rs`:
- Line 56: The PluginError::Conflict variant is being mapped to semantically
incorrect downstream error types, losing conflict-related information needed for
proper error handling. At crates/adaptive/src/error.rs#L56-L56, change the
mapping from Self::InvalidConfig(message) to a conflict or state-failure
semantic error class such as Self::RegistrationFailed or an equivalent conflict
variant that better represents the duplicate-state nature of the error. At
crates/ffi/src/error.rs#L141-L143, change the mapping from
NemoRelayStatus::InvalidArg to NemoRelayStatus::AlreadyExists (or another
dedicated conflict status variant if available) to accurately represent the
conflict semantics when converting PluginError::Conflict to FFI error codes.
Both changes must preserve and forward the conflict context through the error
boundary mappings.

In `@crates/cli/tests/coverage/plugins_tests.rs`:
- Around line 56-74: The test fixtures and assertions for PII redaction are
using an outdated configuration contract that does not match the canonical
definition in crates/core/src/plugins/nemo_anonymizer/component.rs. Update the
pii_redaction_component_config function at lines 56-74 and all affected test
assertions at lines 194-224, 396-416, 1256-1269, and 1390-1402 to align with the
actual core contract: replace "mode": "builtin" with the correct mode schema
(likely "local"), remove the "builtin" configuration section and replace it with
the structure expected by the nemo_anonymizer component, replace any hardcoded
kind literals with the PII_REDACTION_PLUGIN_KIND constant imported from the core
component, and update all schema validation assertions to reflect the corrected
configuration contract.

In `@crates/core/Cargo.toml`:
- Around line 72-73: Remove the unused `regex` and `sha2` dependencies from the
Cargo.toml file in crates/core. Both dependencies are not used in the codebase:
`regex` only appears in comments and `sha2` has no references anywhere in
crates/core/src/. Delete both dependency declarations entirely to eliminate
unused-dependency warnings and maintain a clean dependency tree.

In `@crates/core/src/plugin/dynamic/manifest.rs`:
- Around line 332-343: Replace the Vec-based duplicate detection in the
reject_duplicate_capabilities function with a HashSet to improve performance
from O(n²) to O(n). Change the seen variable from a Vec to a HashSet, and update
the contains check to use the insert method (which returns false if the element
was already present), allowing you to detect duplicates while building the set
in a single pass.

In `@crates/core/src/plugin/dynamic/registry.rs`:
- Around line 59-60: The stamp_creation_metadata function sets both created_at
and updated_at to the same value, but the immediately following touch_metadata
call overwrites updated_at, creating an unintended microsecond gap for newly
added records. Either remove the touch_metadata call since
stamp_creation_metadata already sets updated_at appropriately, or if this
behavior is intentional (treating add as a distinct mutation), add a clear
comment explaining why both calls are necessary.

In `@crates/core/src/plugins/nemo_anonymizer/component.rs`:
- Around line 634-663: The validate_local_mode_requirements function has two
issues: first, the early return on line 640 when config.local.is_some() prevents
the deprecated builtin check from always being enforced; second, the final
push_policy_diag call on line 656 unconditionally rejects missing local settings
even for non-local modes. Restructure the function to always check for and
reject the deprecated builtin configuration regardless of whether local is
present, then gate the final diagnostic about missing local settings to only
emit when the configured mode is explicitly 'local' (check the policy or config
for the actual mode value to conditionally emit this final diagnostic).
- Around line 756-821: The validation logic contains duplicate diagnostic checks
that fire for the same invalid field states. When local.strategy equals
"substitute", both the first if block (checking strategy == "substitute") and
the second if block (checking strategy != "hash") execute and report identical
errors for local.algorithm and local.digest_length. Remove the duplicate checks
for local.algorithm.is_some() and local.digest_length.is_some() from within the
if local.strategy == "substitute" block, keeping only the check for
local.format_template and local.instructions in that block, since the second if
local.strategy != "hash" block will already catch and report the algorithm and
digest_length validation errors comprehensively.

In `@crates/python/tests/coverage/coverage_tests.rs`:
- Around line 48-55: The fake_guardrails_module_prelude function adds python_dir
to sys.path[0], but the with_isolated_nemo_relay_modules isolation helper only
restores sys.modules, not sys.path. This allows later tests to import from the
source tree instead of the test environment. Modify the
with_isolated_nemo_relay_modules function to also save the original sys.path
before executing the test code and restore it afterward, ensuring sys.path
modifications do not leak between tests.

In `@docs/nemo-anonymizer-plugin/about.mdx`:
- Line 49: On line 49 of the about.mdx file, change the word "builtin" to
"built-in" in the phrase "the previous builtin deterministic lane is no longer
part of this plugin" to maintain consistent technical documentation wording
conventions.

In `@python/nemo_relay/_nemo_anonymizer_local.py`:
- Around line 323-334: The _annotated_message_text function currently joins all
text blocks with newlines, which loses the original block boundaries and causes
content to be truncated or shifted when sanitized text changes newline counts.
Instead of returning a single concatenated string, refactor this function to
return the extracted text as a per-block list structure that preserves each
block's original text separately. Update the callers (the OpenAI Responses and
Anthropic overlay helpers at lines 395-428) to overlay sanitized content by
block index instead of splitting the concatenated string on newlines, ensuring
block boundaries are maintained throughout the sanitization and overlay process.
- Around line 302-308: The function _apply_annotated_request_payload extracts
and restores several fields from the payload back to the request object
(messages, model, params, tools, tool_choice), but it does not restore the
sanitized extra field. Since extra fields are included in
_annotated_request_payload and can contain PII that gets sanitized, you must add
a line to assign the sanitized extra field from the payload back to the request
object using the same pattern as the other field assignments (request.extra =
payload.get("extra")).
- Around line 497-502: The sanitize_request function lacks error handling for
codec decoding failures, allowing decode errors to escape instead of falling
back to raw request sanitization like the response path does. Wrap the
codec.decode(request) call in a try-except block in the sanitize_request
function, and when decoding fails, implement a fallback path that sanitizes the
raw request payload directly using runner.sanitize_json without attempting to
annotate or re-encode it, ensuring PII is sanitized even when the codec fails.
- Around line 8-11: The CSV file handling at line 198 uses NamedTemporaryFile
with delete=True and passes the file path to the anonymizer while the file
handle remains open, causing Windows permission errors. Replace the
NamedTemporaryFile approach with TemporaryDirectory to create a temporary
directory, then write the CSV data to a file within that directory with explicit
UTF-8 encoding. Ensure the file is fully closed before passing its path to the
anonymizer runtime, and properly manage the TemporaryDirectory context to clean
up after the anonymizer completes its work.
🪄 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: 1830a9ff-248b-46f1-b26d-aea321b70a8b

📥 Commits

Reviewing files that changed from the base of the PR and between 2841a32 and 4c6eb28.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (36)
  • ATTRIBUTIONS-Rust.md
  • crates/adaptive/src/error.rs
  • crates/cli/src/plugins.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/Cargo.toml
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin.rs
  • crates/core/src/plugin/dynamic.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/src/plugins/mod.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/stream.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/ffi/src/error.rs
  • crates/python/src/lib.rs
  • crates/python/src/py_plugin.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/build-plugins/nemoguardrails.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
💤 Files with no reviewable changes (1)
  • docs/build-plugins/nemoguardrails.mdx
📜 Review details
🧰 Additional context used
📓 Path-based instructions (48)
{docs/**,README.md,CONTRIBUTING.md}

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

{docs/**,README.md,CONTRIBUTING.md}: For docs-only changes, run targeted checks only if commands, package names, or examples changed. Use just docs for docs-site builds and just docs-linkcheck when links changed
Run docs site build with just docs

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
{docs/**,README.md,CONTRIBUTING.md,**/*.md}

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

Run docs link validation with just docs-linkcheck when links change

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • ATTRIBUTIONS-Rust.md
  • docs/nemo-guardrails-plugin/configuration.mdx
{docs/**,README.md}

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

Verify README and docs entry points still match current package names and paths for large or public-facing changes

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
{docs/**,examples/**,README.md}

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

Verify examples still run with documented commands for large or public-facing changes

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
{docs/**,README.md,**/Cargo.toml,**/package.json,**/*.md}

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

Ensure renamed public surfaces are reflected consistently in manifests and docs for large or public-facing changes

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • crates/core/Cargo.toml
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • ATTRIBUTIONS-Rust.md
  • docs/nemo-guardrails-plugin/configuration.mdx
**/*.{md,mdx,py,sh,yaml,yml,toml,json}

📄 CodeRabbit inference engine (.agents/skills/contribute-docs/SKILL.md)

Keep package names, repo references, and build commands current

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • crates/core/Cargo.toml
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
  • ATTRIBUTIONS-Rust.md
  • docs/nemo-guardrails-plugin/configuration.mdx
**/*.mdx

📄 CodeRabbit inference engine (.agents/skills/contribute-docs/SKILL.md)

In MDX files, top-of-file comments must use JSX comment delimiters: {/* to open and */} to close. Do not use HTML comments for MDX SPDX headers.

MDX top-of-file SPDX comments must use {/* ... */} delimiters instead of HTML comment delimiters (Must-Fix)

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
**/*.{html,md,mdx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include SPDX license header in HTML and Markdown files using HTML comment syntax

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • ATTRIBUTIONS-Rust.md
  • docs/nemo-guardrails-plugin/configuration.mdx
docs/**/*.{md,mdx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Update embedded documentation snippets, patch docs, and binding-support notes if examples or supported bindings changed

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
docs/**

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run just docs or ./scripts/build-docs.sh html to regenerate ignored Fern API reference pages before validation for documentation site changes

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
{docs/**,README.md,CONTRIBUTING.md,RELEASING.md,SECURITY.md}

⚙️ CodeRabbit configuration file

{docs/**,README.md,CONTRIBUTING.md,RELEASING.md,SECURITY.md}: Review documentation for technical accuracy against the current API, command correctness, and consistency across language bindings.
Flag stale examples, missing SPDX headers where required, and instructions that no longer match CI or pre-commit behavior.

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • docs/index.yml
  • docs/nemo-anonymizer-plugin/about.mdx
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • docs/nemo-guardrails-plugin/configuration.mdx
**

⚙️ CodeRabbit configuration file

**:

AGENTS.md

This file provides guidance to agents, including Claude Code and OpenAI Codex, when working in this repository.

Project Overview

NeMo Relay is a multi-language agent runtime framework for execution scopes, lifecycle events, middleware, plugins, and observability around tool and LLM calls. The core runtime is Rust. Primary supported bindings are Rust, Python, and Node.js. Go, WebAssembly, and the raw C FFI are experimental and source-first.

The shared runtime model is:

  1. Scope stacks decide where work belongs and which scope-local behavior is visible.
  2. Middleware registries decide what guardrails and intercepts run around managed calls.
  3. Plugins install reusable runtime behavior from configuration.
  4. Events record runtime behavior in ATOF form.
  5. Subscribers and exporters consume events in-process or export them to ATIF, OpenTelemetry, OpenInference, or other backends.

Repository Structure

The repository layout separates the Rust runtime, language bindings, documentation,
integration patches, and agent-facing skills.

crates/
  core/       # Rust core runtime crate, published as nemo-relay
  adaptive/   # Adaptive runtime primitives and plugin components
  python/     # PyO3 native extension for the Python package
  ffi/        # Raw C ABI layer used by downstream bindings such as Go
  node/       # NAPI Node.js binding and JavaScript/TypeScript entry points
  wasm/       # wasm-bindgen WebAssembly binding and JS wrappers
python/
  nemo_relay/  # Python wrapper package: scopes, tools, LLM, middleware, typed helpers, plugins, adaptive helpers
  tests/      # Python tests
go/
  nemo_relay/  # Experimental Go CGo binding and tests
fern/         # Fern documentation site
scripts/      # Stable wrappers and helper scripts; build/test/docs entry points live in justfile
third_party/  # P...

Files:

  • docs/about-nemo-relay/concepts/plugins.mdx
  • crates/core/src/plugins/mod.rs
  • docs/index.yml
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • docs/nemo-anonymizer-plugin/about.mdx
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • docs/nemo-guardrails-plugin/about.mdx
  • docs/nemo-anonymizer-plugin/configuration.mdx
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • python/nemo_relay/_guardrails_local.py
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • python/nemo_relay/_nemo_anonymizer_local.py
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • ATTRIBUTIONS-Rust.md
  • docs/nemo-guardrails-plugin/configuration.mdx
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
**/*.rs

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Use snake_case naming convention for Rust identifiers (e.g., nemo_relay_tool_call)

**/*.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 all FFI work since it is Rust work
Run just test-rust to validate FFI changes
Run cargo clippy --workspace --all-targets -- -D warnings to enforce strict linting on FFI work

When Rust files changed as part of Go work, also run cargo fmt --all, just test-rust, and cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Run cargo fmt --all when Rust files are changed as part of Node work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files are changed as part of Node work
Run just test-rust when Rust files are changed as part of Node work

**/*.rs: Run cargo fmt --all to format all Rust code
Run cargo clippy --workspace --all-targets -- -D warnings to enforce all clippy lints as errors

**/*.rs: Run cargo fmt --all when Rust files changed as part of WebAssembly work
Run cargo clippy --workspace --all-targets -- -D warnings when Rust files changed as part of WebAssembly work

**/*.rs: If any Rust code changed, always run just test-rust
If any Rust code changed, also run cargo fmt --all
If any Rust code changed, also run cargo clippy --workspace --all-targets -- -D warnings
Run Rust formatting with cargo fmt --all
Run Rust linting with cargo clippy --workspace --all-targets -- -D warnings

**/*.rs: Use cargo fmt for Rust code formatting
Run cargo clippy -- -D warnings to lint Rust code and treat all warnings as errors
Use Rust snake_case naming convention for Rust identifiers
Include SPDX license header in all Rust source files using double-slash comment syntax
Validate Rust code with uv run pre-commit run --all-files to enforce cargo fmt formatting check, cargo clippy lints, and cargo deny aud...

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
**/{Cargo.toml,**/*.rs}

📄 CodeRabbit inference engine (.agents/skills/maintain-packaging/SKILL.md)

Maintain consistency between Rust package names in Cargo.toml and their actual usage across the codebase

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
**/*.{h,hpp,c,cpp,rs}

📄 CodeRabbit inference engine (.agents/skills/maintain-packaging/SKILL.md)

Ensure FFI header and library naming follows consistent conventions across platform-specific builds

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
{crates/core,crates/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/plugins/mod.rs
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/core/src/plugin/dynamic.rs
**/*.{rs,toml}

📄 CodeRabbit inference engine (.agents/skills/rename-surfaces/SKILL.md)

Update Rust crate names and module prefixes during coordinated rename operations

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
crates/core/**/*.rs

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

If the change touched crates/core or shared runtime semantics, also use validate-change for broader validation

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/plugins/mod.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/core/src/plugin/dynamic.rs
crates/{core,adaptive}/**

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

If crates/core or crates/adaptive changed, run the full matrix across Rust, Python, Go, Node.js, and WebAssembly

Files:

  • crates/core/src/plugins/mod.rs
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/core/src/plugin/dynamic.rs
**/*.{rs,py,js,ts,tsx,jsx,go,sh,toml,yaml,yml,md}

📄 CodeRabbit inference engine (AGENTS.md)

Keep SPDX headers on source, docs, scripts, and configuration files. The project is Apache-2.0.

Files:

  • crates/core/src/plugins/mod.rs
  • docs/index.yml
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/Cargo.toml
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • python/nemo_relay/_guardrails_local.py
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • python/nemo_relay/_nemo_anonymizer_local.py
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • ATTRIBUTIONS-Rust.md
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
**/*.{rs,py,go,js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions: Rust and Python use snake_case, C FFI exports prefixed nemo_relay_, Go uses PascalCase for public APIs, Node.js uses camelCase.

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • python/nemo_relay/_guardrails_local.py
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • python/nemo_relay/_nemo_anonymizer_local.py
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.rs
crates/**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

crates/**/*.rs: Keep async behavior on the existing tokio-based model. Bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.
Use Json = serde_json::Value in Rust-facing runtime APIs for JSON payload handling.

Files:

  • crates/core/src/plugins/mod.rs
  • crates/cli/src/plugins.rs
  • crates/ffi/src/error.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/python/src/lib.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/cli/tests/coverage/plugins_tests.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/cli/src/plugins/editor_model.rs
  • crates/core/src/plugin/dynamic.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/plugins/mod.rs
  • crates/core/src/plugin.rs
  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/core/src/plugins/nemo_anonymizer/local.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/component.rs
  • crates/core/src/stream.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/core/src/plugins/nemo_guardrails/local.rs
  • crates/core/src/plugins/nemo_anonymizer/mod.rs
  • crates/core/src/plugin/dynamic/registry.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/core/src/api/llm.rs
  • crates/core/src/plugin/dynamic/manifest.rs
  • crates/core/src/plugins/nemo_anonymizer/component.rs
  • crates/core/src/plugin/dynamic.rs
**/*.{py,txt,toml,cfg,yaml,yml}

📄 CodeRabbit inference engine (.agents/skills/rename-surfaces/SKILL.md)

Update Python package names and top-level module imports during coordinated rename operations

Files:

  • docs/index.yml
  • crates/core/Cargo.toml
  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
**/{docs,examples,**/*.md,*.patch,*.diff,.github,*.sh,*.yaml,*.yml}

📄 CodeRabbit inference engine (.agents/skills/rename-surfaces/SKILL.md)

Update documentation, examples, CI configuration, and patch artifacts when performing rename operations

Files:

  • docs/index.yml
  • ATTRIBUTIONS-Rust.md
crates/ffi/**

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

Rebuild the FFI crate in release mode so the shared library and header stay in sync when making changes to crates/ffi

Files:

  • crates/ffi/src/error.rs
crates/ffi/**/*.rs

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

If the change touched crates/ffi, also use test-ffi-surface for validation

Files:

  • crates/ffi/src/error.rs
crates/{python,ffi,node,wasm}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node,wasm}/**/*: Treat binding changes as public API changes. Check for parity with the other language bindings, FFI ownership/lifetime safety,
callback error propagation, stable type conversion, and consistent async/stream semantics.
Flag changes that update one binding without corresponding tests or documentation for the same surface elsewhere.

Files:

  • crates/ffi/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/src/lib.rs
**/Cargo.toml

📄 CodeRabbit inference engine (.agents/skills/rename-surfaces/SKILL.md)

Update WebAssembly crate names and generated package names during coordinated rename operations

Confirm or infer the target release version from upstream/main:Cargo.toml. Derive the release branch as release/<major>.<minor>.

**/Cargo.toml: Maintain Cargo.toml [workspace.package].version as the source of truth for the Rust workspace and Python build versioning
Keep Cargo.toml [workspace.dependencies] self-references aligned with the workspace version when the workspace version changes
After updating workspace package entries, run cargo check --workspace to refresh Cargo.lock

Files:

  • crates/core/Cargo.toml
**/*.toml

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Include SPDX license header in TOML configuration files using hash comment syntax

Files:

  • crates/core/Cargo.toml
{crates/adaptive/**/*.rs,**/*test*.{rs,py,go,ts,js},**/*adaptive*test*.{rs,py,go,ts,js},docs/plugins/adaptive/**}

📄 CodeRabbit inference engine (.agents/skills/maintain-optimizer/SKILL.md)

Maintain documented and tested validation and report behavior for adaptive surfaces

Files:

  • crates/core/tests/integration/pipeline_tests.rs
  • crates/adaptive/src/error.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/cli/tests/coverage/plugins_tests.rs
{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_test.go}

⚙️ CodeRabbit configuration file

{crates/**/tests/**,python/tests/**,go/nemo_relay/**/*_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/integration/pipeline_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/core/tests/unit/plugins/nemo_guardrails/component_tests.rs
  • crates/core/tests/unit/plugins/nemo_anonymizer/component_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/core/tests/unit/plugin_dynamic_tests.rs
  • crates/cli/tests/coverage/plugins_tests.rs
{crates/adaptive/**,python/nemo_relay/adaptive.py,python/nemo_relay/plugin.py,go/nemo_relay/adaptive/**,go/nemo_relay/!(adaptive)/**,**/node/**,**/wasm/**}

📄 CodeRabbit inference engine (.agents/skills/maintain-optimizer/SKILL.md)

Keep adaptive surface in sync across crates/adaptive, shared plugin behavior in core and bindings, Python adaptive/plugin wrappers in python/nemo_relay/adaptive.py and python/nemo_relay/plugin.py, Go adaptive helpers under go/nemo_relay/adaptive plus shared plugin helpers in go/nemo_relay, and Node/WebAssembly adaptive helpers and plugin wrappers

Files:

  • crates/adaptive/src/error.rs
{crates/adaptive/**,python/nemo_relay/plugin.py,go/nemo_relay/**,**/node/**,**/wasm/**}

📄 CodeRabbit inference engine (.agents/skills/maintain-optimizer/SKILL.md)

{crates/adaptive/**,python/nemo_relay/plugin.py,go/nemo_relay/**,**/node/**,**/wasm/**}: Maintain consistent plugin lifecycle across all language bindings (Python, Go, Node/WebAssembly, and Rust)
Keep plugin context surfaces aligned across all language implementations

Files:

  • crates/adaptive/src/error.rs
crates/python/**/*.rs

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

If the native Rust bridge changed, add the Rust crate tests for nemo-relay-python

Files:

  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_plugin.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/src/lib.rs
crates/core/src/api/**/*.rs

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Implement behavior first in Rust core API modules: crates/core/src/api/ and related core modules such as crates/core/src/api/runtime/, crates/core/src/codec/, or crates/core/src/json.rs

Files:

  • crates/core/src/api/llm.rs
crates/core/src/api/{tool,llm}.rs

📄 CodeRabbit inference engine (.agents/skills/add-middleware/SKILL.md)

Wire the new middleware chain into the execute path in crates/core/src/api/tool.rs or crates/core/src/api/llm.rs at the appropriate pipeline stage

Files:

  • crates/core/src/api/llm.rs
{crates/python/src/py_api/**/*.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi}

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Update Python native binding in crates/python/src/py_api/mod.rs with Python wrapper docstring in python/nemo_relay/<module>.py and type stubs in python/nemo_relay/*.pyi modules

Files:

  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (.agents/skills/add-binding-feature/SKILL.md)

Use snake_case naming convention for Python identifiers (e.g., nemo_relay.tools.call)

Format changed Python wrapper and test files with uv run ruff format python

Python wrapper modules live under python/nemo_relay/; the native extension is built from crates/python with maturin.

Files:

  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
{pyproject.toml,**/*.py}

📄 CodeRabbit inference engine (.agents/skills/maintain-packaging/SKILL.md)

Maintain consistency between Python package names in pyproject.toml and import paths used throughout the codebase

Files:

  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
**/*.py

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

**/*.py: Run Python formatting with uv run ruff format python
Run Python testing with uv run pytest -k "<pattern>"

**/*.py: Use Ruff with rule sets E, F, W, I for Python linting
Use Ruff formatter with line length 120 and double quotes for Python code formatting
Run ty for Python type checking
Use Python snake_case naming convention for Python identifiers
Include SPDX license header in all Python source files using hash comment syntax
Validate Python code with uv run pre-commit run --all-files to enforce Ruff linting and formatting, and ty type checking

Files:

  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
python/nemo_relay/**/*

⚙️ CodeRabbit configuration file

python/nemo_relay/**/*: Review Python wrapper changes for typed API consistency, contextvars-based scope isolation, async behavior, and parity with the native extension.
Stubs and runtime implementations should stay aligned.

Files:

  • python/nemo_relay/_guardrails_local.py
  • python/nemo_relay/_nemo_anonymizer_local.py
**/*.{md,rst,html,txt}

📄 CodeRabbit inference engine (.agents/skills/review-doc-style/assets/nvidia-style-brand-terminology.md)

**/*.{md,rst,html,txt}: Always spell NVIDIA in all caps. Do not use Nvidia, nvidia, nVidia, nVIDIA, or NV.
Use an NVIDIA before a noun because the name starts with an 'en' sound.
Do not add a registered trademark symbol after NVIDIA when referring to the company.
Use trademark symbols with product names only when the document type or legal guidance requires them.
Verify official capitalization, spacing, and hyphenation for product names.
Precede NVIDIA product names with NVIDIA on first mention when it is natural and accurate.
Do not rewrite product names for grammar or title-case rules.
Preserve third-party product names according to the owner's spelling.
Include the company name and full model qualifier on first use when it helps identify the model.
Preserve the official capitalization and punctuation of model names.
Use shorter family names only after the full name is established.
Spell out a term on first use and put the acronym in parentheses unless the acronym is widely understood by the intended audience.
Use the acronym on later mentions after it has been defined.
For long documents, reintroduce the full term if readers might lose context.
Form plurals of acronyms with s, not an apostrophe, such as GPUs.
In headings, common acronyms can remain abbreviated. Spell out the term in the first or second sentence of the body.
Common terms such as CPU, GPU, PC, API, and UI usually do not need to be spelled out for developer audiences.

Files:

  • ATTRIBUTIONS-Rust.md
**/*.{md,rst,html}

📄 CodeRabbit inference engine (.agents/skills/review-doc-style/assets/nvidia-style-brand-terminology.md)

Link the first mention of a product name when the destination helps the reader.

Files:

  • ATTRIBUTIONS-Rust.md
**/*.md

📄 CodeRabbit inference engine (.agents/skills/contribute-integration/SKILL.md)

Documentation must be updated if activation or usage changed

**/*.md: Use title case consistently in technical documentation headings
Avoid quotation marks, ampersands, and exclamation marks in headings
Keep product, event, research, and whitepaper names in their official title case
Use title case for table headers
Do not force social-media sentence case into technical docs
Format code elements, commands, parameters, package names, and expressions in monospace
Format directories, file names, and paths in monospace using backticks
Use angle brackets inside monospace for variables inside paths, such as /home/<username>/.login
Format error messages and strings in quotation marks, keeping literal code strings in code formatting when clearer
Format UI buttons, menus, fields, and labels in bold
Use angle brackets between UI labels for menu paths, such as File > Save As
Use italics for new terms on first use, sparingly and only when introducing the term
Use italics for publication titles
Format keyboard shortcuts in plain text, such as Press Ctrl+Alt+Delete
Use owner/repo link text for GitHub repositories, preferring [NVIDIA/NeMo](link) over prose references like 'the GitHub repo'
Introduce every code block with a complete sentence
Do not make a code block complete the grammar of the previous sentence
Do not continue a sentence after a code block
Use syntax highlighting when the format supports it for code blocks
Avoid the word 'snippet' unless the surrounding docs already use it as a term of art
Keep inline method, function, and class references consistent with nearby docs, omitting empty parentheses for prose readability when no call is shown
Use descriptive anchor text that matches the destination title when possible for links
Avoid raw URLs in running text
Avoid generic anchor text such as 'here,' 'this page,' and 'read more'
Include acronyms in link text when a linked term includes an acronym
Do not link long sentences or multiple sentences
Avoid links ...

Files:

  • ATTRIBUTIONS-Rust.md
**/*.{md,rst,txt}

📄 CodeRabbit inference engine (.agents/skills/review-doc-style/assets/nvidia-style-guide.md)

Spell NVIDIA in all caps. Do not use Nvidia, nvidia, or NV.

Files:

  • ATTRIBUTIONS-Rust.md
**/*.{md,rst}

📄 CodeRabbit inference engine (.agents/skills/review-doc-style/assets/nvidia-style-guide.md)

**/*.{md,rst}: Format commands, code elements, expressions, package names, file names, and paths as inline code.
Use descriptive link text. Avoid raw URLs and weak anchors such as "here" or "read more."
Use title case consistently for technical documentation headings.
Introduce code blocks, lists, tables, and images with complete sentences.
Write procedures as imperative steps. Keep steps parallel and split long procedures into smaller tasks.
Prefer active voice, present tense, short sentences, contractions, and plain English.
Use can for possibility and reserve may for permission.
Use after for temporal relationships instead of once.
Prefer refer to over see when the wording points readers to another resource.
Avoid culture-specific idioms, unnecessary Latinisms, jokes, and marketing exaggeration in technical docs.
Spell out months in body text, avoid ordinal dates, and use clear time zones.
Spell out whole numbers from zero through nine unless they are technical values, parameters, versions, or UI values.
Use numerals for 10 or greater and include commas in thousands.
Do not add trademark symbols to learning-oriented docs unless the source, platform, or legal guidance explicitly requires them.

Files:

  • ATTRIBUTIONS-Rust.md
ATTRIBUTIONS-Rust.md

📄 CodeRabbit inference engine (.agents/skills/update-project-version/SKILL.md)

Regenerate ATTRIBUTIONS-Rust.md with ./scripts/generate_attributions.sh rust if Cargo metadata changed and committed attribution files must stay fresh

Files:

  • ATTRIBUTIONS-Rust.md
🪛 ast-grep (0.43.0)
python/nemo_relay/_guardrails_local.py

[info] 138-145: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"tool_name": name,
"arguments": args,
},
sort_keys=True,
separators=(",", ":"),
)
Note: Security best practice.

(use-jsonify)


[info] 149-157: use jsonify instead of json.dumps for JSON output
Context: json.dumps(
{
"tool_name": name,
"arguments": args,
"result": result,
},
sort_keys=True,
separators=(",", ":"),
)
Note: Security best practice.

(use-jsonify)

python/nemo_relay/_nemo_anonymizer_local.py

[info] 359-359: use jsonify instead of json.dumps for JSON output
Context: json.dumps(sanitized_call["arguments"], separators=(",", ":"))
Note: Security best practice.

(use-jsonify)


[info] 375-375: use jsonify instead of json.dumps for JSON output
Context: json.dumps(sanitized_call["arguments"], separators=(",", ":"))
Note: Security best practice.

(use-jsonify)

🪛 LanguageTool
docs/nemo-anonymizer-plugin/about.mdx

[grammar] ~49-~49: Ensure spelling is correct
Context: ...ackend. In particular: - the previous builtin deterministic lane is no longer part of...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

docs/nemo-guardrails-plugin/configuration.mdx

[style] ~294-~294: To form a complete sentence, be sure to include a subject.
Context: ...g_yamlis required. -colang_contentcan only be used withconfig_yaml. - rem...

(MISSING_IT_THERE)

🪛 markdownlint-cli2 (0.22.1)
ATTRIBUTIONS-Rust.md

[warning] 3127-3127: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 3127-3127: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 7481-7481: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 7484-7484: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 7484-7484: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 7485-7485: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 7485-7485: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 8111-8111: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 8111-8111: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 8768-8768: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 8768-8768: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 32177-32177: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 32180-32180: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 32180-32180: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 32181-32181: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 32181-32181: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 39251-39251: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Above

(MD022, blanks-around-headings)


[warning] 39251-39251: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🪛 Ruff (0.15.17)
python/nemo_relay/_guardrails_local.py

[warning] 75-78: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 79-82: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 92-92: Dynamically typed expressions (typing.Any) are disallowed in status

(ANN401)


[warning] 96-96: Dynamically typed expressions (typing.Any) are disallowed in annotated

(ANN401)


[warning] 101-101: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 102-102: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 103-103: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 131-135: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 165-169: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 172-176: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 180-180: Dynamically typed expressions (typing.Any) are disallowed in result

(ANN401)


[warning] 183-189: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 193-193: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 194-194: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 195-195: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 211-211: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 215-215: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 215-215: Dynamically typed expressions (typing.Any) are disallowed in _output_streaming_config

(ANN401)


[warning] 219-219: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 224-224: Too many return statements (10 > 6)

(PLR0911)


[warning] 224-224: Too many branches (13 > 12)

(PLR0912)


[warning] 280-280: Missing return type annotation for private function _queue_string_stream

(ANN202)


[warning] 280-280: Remove quotes from type annotation

Remove quotes

(UP037)


[warning] 290-290: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 292-292: Remove quotes from type annotation

Remove quotes

(UP037)


[warning] 309-313: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 316-316: Dynamically typed expressions (typing.Any) are disallowed in rails_config_cls

(ANN401)


[warning] 316-316: Dynamically typed expressions (typing.Any) are disallowed in _build_guardrails_config

(ANN401)


[warning] 328-328: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 333-333: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 334-334: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 335-335: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 356-356: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 357-357: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 358-358: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 385-385: Missing return type annotation for private function _make_llm_intercept

(ANN202)


[warning] 387-387: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 388-388: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 389-389: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 394-394: Missing return type annotation for private function intercept

(ANN202)


[warning] 424-424: Missing return type annotation for private function _make_llm_stream_intercept

(ANN202)


[warning] 426-426: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 427-427: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 428-428: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 434-434: Missing return type annotation for private function stream_intercept

(ANN202)


[warning] 452-455: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 459-462: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 467-467: Missing return type annotation for private function guarded_provider_stream

(ANN202)


[warning] 500-500: Missing return type annotation for private function _make_tool_intercept

(ANN202)


[warning] 502-502: Dynamically typed expressions (typing.Any) are disallowed in rails

(ANN401)


[warning] 503-503: Dynamically typed expressions (typing.Any) are disallowed in rail_type

(ANN401)


[warning] 504-504: Dynamically typed expressions (typing.Any) are disallowed in rail_status

(ANN401)


[warning] 508-508: Missing return type annotation for private function tool_intercept

(ANN202)


[warning] 536-536: Dynamically typed expressions (typing.Any) are disallowed in result

(ANN401)


[warning] 542-547: Avoid specifying long messages outside the exception class

(TRY003)

python/nemo_relay/_nemo_anonymizer_local.py

[warning] 13-13: Import from collections.abc instead: Callable

Import from collections.abc

(UP035)


[warning] 63-63: Remove quotes from type annotation

Remove quotes

(UP037)


[warning] 113-116: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 117-120: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 154-154: Dynamically typed expressions (typing.Any) are disallowed in _build_replace_method

(ANN401)


[warning] 180-180: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 291-291: Dynamically typed expressions (typing.Any) are disallowed in request

(ANN401)


[warning] 302-302: Dynamically typed expressions (typing.Any) are disallowed in request

(ANN401)


[warning] 310-310: Dynamically typed expressions (typing.Any) are disallowed in response

(ANN401)


[warning] 519-519: Do not catch blind exception: Exception

(BLE001)

Comment thread crates/adaptive/src/error.rs Outdated
fn from(value: PluginError) -> Self {
match value {
PluginError::InvalidConfig(message) => Self::InvalidConfig(message),
PluginError::Conflict(message) => Self::InvalidConfig(message),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PluginError::Conflict is being flattened into the wrong downstream error classes. The shared root cause is conflict-semantics loss at boundary mappings, which breaks accurate machine-level handling of duplicate-state failures.

  • crates/adaptive/src/error.rs#L56-L56: map PluginError::Conflict to a conflict/state-failure adaptive class (or RegistrationFailed) instead of InvalidConfig.
  • crates/ffi/src/error.rs#L141-L143: map PluginError::Conflict to NemoRelayStatus::AlreadyExists (or a dedicated conflict status if added) instead of InvalidArg.
📍 Affects 2 files
  • crates/adaptive/src/error.rs#L56-L56 (this comment)
  • crates/ffi/src/error.rs#L141-L143
🤖 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/adaptive/src/error.rs` at line 56, The PluginError::Conflict variant
is being mapped to semantically incorrect downstream error types, losing
conflict-related information needed for proper error handling. At
crates/adaptive/src/error.rs#L56-L56, change the mapping from
Self::InvalidConfig(message) to a conflict or state-failure semantic error class
such as Self::RegistrationFailed or an equivalent conflict variant that better
represents the duplicate-state nature of the error. At
crates/ffi/src/error.rs#L141-L143, change the mapping from
NemoRelayStatus::InvalidArg to NemoRelayStatus::AlreadyExists (or another
dedicated conflict status variant if available) to accurately represent the
conflict semantics when converting PluginError::Conflict to FFI error codes.
Both changes must preserve and forward the conflict context through the error
boundary mappings.

Comment on lines +56 to +74
fn pii_redaction_component_config() -> serde_json::Map<String, Value> {
json!({
"mode": "builtin",
"codec": "openai_chat",
"output": true,
"input": false,
"tool_input": false,
"tool_output": false,
"builtin": {
"action": "regex_replace",
"pattern": "sk-[A-Za-z0-9_-]+",
"replacement": "[REDACTED]",
"target_paths": ["/message"]
}
})
.as_object()
.unwrap()
.clone()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PII redaction tests are pinned to an outdated config contract.

The new fixtures/assertions assume mode = "builtin" and kind = "pii_redaction", but the current core contract (in crates/core/src/plugins/nemo_anonymizer/component.rs) defines PII_REDACTION_PLUGIN_KIND as an alias to nemo_anonymizer and constrains mode schema to local. This makes several new tests assert invalid behavior.

Please align the fixture and assertions with the canonical config contract (including schema expectations and kind rendering checks using PII_REDACTION_PLUGIN_KIND, not a hardcoded literal).

Suggested direction
-    "mode": "builtin",
+    "mode": "local",
...
-    assert_eq!(schema.field("mode").unwrap().enum_values, &["builtin", "anonymizer_local"]);
+    assert_eq!(schema.field("mode").unwrap().enum_values, &["local"]);
...
-    assert!(rendered.contains("kind = \"pii_redaction\""));
+    assert!(rendered.contains(&format!("kind = \"{}\"", PII_REDACTION_PLUGIN_KIND)));

Also applies to: 194-224, 396-416, 1256-1269, 1390-1402

🤖 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/plugins_tests.rs` around lines 56 - 74, The test
fixtures and assertions for PII redaction are using an outdated configuration
contract that does not match the canonical definition in
crates/core/src/plugins/nemo_anonymizer/component.rs. Update the
pii_redaction_component_config function at lines 56-74 and all affected test
assertions at lines 194-224, 396-416, 1256-1269, and 1390-1402 to align with the
actual core contract: replace "mode": "builtin" with the correct mode schema
(likely "local"), remove the "builtin" configuration section and replace it with
the structure expected by the nemo_anonymizer component, replace any hardcoded
kind literals with the PII_REDACTION_PLUGIN_KIND constant imported from the core
component, and update all schema validation assertions to reflect the corrected
configuration contract.

Comment thread crates/core/Cargo.toml Outdated
Comment on lines +72 to +73
regex = "1"
sha2 = "0.10"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for regex and sha2 usage in crates/core
echo "=== regex usage ==="
rg -n --type rust '\bregex\b' crates/core/src/ || echo "No regex usage found"

echo ""
echo "=== sha2 usage ==="
rg -n --type rust '\bsha2\b' crates/core/src/ || echo "No sha2 usage found"

Repository: NVIDIA/NeMo-Relay

Length of output: 251


Remove unused regex and sha2 dependencies from crates/core/Cargo.toml.

Verification found no actual usage of either dependency: regex appears only in a comment, and sha2 has zero references anywhere in crates/core/src/. Remove lines 72–73 to avoid unused-dependency warnings and keep the dependency tree clean.

🤖 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/Cargo.toml` around lines 72 - 73, Remove the unused `regex` and
`sha2` dependencies from the Cargo.toml file in crates/core. Both dependencies
are not used in the codebase: `regex` only appears in comments and `sha2` has no
references anywhere in crates/core/src/. Delete both dependency declarations
entirely to eliminate unused-dependency warnings and maintain a clean dependency
tree.

Comment on lines +332 to +343
fn reject_duplicate_capabilities(capabilities: &[DynamicPluginCapability]) -> Result<()> {
let mut seen = Vec::with_capacity(capabilities.len());
for capability in capabilities {
if seen.contains(capability) {
return Err(PluginError::InvalidConfig(format!(
"capabilities.items contains duplicate capability '{capability:?}'"
)));
}
seen.push(*capability);
}
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider HashSet for duplicate detection.

reject_duplicate_capabilities uses a Vec with linear search, making this O(n²). Since DynamicPluginCapability implements Hash + Eq, a HashSet would be O(n). Not critical given typical capability counts, but a minor efficiency improvement.

♻️ Suggested refactor
+use std::collections::HashSet;
+
 fn reject_duplicate_capabilities(capabilities: &[DynamicPluginCapability]) -> Result<()> {
-    let mut seen = Vec::with_capacity(capabilities.len());
+    let mut seen = HashSet::with_capacity(capabilities.len());
     for capability in capabilities {
-        if seen.contains(capability) {
+        if !seen.insert(*capability) {
             return Err(PluginError::InvalidConfig(format!(
                 "capabilities.items contains duplicate capability '{capability:?}'"
             )));
         }
-        seen.push(*capability);
     }
     Ok(())
 }
🤖 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/plugin/dynamic/manifest.rs` around lines 332 - 343, Replace
the Vec-based duplicate detection in the reject_duplicate_capabilities function
with a HashSet to improve performance from O(n²) to O(n). Change the seen
variable from a Vec to a HashSet, and update the contains check to use the
insert method (which returns false if the element was already present), allowing
you to detect duplicates while building the set in a single pass.

Comment on lines +59 to +60
stamp_creation_metadata(&mut record.metadata);
touch_metadata(&mut record.metadata);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | 💤 Low value

created_at and updated_at will differ for freshly added records.

stamp_creation_metadata sets both timestamps to the same value, but then touch_metadata immediately overwrites updated_at. This results in a microsecond gap between created_at and updated_at even for brand-new records. If this is intentional (treating add as a mutation distinct from creation), document it; otherwise, consider removing the touch_metadata call here since stamp_creation_metadata already sets updated_at.

🤖 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/plugin/dynamic/registry.rs` around lines 59 - 60, The
stamp_creation_metadata function sets both created_at and updated_at to the same
value, but the immediately following touch_metadata call overwrites updated_at,
creating an unintended microsecond gap for newly added records. Either remove
the touch_metadata call since stamp_creation_metadata already sets updated_at
appropriately, or if this behavior is intentional (treating add as a distinct
mutation), add a clear comment explaining why both calls are necessary.

Comment thread docs/nemo-anonymizer-plugin/about.mdx Outdated

In particular:

- the previous builtin deterministic lane is no longer part of this plugin

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use built-in Instead Of builtin In Prose

Line 49 uses builtin; use built-in for consistent technical documentation wording.

Suggested edit
-- the previous builtin deterministic lane is no longer part of this plugin
+- the previous built-in deterministic lane is no longer part of this plugin

As per coding guidelines, "User-facing text quality issues such as typographical and grammatical errors in documentation and UI strings" should be flagged and fixed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- the previous builtin deterministic lane is no longer part of this plugin
- the previous built-in deterministic lane is no longer part of this plugin
🧰 Tools
🪛 LanguageTool

[grammar] ~49-~49: Ensure spelling is correct
Context: ...ackend. In particular: - the previous builtin deterministic lane is no longer part of...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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 `@docs/nemo-anonymizer-plugin/about.mdx` at line 49, On line 49 of the
about.mdx file, change the word "builtin" to "built-in" in the phrase "the
previous builtin deterministic lane is no longer part of this plugin" to
maintain consistent technical documentation wording conventions.

Sources: Coding guidelines, Linters/SAST tools

Comment on lines +8 to +11
import csv
import importlib
import json
import tempfile

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Python tempfile NamedTemporaryFile Windows reopen by name while file is open delete=True documentation

💡 Result:

On Windows, you generally cannot reopen a NamedTemporaryFile by name while it is still open if you use the default delete=True parameter [1][2]. When delete=True, Python creates the file with the O_TEMPORARY flag on Windows [3]. This flag instructs the operating system to set FILE_FLAG_DELETE_ON_CLOSE and, crucially, to share delete access [2][4]. Because of how the Windows API handles this, the file can only be reopened by other processes (or by subsequent calls in your own program) if those subsequent opens also share delete access [1][2]. The standard Python open function does not share this access, leading to a sharing violation (PermissionError) [2][4]. To work around this limitation, you have the following options: 1. Use delete=False: This is the most common approach [5][6]. By setting delete=False, the file is not automatically deleted when closed [3]. You are then responsible for manually closing the file object and deleting the file (using os.unlink) once you are finished with it [7][6]. 2. Use delete=True and delete_on_close=False (Python 3.12+): If you are using a newer version of Python, you can set delete=True (default) and delete_on_close=False [1][8]. This prevents the file from being deleted immediately upon closing the handle, allowing you to reopen it by name while the context manager is still active [1]. Note that you must ensure the file is closed before the context manager exits, or the final cleanup (which attempts to unlink the file) may fail with a PermissionError if other handles remain open [1][8]. Example of the recommended approach (using delete_on_close=False for newer Python versions): with tempfile.NamedTemporaryFile(delete=True, delete_on_close=False) as fp: fp.write(b'data') fp.close # Now safe to reopen with open(fp.name, 'rb') as f: data = f.read # Automatically deleted on exit Example of the manual approach (for older Python versions): try: tmp = tempfile.NamedTemporaryFile(delete=False) tmp.write(b'data') tmp.close # Now safe to reopen with open(tmp.name, 'rb') as f: data = f.read finally: os.unlink(tmp.name) # Manual cleanup [6]

Citations:


Close the CSV file before passing its path to the anonymizer runtime.

The code at line 198 opens a NamedTemporaryFile with delete=True (the default) and passes handle.name to the anonymizer while the handle remains open. On Windows, this prevents the anonymizer from reopening the file: the OS creates it with FILE_FLAG_DELETE_ON_CLOSE and share-delete access, but standard open() does not share that access, raising PermissionError. Use TemporaryDirectory with explicit UTF-8 encoding instead.

Proposed portability fix
 import importlib
 import json
 import tempfile
 from dataclasses import dataclass
+from pathlib import Path
 from typing import Any, Callable, NamedTuple, cast
@@
-        with tempfile.NamedTemporaryFile("w", suffix=".csv", newline="", delete=True) as handle:
-            writer = csv.DictWriter(handle, fieldnames=["record_id", "text"])
-            writer.writeheader()
-            for index, text in enumerate(texts):
-                writer.writerow({"record_id": index, "text": text})
-            handle.flush()
-
+        with tempfile.TemporaryDirectory() as tmpdir:
+            input_path = Path(tmpdir) / "input.csv"
+            with input_path.open("w", newline="", encoding="utf-8") as handle:
+                writer = csv.DictWriter(handle, fieldnames=["record_id", "text"])
+                writer.writeheader()
+                for index, text in enumerate(texts):
+                    writer.writerow({"record_id": index, "text": text})
+
             result = self._anonymizer.preview(
                 config=self._anonymizer_config,
                 data=self._runtime.anonymizer_input_cls(
-                    source=handle.name,
+                    source=str(input_path),
                     text_column="text",
                     id_column="record_id",
                     data_summary=self._config.data_summary,
🤖 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 `@python/nemo_relay/_nemo_anonymizer_local.py` around lines 8 - 11, The CSV
file handling at line 198 uses NamedTemporaryFile with delete=True and passes
the file path to the anonymizer while the file handle remains open, causing
Windows permission errors. Replace the NamedTemporaryFile approach with
TemporaryDirectory to create a temporary directory, then write the CSV data to a
file within that directory with explicit UTF-8 encoding. Ensure the file is
fully closed before passing its path to the anonymizer runtime, and properly
manage the TemporaryDirectory context to clean up after the anonymizer completes
its work.

Comment on lines +302 to +308
def _apply_annotated_request_payload(request: Any, payload: dict[str, Json]) -> None:
request.messages = payload["messages"]
request.model = payload.get("model")
request.params = payload.get("params")
request.tools = payload.get("tools")
request.tool_choice = payload.get("tool_choice")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Write sanitized extra fields back to the annotated request.

_annotated_request_payload includes extra, so target paths under /extra can be sanitized, but Line 308 drops the sanitized value before re-encoding. Provider-specific request fields can leave PII unchanged.

Proposed fix
 def _apply_annotated_request_payload(request: Any, payload: dict[str, Json]) -> None:
     request.messages = payload["messages"]
     request.model = payload.get("model")
     request.params = payload.get("params")
     request.tools = payload.get("tools")
     request.tool_choice = payload.get("tool_choice")
+    request.extra = payload.get("extra")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _apply_annotated_request_payload(request: Any, payload: dict[str, Json]) -> None:
request.messages = payload["messages"]
request.model = payload.get("model")
request.params = payload.get("params")
request.tools = payload.get("tools")
request.tool_choice = payload.get("tool_choice")
def _apply_annotated_request_payload(request: Any, payload: dict[str, Json]) -> None:
request.messages = payload["messages"]
request.model = payload.get("model")
request.params = payload.get("params")
request.tools = payload.get("tools")
request.tool_choice = payload.get("tool_choice")
request.extra = payload.get("extra")
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 302-302: Dynamically typed expressions (typing.Any) are disallowed in request

(ANN401)

🤖 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 `@python/nemo_relay/_nemo_anonymizer_local.py` around lines 302 - 308, The
function _apply_annotated_request_payload extracts and restores several fields
from the payload back to the request object (messages, model, params, tools,
tool_choice), but it does not restore the sanitized extra field. Since extra
fields are included in _annotated_request_payload and can contain PII that gets
sanitized, you must add a line to assign the sanitized extra field from the
payload back to the request object using the same pattern as the other field
assignments (request.extra = payload.get("extra")).

Comment on lines +323 to +334
def _annotated_message_text(message: Json) -> str | None:
if isinstance(message, str):
return message
if not isinstance(message, list):
return None
parts = []
for part in message:
if not isinstance(part, dict):
continue
if part.get("type") == "text" and isinstance(part.get("text"), str):
parts.append(cast(str, part["text"]))
return "\n".join(parts) if parts else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve content-block boundaries during response overlays.

_annotated_message_text collapses all text blocks with "\n", and the OpenAI Responses and Anthropic overlay helpers split that string back on "\n". A single original block containing a newline, or sanitized text that changes newline counts, can be truncated or shifted across blocks. Carry sanitized per-block text and overlay by block index instead.

Also applies to: 395-428

🤖 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 `@python/nemo_relay/_nemo_anonymizer_local.py` around lines 323 - 334, The
_annotated_message_text function currently joins all text blocks with newlines,
which loses the original block boundaries and causes content to be truncated or
shifted when sanitized text changes newline counts. Instead of returning a
single concatenated string, refactor this function to return the extracted text
as a per-block list structure that preserves each block's original text
separately. Update the callers (the OpenAI Responses and Anthropic overlay
helpers at lines 395-428) to overlay sanitized content by block index instead of
splitting the concatenated string on newlines, ensuring block boundaries are
maintained throughout the sanitization and overlay process.

Comment on lines +497 to +502
def sanitize_request(request: LLMRequest) -> LLMRequest:
annotated = codec.decode(request)
payload = _annotated_request_payload(annotated)
sanitized_payload = cast(dict[str, Json], runner.sanitize_json(payload))
_apply_annotated_request_payload(annotated, sanitized_payload)
return codec.encode(annotated, request)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fallback to raw request sanitization when codec decoding fails.

The response path sanitizes the raw payload if decoding fails, but the request path lets decode errors escape. The Rust wrapper’s sanitizer fallback can then return the original request, leaving PII unsanitized.

Proposed fix
     codec = _CODECS[codec_name]()
 
     def sanitize_request(request: LLMRequest) -> LLMRequest:
-        annotated = codec.decode(request)
+        try:
+            annotated = codec.decode(request)
+        except Exception:
+            request.content = cast(dict[str, Json], runner.sanitize_json(cast(Json, request.content)))
+            return request
         payload = _annotated_request_payload(annotated)
         sanitized_payload = cast(dict[str, Json], runner.sanitize_json(payload))
         _apply_annotated_request_payload(annotated, sanitized_payload)
🤖 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 `@python/nemo_relay/_nemo_anonymizer_local.py` around lines 497 - 502, The
sanitize_request function lacks error handling for codec decoding failures,
allowing decode errors to escape instead of falling back to raw request
sanitization like the response path does. Wrap the codec.decode(request) call in
a try-except block in the sanitize_request function, and when decoding fails,
implement a fallback path that sanitizes the raw request payload directly using
runner.sanitize_json without attempting to annotate or re-encode it, ensuring
PII is sanitized even when the codec fails.

Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv afourniernv force-pushed the afournier/relay-337-plugin-control-plane-model branch from 4c6eb28 to f0bad61 Compare June 16, 2026 18:44
@github-actions github-actions Bot added size:XL PR is extra large and removed size:XXL PR is very large lang:python PR changes/introduces Python code labels Jun 16, 2026
@afourniernv afourniernv marked this pull request as draft June 16, 2026 19:01
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@willkill07 willkill07 removed request for a team and lvojtku June 16, 2026 19:25
@afourniernv

Copy link
Copy Markdown
Collaborator Author

A little more implementation detail on what this foundation slice is actually establishing.

This PR is centered on one durable control-plane record and one authored manifest contract.

Record contract

The core type is DynamicPluginRecord, which is split into:

  • metadata
  • source
  • spec
  • status

The intended contract is:

  • metadata = stable identity and record-level bookkeeping
  • source = resolved manifest/artifact/environment references known to the control plane
  • spec = desired state owned by user-facing lifecycle operations
  • status = observed validation/runtime state owned by reconciliation/runtime updates

That separation is the main mechanism that keeps “what the user wants” separate from “what Relay has observed”.

spec currently carries:

  • present
  • enabled
  • config_ref

status currently carries:

  • decomposed validation state
  • runtime state
  • startup classification
  • attestation mode
  • last failure summary

Generation lives on metadata, and only desired-state mutations bump generation. Status updates do not. That is the reconciliation mechanism the later runtime and CLI work will build on.

Manifest contract

The authored manifest is relay-plugin.toml.

This PR establishes:

  • kind-specific manifest parsing
  • strict validation
  • manifest-to-record conversion

The current manifest model distinguishes:

  • plugin.kind = "rust_dynamic"
  • plugin.kind = "worker"

with kind-specific requirements for:

  • compatibility declarations
  • load contract fields
  • capability shape

Examples of what is enforced now:

  • compat.relay is required for all plugins
  • compat.native_api is required for rust_dynamic
  • compat.worker_protocol is required for worker
  • worker manifests require runtime + entrypoint
  • native manifests require library + symbol
  • plugins are rejected if they claim invalid kind/load/compatibility combinations
  • plugins are rejected if defaults.enabled = true

So this PR is not just adding structs; it is making the authored contract executable.

Registry mechanisms

DynamicPluginRegistry is intentionally in-memory for now, but the lifecycle semantics are already explicit.

It currently provides:

  • add
  • add_manifest
  • get
  • list
  • enable
  • disable
  • remove
  • validation/runtime/last-error status updates

Important behaviors already in place:

  • add_manifest validates and converts authored manifests before registration
  • duplicate live plugin IDs fail with PluginError::Conflict
  • remove is tombstone-based, not destructive
  • tombstoned IDs can be revived while preserving lineage
  • list(false) hides tombstones
  • list(true) includes them
  • enable / disable / remove are desired-state mutations and bump generation
  • status updates stamp observed timestamps but do not bump generation

That gives later CLI/runtime work a stable lifecycle model instead of re-deciding semantics.

Boundary decisions in this PR

This slice intentionally stops at the control-plane boundary.

It does not yet implement:

  • CLI lifecycle commands
  • worker runtime protocol
  • Python worker supervision
  • native dynamic loading
  • host policy evaluation
  • persistence backend / cross-process coordination

Those are follow-on issues, but this PR is meant to establish the contracts they plug into.

Why this shape

The main reason for this split is to avoid backing into the plugin model through CLI or runtime implementation details.

The order here is:

  1. make the record model concrete
  2. make the authored manifest contract concrete
  3. make lifecycle mutation semantics concrete
  4. build CLI, policy, worker, and native runtime work on top of those contracts

That is the actual purpose of this foundation PR.

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.

1 participant