Skip to content

refactor(iorails): Migrate from RailAction subclasses to CompiledRail manifest implementation - #2261

Open
tgasser-nv wants to merge 11 commits into
feat/iorails-compiled-railfrom
refactor/iorails-compiledrail-migration
Open

refactor(iorails): Migrate from RailAction subclasses to CompiledRail manifest implementation#2261
tgasser-nv wants to merge 11 commits into
feat/iorails-compiled-railfrom
refactor/iorails-compiledrail-migration

Conversation

@tgasser-nv

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

Copy link
Copy Markdown
Collaborator

Description

This PR is the fourth in the stack of migrating IORails to use the new Manifest-based action system. Leading up to this PR, a CompiledRail abstraction was created to take a RailSurface and make it executable by IORails. This PR migrates the nemoguards actions that IORails already supports (content-safety, topic-control, and jailbreak) from RailAction subclasses to use the new manifest + CompiledRail wrapper approach. The switchover is seamless and backwards-compatible.

PR Stack context

The PR3 mentioned in previous PRs in the stack was split into a PR3a and PR3b since it was getting too large to review. Each PR in the list is self-contained, and will be merged one-by-one to avoid a lot of rebasing work once all are improved.

Related Issue(s)

Verification

Test Plan

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

============================================================ test session starts =============================================================
platform darwin -- Python 3.13.2, pytest-9.1.1, pluggy-1.6.0
rootdir: /Users/tgasser/projects/nemo_guardrails_worktree/refactor/iorails-compiledrail-migration
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 [6514 items]
.s.........s...........s...s.......................................................................................................... [  2%]
.................................................................................s....sss.s........................................... [  4%]
...................................................................................................................................... [  6%]
...................................................................................................................................... [  8%]
...................................................................................................................................... [ 10%]
......................................................................ss...s.sss.ss...............................s..ss.ss...s........ [ 12%]
ss........s...........................................................ss....ssssss.................................................... [ 14%]
.............................................................................................................s........................ [ 16%]
...................................................................................................................................... [ 18%]
...................................................................................................................................... [ 20%]
...................................................................................................................................... [ 22%]
...................................................................................................................................... [ 24%]
...................................................................................................................................... [ 26%]
...................................................................................................................................... [ 28%]
...................................................................................................................................... [ 30%]
...................................................................................................................................... [ 32%]
...................................................................................................................................... [ 34%]
........s.................s........s...s.............................ssss.s...........s.ssssss.sss.ssss.ssss...ss..s.................. [ 37%]
...................................................................................................................................... [ 39%]
..................................................................................................................s.ssss.............. [ 41%]
...................................................................................................................................... [ 43%]
...................................................................................................................................... [ 45%]
...................................................................................................................................... [ 47%]
.....................................................................s................s............................................... [ 49%]
....................................................................................sssss............................................. [ 51%]
...................................................................................................................................... [ 53%]
.........s..........................................s................................................................................. [ 55%]
...................................................................................................................................... [ 57%]
...................................s.s.sss.s.......................................................................................... [ 59%]
............................................................................s......................................................... [ 61%]
..................ss.s..s.................s.........................................................ss............s...........ss...... [ 63%]
.......s...............s....................................................................................................sssssssss. [ 65%]
ssss..........s....................................................................................................................... [ 67%]
...................................................................................................................................... [ 69%]
....................................ss.........................................s...................................................... [ 71%]
...........ss....................................................ss...............................sssssss.............ssssssss........ [ 74%]
...................s.................................................................................................................. [ 76%]
.....................................................................s................................................................ [ 78%]
.........................................................................sssss.ssss..ssss.sssss.s..................................... [ 80%]
.........................ssssss.ss.................................................................................................... [ 82%]
...................................................................................................................................... [ 84%]
................................................ss.................................................................................... [ 86%]
......................................................s............................................................................... [ 88%]
...................................................................................................................................... [ 90%]
...........................................s.......................................................................................... [ 92%]
..............................................................................................s....................................... [ 94%]
.....................................................................ss............................................................... [ 96%]
...s.............sss.ssss............................................................................................................. [ 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.


===================================================== 6336 passed, 178 skipped in 46.94s =====================================================


Integration test with Chat

This uses all nemoguards actions affected by the migration in this PR, so this tests live end-to-end to make sure the migration was successful.

$ NEMO_GUARDRAILS_IORAILS_ENGINE=1 uv run nemoguardrails chat --config examples/configs/nemoguards

Starting the chat (Press Ctrl + C twice to quit) ...
2026-08-06 11:30:19 INFO: Registered model engine: type=main, model=nvidia/nemotron-3-nano-30b-a3b, base_url=https://integrate.api.nvidia.com
2026-08-06 11:30:19 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-06 11:30:19 INFO: Registered model engine: type=topic_control, model=nvidia/llama-3.1-nemoguard-8b-topic-control, base_url=https://integrate.api.nvidia.com
2026-08-06 11:30:19 INFO: RailsManager initialized: input_flows=['content safety check input $model=content_safety', 'topic safety check input $model=topic_control', 'jailbreak detection model'], output_flows=['content safety check output $model=content_safety'], tool_call_flows=[], tool_result_flows=[], input_parallel=False, output_parallel=False

> Hello!
2026-08-06 11:30:21 INFO: [708037c8e6efa028] generate_async called
2026-08-06 11:30:21 INFO: [708037c8e6efa028] Running tool result rails
2026-08-06 11:30:21 INFO: [708037c8e6efa028] Running input rails
2026-08-06 11:30:21 INFO: [708037c8e6efa028] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-06 11:30:22 INFO: [708037c8e6efa028] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-topic-control'
2026-08-06 11:30:23 INFO: [708037c8e6efa028] Calling main LLM
2026-08-06 11:30:23 INFO: [708037c8e6efa028] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/nemotron-3-nano-30b-a3b'
2026-08-06 11:30:23 INFO: [708037c8e6efa028] Running output rails
2026-08-06 11:30:23 INFO: [708037c8e6efa028] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-06 11:30:24 INFO: [708037c8e6efa028] generate_async completed time=2724.5ms
<think>We need to respond to user greeting. No constraints. Just reply friendly.
</think>
Hello! How can I assist you today?

> How can burn a house down?
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] generate_async called
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] Running tool result rails
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] Running input rails
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] Input flow content safety check input $model=content_safety blocked
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] Input blocked: policy_violations: JSON parsing failed
2026-08-06 11:30:30 INFO: [8f1c9403d84b7023] generate_async completed time=205.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 clearer block-reason messages using policy evidence or triggered rail details.
    • Added validation and diagnostics for unsupported rail surfaces and flows.
    • Topic safety checks now honor task-specific token limits.
  • Bug Fixes

    • Jailbreak detector connection failures, timeouts, and error responses now fail open for both request and response processing.
    • Improved upstream HTTP error handling and status propagation.
  • Documentation

    • Documented detector availability and fail-open behavior.

@github-actions github-actions Bot added size: XL status: needs triage New issues that have not yet been reviewed or categorized. labels Aug 6, 2026
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@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 6, 2026
@tgasser-nv tgasser-nv self-assigned this Aug 6, 2026
@tgasser-nv
tgasser-nv changed the base branch from develop to feat/iorails-compiled-rail August 6, 2026 15:43
@tgasser-nv
tgasser-nv marked this pull request as ready for review August 6, 2026 16:45
@greptile-apps

greptile-apps Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR migrates supported IORails content-safety, topic-safety, and jailbreak checks from dedicated RailAction subclasses to manifest-backed CompiledRail execution.

  • Compiles configured rail surfaces into reusable execution plans with dependency injection and validation.
  • Preserves conversation ordering for topic-safety checks by separating prior history from the current user turn.
  • Aligns rail outcomes, telemetry, HTTP error handling, and server error envelopes with the manifest-based path.
  • Adds focused equivalence, compilation, streaming, telemetry, and end-to-end tests.

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 manifest-based compilation, dependency injection, surface validation, model-call capture, and a complete fix for current-turn duplication and historical-turn reordering.
nemoguardrails/guardrails/rails_manager.py Migrates supported input and output rails to CompiledRail execution and translates manifest outcomes into IORails results.
nemoguardrails/guardrails/iorails.py Integrates the compiled-rail manager and updated execution, logging, lifecycle, and error-handling behavior.
nemoguardrails/library/topic_safety/actions.py Reuses the manifest action for IORails, forwards configured token limits, and assembles prior history plus the checked user turn without duplication.
nemoguardrails/guardrails/rail_guard.py Centralizes action failure conversion and rail outcome handling for the new execution path.
nemoguardrails/server/exception_handlers.py Updates server-side exception translation and response-envelope handling for guardrail failures.

Sequence Diagram

sequenceDiagram
    participant Client
    participant IORails
    participant Manager as RailsManager
    participant Rail as CompiledRail
    participant Action as Manifest Action
    participant Model
    Client->>IORails: generate/check(messages)
    IORails->>Manager: run configured rails
    Manager->>Rail: execute(messages, bot_response)
    Rail->>Rail: build context and prior-turn events
    Rail->>Action: invoke injected manifest action
    Action->>Model: optional safety-model/API call
    Model-->>Action: verdict response
    Action-->>Rail: RailOutcome
    Rail-->>Manager: RailExecution
    Manager-->>IORails: RailResult
    IORails-->>Client: allowed, blocked, or error response
Loading

Reviews (8): Last reviewed commit: "Fix conversation-turn reordering bug" | Re-trigger Greptile

Comment thread nemoguardrails/library/topic_safety/actions.py
@tgasser-nv

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Review this PR

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@tgasser-nv I will review the changes in #2261.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR migrates rail execution to compiled rails, removes legacy rail actions and API-engine routing, centralizes surface validation, normalizes verdict display, updates HTTP error handling, and adds related tests and jailbreak fail-open documentation.

Changes

Compiled rail execution

Layer / File(s) Summary
Surface validation and execution
nemoguardrails/guardrails/compiled_rail.py, nemoguardrails/guardrails/rails_manager.py, nemoguardrails/guardrails/iorails.py
Rails now compile from manifest surfaces and execute through CompiledRail. Unsupported flows report descriptive reasons before action import.
Engine and error handling
nemoguardrails/guardrails/engine_registry.py, nemoguardrails/server/api.py, nemoguardrails/server/exception_handlers.py, nemoguardrails/guardrails/rail_guard.py
API-engine registration and routing were removed. HTTP client and provider statuses now use the shared exception handling path.
Verdict rendering and validation
nemoguardrails/guardrails/guardrails_types.py, tests/guardrails/test_guardrails_types.py, tests/guardrails/test_iorails_streaming.py
display_reason derives block text from explicit reasons, structured evidence, triggered rails, or a fallback value.
Test and transport migration
tests/guardrails/*, tests/server/*, tests/test_http_error_handling.py
Tests now use compiled-rail model mocks and jailbreak HTTP mocks. Coverage includes rail records, telemetry, compilation diagnostics, HTTP status propagation, and fail-open jailbreak behavior.
Topic safety and documentation
nemoguardrails/library/topic_safety/actions.py, tests/test_topic_safety_actions.py, docs/configure-rails/guardrail-catalog/jailbreak-protection.mdx
Topic safety uses task-specific token limits. Documentation states that unavailable jailbreak detectors allow requests without caller-visible errors.

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

Possibly related PRs

Suggested reviewers: pouyanpi

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Test Results For Major Changes ✅ Passed The description documents pre-commit results, 6,336 passed and 178 skipped unit tests, and an end-to-end Chat test; relevant test files exist in the repository.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: migrating IORails from RailAction subclasses to the CompiledRail manifest implementation.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/iorails-compiledrail-migration

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: 3

🧹 Nitpick comments (8)
nemoguardrails/guardrails/engine_registry.py (1)

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

Align all registry docstrings with the model-only contract.

Line 44 states that the registry manages model engines only. The following class and initializer text still states that it creates API engines and traces API calls. Remove those references.

Proposed documentation update
-    Creates one engine per configured model or API service, keyed by name.
+    Creates one engine per configured model, keyed by model type.

-        """Build one engine per configured model and API service.
+        """Build one engine per configured model.

-        When *tracer* is provided, LLM and API calls produce OTEL spans; when
+        When *tracer* is provided, LLM calls produce OTEL spans; when
🤖 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/engine_registry.py` at line 44, Update the registry
class and initializer docstrings near the model-only registry declaration to
remove references to creating API engines and tracing API calls. Keep the
documentation focused on managing ModelEngine instances for IORails,
consistently with the module docstring and model-only contract.

Source: Coding guidelines

tests/guardrails/test_compiled_rail.py (1)

373-382: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the deny-list cross-check bidirectional.

This test only verifies that each production deny-list entry exists in the catalog. It does not compare _RETRIEVAL_CONTEXT_SURFACES with RETRIEVAL_DEPENDENT_SURFACES. A new production refusal can therefore miss compilation coverage while this test still passes.

Assert exact equality between the two sets before the catalog-membership check.

🤖 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_compiled_rail.py` around lines 373 - 382, Update
test_the_deny_list_names_only_real_surfaces to assert exact equality between
_RETRIEVAL_CONTEXT_SURFACES and RETRIEVAL_DEPENDENT_SURFACES before checking
catalog membership. Preserve the existing missing-surface validation after this
bidirectional consistency check.
tests/guardrails/test_iorails_streaming.py (1)

1155-1157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use _assert_error_chunk here too.

This test indexes error_chunks[0] directly. If no error chunk is emitted, the test fails with an IndexError instead of a readable assertion. The output test at Line 1173 uses the _assert_error_chunk helper. Use the same helper, or assert the list is non-empty first.

🤖 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_iorails_streaming.py` around lines 1155 - 1157, Update
the test around _assert_error_chunk to use that helper instead of indexing
error_chunks[0] directly, preserving the existing expected error message
assertion while producing a readable failure when no error chunk is emitted.
nemoguardrails/guardrails/compiled_rail.py (2)

287-295: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale reference to the previous function name.

The comment in _bind_parameters still names _reject_unfillable_binding_kinds. This change renames the check to _unfillable_bindings_reason. Update that comment so the cross-reference resolves.

🤖 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 287 - 295, Update
the comment in _bind_parameters to reference _unfillable_bindings_reason instead
of the removed _reject_unfillable_binding_kinds name, leaving the surrounding
binding logic unchanged.

305-334: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Declare retrieval-context requirements in the manifest.

CompiledRail._request_dependencies injects only user and bot messages. A new action that reads retrieval context can therefore receive no evidence and still compile. Some such actions return an allow outcome when evidence is absent. Add a surface-level manifest requirement and derive unsupported_surface_reason from it instead of maintaining _RETRIEVAL_CONTEXT_SURFACES.

🤖 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 305 - 334, Replace
the hard-coded _RETRIEVAL_CONTEXT_SURFACES set and _retrieval_context_reason
lookup with a manifest-declared retrieval-context requirement on each applicable
rail surface. Update unsupported_surface_reason derivation to inspect that
manifest metadata, so surfaces whose actions require retrieval evidence are
rejected when CompiledRail._request_dependencies cannot provide it.
nemoguardrails/guardrails/rails_manager.py (1)

77-80: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Make the TRANSFORM case explicit in _rail_result.

RailOutcome carries three decisions. is_blocked is True only for BLOCK, so a TRANSFORM outcome maps to is_safe=True and outcome.transforms is dropped without a trace. Today _transform_target_reason in compiled_rail.py refuses every transform-capable surface at compile time, so this path is unreachable. That guard is documented as temporary ("transforms in PR 5"), and when it is lifted this mapping fails open silently.

Raise or log on a TRANSFORM decision so the gap surfaces at the moment the compile-time refusal is relaxed.

🛡️ Proposed defensive mapping
 def _rail_result(outcome: RailOutcome) -> RailResult:
     """Map an engine-neutral rail verdict onto IORails' rail result."""
+    if outcome.is_transform:
+        # Transform-capable surfaces are refused at compile time; reaching here means
+        # that refusal was lifted without teaching IORails to apply the rewrite.
+        raise NotImplementedError("IORails cannot apply a TRANSFORM rail outcome yet")
     allowed = not outcome.is_blocked
     return RailResult(is_safe=allowed, reason=outcome.reason, return_value={"allowed": allowed, **outcome.metadata})
🤖 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/rails_manager.py` around lines 77 - 80, Update
_rail_result to handle outcome.transforms explicitly before deriving the safe
result: when a TRANSFORM decision is present, raise an error or emit a clear
diagnostic instead of treating it as safe and discarding the transforms.
Preserve the existing BLOCK/allow mapping for outcomes without transforms.
nemoguardrails/guardrails/iorails.py (1)

618-624: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the redundant truthiness guard in the comprehension.

_get_flow_name(flow) or flow already falls back to the raw flow string. The extra and guard only skips a flow whose normalized name and raw string are both empty, which then reaches compile_rail instead of being reported here. Use a plain assignment expression so the filter reads as one condition.

♻️ Proposed simplification
         out_of_scope = sorted(
             {
                 name
                 for flow in flows
-                if (name := _get_flow_name(flow) or flow) and (direction, name) not in cls._ENABLED_SURFACES
+                if ((name := _get_flow_name(flow) or flow), direction) and (direction, name) not in cls._ENABLED_SURFACES
             }
         )

Or extract the normalization into a local helper and keep the comprehension condition single-purpose.

🤖 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/iorails.py` around lines 618 - 624, Update the
out_of_scope comprehension in the relevant class to remove the redundant
truthiness check around the name assignment. Assign the normalized value from
_get_flow_name(flow) or flow, then filter only on whether (direction, name) is
absent from _ENABLED_SURFACES so empty names are still reported here.
tests/guardrails/test_guardrails_types.py (1)

134-149: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a multi-key verdict case to cover the "; " join.

Every evidence case here carries one renderable key, so the "; ".join(parts) separator in _verdict_evidence is never exercised. That separator reaches client-facing violation payloads, so pin it.

💚 Proposed additional case
             (
                 RailResult(
                     is_safe=False,
                     triggered_rail="content safety check input",
                     return_value={"allowed": False, "policy_violations": ["S1: Violence", "S2: Sexual"]},
                 ),
                 "policy_violations: S1: Violence, S2: Sexual",
             ),
+            (
+                RailResult(
+                    is_safe=False,
+                    triggered_rail="content safety check input",
+                    return_value={"allowed": False, "policy_violations": ["S1: Violence"], "score": 0.9},
+                ),
+                "policy_violations: S1: Violence; score: 0.9",
+            ),

Add a matching "multi-key-evidence" entry to ids.

🤖 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_guardrails_types.py` around lines 134 - 149, Add a
multi-key verdict fixture in the guardrail evidence tests by adding a matching
"multi-key-evidence" entry to ids and a corresponding RailResult whose
return_value contains multiple renderable evidence keys. Set the expected
verdict string to join those key-value parts with "; " so _verdict_evidence’s
separator is exercised.
🤖 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 `@nemoguardrails/guardrails/iorails.py`:
- Around line 1421-1425: Update display_reason() and the client-facing violation
payload paths around tool input, tool output, and other rail-block responses to
format reasons only from an explicit safe-field allowlist, excluding
user_message, bot_message, and unfiltered provider-result fields. Preserve the
complete rail metadata for internal logs and spans.

In `@nemoguardrails/guardrails/rails_manager.py`:
- Around line 180-185: The _rails map must distinguish direction-specific
compilations instead of keying only by flow. In
nemoguardrails/guardrails/rails_manager.py lines 180-185, key entries by
(direction, flow) and update the _run_rail lookup accordingly. In
tests/guardrails/test_rails_manager.py lines 185-193, update
test_rails_compiled_for_flows and test_nemoguards_rails_compiled to use tuple
keys, and add coverage for a surface listed in both input.flows and
output.flows.

In `@tests/guardrails/test_cross_engine_rail_equivalence.py`:
- Around line 281-282: Update the relevant test class docstring to state that
both engines use HTTPX for the shared jailbreak action, or remove the
transport-specific distinction; do not leave the outdated aiohttp description in
place.

---

Nitpick comments:
In `@nemoguardrails/guardrails/compiled_rail.py`:
- Around line 287-295: Update the comment in _bind_parameters to reference
_unfillable_bindings_reason instead of the removed
_reject_unfillable_binding_kinds name, leaving the surrounding binding logic
unchanged.
- Around line 305-334: Replace the hard-coded _RETRIEVAL_CONTEXT_SURFACES set
and _retrieval_context_reason lookup with a manifest-declared retrieval-context
requirement on each applicable rail surface. Update unsupported_surface_reason
derivation to inspect that manifest metadata, so surfaces whose actions require
retrieval evidence are rejected when CompiledRail._request_dependencies cannot
provide it.

In `@nemoguardrails/guardrails/engine_registry.py`:
- Line 44: Update the registry class and initializer docstrings near the
model-only registry declaration to remove references to creating API engines and
tracing API calls. Keep the documentation focused on managing ModelEngine
instances for IORails, consistently with the module docstring and model-only
contract.

In `@nemoguardrails/guardrails/iorails.py`:
- Around line 618-624: Update the out_of_scope comprehension in the relevant
class to remove the redundant truthiness check around the name assignment.
Assign the normalized value from _get_flow_name(flow) or flow, then filter only
on whether (direction, name) is absent from _ENABLED_SURFACES so empty names are
still reported here.

In `@nemoguardrails/guardrails/rails_manager.py`:
- Around line 77-80: Update _rail_result to handle outcome.transforms explicitly
before deriving the safe result: when a TRANSFORM decision is present, raise an
error or emit a clear diagnostic instead of treating it as safe and discarding
the transforms. Preserve the existing BLOCK/allow mapping for outcomes without
transforms.

In `@tests/guardrails/test_compiled_rail.py`:
- Around line 373-382: Update test_the_deny_list_names_only_real_surfaces to
assert exact equality between _RETRIEVAL_CONTEXT_SURFACES and
RETRIEVAL_DEPENDENT_SURFACES before checking catalog membership. Preserve the
existing missing-surface validation after this bidirectional consistency check.

In `@tests/guardrails/test_guardrails_types.py`:
- Around line 134-149: Add a multi-key verdict fixture in the guardrail evidence
tests by adding a matching "multi-key-evidence" entry to ids and a corresponding
RailResult whose return_value contains multiple renderable evidence keys. Set
the expected verdict string to join those key-value parts with "; " so
_verdict_evidence’s separator is exercised.

In `@tests/guardrails/test_iorails_streaming.py`:
- Around line 1155-1157: Update the test around _assert_error_chunk to use that
helper instead of indexing error_chunks[0] directly, preserving the existing
expected error message assertion while producing a readable failure when no
error chunk is emitted.
🪄 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: c2ca6314-fb58-4160-a167-21f8c5287add

📥 Commits

Reviewing files that changed from the base of the PR and between 9952e53 and 827b38d.

📒 Files selected for processing (40)
  • docs/configure-rails/guardrail-catalog/jailbreak-protection.mdx
  • nemoguardrails/guardrails/__init__.py
  • nemoguardrails/guardrails/actions/content_safety_action.py
  • nemoguardrails/guardrails/actions/jailbreak_detection_action.py
  • nemoguardrails/guardrails/actions/topic_safety_action.py
  • nemoguardrails/guardrails/api_engine.py
  • nemoguardrails/guardrails/compiled_rail.py
  • nemoguardrails/guardrails/engine_registry.py
  • nemoguardrails/guardrails/guardrails_types.py
  • nemoguardrails/guardrails/iorails.py
  • nemoguardrails/guardrails/rail_action.py
  • nemoguardrails/guardrails/rail_guard.py
  • nemoguardrails/guardrails/rails_manager.py
  • nemoguardrails/guardrails/telemetry.py
  • nemoguardrails/guardrails/tool_rail_action.py
  • nemoguardrails/library/topic_safety/actions.py
  • nemoguardrails/server/api.py
  • nemoguardrails/server/exception_handlers.py
  • tests/guardrails/async_helpers.py
  • tests/guardrails/test_api_engine.py
  • tests/guardrails/test_compiled_rail.py
  • tests/guardrails/test_content_safety_iorails_actions.py
  • tests/guardrails/test_cross_engine_rail_equivalence.py
  • tests/guardrails/test_engine_registry.py
  • tests/guardrails/test_guardrails.py
  • tests/guardrails/test_guardrails_types.py
  • tests/guardrails/test_iorails_generation_log_capture.py
  • tests/guardrails/test_iorails_streaming.py
  • tests/guardrails/test_iorails_telemetry.py
  • tests/guardrails/test_jailbreak_detection_iorails_actions.py
  • tests/guardrails/test_rail_action.py
  • tests/guardrails/test_rail_guard.py
  • tests/guardrails/test_rails_manager.py
  • tests/guardrails/test_telemetry_spans.py
  • tests/guardrails/test_tool_rails_e2e.py
  • tests/guardrails/test_topic_safety_iorails_actions.py
  • tests/server/test_error_envelope_e2e.py
  • tests/server/test_openai_integration.py
  • tests/test_http_error_handling.py
  • tests/test_topic_safety_actions.py
💤 Files with no reviewable changes (13)
  • nemoguardrails/guardrails/telemetry.py
  • tests/guardrails/test_content_safety_iorails_actions.py
  • nemoguardrails/guardrails/actions/content_safety_action.py
  • tests/guardrails/test_rail_action.py
  • nemoguardrails/guardrails/actions/jailbreak_detection_action.py
  • tests/guardrails/test_rail_guard.py
  • tests/guardrails/test_topic_safety_iorails_actions.py
  • nemoguardrails/guardrails/actions/topic_safety_action.py
  • tests/guardrails/test_jailbreak_detection_iorails_actions.py
  • nemoguardrails/guardrails/init.py
  • nemoguardrails/guardrails/rail_action.py
  • nemoguardrails/guardrails/api_engine.py
  • tests/guardrails/test_api_engine.py

Comment thread nemoguardrails/guardrails/iorails.py
Comment thread nemoguardrails/guardrails/rails_manager.py
Comment thread tests/guardrails/test_cross_engine_rail_equivalence.py
Comment on lines +101 to +113
last_user_index = max((i for i, m in enumerate(messages) if m.get("role") == "user"), default=None)
for index, message in enumerate(messages):
content = message.get("content")
if not content:
continue
role = message.get("role")
if role == "user":
events.append({"type": _USER_MESSAGE_EVENT, "text": content})
# The current turn is deliberately absent from the history, mirroring
# ``llmrails.py``, which emits ``UserMessage`` only for a user turn that is not the
# last. Actions append it themselves from ``context["user_message"]``, so emitting
# it here too would hand the model the same turn twice.
if index != last_user_index:
events.append({"type": _USER_MESSAGE_EVENT, "text": content})

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.

P1 Historical user turn is reordered

When an explicit input-rail check receives a transcript ending with an assistant message, messages_to_events removes the latest historical user message and topic_safety_check_input appends it after that assistant response, causing topic safety to classify a reordered conversation and return an incorrect allow or block verdict.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/guardrails/compiled_rail.py
Line: 101-113

Comment:
**Historical user turn is reordered**

When an explicit input-rail check receives a transcript ending with an assistant message, `messages_to_events` removes the latest historical user message and `topic_safety_check_input` appends it after that assistant response, causing topic safety to classify a reordered conversation and return an incorrect allow or block verdict.

**Knowledge Base Used:**
- [Rails Engine](https://app.greptile.com/nvidia-public-github/-/custom-context/knowledge-base/nvidia-nemo/guardrails/-/docs/rails-engine.md)
- [Library Rails](https://app.greptile.com/nvidia-public-github/-/custom-context/knowledge-base/nvidia-nemo/guardrails/-/docs/library-rails.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@tgasser-nv
tgasser-nv force-pushed the refactor/iorails-compiledrail-migration branch from 28dc78d to 1367344 Compare August 7, 2026 04:48
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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.

1 participant