Skip to content

feat: expose event sanitizers in Python [3/6]#373

Merged
rapids-bot[bot] merged 4 commits into
NVIDIA:mainfrom
willkill07:feat/relay-409-python-event-sanitizers
Jul 9, 2026
Merged

feat: expose event sanitizers in Python [3/6]#373
rapids-bot[bot] merged 4 commits into
NVIDIA:mainfrom
willkill07:feat/relay-409-python-event-sanitizers

Conversation

@willkill07

@willkill07 willkill07 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Overview

Exposes the mark and scope event sanitizer registries from PRs #371 and #372 through the Python binding and worker SDK.

  • 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

  • Adds Python callback types and global/scope-local registration helpers.
  • Extends the in-process PluginContext and Python gRPC worker SDK.
  • Adds mark to typed PII configuration helpers.
  • Tests field conversion/removal, errors, ordering, scope inheritance, plugin rollback/cleanup, worker surfaces, and PII opt-out.
  • Contains no documentation changes; documentation is isolated in a final independent PR.
  • Breaking changes: none.

Validation: just test-python, just test-python-plugin, focused native tests and coverage, Ruff, ty, and pre-commit.

Where should the reviewer start?

Start with python/nemo_relay/guardrails.py, python/nemo_relay/scope_local.py, and python/tests/test_event_sanitizers.py.

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

  • Relates to: RELAY-409

Summary by CodeRabbit

  • New Features
    • Added Python event sanitization guardrails for mark events and scope start/end events, available globally and per-scope.
    • Introduced EventSanitizeFields and EventSanitizeGuardrail typing for sanitizer callbacks, plus new registration/deregistration helpers.
    • Extended the plugin registration surface and updated public typings to match.
    • Added a mark option to PII redaction configuration.
  • Bug Fixes
    • Improved “fail open” behavior: if a sanitizer raises or returns an invalid value, sanitization safely falls back to the original event fields.

@github-actions github-actions Bot added size:L PR is large Feature a new feature lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code labels Jul 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 33da030c-7080-42ab-b919-bbc354211942

📥 Commits

Reviewing files that changed from the base of the PR and between f224e4b and 34283fc.

📒 Files selected for processing (4)
  • python/nemo_relay/_native.pyi
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • python/tests/plugin/test_worker_sdk.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Check / Run
  • GitHub Check: Preview docs
🧰 Additional context used
📓 Path-based instructions (17)
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: 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, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: When changing the Python wrapper package, tests, or docs tooling, lint with Ruff (E, F, W, I), format with Ruff formatter (120-character lines, double quotes), and pass ty type checking.
Add the SPDX license header to all Python source files using the # comment form.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/core/src/plugin/dynamic/**,crates/plugin/**,crates/worker/**,crates/worker-proto/**,crates/types/**,python/plugin/**,examples/rust-native-plugin/**,examples/python-grpc-worker-plugin/**,docs/build-plugins/**}

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

Keep the stable boundary explicit: native plugins cross a C ABI, and worker plugins cross grpc-v1.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/**/src/**/*.rs,python/**/*.py}

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

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/core/src/plugin/dynamic/**/*.rs,crates/plugin/**/*.rs,crates/worker/**/*.rs,crates/worker-proto/**/*.rs,python/plugin/**/*.py}

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

Manifest validation must cover kind, compatibility, load contract, integrity, capability mismatch, and disabled-plugin behavior.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/plugin/**/*.rs,python/plugin/**/*.py}

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

Rust and Python SDKs must expose every supported registration surface.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{py,go,js,ts}

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

Keep Python, Go, and Node.js config objects and subscriber/exporter methods aligned so all bindings expose the same logical knobs and semantics.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*

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

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/nemo_relay/_native.pyi
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,go,js,ts}

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

If a language surface changed, always run that language's test target even when Rust core did not change.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**

⚙️ 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 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, integrations, 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
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
skills/       # Published Codex/agent skills for NeMo Relay usage patterns

Prerequisites

Insta...

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/nemo_relay/_native.pyi
  • python/plugin/src/nemo_relay_plugin/_api.py
python/tests/**/*.py

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

python/tests/**/*.py: Pytest is used to run tests.
Do not add @pytest.mark.asyncio to any test; async tests are automatically detected and run by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, do not define a new class; use unittest.mock.MagicMock or unittest.mock.AsyncMock, with the spec constructor argument when necessary.
Name mocked classes with the mock prefix, not fake.
Prefer pytest fixtures over helper methods.
Do not repeat fixtures; if a fixture is needed in multiple test files, place it in a conftest.py file.
When creating a fixture, use @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and define the fixture function as def <fixture_name>_fixture() -> <return_type>:; only specify scope when it is not function.
Prefer pytest.mark.parametrize over creating individual tests for different input types.

Files:

  • python/tests/plugin/test_worker_sdk.py
{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:

  • python/tests/plugin/test_worker_sdk.py
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

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

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/_native.pyi
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

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

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/_native.pyi
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/_native.pyi
🔇 Additional comments (6)
python/nemo_relay/_native.pyi (1)

27-43: LGTM!

Also applies to: 1199-1224

python/plugin/src/nemo_relay_plugin/__init__.py (1)

21-21: LGTM!

Also applies to: 42-42, 71-72, 112-113

python/plugin/src/nemo_relay_plugin/_api.py (2)

79-99: Previously-flagged collision fix confirmed.

Both prior review comments (sparse docstrings, duplicated surface→dict mapping) are resolved here: _EVENT_SANITIZER_HANDLER_ATTRIBUTES is now the single source of truth used by both _register_event_sanitizer and _invoke_result, and the three register_*_sanitize_guardrail methods have full Args/Callback errors docstrings matching sibling methods.

Also applies to: 722-725, 751-753, 771-773, 806-811, 844-920


1844-1856: LGTM!

Also applies to: 2019-2021

python/tests/plugin/test_worker_sdk.py (2)

278-286: Collision regression test confirmed present.

This test reuses "event_sanitize" across register_mark_sanitize_guardrail and register_scope_sanitize_start_guardrail and asserts surface-specific output, which is exactly the regression coverage requested in the earlier review for the shared-dict collision bug. Good coverage.

As per path instructions, "Tests should cover the behavior promised by the changed API surface, including error paths and cross-request isolation where relevant."

Also applies to: 332-334, 702-747

Source: Path instructions


376-378: LGTM!

Also applies to: 417-419, 2520-2522


Walkthrough

Adds event-sanitize guardrail APIs for mark and scope start/end events across Rust and Python bindings, extends the plugin worker dispatch and coverage, and adds a mark field to PiiRedactionConfig.

Changes

Event sanitize guardrail feature

Layer / File(s) Summary
Rust callback wrapper
crates/python/src/py_callable.rs
Adds wrap_py_event_sanitize_fn to round-trip sanitize fields through Python with fail-open fallback.
Rust py_api registration
crates/python/src/py_api/mod.rs
Adds global and scope-local sanitize guardrail register/deregister functions and module wiring.
Python sanitize types and protocols
python/nemo_relay/__init__.py, __init__.pyi, _native.pyi, plugin.py, plugin.pyi
Adds sanitize field types, callback aliases, plugin protocol methods, and native stubs for the new guardrail surfaces.
Python guardrail wrappers
python/nemo_relay/guardrails.py, scope_local.py
Adds global and scope-local Python wrappers delegating to native sanitize guardrail bindings.
Plugin SDK sanitize registration and dispatch
python/plugin/src/nemo_relay_plugin/__init__.py, _api.py
Adds sanitizer callback types, handler registries, registration methods, worker dispatch, handshake surface updates, and public re-exports.
Plugin SDK and doc coverage tests
crates/python/tests/coverage/*, python/tests/plugin/test_public_api_docstrings.py
Extends binding exposure checks, plugin coverage, and docstring alias coverage for the new sanitize APIs.
Worker and end-to-end sanitizer tests
python/tests/plugin/test_worker_sdk.py, python/tests/test_event_sanitizers.py
Adds worker-surface tests and end-to-end sanitizer tests for ordering, fail-open behavior, scope lifecycle, inheritance, and rollback.

PII redaction mark flag

Layer / File(s) Summary
PII redaction config
python/nemo_relay/pii_redaction.py, pii_redaction.pyi, python/tests/test_pii_redaction_plugin.py
Adds the mark config field, serializes it, updates the stub, and extends wrapper assertions.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginContext
  participant _WorkerService
  participant EventSanitizeCallback
  participant Client

  PluginContext->>_WorkerService: register sanitize guardrail surface
  Client->>_WorkerService: invoke sanitize surface with event payload
  _WorkerService->>EventSanitizeCallback: call(event, fields)
  EventSanitizeCallback-->>_WorkerService: sanitized fields
  _WorkerService-->>Client: JSON response with sanitized fields
Loading

Possibly related PRs

  • NVIDIA/NeMo-Relay#371: Adds the core mark/scope sanitize guardrail surfaces that this PR binds into Python and the plugin SDK.

Suggested labels: DO NOT MERGE


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Title check ❌ Error Title is related, but the trailing “[3/6]” breaks the required Conventional Commits format. Remove the “[3/6]” suffix and keep a plain Conventional Commit title, e.g. “feat: expose event sanitizers in Python”.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed Description matches the template sections and includes overview, details, reviewer start, checklist, and related issue.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@willkill07 willkill07 added this to the 0.6 milestone Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

@willkill07 willkill07 self-assigned this Jul 7, 2026
@willkill07 willkill07 changed the title feat: expose event sanitizers in Python feat: expose event sanitizers in Python [3/6] Jul 7, 2026
@willkill07
willkill07 changed the base branch from fix/relay-409-pii-event-redaction to main July 8, 2026 21:36
@github-actions github-actions Bot added size:XL PR is extra large and removed size:L PR is large labels Jul 8, 2026
@willkill07
willkill07 force-pushed the feat/relay-409-python-event-sanitizers branch from 9e3ab05 to 5d4a20b Compare July 8, 2026 21:40
@github-actions github-actions Bot added size:L PR is large and removed size:XL PR is extra large labels Jul 8, 2026
Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07
willkill07 force-pushed the feat/relay-409-python-event-sanitizers branch from 5d4a20b to c37d47f Compare July 9, 2026 02:10
@willkill07
willkill07 marked this pull request as ready for review July 9, 2026 11:58
@willkill07
willkill07 requested a review from a team as a code owner July 9, 2026 11:58

@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: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/plugin/src/nemo_relay_plugin/_api.py (1)

432-465: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Shared event_sanitizers dict causes cross-surface name collisions and misrouted callbacks.

event_sanitizers is a single dict[str, EventSanitizeCallback] shared across three distinct surfaces (MARK_SANITIZE_GUARDRAIL, SCOPE_SANITIZE_START_GUARDRAIL, SCOPE_SANITIZE_END_GUARDRAIL). _register_event_sanitizer (Line 518-526) stores callbacks keyed only by name, and _invoke_result (Line 1469-1484) dispatches via self._handler(self._handlers.event_sanitizers, request.registration_name) — also keyed only by name, ignoring surface.

The class docstring explicitly states: "Registration names need to be unique only within a registration surface" (Line 477-479), and this exact pattern (same name across two surfaces) is demonstrated for tool sanitizers, which correctly use separate dicts (tool_sanitize_requests / tool_sanitize_responses) to avoid collision. If a plugin registers, e.g., "sanitize" for both register_mark_sanitize_guardrail and register_scope_sanitize_start_guardrail (which _push_registration's per-surface uniqueness check permits), the second call silently overwrites the first entry in the shared dict, and the wrong callback runs for the other surface — misrouting observability sanitization, with real risk of unsanitized/leaked data reaching exporters since this is a redaction path.

🐛 Proposed fix: split into per-surface dicts
 class _Handlers:
     registrations: list[Any]
     subscribers: dict[str, SubscriberCallback]
-    event_sanitizers: dict[str, EventSanitizeCallback]
+    mark_sanitizers: dict[str, EventSanitizeCallback]
+    scope_start_sanitizers: dict[str, EventSanitizeCallback]
+    scope_end_sanitizers: dict[str, EventSanitizeCallback]
     tool_sanitize_requests: dict[str, ToolSanitizeCallback]
     ...
     `@classmethod`
     def empty(cls) -> _Handlers:
         return cls(
             registrations=[],
             subscribers={},
-            event_sanitizers={},
+            mark_sanitizers={},
+            scope_start_sanitizers={},
+            scope_end_sanitizers={},
             ...
         )
-    def _register_event_sanitizer(
-        self,
-        name: str,
-        callback: EventSanitizeCallback,
-        surface: int,
-        priority: int,
-    ) -> None:
-        self._push_registration(name, surface, priority, False)
-        self._handlers.event_sanitizers[name] = callback
-
     def register_mark_sanitize_guardrail(
         self, name: str, callback: EventSanitizeCallback, *, priority: int = 0
     ) -> None:
         """Register a sanitizer for mark event observability fields."""
-        self._register_event_sanitizer(name, callback, pb.MARK_SANITIZE_GUARDRAIL, priority)
+        self._push_registration(name, pb.MARK_SANITIZE_GUARDRAIL, priority, False)
+        self._handlers.mark_sanitizers[name] = callback

     def register_scope_sanitize_start_guardrail(
         self, name: str, callback: EventSanitizeCallback, *, priority: int = 0
     ) -> None:
         """Register a sanitizer for scope start event observability fields."""
-        self._register_event_sanitizer(name, callback, pb.SCOPE_SANITIZE_START_GUARDRAIL, priority)
+        self._push_registration(name, pb.SCOPE_SANITIZE_START_GUARDRAIL, priority, False)
+        self._handlers.scope_start_sanitizers[name] = callback

     def register_scope_sanitize_end_guardrail(
         self, name: str, callback: EventSanitizeCallback, *, priority: int = 0
     ) -> None:
         """Register a sanitizer for scope end event observability fields."""
-        self._register_event_sanitizer(name, callback, pb.SCOPE_SANITIZE_END_GUARDRAIL, priority)
+        self._push_registration(name, pb.SCOPE_SANITIZE_END_GUARDRAIL, priority, False)
+        self._handlers.scope_end_sanitizers[name] = callback
-            if request.surface in {
-                pb.MARK_SANITIZE_GUARDRAIL,
-                pb.SCOPE_SANITIZE_START_GUARDRAIL,
-                pb.SCOPE_SANITIZE_END_GUARDRAIL,
-            }:
+            event_sanitizer_dicts = {
+                pb.MARK_SANITIZE_GUARDRAIL: self._handlers.mark_sanitizers,
+                pb.SCOPE_SANITIZE_START_GUARDRAIL: self._handlers.scope_start_sanitizers,
+                pb.SCOPE_SANITIZE_END_GUARDRAIL: self._handlers.scope_end_sanitizers,
+            }
+            if request.surface in event_sanitizer_dicts:
                 event = _decode_required_envelope(request.event, "event", EVENT_SCHEMA)
                 fields: EventSanitizeFields = {
                     "data": event.get("data"),
                     "category_profile": event.get("category_profile"),
                     "metadata": event.get("metadata"),
                 }
-                return _json_response(
-                    await _maybe_await(
-                        self._handler(self._handlers.event_sanitizers, request.registration_name)(event, fields)
-                    )
-                )
+                handler = self._handler(event_sanitizer_dicts[request.surface], request.registration_name)
+                return _json_response(await _maybe_await(handler(event, fields)))

Also applies to: 518-545, 1469-1484

🤖 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/plugin/src/nemo_relay_plugin/_api.py` around lines 432 - 465, The
shared event sanitizer registry in _Handlers is causing callbacks to collide
across MARK_SANITIZE_GUARDRAIL, SCOPE_SANITIZE_START_GUARDRAIL, and
SCOPE_SANITIZE_END_GUARDRAIL because _register_event_sanitizer and
_invoke_result key only by registration name. Split event_sanitizers into
separate per-surface maps, update _Handlers.empty and the related
registration/dispatch paths in _register_event_sanitizer and _invoke_result to
use the correct surface-specific dict, and keep the lookup aligned with the
class’s per-surface uniqueness rule.
🤖 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/python/src/py_plugin.rs`:
- Around line 214-301: The new guardrail helper in PyPluginContext only covers
the three sanitize-event registrations, but the same
qualify/register/track/deregister-closure pattern is still duplicated across the
older registration methods like register_tool_sanitize_request_guardrail and
register_llm_sanitize_response_guardrail. Extract a more generic helper from
register_event_sanitizer that also accepts the wrap function and the underlying
register/deregister hooks, then refactor the remaining methods to use it so the
repeated registrations all share the same logic.

In `@python/plugin/src/nemo_relay_plugin/_api.py`:
- Around line 528-544: The three new sanitizer registration methods in
PluginContext are missing the same docstring structure used by the other
register_* APIs. Update register_mark_sanitize_guardrail,
register_scope_sanitize_start_guardrail, and
register_scope_sanitize_end_guardrail to include the standard Args section for
name/callback/priority and the Callback errors section, matching the style of
register_subscriber and register_tool_sanitize_request_guardrail. Keep the
method behavior unchanged and make the documentation consistent across these
registration helpers.

In `@python/tests/plugin/test_worker_sdk.py`:
- Around line 235-237: Add a test that reuses the same sanitizer registration
name across two different sanitizer surfaces with different callbacks, similar
to the existing tool sanitizer coverage. Update the test around
ctx.register_mark_sanitize_guardrail,
ctx.register_scope_sanitize_start_guardrail, and
ctx.register_scope_sanitize_end_guardrail to include an analogous same-name case
so it exercises per-surface uniqueness and would catch the shared-dict collision
in event_sanitizers. Use the relevant guardrail registration methods in
test_worker_sdk.py to locate the spot and assert both callbacks remain distinct.

In `@python/tests/test_event_sanitizers.py`:
- Line 21: The test functions in this module are annotated with forbidden `->
None` return types. Remove the return annotation from all affected test
definitions, including
`test_global_mark_sanitizers_order_convert_fields_and_remove_values` and the
other test functions in this file, while keeping the test names and bodies
unchanged.
- Around line 106-131: This test leaves scope and subscriber state behind if an
assertion or event call fails; wrap the scope.push/scope.event/scope.pop
sequence and the subscribers.deregister(capture_name) cleanup in try/finally.
Use the existing test helpers and symbols like scope.push, scope.pop,
scope_local.register_mark_sanitize, and subscribers.deregister so owner/child
scopes are always closed and the capture subscriber is always removed even on
failure.
- Around line 14-18: Convert the duplicated _capture_events helper into a pytest
fixture that uses yield for setup/teardown, and update the tests to depend on
that fixture instead of calling the helper directly. Move the
subscribers.register(name, events.append) logic into the fixture setup and
ensure subscribers.deregister(capture_name) is always performed in the fixture
cleanup so teardown is centralized and consistent across the file.

---

Outside diff comments:
In `@python/plugin/src/nemo_relay_plugin/_api.py`:
- Around line 432-465: The shared event sanitizer registry in _Handlers is
causing callbacks to collide across MARK_SANITIZE_GUARDRAIL,
SCOPE_SANITIZE_START_GUARDRAIL, and SCOPE_SANITIZE_END_GUARDRAIL because
_register_event_sanitizer and _invoke_result key only by registration name.
Split event_sanitizers into separate per-surface maps, update _Handlers.empty
and the related registration/dispatch paths in _register_event_sanitizer and
_invoke_result to use the correct surface-specific dict, and keep the lookup
aligned with the class’s per-surface uniqueness rule.
🪄 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: cc515614-9a4d-404a-a841-ef5b938e68a7

📥 Commits

Reviewing files that changed from the base of the PR and between c83c8f2 and c37d47f.

📒 Files selected for processing (21)
  • crates/python/src/py_api/mod.rs
  • crates/python/src/py_callable.rs
  • crates/python/src/py_plugin.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/nemo_relay/_native.pyi
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/pii_redaction.pyi
  • python/nemo_relay/plugin.py
  • python/nemo_relay/plugin.pyi
  • python/nemo_relay/scope_local.py
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/tests/test_pii_redaction_plugin.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (22)
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: 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, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: When changing the Python wrapper package, tests, or docs tooling, lint with Ruff (E, F, W, I), format with Ruff formatter (120-character lines, double quotes), and pass ty type checking.
Add the SPDX license header to all Python source files using the # comment form.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/**/src/**/*.rs,python/**/*.py}

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

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{py,go,js,ts}

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

Keep Python, Go, and Node.js config objects and subscriber/exporter methods aligned so all bindings expose the same logical knobs and semantics.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • python/plugin/src/nemo_relay_plugin/_api.py
python/tests/**/*.py

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

python/tests/**/*.py: Pytest is used to run tests.
Do not add @pytest.mark.asyncio to any test; async tests are automatically detected and run by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, do not define a new class; use unittest.mock.MagicMock or unittest.mock.AsyncMock, with the spec constructor argument when necessary.
Name mocked classes with the mock prefix, not fake.
Prefer pytest fixtures over helper methods.
Do not repeat fixtures; if a fixture is needed in multiple test files, place it in a conftest.py file.
When creating a fixture, use @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and define the fixture function as def <fixture_name>_fixture() -> <return_type>:; only specify scope when it is not function.
Prefer pytest.mark.parametrize over creating individual tests for different input types.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
**/*

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

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.pyi
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/plugin.pyi
  • crates/python/src/py_plugin.rs
  • python/nemo_relay/_native.pyi
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,go,js,ts}

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

If a language surface changed, always run that language's test target even when Rust core did not change.

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
**

⚙️ 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 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, integrations, 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
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
skills/       # Published Codex/agent skills for NeMo Relay usage patterns

Prerequisites

Insta...

Files:

  • python/tests/test_pii_redaction_plugin.py
  • python/nemo_relay/pii_redaction.pyi
  • python/nemo_relay/pii_redaction.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/tests/plugin/test_public_api_docstrings.py
  • python/nemo_relay/scope_local.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/plugin.pyi
  • crates/python/src/py_plugin.rs
  • python/nemo_relay/_native.pyi
  • crates/python/src/py_api/mod.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
{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:

  • python/tests/test_pii_redaction_plugin.py
  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • python/tests/plugin/test_public_api_docstrings.py
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
{crates/python/src/py_api/mod.rs,python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go,crates/node/src/api/**/*.rs}

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

Update the language-native bindings for every exposed surface in Python, Go, and Node.js.

Files:

  • python/nemo_relay/pii_redaction.pyi
  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/plugin.pyi
  • python/nemo_relay/_native.pyi
  • crates/python/src/py_api/mod.rs
{python/nemo_relay/**/*.py,python/nemo_relay/**/*.pyi,go/nemo_relay/**/*.go}

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

Update language wrapper helpers such as Python wrapper modules, Python type stubs, and Go shorthand packages when the new behavior belongs in those helper layers.

Files:

  • python/nemo_relay/pii_redaction.pyi
  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/plugin.pyi
  • python/nemo_relay/_native.pyi
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/pii_redaction.pyi
  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
  • python/nemo_relay/guardrails.py
  • python/nemo_relay/plugin.pyi
  • python/nemo_relay/_native.pyi
python/nemo_relay/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/scope_local.py
  • python/nemo_relay/plugin.py
  • python/nemo_relay/__init__.py
  • python/nemo_relay/guardrails.py
**/*.rs

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

**/*.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

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.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.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

  • crates/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
crates/{python,ffi,node}/**/*

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: 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/python/tests/coverage/py_callable_coverage_tests.rs
  • crates/python/tests/coverage/coverage_tests.rs
  • crates/python/tests/coverage/py_plugin_coverage_tests.rs
  • crates/python/src/py_callable.rs
  • crates/python/src/py_plugin.rs
  • crates/python/src/py_api/mod.rs
{crates/core/src/plugin/dynamic/**,crates/plugin/**,crates/worker/**,crates/worker-proto/**,crates/types/**,python/plugin/**,examples/rust-native-plugin/**,examples/python-grpc-worker-plugin/**,docs/build-plugins/**}

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

Keep the stable boundary explicit: native plugins cross a C ABI, and worker plugins cross grpc-v1.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/core/src/plugin/dynamic/**/*.rs,crates/plugin/**/*.rs,crates/worker/**/*.rs,crates/worker-proto/**/*.rs,python/plugin/**/*.py}

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

Manifest validation must cover kind, compatibility, load contract, integrity, capability mismatch, and disabled-plugin behavior.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/plugin/**/*.rs,python/plugin/**/*.py}

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

Rust and Python SDKs must expose every supported registration surface.

Files:

  • python/plugin/src/nemo_relay_plugin/__init__.py
  • python/plugin/src/nemo_relay_plugin/_api.py
python/nemo_relay/{adaptive.py,plugin.py}

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

Keep Python adaptive/plugin wrappers in python/nemo_relay/adaptive.py and python/nemo_relay/plugin.py synchronized with the shared adaptive/plugin boundary and lifecycle.

Files:

  • python/nemo_relay/plugin.py
🪛 Ruff (0.15.20)
python/nemo_relay/__init__.py

[warning] 82-82: Import from collections.abc instead: AsyncIterator, Awaitable, Callable

Import from collections.abc

(UP035)

python/tests/test_event_sanitizers.py

[warning] 14-14: Missing return type annotation for private function _capture_events

(ANN202)


[warning] 135-135: Missing return type annotation for private function validate

Add return type annotation: None

(ANN202)


[warning] 138-138: Missing return type annotation for private function register

Add return type annotation: None

(ANN202)


[warning] 170-170: Missing return type annotation for private function validate

Add return type annotation: None

(ANN202)


[warning] 173-173: Missing return type annotation for private function register

Add return type annotation: Never

(ANN202)


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

(TRY003)

🔇 Additional comments (22)
python/nemo_relay/pii_redaction.py (1)

137-137: LGTM!

Also applies to: 154-154

python/nemo_relay/pii_redaction.pyi (1)

59-59: LGTM!

python/tests/test_pii_redaction_plugin.py (1)

40-43: LGTM!

python/nemo_relay/__init__.py (1)

82-82: LGTM!

Also applies to: 141-155, 494-495

python/nemo_relay/__init__.pyi (1)

26-26: LGTM!

Also applies to: 145-153

python/nemo_relay/_native.pyi (1)

27-43: LGTM!

Also applies to: 1190-1215

python/nemo_relay/plugin.py (1)

18-18: LGTM!

Also applies to: 86-99

python/nemo_relay/plugin.pyi (1)

9-9: LGTM!

Also applies to: 39-45

python/nemo_relay/guardrails.py (1)

24-24: LGTM!

Also applies to: 40-46, 65-71, 82-116, 365-370

python/nemo_relay/scope_local.py (2)

40-48: LGTM!

Also applies to: 85-93, 663-669


113-147: 📐 Maintainability & Code Quality

No change needed for these wrappers

These functions match the untyped wrapper style used throughout python/nemo_relay/scope_local.py, so adding annotations here would be inconsistent with the rest of the module.

			> Likely an incorrect or invalid review comment.
python/plugin/src/nemo_relay_plugin/__init__.py (1)

21-21: LGTM!

Also applies to: 35-35, 64-65, 98-99

python/plugin/src/nemo_relay_plugin/_api.py (1)

72-72: LGTM!

Also applies to: 83-92, 406-409, 1647-1649

python/tests/plugin/test_public_api_docstrings.py (1)

24-24: LGTM!

python/tests/plugin/test_worker_sdk.py (1)

187-189: LGTM!

Also applies to: 279-281, 320-322, 605-645, 2379-2381

crates/python/src/py_callable.rs (2)

28-37: LGTM!


857-917: 🔒 Security & Privacy

No change needed — event sanitizer callbacks intentionally fall back to the original fields on conversion, call, and shape errors, matching the existing sanitizer contract.

			> Likely an incorrect or invalid review comment.
crates/python/tests/coverage/py_callable_coverage_tests.rs (1)

598-651: LGTM!

crates/python/src/py_api/mod.rs (1)

878-915: LGTM!

Also applies to: 1313-1358, 1738-1754, 1831-1851

crates/python/tests/coverage/coverage_tests.rs (1)

251-256: LGTM!

Also applies to: 287-292

crates/python/src/py_plugin.rs (1)

19-33: LGTM!

Also applies to: 43-47

crates/python/tests/coverage/py_plugin_coverage_tests.rs (1)

123-125: LGTM!

Also applies to: 169-189, 277-287

Comment thread crates/python/src/py_plugin.rs
Comment thread python/plugin/src/nemo_relay_plugin/_api.py Outdated
Comment thread python/tests/plugin/test_worker_sdk.py Outdated
Comment thread python/tests/test_event_sanitizers.py Outdated
Comment thread python/tests/test_event_sanitizers.py Outdated
Comment thread python/tests/test_event_sanitizers.py Outdated
@bbednarski9

Copy link
Copy Markdown
Contributor

Worth a unit test to confirm/enforce:
#373: sanitizer callbacks collide across surfaces.
_api.py#L518-L526 stores mark, scope-start, and scope-end callbacks in one dictionary keyed only by name. Registering the same name on two surfaces is allowed, but the later callback overwrites the earlier one; both invocations then use the wrong callback. Use separate maps or key by (surface, name), with a regression test.

Comment thread crates/python/src/py_api/mod.rs
Signed-off-by: Will Killian <wkillian@nvidia.com>
@willkill07 willkill07 changed the title feat: expose event sanitizers in Python [3/6] feat: expose event sanitizers in Python Jul 9, 2026
@github-actions github-actions Bot removed the size:L PR is large label Jul 9, 2026
@github-actions github-actions Bot added the size:XL PR is extra large label Jul 9, 2026
@coderabbitai coderabbitai Bot added the DO NOT MERGE PR should not be merged; see PR for details label Jul 9, 2026

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/plugin/src/nemo_relay_plugin/_api.py`:
- Around line 1522-1535: Centralize the sanitizer surface-to-handler association
so registration and dispatch use the same source of truth, instead of
duplicating the mapping in `_register_event_sanitizer` call sites and
`_invoke_result`. Add a single shared class-level mapping (for example, from
`pb.MARK_SANITIZE_GUARDRAIL`, `pb.SCOPE_SANITIZE_START_GUARDRAIL`, and
`pb.SCOPE_SANITIZE_END_GUARDRAIL` to the corresponding `self._handlers`
attribute names) and have both `_register_event_sanitizer` and `_invoke_result`
look up handlers through it. This keeps the `*_sanitizers` storage selection
consistent and prevents future surface additions from reintroducing the
collision bug.
🪄 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: d5db4376-046e-42b9-bdfd-f749e8ce6b34

📥 Commits

Reviewing files that changed from the base of the PR and between c37d47f and f224e4b.

📒 Files selected for processing (4)
  • crates/python/src/py_plugin.rs
  • python/plugin/src/nemo_relay_plugin/_api.py
  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (17)
**/*.{rs,py}

📄 CodeRabbit inference engine (AGENTS.md)

Follow binding naming conventions in Rust and Python: use snake_case.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
**/*.{rs,py,js,mjs,cjs,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{rs,py,js,mjs,cjs,ts,tsx}: 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, and keep errors explicit and binding-appropriate at the wrapper layer.
Keep async behavior on the existing tokio-based model; bindings should preserve callback and future lifetimes rather than blocking or hiding async work unexpectedly.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*.py: When changing the Python wrapper package, tests, or docs tooling, lint with Ruff (E, F, W, I), format with Ruff formatter (120-character lines, double quotes), and pass ty type checking.
Add the SPDX license header to all Python source files using the # comment form.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.{rs,py,go,js,ts,c,h}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Use language-appropriate naming conventions: Rust snake_case, C FFI exports prefixed nemo_relay_, Go PascalCase, Node.js camelCase, and Python snake_case.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
{crates/**/src/**/*.rs,python/**/*.py}

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

Do not add tests under src; Rust tests belong in crate tests/ trees, and Python SDK tests belong under python/tests.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
**/*.{py,go,js,ts}

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

Keep Python, Go, and Node.js config objects and subscriber/exporter methods aligned so all bindings expose the same logical knobs and semantics.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
python/tests/**/*.py

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

python/tests/**/*.py: Pytest is used to run tests.
Do not add @pytest.mark.asyncio to any test; async tests are automatically detected and run by the async runner.
Do not add a -> None return type annotation to test functions.
When mocking a class, do not define a new class; use unittest.mock.MagicMock or unittest.mock.AsyncMock, with the spec constructor argument when necessary.
Name mocked classes with the mock prefix, not fake.
Prefer pytest fixtures over helper methods.
Do not repeat fixtures; if a fixture is needed in multiple test files, place it in a conftest.py file.
When creating a fixture, use @pytest.fixture(name="<fixture_name>"[, scope="<scope>"]) and define the fixture function as def <fixture_name>_fixture() -> <return_type>:; only specify scope when it is not function.
Prefer pytest.mark.parametrize over creating individual tests for different input types.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
**/*

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

**/*: Format changed files with the language-native formatter before the final lint/test pass.
If dynamic plugin behavior changed, use maintain-dynamic-plugins and include the native SDK, worker protocol, Python SDK, docs, packaging, and Codecov surfaces in the validation plan.
If code changes alter APIs, bindings, commands, paths, packaging behavior, observability/adaptive semantics, or documented best practices, update any dependent maintainer or consumer skills in the same branch.
During iteration, prefer uv run pre-commit run --files <changed files...>.
Before review or handoff, run uv run pre-commit run --all-files.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
**/*.{rs,py,go,js,ts}

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

If a language surface changed, always run that language's test target even when Rust core did not change.

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.rs
**

⚙️ 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 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, integrations, 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
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
skills/       # Published Codex/agent skills for NeMo Relay usage patterns

Prerequisites

Insta...

Files:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
  • python/plugin/src/nemo_relay_plugin/_api.py
  • crates/python/src/py_plugin.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:

  • python/tests/plugin/test_worker_sdk.py
  • python/tests/test_event_sanitizers.py
{crates/core/src/plugin/dynamic/**,crates/plugin/**,crates/worker/**,crates/worker-proto/**,crates/types/**,python/plugin/**,examples/rust-native-plugin/**,examples/python-grpc-worker-plugin/**,docs/build-plugins/**}

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

Keep the stable boundary explicit: native plugins cross a C ABI, and worker plugins cross grpc-v1.

Files:

  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/core/src/plugin/dynamic/**/*.rs,crates/plugin/**/*.rs,crates/worker/**/*.rs,crates/worker-proto/**/*.rs,python/plugin/**/*.py}

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

Manifest validation must cover kind, compatibility, load contract, integrity, capability mismatch, and disabled-plugin behavior.

Files:

  • python/plugin/src/nemo_relay_plugin/_api.py
{crates/plugin/**/*.rs,python/plugin/**/*.py}

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

Rust and Python SDKs must expose every supported registration surface.

Files:

  • python/plugin/src/nemo_relay_plugin/_api.py
**/*.rs

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

**/*.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

When changing the core Rust runtime or Rust-facing API surface, format Rust code with cargo fmt (rustfmt defaults), keep cargo clippy -- -D warnings clean, and satisfy cargo deny check per deny.toml.

**/*.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.
For Rust changes headed for review, run cargo fmt --all and cargo clippy --workspace --all-targets -- -D warnings even if relying on pre-commit.

Files:

  • crates/python/src/py_plugin.rs
**/*.{rs,go,js,ts}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add the SPDX license header to all Rust, Go, JavaScript, and TypeScript source files using the corresponding // comment form.

Files:

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

⚙️ CodeRabbit configuration file

crates/{python,ffi,node}/**/*: 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/python/src/py_plugin.rs
🔇 Additional comments (9)
python/plugin/src/nemo_relay_plugin/_api.py (2)

435-437: 🎯 Functional Correctness

Collision fix confirmed: separate dicts per sanitizer surface.

Splitting mark_sanitizers/scope_start_sanitizers/scope_end_sanitizers into distinct maps resolves the earlier shared-dict collision (same name across mark/scope-start/scope-end no longer overwrites callbacks).

Also applies to: 455-457


522-598: LGTM! Registration methods correctly route each surface into its own dict, and docstrings now include Args/Callback errors sections matching sibling methods.

crates/python/src/py_plugin.rs (2)

244-563: LGTM! DRY refactor now applied consistently to all registration methods (subscriber, tool, LLM, sanitize guardrails, intercepts), fully addressing the earlier request to extend the pattern beyond just the sanitize guardrails.


214-238: LGTM! register_callback cleanly consolidates the qualify/register/track/deregister boilerplate.

python/tests/plugin/test_worker_sdk.py (2)

241-243: 🎯 Functional Correctness

LGTM! Reusing "event_sanitize" across mark and scope-start surfaces with distinct callbacks, and parametrizing the invoke test over all three surfaces, directly exercises the cross-surface name-collision fix requested previously.

Also applies to: 624-632


285-287: LGTM! Proto ID assertions and expected-registrations list correctly reflect the three new sanitizer surfaces.

Also applies to: 324-340

python/tests/test_event_sanitizers.py (3)

15-22: LGTM! Fixture follows the required @pytest.fixture(name=...) / <fixture_name>_fixture convention with proper yield-based teardown.


24-56: LGTM! -> None annotations removed from test functions per path instructions.

Also applies to: 59-72, 75-104, 136-197


106-134: LGTM! try/finally now correctly guards scope push/pop so owner/child scopes can't leak on assertion failure.

Comment thread python/plugin/src/nemo_relay_plugin/_api.py Outdated
@willkill07 willkill07 removed the DO NOT MERGE PR should not be merged; see PR for details label Jul 9, 2026
@willkill07 willkill07 changed the title feat: expose event sanitizers in Python feat: expose event sanitizers in Python [3/6] Jul 9, 2026
@willkill07
willkill07 requested a review from bbednarski9 July 9, 2026 14:57
@willkill07

Copy link
Copy Markdown
Member Author

/merge

@rapids-bot
rapids-bot Bot merged commit e16e1cd into NVIDIA:main Jul 9, 2026
41 checks passed
rapids-bot Bot pushed a commit that referenced this pull request Jul 15, 2026
#### Overview

Documents the event sanitizer and PII redaction behavior implemented by the merged RELAY-409 stack (#371#375). This docs-only PR is the final part of that six-PR stack.

- [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license.
- [x] I searched existing issues and open pull requests, and this does not duplicate existing work.

#### Details

- Adds a dedicated event sanitizer reference covering callback replacement semantics, ordering, registration lifetimes, cross-language APIs, and dynamic-plugin protocol values.
- Explains which mark and scope event fields sanitizers can replace and which lifecycle and identity fields remain immutable.
- Documents PII `mark = true` defaults, opt-out behavior, generic scope `input` and `output` coverage, and subscriber/exporter privacy guarantees.
- Updates related middleware, event, scope/mark, plugin-authoring, Python, Node.js, Go, and observability-skill guidance.
- Contains no implementation changes.
- Breaking changes: none.

Validation:

- `just docs`
- `just docs-linkcheck`
- `uv run pre-commit run --all-files`

#### Where should the reviewer start?

Start with `docs/reference/event-sanitizers.mdx`, then review `docs/configure-plugins/pii-redaction/configuration.mdx` for the privacy contract.

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

- Relates to: RELAY-409
- Relates to: #371, #372, #373, #374, and #375



## Summary by CodeRabbit

- **New Features**
  - Added observability sanitization for **mark** and **scope** events (including scope-start and scope-end) before delivery to subscribers/exporters.
  - Sanitizers can rewrite observability fields (`data`, `category_profile`, `metadata`) while keeping core identity/context fields immutable.
  - Expanded privacy redaction support with a configurable `mark` option (default enabled) for mark and generic scope events.

- **Documentation**
  - Added a new Event Sanitizers reference covering registration levels, ordering, callback contracts, and examples.
  - Updated middleware and language-binding guidance plus Node.js/Go/Python overviews to reflect mark/scope sanitizer capabilities.

Authors:
  - Will Killian (https://github.com/willkill07)

Approvers:
  - https://github.com/lvojtku
  - Bryan Bednarski (https://github.com/bbednarski9)

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants