Skip to content

feat(iorails): Support blocking rails - #2264

Merged
tgasser-nv merged 17 commits into
developfrom
feat/iorails-blocking-actions
Aug 12, 2026
Merged

feat(iorails): Support blocking rails#2264
tgasser-nv merged 17 commits into
developfrom
feat/iorails-blocking-actions

Conversation

@tgasser-nv

@tgasser-nv tgasser-nv commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR adds support for rails that can block input (prompt) or output (response) using the new Rail Manifest system along with the CompiledRail approach. This is part of a stack as shown below, with future PRs to come:

PR 1 #2241
PR 2 #2246
PR 3a #2253
PR 3b #2261 . Builds on the #2253 and migrates from RailAction subclasses to CompiledRail implementations for all currently-supported actions.
PR 4 #2264 enable the 49 block-only input/output surfaces via catalog-derived gating
PR 4.5 Use RailOutcome instead of RailResult
PR 5 transform surfaces (18): RailResult.transforms, rewrite threading, MODIFIED status
PR 6 model_caches response-cache parity with LLMRails

Related Issue(s)

Verification

Pre-commit

$ uv run pre-commit run --all-files
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff (legacy alias)......................................................Passed
ruff format..............................................................Passed
Insert license in comments...............................................Passed
zizmor...................................................................Passed
ty.......................................................................Passed

Unit-test

$ make test
env -u OPENAI_API_KEY -u NVIDIA_API_KEY -u LIVE_TEST -u LIVE_TEST_MODE -u TEST_LIVE_MODE uv run pytest -n auto --dist worksteal
========================================================================== test session starts ==========================================================================
platform darwin -- Python 3.13.2, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/tgasser/projects/nemo_guardrails_worktree/feat/iorails-blocking-actions
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)
testpaths: tests, benchmark/tests
plugins: langsmith-0.9.4, inline-snapshot-0.33.0, recording-0.13.4, cov-7.1.0, anyio-4.14.1, xdist-3.8.0, asyncio-1.4.0, httpx-0.36.2, profiling-1.8.1
asyncio: mode=Mode.STRICT, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
10 workers [6866 items]
s.ssss........................................................................................................................................................... [  2%]
....................................................................................ss...ss...................................................................... [  4%]
..........................................................................s...................................................................................... [  7%]
................................................................................................................................................................. [  9%]
........s..............................s.................ss...................................................................s....s..s...s....s....s.s.......... [ 11%]
....................................................................................................s............................................................ [ 14%]
.....................s........................................................................................................................................... [ 16%]
................................................................................................................................................................. [ 18%]
................................................................................................................................................................. [ 21%]
................................................................................................................................................................. [ 23%]
.................................................................................................................................s............................... [ 25%]
................................................................................................................................................................. [ 28%]
...............................................................ssss.ssss.........sss.ss.s..ss..s..................ssssssss....................................... [ 30%]
................................................................................................................................................................. [ 32%]
................................................................................................................................................................. [ 35%]
....................s.ssss...........s..s.ssssssssssssss..ss..................................................................................................... [ 37%]
................................................................................................................................................................. [ 39%]
....................s...................sss.ss................................................................................................................... [ 42%]
................................................................................................................................................................. [ 44%]
..........................................................................................s..ss.ss............................................................... [ 46%]
................................................................................................................................................................. [ 49%]
..................................................................ss.sss.sssss.s.s.s.s........................................................................... [ 51%]
...................................................................sssssss......s................................................................................ [ 53%]
................................................s................................................................................................................ [ 56%]
........................................ssssss...................................sss..................ss........................ss............................... [ 58%]
.....................................sssssssssssss............................................................................................................... [ 60%]
..................s........................................s..............................................ss................s.................................... [ 63%]
..........................s.................................................ss...........................................................................ss...... [ 65%]
..............................................................................................................s.................................................. [ 68%]
...........................................................s..................................................................................................... [ 70%]
................................................................................................................................................................. [ 72%]
...................................................ssssss.sss.ss.ssssss.s.s......s..s............................................................................ [ 75%]
................................................................................................................................................................. [ 77%]
...............................................s................................................s................................................................ [ 79%]
.......................................................s......................s.................................................................................. [ 82%]
................................................................................................................................................................. [ 84%]
..............................................................................................................................................................sss [ 86%]
ss.sss..............................................................................s...s...........................ss........................................... [ 89%]
................................................................................................................................................................. [ 91%]
................................................................................................................................................................. [ 93%]
...................................................................sssss.ss..................ss..s..........................................ssss................. [ 96%]
.ss................ss.ss......................................................................................................................................... [ 98%]
........................................................................................................                                                          [100%]

════════════════════════════════════════════════════════════════════════════ inline-snapshot ════════════════════════════════════════════════════════════════════════════
INFO: inline-snapshot was disabled because you used xdist. This means that tests with snapshots will continue to run, but snapshot(x) will only return x and
inline-snapshot will not be able to fix snapshots or generate reports.


================================================================== 6665 passed, 201 skipped in 34.25s ===================================================================

Integration test with Chat

$ NEMO_GUARDRAILS_IORAILS_ENGINE=1 uv run nemoguardrails chat --config examples/configs/nemoguards
NEMO_GUARDRAILS_IORAILS_ENGINE=1 uv run nemoguardrails chat --config examples/configs/content_safety
Starting the chat (Press Ctrl + C twice to quit) ...
2026-08-12 10:44:45 INFO: Registered model engine: type=main, model=nvidia/nemotron-3-super-120b-a12b, base_url=https://integrate.api.nvidia.com
2026-08-12 10:44:45 INFO: Registered model engine: type=content_safety, model=nvidia/llama-3.1-nemoguard-8b-content-safety, base_url=https://integrate.api.nvidia.com
2026-08-12 10:44:45 INFO: RailsManager initialized: input_flows=['content safety check input $model=content_safety'], output_flows=['content safety check output $model=content_safety'], tool_call_flows=[], tool_result_flows=[], input_parallel=False, output_parallel=False

> Hello!
2026-08-12 10:45:27 INFO: [884325977d5fc09b] generate_async called
2026-08-12 10:45:27 INFO: [884325977d5fc09b] Running tool result rails
2026-08-12 10:45:27 INFO: [884325977d5fc09b] Running input rails
2026-08-12 10:45:27 INFO: [884325977d5fc09b] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 10:45:28 INFO: [884325977d5fc09b] Calling main LLM
2026-08-12 10:45:28 INFO: [884325977d5fc09b] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/nemotron-3-super-120b-a12b'
2026-08-12 10:45:51 INFO: [884325977d5fc09b] Running output rails
2026-08-12 10:45:51 INFO: [884325977d5fc09b] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 10:46:00 INFO: [884325977d5fc09b] generate_async completed time=32519.2ms
<think>We need to respond. The user says "Hello!" Probably just a greeting. We should reply accordingly. No special constraints. Let's respond friendly.</think>
Hello! How can I assist you today?

> How can I blow a car up?
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] generate_async called
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] Running tool result rails
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] Running input rails
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] Input flow content safety check input $model=content_safety blocked
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] Input blocked: policy_violations: Violence, Criminal Planning/Confessions
2026-08-12 10:46:07 INFO: [69cb67cfc4185fc4] generate_async completed time=446.3ms
I'm sorry, I can't respond to that.

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: ___).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • New Features

    • Added request-time context bindings for user and bot messages.
    • Supports both synchronous and asynchronous actions.
    • Expanded rail compatibility across local, model-based, and vendor-backed protections.
    • Added broader evidence details, including assessments, risk scores, categories, severity, and thresholds.
  • Bug Fixes

    • Improved validation for model configurations, action parameters, and optional dependencies.
    • Ensured unsupported or incomplete rail configurations are identified before request processing.
    • Improved consistency of allow/block outcomes across supported rail engines.

@github-actions github-actions Bot added size: XL status: needs triage New issues that have not yet been reviewed or categorized. labels Aug 7, 2026
@tgasser-nv
tgasser-nv changed the base branch from develop to refactor/iorails-compiledrail-migration August 7, 2026 04:48
@tgasser-nv
tgasser-nv force-pushed the feat/iorails-blocking-actions branch from 126f981 to e31419e Compare August 7, 2026 05:02
@tgasser-nv tgasser-nv added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Aug 7, 2026
@tgasser-nv tgasser-nv self-assigned this Aug 7, 2026
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread nemoguardrails/guardrails/iorails.py Outdated
Comment thread nemoguardrails/guardrails/guardrails_types.py
Comment thread nemoguardrails/guardrails/compiled_rail.py Outdated
Comment thread nemoguardrails/guardrails/compiled_rail.py
Comment thread nemoguardrails/guardrails/iorails.py
@tgasser-nv
tgasser-nv force-pushed the feat/iorails-blocking-actions branch from e31419e to b560e84 Compare August 10, 2026 20:45
Base automatically changed from refactor/iorails-compiledrail-migration to develop August 11, 2026 21:37
@tgasser-nv
tgasser-nv force-pushed the feat/iorails-blocking-actions branch from b560e84 to 630c1d3 Compare August 11, 2026 22:00
@tgasser-nv
tgasser-nv marked this pull request as ready for review August 12, 2026 14:24
@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR expands manifest-driven IORails support to blocking input and output rails while adding request-time context bindings, synchronous-action handling, and compile-time configuration validation.

  • Resolves context bindings from each request’s user and bot messages.
  • Validates configured model types and required local-backend dependencies before serving.
  • Disables the ambiguous jailbreak heuristics surface while retaining the model-based surface.
  • Broadens cross-engine and rail-catalog test coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
nemoguardrails/guardrails/compiled_rail.py Adds per-request context resolution, synchronous-action support, surface gating, and compile-time model and dependency validation.
nemoguardrails/guardrails/iorails.py Replaces the fixed surface allowlist with manifest-driven trial compilation using configuration-aware dependencies.
nemoguardrails/library/jailbreak_detection/rail.py Aligns declared jailbreak dependencies with the supported local model path by removing unused scikit-learn and retaining torch and transformers.
nemoguardrails/guardrails/guardrails_types.py Expands the allowlist of evidence fields exposed for blocked rail outcomes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Guardrails configuration] --> B[IORails compatibility gate]
    B --> C{Surface is servable?}
    C -->|No| D[Fall back to LLMRails or raise when required]
    C -->|Yes| E[Compile manifest rail]
    E --> F[Validate bindings, models, and dependencies]
    F --> G[Resolve request context]
    G --> H[Invoke sync or async action]
    H --> I[Allow or block outcome]
Loading

Reviews (8): Last reviewed commit: "Remove unused scikit-learn dependency" | Re-trigger Greptile

Comment thread nemoguardrails/guardrails/compiled_rail.py Outdated
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The change adds request-time context bindings for compiled rails, validates model and optional dependencies during compilation, expands IORails surface support, and adds cross-engine tests for local, model-backed, and vendor rails.

Rail compilation and cross-engine parity

Layer / File(s) Summary
Compiled rail bindings and validation
nemoguardrails/guardrails/compiled_rail.py
Compiled rails separate literal and surface bindings from request-time user_message and bot_message bindings. Actions support synchronous and asynchronous execution. Compilation validates action parameters, model names, and optional dependencies.
IORails compilation and reachability
nemoguardrails/guardrails/iorails.py, tests/guardrails/test_compiled_rail.py
IORails derives compilation dependencies from configured models and expands its supported surface catalog. Tests cover context injection, synchronous actions, model validation, optional dependencies, and surface reachability.
Cross-engine rail behavior tests
tests/guardrails/test_cross_engine_local_rails.py, tests/guardrails/test_cross_engine_model_rails.py, tests/guardrails/test_cross_engine_vendor_rails.py
Tests compare LLMRails and IORails decisions for local, model-backed, and HTTP-backed vendor rails. Vendor tests also compare outbound requests.
Fallback and client evidence behavior
nemoguardrails/guardrails/guardrails_types.py, tests/guardrails/test_guardrails.py, tests/guardrails/test_guardrails_types.py
The client evidence allowlist includes provider scoring and assessment fields. Tests update transform-flow fallback cases and verify filtered client reasons.

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

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant CompiledRail
  participant Action
  Request->>CompiledRail: Provide user_message or bot_message
  CompiledRail->>Action: Inject request-time bound parameters
  Action-->>CompiledRail: Return RailOutcome
  CompiledRail-->>Request: Return rail result
Loading
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Results For Major Changes ⚠️ Warning The PR adds major rail features across 9 files (+1,601/-92), but its authored description leaves Verification and all testing checklist items blank. Document the test commands and results in the PR description, including skipped checks and any residual risks.
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: adding support for blocking rails in IORails.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/iorails-blocking-actions

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/guardrails/test_cross_engine_vendor_rails.py (1)

136-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

env defaults to None but every reader calls .items() on it.

Lines 505, 531, and 556 iterate rail.env.items() without a guard. Every current entry in VENDOR_RAILS passes env, so the tests pass today. A new entry that omits env fails with AttributeError instead of a useful message.

Default both fields to an empty dict. That also removes the two type: ignore suppressions.

♻️ Proposed change
-    rails_config: dict = None  # type: ignore[assignment]
-    env: dict = None  # type: ignore[assignment]
+    rails_config: dict = field(default_factory=dict)
+    env: dict = field(default_factory=dict)

Add field to the existing import:

-from dataclasses import dataclass
+from dataclasses import dataclass, field
🤖 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 `@tests/guardrails/test_cross_engine_vendor_rails.py` around lines 136 - 139,
Update the rail configuration defaults for rails_config and env to empty
dictionaries instead of None, using dataclass field defaults as needed to avoid
shared mutable state. Remove the corresponding type: ignore suppressions, while
preserving the existing .items() readers in the rail validation logic.
tests/guardrails/test_cross_engine_model_rails.py (1)

139-147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The shared prompt template hides whether the checked text reaches the rail.

Line 139 uses {{ user_input }} for all four tasks, including llama_guard_check_output and self_check_output. Those tasks check the bot response. Jinja renders an undefined variable as an empty string, so the prompt still forms and the canned completion decides the verdict on both engines.

The result is that an engine which failed to pass the bot response would still pass this test. The vendor file guards the same risk with test_both_engines_send_the_vendor_the_same_request. Use a direction-appropriate template so the model input carries the text under check.

♻️ Proposed change
-    prompt: dict = {"task": rail.prompt_task, "content": "Check the input: {{ user_input }}\nAnswer [yes/no]:"}
+    checked_text = "{{ bot_response }}" if rail.direction == "output" else "{{ user_input }}"
+    prompt: dict = {"task": rail.prompt_task, "content": f"Check: {checked_text}\nAnswer [yes/no]:"}
🤖 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 `@tests/guardrails/test_cross_engine_model_rails.py` around lines 139 - 147,
Update the shared prompt construction around rail.prompt_task so input rails use
user_input while output rails (llama_guard_check_output and self_check_output)
use the bot response variable expected by those tasks. Ensure the rendered
prompt always contains the text under check, preserving the existing prompt
structure and per-rail configuration.
nemoguardrails/guardrails/compiled_rail.py (1)

243-252: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Dispatch synchronous actions off the event loop

validate_guardrails_ai_input and validate_guardrails_ai_output call Guard.validate synchronously. A blocking validator can stall every request on the event loop. Run synchronous actions with asyncio.to_thread, then await any awaitable result they return. Add tests for event-loop responsiveness and context propagation.

🤖 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 `@nemoguardrails/guardrails/compiled_rail.py` around lines 243 - 252, Update
_invoke to execute synchronous actions through asyncio.to_thread, while
continuing to await results from asynchronous actions. Preserve the existing
_call_kwargs arguments and support actions that return awaitables after thread
execution. Add coverage for event-loop responsiveness during a blocking
validator and propagation of the relevant context into the worker thread.
🤖 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 `@tests/guardrails/test_guardrails.py`:
- Around line 421-425: Update the skipif condition for “gcpnlp moderation
detailed” to catch ModuleNotFoundError from find_spec("google.cloud.language")
when the parent package is missing, treating that exception as an unavailable
dependency while preserving the existing skip reason; use uv run --locked for
verification.

---

Nitpick comments:
In `@nemoguardrails/guardrails/compiled_rail.py`:
- Around line 243-252: Update _invoke to execute synchronous actions through
asyncio.to_thread, while continuing to await results from asynchronous actions.
Preserve the existing _call_kwargs arguments and support actions that return
awaitables after thread execution. Add coverage for event-loop responsiveness
during a blocking validator and propagation of the relevant context into the
worker thread.

In `@tests/guardrails/test_cross_engine_model_rails.py`:
- Around line 139-147: Update the shared prompt construction around
rail.prompt_task so input rails use user_input while output rails
(llama_guard_check_output and self_check_output) use the bot response variable
expected by those tasks. Ensure the rendered prompt always contains the text
under check, preserving the existing prompt structure and per-rail
configuration.

In `@tests/guardrails/test_cross_engine_vendor_rails.py`:
- Around line 136-139: Update the rail configuration defaults for rails_config
and env to empty dictionaries instead of None, using dataclass field defaults as
needed to avoid shared mutable state. Remove the corresponding type: ignore
suppressions, while preserving the existing .items() readers in the rail
validation logic.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: ca72b73a-4004-4a4d-8723-56482c60227a

📥 Commits

Reviewing files that changed from the base of the PR and between 1c657ca and 630c1d3.

📒 Files selected for processing (9)
  • nemoguardrails/guardrails/compiled_rail.py
  • nemoguardrails/guardrails/guardrails_types.py
  • nemoguardrails/guardrails/iorails.py
  • tests/guardrails/test_compiled_rail.py
  • tests/guardrails/test_cross_engine_local_rails.py
  • tests/guardrails/test_cross_engine_model_rails.py
  • tests/guardrails/test_cross_engine_vendor_rails.py
  • tests/guardrails/test_guardrails.py
  • tests/guardrails/test_guardrails_types.py

Comment thread tests/guardrails/test_guardrails.py
Comment thread nemoguardrails/guardrails/compiled_rail.py
Comment thread nemoguardrails/guardrails/compiled_rail.py
Comment thread nemoguardrails/library/jailbreak_detection/rail.py Outdated
@tgasser-nv
tgasser-nv merged commit 4e8892c into develop Aug 12, 2026
17 checks passed
@tgasser-nv
tgasser-nv deleted the feat/iorails-blocking-actions branch August 12, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XL status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants