Skip to content

feat: add in-process Rampart PII redaction plugin - #558

Open
afourniernv wants to merge 86 commits into
NVIDIA:mainfrom
afourniernv:feat/pii-worker-provider
Open

feat: add in-process Rampart PII redaction plugin#558
afourniernv wants to merge 86 commits into
NVIDIA:mainfrom
afourniernv:feat/pii-worker-provider

Conversation

@afourniernv

@afourniernv afourniernv commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Overview

Add pii_rampart, a separate first-party PII redaction plugin that runs the pinned nationaldesignstudio/rampart ONNX model inside the Relay Rust process.

The existing pii_redaction plugin remains deterministic. pii_rampart owns its model-backed configuration and lifecycle without adding a generic inference surface or changing the gRPC worker protocol.

This PR builds on the async primary-middleware boundary introduced by #571. Rampart uses that boundary to await sanitization without running tokenization or ONNX inference on Tokio executor threads or Tokio's shared blocking pool.

  • 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 an independent pii_rampart component kind with its own configuration, registration lifecycle, and Rust/Python/Node/Go helpers.
  • Load and optimize the pinned Rampart ONNX graph in-process with tract-onnx. Activation requires an absolute local snapshot path and verifies SHA-256 digests for the graph, config, vocabulary, and tokenizer metadata.
  • Keep model acquisition out of Relay. There is no download path, network dependency, or model file in this PR.
  • Implement the pinned BERT tokenizer in Rust, including normalization, WordPiece splitting, special tokens, and original UTF-8 byte offsets.
  • Apply Rampart's required deterministic prefilter for SSNs, Luhn-valid cards, email addresses, URLs, and IP/MAC addresses. Structured values become typed sentinels before tokenization; model spans are projected back to original UTF-8 byte offsets and merged with score-1 deterministic detections.
  • Require explicit JSON-pointer selectors. Only selected observability strings reach the model; provider/tool callback arguments and return values are not changed.
  • Validate model output before applying confidence, excluded-label, and replacement policy. Model errors, malformed spans, payload-limit failures, and bounded-admission failures fail closed for affected selected fields. Codec failures omit the observable LLM body rather than applying normalized selectors to raw data.
  • Run sanitizer CPU work on a dedicated per-activation executor with up to three workers, limited by host parallelism. At most 16 operations are admitted; admitted operations wait asynchronously for a worker for up to 500 ms. This isolates inference from both Tokio executor threads and Tokio's shared blocking pool.
  • Keep sanitization in the existing awaited middleware boundary. Managed calls wait at the observability checkpoint, preserving sanitizer ordering and guaranteeing subscribers see the sanitized event. A detached background publication queue was prototyped and rejected here because it changes core event-delivery semantics and dropped most selected bodies under burst load while accumulating detached tasks.
  • Bound each ONNX call to at most 512 padded tokens. There is no cross-request batching or model instance per request.
  • Register the component in the CLI, FFI, Python, and Node hosts, and add configuration helpers and tests for Python, Node, and Go.
  • Keep tract-onnx and the direct rayon dependency behind the crate's rampart feature. Rayon was already present transitively through Tract; the lockfile adds no new package.
  • Document pinned snapshot provisioning, explicit selectors, activation, concurrency, and binding registration in the crate README.
  • Keep the docs site, examples, worker SDK, and worker protocol unchanged. Benchmark code, output, and model files are not included in the repository.

Real-model concurrency validation on an Apple M4 Pro used the pinned snapshot through public managed OpenAI, Anthropic, tool, event, and streaming paths:

  • Claude-style fan-out, 5 agents x 5 turns plus 3 tools: three runs completed in 4.57-4.71 s with 668-683 ms p95 sanitizer latency and 0/200 fail-closed bodies.
  • Codex-style workload, 10 turns plus 6 parallel tools: completed in 6.322 s with 512 ms p95 sanitizer latency and no fail-closed bodies.
  • Sixteen concurrent streaming calls: completed in 496 ms with 0/32 request/response bodies failing closed.
  • Deliberate saturation, 32 concurrent 8 KiB inputs: completed in 1.902 s; 52/64 bodies failed closed after hitting the bounded wait. This is the intended overload behavior rather than unbounded queue growth.
  • With Tokio's only blocking-pool thread deliberately occupied, Rampart remained isolated at 22 ms p50 / 26 ms p95 with no failures. The prior spawn_blocking path reached about 501 ms p50 and failed 12/16 calls in the same condition.
  • One hundred activation/teardown cycles completed without a hang, stale callback, or failed call. Observed maximum RSS was approximately 90 MB in the concurrency run.

The main package-size cost remains unchanged. A minimal unstripped release binary grew from 4,307,520 bytes without Rampart to 36,344,480 bytes with it. Prior local package builds produced a 17,693,724-byte Python wheel and a 16,717,279-byte Node tarball.

Validation:

  • uv run pre-commit run --all-files
  • cargo clippy --workspace --all-targets -- -D warnings
  • just test-python (616 passed)
  • just test-node (347 passed)
  • just test-go
  • cargo test -p nemo-relay-pii-redaction --all-features (144 passed)
  • cargo test -p nemo-relay-pii-redaction --no-default-features (105 passed)
  • Real pinned-model fan-out, streaming, saturation, blocking-pool isolation, cancellation, and 100-cycle lifecycle smoke

just test-rust passed every compiled unit and integration suite, including 1,085 core tests and 144 PII tests. Its final core doctest failed on the pre-existing nemo_relay::Result example in crates/core/src/api/runtime/scope_stack.rs; the same invalid example is present on main and is unrelated to this change.

Where should the reviewer start?

Start with crates/pii-redaction/src/rampart/mod.rs for the independent plugin boundary, then prefilter.rs for the pinned model's structured-input contract, model.rs for model ownership, tokenizer.rs for offset fidelity, and sanitizer.rs for the dedicated executor, bounded admission, selection, and fail-closed behavior.

The key tradeoff is explicit: running in-process avoids a separate deployment and IPC path, but it adds roughly 32 MB to an unstripped release binary and gives up process-level crash isolation. Calls await sanitization for up to the bounded deadline; sustained overload favors privacy and bounded resource use by redacting or omitting selected observability content rather than growing an unbounded queue.

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

Summary by CodeRabbit

  • New Features

    • Added the optional Rampart PII redaction plugin for detecting and redacting sensitive information.
    • Added configuration for selectors, scoring, replacements, batching, codecs, and policies.
    • Added Rampart PII support across CLI, Node.js, Python, and Go integrations.
    • Added editor support for configuring, enabling, disabling, and persisting settings.
  • Documentation

    • Added setup, model provisioning, configuration, and runtime behavior guidance.
  • Tests

    • Added coverage for configuration, validation, redaction, detection, and integrations.

@copy-pr-bot

copy-pr-bot Bot commented Jul 26, 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.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 5fae45ff-7aef-4eb2-8d27-6b57192f0c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 79a861d and 207fa57.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (22)
  • ATTRIBUTIONS-Rust.md
  • Cargo.toml
  • crates/cli/src/plugins/prompt.rs
  • crates/cli/src/server/mod.rs
  • crates/cli/tests/coverage/shared/plugins_tests.rs
  • crates/node/package.json
  • crates/node/pii_rampart.d.ts
  • crates/node/pii_rampart.js
  • crates/node/src/api/mod.rs
  • crates/node/tests/pii_rampart_tests.mjs
  • crates/pii-redaction/Cargo.toml
  • crates/pii-redaction/README.md
  • crates/pii-redaction/src/rampart/mod.rs
  • crates/pii-redaction/src/rampart/model.rs
  • crates/pii-redaction/src/rampart/sanitizer.rs
  • crates/pii-redaction/src/rampart/tokenizer.rs
  • go/nemo_relay/pii_rampart.go
  • go/nemo_relay/pii_rampart/pii_rampart.go
  • go/nemo_relay/pii_rampart/pii_rampart_test.go
  • go/nemo_relay/pii_rampart_test.go
  • python/nemo_relay/__init__.py
  • python/nemo_relay/__init__.pyi
 ___________________________________________________
< This is not a microservice. This is a macro-mess. >
 ---------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).

Walkthrough

Changes

Rampart PII support now includes an ONNX detector, structured prefilter, bounded asynchronous sanitizer, plugin registration, CLI editor integration, and Node, Go, and Python APIs. Host initialization paths register the component and expose validation and configuration helpers.

Rampart PII plugin

Layer / File(s) Summary
Detector and plugin contract
crates/pii-redaction/src/rampart/*, crates/pii-redaction/Cargo.toml, crates/pii-redaction/README.md
Adds configuration validation, tokenizer, structured PII prefiltering, verified model loading, ONNX inference, and plugin registration.
Sanitizer callbacks
crates/pii-redaction/src/rampart/sanitizer.rs, crates/pii-redaction/src/builtin.rs
Adds bounded executor admission, codec-aware redaction, fail-closed behavior, and asynchronous sanitizer callback execution.
Host and CLI integration
crates/ffi/src/api/plugin.rs, crates/python/src/lib.rs, crates/node/src/api/mod.rs, crates/cli/src/server/mod.rs, crates/cli/src/plugins/*, crates/cli/tests/coverage/shared/plugins_tests.rs
Registers Rampart during host initialization and adds CLI editing, persistence, summaries, schema handling, and registration errors.
Language APIs
crates/node/pii_rampart.*, go/nemo_relay/pii_rampart*, python/nemo_relay/pii_rampart.*, python/nemo_relay/__init__.*
Adds configuration types, component builders, metadata constants, validation helpers, package exports, and type declarations.
Validation and supporting updates
crates/node/tests/pii_rampart_tests.mjs, go/nemo_relay/pii_rampart*_test.go, python/tests/test_pii_rampart_plugin.py, Cargo.toml, ATTRIBUTIONS-Rust.md, .gitattributes
Adds cross-language coverage and enables the Rampart feature with refreshed attribution metadata.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Host
  participant PluginRegistry
  participant RampartDetector
  participant RampartSanitizer
  participant Guardrail
  Host->>PluginRegistry: register Rampart PII component
  PluginRegistry->>RampartDetector: verify and load model artifacts
  PluginRegistry->>RampartSanitizer: create sanitizer
  PluginRegistry->>Guardrail: register surface callbacks
  Guardrail->>RampartSanitizer: sanitize selected payload
  RampartSanitizer->>RampartDetector: detect text batch
  RampartDetector-->>RampartSanitizer: return scored spans
  RampartSanitizer-->>Guardrail: return redacted payload
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the allowed feat type, provides a concise imperative summary, and is 49 characters without a trailing period.
Description check ✅ Passed The description includes all required sections, completed checklist items, detailed changes, reviewer guidance, and a related issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

@github-actions github-actions Bot added size:XXL PR is very large Feature a new feature lang:go PR changes/introduces Go code lang:js PR changes/introduces Javascript/Typescript code lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code labels Jul 26, 2026
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv
afourniernv force-pushed the feat/pii-worker-provider branch from b73ac66 to 385c241 Compare July 27, 2026 00:03
@willkill07 willkill07 added this to the 0.7 milestone Jul 27, 2026
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv afourniernv changed the title feat: add worker-backed PII local models feat: add worker-backed PII detection Jul 27, 2026
@afourniernv
afourniernv marked this pull request as ready for review July 27, 2026 18:39
@afourniernv
afourniernv requested review from a team as code owners July 27, 2026 18:39
@afourniernv afourniernv changed the title feat: add worker-backed PII detection feat: run local PII models through gRPC workers Jul 27, 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: 11

Caution

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

⚠️ Outside diff range comments (1)
crates/worker/src/lib.rs (1)

676-705: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Merge push_registration into push_contract_registration to remove duplication.

Both functions build an identical Registration except for the contract field. Python's SDK already unifies this via a single _push_registration(..., *, contract: str = "") (see python/plugin/src/nemo_relay_plugin/_api.py lines 1288-1310); the Rust SDK diverging into two near-duplicate helpers risks future drift (e.g., a new field added to one but not the other).

♻️ Proposed refactor
-    fn push_registration(
-        &mut self,
-        name: &str,
-        surface: RegistrationSurface,
-        priority: i32,
-        break_chain: bool,
-    ) {
-        self.handlers.registrations.push(Registration {
-            local_name: name.into(),
-            surface: surface as i32,
-            priority,
-            break_chain,
-            contract: String::new(),
-        });
-    }
-
-    fn push_contract_registration(
-        &mut self,
-        name: &str,
-        surface: RegistrationSurface,
-        contract: &str,
-    ) {
-        self.handlers.registrations.push(Registration {
-            local_name: name.into(),
-            surface: surface as i32,
-            priority: 0,
-            break_chain: false,
-            contract: contract.into(),
-        });
-    }
+    fn push_registration(
+        &mut self,
+        name: &str,
+        surface: RegistrationSurface,
+        priority: i32,
+        break_chain: bool,
+    ) {
+        self.push_contract_registration(name, surface, priority, break_chain, "");
+    }
+
+    fn push_contract_registration(
+        &mut self,
+        name: &str,
+        surface: RegistrationSurface,
+        priority: i32,
+        break_chain: bool,
+        contract: &str,
+    ) {
+        self.handlers.registrations.push(Registration {
+            local_name: name.into(),
+            surface: surface as i32,
+            priority,
+            break_chain,
+            contract: contract.into(),
+        });
+    }

Then update the call in register_worker_inference to self.push_contract_registration(name, RegistrationSurface::WorkerInference, 0, false, contract);.

🤖 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/worker/src/lib.rs` around lines 676 - 705, Merge push_registration
into push_contract_registration by giving push_contract_registration priority
and break_chain parameters, with contract supplied as the final argument and
defaulting to an empty string where appropriate. Remove the duplicate helper,
update all callers including register_worker_inference to pass the unified
arguments, and preserve existing Registration field values.
🤖 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/pii-redaction/src/component.rs`:
- Around line 960-962: Update is_valid_json_pointer_pattern to validate wildcard
segments according to JsonPointerPattern::matches: reject any path segment that
contains '*' unless the entire segment is exactly "*". Preserve existing JSON
Pointer validation for all other segments and continue accepting standalone
wildcard segments.

In `@crates/pii-redaction/src/local.rs`:
- Around line 732-740: The empty-paths branch in llm_sanitize_request_callback
must sanitize request headers as well as request.content, preserving the
intended pointer prefixes for the headers and content roots. Reuse the existing
request/header sanitization behavior used by sanitize_raw_request or the builtin
flow, and ensure the broad-coverage path returns a request with sanitized header
values.

In `@crates/pii-redaction/tests/unit/component_tests.rs`:
- Around line 1916-1921: Update the table-driven assertion in the
validate_plugin_config test loop to include failure context identifying the
current config/field/message case and the produced diagnostics. Preserve the
existing matching condition while supplying a descriptive assertion message so
failures reveal which case failed and the actual report contents.

In `@crates/pii-redaction/tests/worker_detection_tests.rs`:
- Around line 54-58: Remove the duplicate “/message” selector from either
target_paths or target_path_patterns in the test configuration, keeping it in
only one collection so the test expresses a single intent.
- Around line 268-285: Update the fail-closed assertion in the worker exit test
around the event emitted by “worker-pii-exit” to use a message value that the
healthy fixture worker does not redact, while retaining the expected redaction
for “unselected” if applicable. Ensure the assertion can only pass when the
crashed batch is handled fail-closed, rather than matching normal “PRIVATE”
detection behavior.

In `@crates/pii-redaction/workers/rampart/README.md`:
- Around line 134-136: Update the Runtime Bounds section in the README by adding
a complete introductory sentence before the existing bullet list; leave the
documented limits unchanged and ensure the lead-in grammatically introduces the
list.

In `@docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx`:
- Line 168: Rename the “Register worker inference” heading to “Register Worker
Inference” in docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx at
lines 168-168 and docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx
at lines 71-71, preserving the existing heading structure.

In `@docs/configure-plugins/pii-redaction/configuration.mdx`:
- Around line 424-431: The omission guidance following the “Path Semantics”
section is outdated. Update the paragraph describing manual LLM calls with
normalized target_paths and no active or fallback codec to state that payloads
are sanitized using the configured raw paths and emitted, matching the
early-return behavior in builtin and local redaction flows; preserve the
documented fail-closed contract.
- Around line 329-331: Add a complete introductory sentence immediately before
the TOML code block following the sanitizer registration-rejection paragraph,
clearly describing what the configuration example demonstrates. Keep the
existing TOML content unchanged.

In `@go/nemo_relay/pii_redaction/pii_redaction_test.go`:
- Around line 47-62: Extend the validation condition in the NewComponentSpec
test to assert that spec.Config.Local.Backend matches the configured backend
value from the test setup. Keep the existing configuration assertions unchanged
and include the backend check alongside the other Local fields.

In `@python/plugin/README.md`:
- Around line 108-125: Update the Worker Inference example to establish that ctx
is a PluginContext available inside WorkerPlugin.register, either by showing the
enclosing register method or explicitly stating that scope. Keep the
register_worker_inference usage and handler behavior unchanged.

---

Outside diff comments:
In `@crates/worker/src/lib.rs`:
- Around line 676-705: Merge push_registration into push_contract_registration
by giving push_contract_registration priority and break_chain parameters, with
contract supplied as the final argument and defaulting to an empty string where
appropriate. Remove the duplicate helper, update all callers including
register_worker_inference to pass the unified arguments, and preserve existing
Registration field values.
🪄 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: b6f7fc1b-5518-49b8-af16-dd1759b6fa64

📥 Commits

Reviewing files that changed from the base of the PR and between 42c7655 and 7541bec.

📒 Files selected for processing (60)
  • crates/cli/src/server/mod.rs
  • crates/core/src/lib.rs
  • crates/core/src/plugin.rs
  • crates/core/src/plugin/dynamic/host.rs
  • crates/core/src/plugin/dynamic/worker.rs
  • crates/core/src/plugin/worker_inference.rs
  • crates/core/tests/fixtures/worker_plugin/src/main.rs
  • crates/core/tests/integration/worker_plugin_tests.rs
  • crates/core/tests/unit/dynamic_worker_tests.rs
  • crates/core/tests/unit/plugin_tests.rs
  • crates/core/tests/unit/worker_inference_tests.rs
  • crates/node/pii_redaction.d.ts
  • crates/node/pii_redaction.js
  • crates/node/tests/pii_redaction_tests.mjs
  • crates/pii-redaction/Cargo.toml
  • crates/pii-redaction/README.md
  • crates/pii-redaction/src/builtin.rs
  • crates/pii-redaction/src/component.rs
  • crates/pii-redaction/src/local.rs
  • crates/pii-redaction/tests/unit/component_tests.rs
  • crates/pii-redaction/tests/unit/local_tests.rs
  • crates/pii-redaction/tests/worker_detection_tests.rs
  • crates/pii-redaction/workers/rampart/MANIFEST.in
  • crates/pii-redaction/workers/rampart/README.md
  • crates/pii-redaction/workers/rampart/THIRD_PARTY_NOTICES.md
  • crates/pii-redaction/workers/rampart/config.schema.json
  • crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/__init__.py
  • crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/detector.py
  • crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/prefetch.py
  • crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/py.typed
  • crates/pii-redaction/workers/rampart/nemo_relay_pii_rampart/worker.py
  • crates/pii-redaction/workers/rampart/pyproject.toml
  • crates/pii-redaction/workers/rampart/relay-plugin.toml
  • crates/pii-redaction/workers/rampart/tests/test_detector.py
  • crates/pii-redaction/workers/rampart/tests/test_worker.py
  • crates/worker-proto/README.md
  • crates/worker-proto/proto/nemo/relay/worker/v1/plugin_worker.proto
  • crates/worker-proto/tests/proto_tests.rs
  • crates/worker/README.md
  • crates/worker/src/lib.rs
  • crates/worker/tests/worker_sdk_tests.rs
  • docs/about-nemo-relay/release-notes/index.mdx
  • docs/build-plugins/dynamic-plugins/grpc-worker/grpc-worker-protocol.mdx
  • docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx
  • docs/build-plugins/dynamic-plugins/grpc-worker/rust/about.mdx
  • docs/configure-plugins/pii-redaction/about.mdx
  • docs/configure-plugins/pii-redaction/configuration.mdx
  • go/nemo_relay/pii_redaction.go
  • go/nemo_relay/pii_redaction/pii_redaction.go
  • go/nemo_relay/pii_redaction/pii_redaction_test.go
  • go/nemo_relay/pii_redaction_test.go
  • justfile
  • python/nemo_relay/pii_redaction.py
  • python/nemo_relay/pii_redaction.pyi
  • python/plugin/README.md
  • 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_pii_redaction_plugin.py

Comment thread crates/pii-redaction/src/component.rs Outdated
Comment thread crates/pii-redaction/src/local.rs Outdated
Comment thread crates/pii-redaction/tests/unit/component_tests.rs Outdated
Comment thread crates/pii-redaction/tests/worker_detection_tests.rs Outdated
Comment thread crates/pii-redaction/tests/worker_detection_tests.rs Outdated
Comment thread docs/build-plugins/dynamic-plugins/grpc-worker/python/about.mdx Outdated
Comment thread docs/configure-plugins/pii-redaction/configuration.mdx Outdated
Comment thread docs/configure-plugins/pii-redaction/configuration.mdx Outdated
Comment thread go/nemo_relay/pii_redaction/pii_redaction_test.go Outdated
Comment thread python/plugin/README.md Outdated
Signed-off-by: Alex Fournier <afournier@nvidia.com>

@ericevans-nv ericevans-nv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You could refine local_model to resolve an integration-provided callback. The PII middleware would execute that callback with the selected text and detector settings, then receive the detected spans, labels, and confidence scores. The integration could implement the callback using any model, runtime, or transport it chooses, while the PII component continues to own field selection, detection validation, policy, and redaction.

Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv
afourniernv marked this pull request as draft July 30, 2026 02:17
@afourniernv

Copy link
Copy Markdown
Contributor Author

Moving this back to draft and holding it for 0.8. The current single-flight design avoids blocking Tokio executor threads, but concurrent subagents, parallel tool calls, or agent fan-out within one Relay runtime share a Rampart admission slot and can trigger fail-closed fallback. I want to do the shared-runtime concurrency design and validation properly before asking to land this.

@afourniernv afourniernv modified the milestones: 0.7, 0.8 Jul 30, 2026
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	crates/core/src/api/llm.rs
#	crates/core/src/api/runtime.rs
#	crates/core/src/api/runtime/continuation_context.rs
#	crates/core/src/api/runtime/state.rs
#	crates/core/src/api/runtime/subscriber_dispatcher.rs
#	crates/core/src/api/tool.rs
#	crates/core/src/plugin/dynamic/native.rs
#	crates/core/tests/integration/middleware_tests.rs
#	crates/core/tests/integration/pipeline_tests.rs
#	crates/core/tests/integration/subscriber_dispatcher_tests.rs
#	crates/core/tests/unit/continuation_context_tests.rs
#	crates/core/tests/unit/dynamic_worker_tests.rs
#	crates/core/tests/unit/native_plugin_tests.rs
#	crates/core/tests/unit/subscriber_dispatcher_tests.rs
#	crates/node/src/api/mod.rs
#	crates/node/src/callback_factory.rs
#	crates/node/src/promise_call.rs
#	crates/node/tests/llm_tests.mjs
#	crates/node/tests/tools_tests.mjs
#	crates/plugin/src/lib.rs
#	crates/plugin/tests/typed_callbacks.rs
#	crates/python/src/py_callable.rs
#	docs/about-nemo-relay/concepts/middleware.mdx
#	docs/build-plugins/dynamic-plugins/native-dynamic/about.mdx
#	docs/reference/event-sanitizers.mdx
#	docs/reference/migration-guides.mdx
#	python/tests/test_llm.py
@afourniernv
afourniernv changed the base branch from wkk_relay-509-async-primary-middleware to main July 31, 2026 03:07
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv

Copy link
Copy Markdown
Contributor Author

/ok to test 9639f29

Signed-off-by: Alex Fournier <afournier@nvidia.com>

# Conflicts:
#	Cargo.toml
@afourniernv

Copy link
Copy Markdown
Contributor Author

/ok to test 1e37b71

@afourniernv

Copy link
Copy Markdown
Contributor Author

Current-head production benchmark

I reran the Rampart validation on commit 6cdd0ed9 using a release build on an Apple M4 Pro (14 logical CPUs, 48 GiB RAM) and the exact pinned, hash-verified Q4 model snapshot. The benchmark harness and model files were temporary and were not added to the branch.

Reliability and executor behavior

  • 13,056 managed calls and 26,192 emitted events across 132 repeated runs
  • Zero application errors, callback payload mutations, or privacy-canary leaks
  • Zero unexpected fail-closed bodies in the normal workloads
  • Activation averaged 105.26 ms, with a 101.70-117.03 ms range
  • Scenario throughput coefficient of variation was 0.29%-0.79%
  • A one-worker Tokio runtime matched the four-worker result
  • Normal-workload heartbeat p99 stayed below 2.1 ms
  • Saturating Tokio's blocking pool did not stall inference, which confirms that the dedicated Rayon pool is isolated from both Tokio executor workers and spawn_blocking

Agent-shaped workloads

These timings include request and response sanitization plus simulated provider and tool delay.

Workload Throughput p50 p95 p99
Sequential request/response 18.74 calls/s 26.90 ms 109.46 ms 110.27 ms
Codex-style parallel tools 32.14 calls/s 47.67 ms 112.76 ms 114.25 ms
Claude-style agent fan-out 43.64 calls/s 222.55 ms 285.56 ms 299.17 ms

A 30-call provider lifecycle run completed without event loss at 24.25 ms p99. An 80-call streaming run, 32 mark events, and 64 manual tool lifecycle events also completed without failures or canary leaks.

Selected-field size scaling

This uses a complete managed tool lifecycle, so each call sanitizes both input and output.

Selected text per body Throughput p50 p95 p99
64 B 122.21 calls/s 8.10 ms 8.82 ms 9.68 ms
256 B 45.02 calls/s 22.17 ms 22.59 ms 23.78 ms
1 KiB 11.55 calls/s 86.47 ms 88.72 ms 88.72 ms
8 KiB 1.10 calls/s 911.25 ms 920.86 ms 920.86 ms
16 KiB 0.52 calls/s 1,913.43 ms 1,973.91 ms 1,973.91 ms

Admission stayed lossless through 16 concurrent 1 KiB calls and through six concurrent 8 KiB calls. At deliberate overload, 48 of 144 1 KiB bodies failed closed at concurrency 24, and 36 of 72 8 KiB bodies failed closed at concurrency 12. That is the intended bounded-admission behavior: application calls still complete, while affected observable bodies are omitted rather than queued without limit.

Sustained saturation

A 1,200-call soak completed in 62.04 seconds at 19.34 calls/s:

  • Zero application errors and zero privacy-canary leaks
  • 192 of 2,400 observable bodies failed closed under sustained CPU saturation
  • Heartbeat p99 1.82 ms, max 8.97 ms
  • Max RSS 247.7 MB
  • 183.64 seconds user CPU over 62.17 seconds real time, approximately 2.95 CPU cores

The normal Claude-shaped run peaked at 86.6 MB RSS; deliberate saturation peaked around 245-248 MB.

Redaction quality

I evaluated 210 balanced English, Spanish, French, German, Italian, Portuguese, and Dutch rows from the AI4Privacy PII Masking Nano 1K dataset, using the pinned Rampart model:

  • 1,124 of 1,128 annotated sensitive spans redacted: 99.645%
  • Wilson 95% interval: 99.092%-99.862%
  • 460 of 460 intentionally unmodeled public terms retained
  • Four misses: one SOCIALNUM, two CITY spans, and one questionable BUILDINGNUM annotation for 21 in 21 petals

This is not a replacement for the model author's evaluation, but it exercises the exact Relay preprocessing, offset projection, policy, and replacement path.

Conclusion and remaining limits

The current design is production-defensible as an opt-in sanitizer for narrow, explicitly selected observability fields, especially at or below roughly 1 KiB. The dedicated bounded Rayon pool prevents Tokio executor starvation and avoids coupling inference to Tokio's blocking pool. Admission, CPU use, and memory growth remain bounded, and overload removes observable content without changing the application payload.

The main limits are also visible in the numbers: 8-16 KiB selected fields add second-scale latency, sustained CPU saturation can intentionally fail closed, and pressure runs reach roughly 248 MB RSS. I would document/select conservative payload limits rather than position this as a generic large-field sanitizer. Linux x86_64 and aarch64 performance validation also remains outstanding before making broader platform claims.

Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
Signed-off-by: Alex Fournier <afournier@nvidia.com>
@afourniernv
afourniernv marked this pull request as ready for review August 3, 2026 17:58
@afourniernv

Copy link
Copy Markdown
Contributor Author

/ok to test 121f563

@willkill07

Copy link
Copy Markdown
Member

/ok to test 207fa57

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

Labels

Feature a new feature lang:go PR changes/introduces Go code lang:js PR changes/introduces Javascript/Typescript code lang:python PR changes/introduces Python code lang:rust PR changes/introduces Rust code size:XXL PR is very large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants