Skip to content

feat(actions): add RailOutcome rail-result contract#2150

Merged
Pouyanpi merged 2 commits into
developfrom
pouyanpi/rail-library-stack-1-outcome
Jul 15, 2026
Merged

feat(actions): add RailOutcome rail-result contract#2150
Pouyanpi merged 2 commits into
developfrom
pouyanpi/rail-library-stack-1-outcome

Conversation

@Pouyanpi

@Pouyanpi Pouyanpi commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Introduce the engine-neutral RailOutcome contract for rail decisions: allow,
block, and transform, with neutral reasons, metadata, and transform targets.
The strict validator rejects legacy action return values. This is the contract
used by the migration in #2151

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change.

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 documentation if applicable.
  • I’ve added tests where 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 review comments, or replied with why no change is needed.
  • I’ve mentioned the person or team responsible for review.

Summary by CodeRabbit

  • New Features

    • Added a standardized result format for rail checks, supporting allow, block, and transform decisions.
    • Added optional reasons, metadata, and targeted message transformations.
    • Added validation to ensure decisions include only valid transformation details.
  • Tests

    • Added coverage for decision states, metadata, transformations, immutability, and invalid results.

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

codecov Bot commented Jul 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch 2 times, most recently from 1e5839e to aa8f102 Compare July 9, 2026 11:38
@github-actions github-actions Bot added size: L and removed size: M labels Jul 9, 2026
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch from aa8f102 to b54604f Compare July 9, 2026 11:53
@github-actions github-actions Bot added size: M and removed size: L labels Jul 9, 2026
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch 3 times, most recently from ba19362 to ef00c0e Compare July 10, 2026 11:07
@Pouyanpi
Pouyanpi changed the base branch from develop to pouyanpi/fix-streaming-output-rail-failures July 10, 2026 11:20
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/fix-streaming-output-rail-failures branch 2 times, most recently from 22b20db to 031bfa0 Compare July 13, 2026 09:51
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch from ef00c0e to 8c1b769 Compare July 13, 2026 10:00
@Pouyanpi
Pouyanpi marked this pull request as ready for review July 13, 2026 10:40
@Pouyanpi Pouyanpi 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 Jul 13, 2026
@Pouyanpi Pouyanpi self-assigned this Jul 13, 2026
@Pouyanpi Pouyanpi added this to the v0.24.0 milestone Jul 13, 2026
@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an engine-neutral rail result contract. The main changes are:

  • New RailOutcome decisions for allow, block, and transform.
  • Transform targets and transform payload validation.
  • Strict rejection of legacy action return values.
  • Tests for decision normalization, transforms, metadata handling, and validation.

Confidence Score: 4/5

This is close, but the metadata handling should be fixed before merging.

  • Enum values are normalized before the identity checks and transform map access.
  • The copied metadata is still exposed as a mutable dictionary.
  • A shared outcome can have its evidence changed after construction.

nemoguardrails/actions/rail_outcome.py

Important Files Changed

Filename Overview
nemoguardrails/actions/rail_outcome.py Adds the RailOutcome contract, validation, factories, and strict result checking.
tests/test_rail_outcome.py Adds tests for allowed decisions, blocked decisions, transforms, validation errors, and metadata copying.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
nemoguardrails/actions/rail_outcome.py:101
**Metadata stays mutable**

This stores the copied metadata as a plain `dict`, so callers can still change an already-created frozen outcome with operations like `outcome.metadata["reason"] = "changed"`, `update()`, or `pop()`. The constructor now protects against later writes to the original input mapping, but the mapping exposed from the outcome itself remains mutable. When an engine or audit path shares the outcome as immutable evidence, a later in-place metadata write can change what other consumers read or log.

Reviews (5): Last reviewed commit: "fix(actions): validate RailOutcome const..." | Re-trigger Greptile

Comment thread nemoguardrails/actions/rail_outcome.py
Comment thread nemoguardrails/actions/rail_outcome.py
Comment thread nemoguardrails/actions/rail_outcome.py Outdated
@Pouyanpi

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
✅ 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 Jul 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an immutable, engine-neutral RailOutcome model for ALLOW, BLOCK, and TRANSFORM decisions, including metadata, transform specifications, validation helpers, and comprehensive behavioral tests.

Changes

Rail outcome contract

Layer / File(s) Summary
Outcome model and constructors
nemoguardrails/actions/rail_outcome.py
Defines rail decision and transform-target enums, immutable outcome and transform dataclasses, transform invariants, convenience constructors, derived properties, and require_rail_outcome.
Outcome behavior tests
tests/test_rail_outcome.py
Tests default outcomes, metadata, blocking and transformations, constructor validation, immutability, and type validation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Test Results For Major Changes ⚠️ Warning This is a major new RailOutcome feature, but the PR description only gives a summary and checklist—no test results or testing info is documented. Add a brief testing note to the description (e.g. commands run and results, or why verification couldn’t be run).
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 matches the main change: adding the new RailOutcome rail-result contract in actions.
✨ 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 pouyanpi/rail-library-stack-1-outcome

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)
nemoguardrails/actions/rail_outcome.py (3)

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

Consider declaring __all__ for the public surface.

The module exports RailDecision, RailOutcome, TransformSpec, TransformTarget, require_rail_outcome without an explicit __all__. As per coding guidelines, "The public surface is what top-level __all__ exports," so an explicit list would make the compatibility-sensitive surface unambiguous for this new module.

🤖 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/actions/rail_outcome.py` around lines 1 - 127, Add an explicit
top-level __all__ in rail_outcome.py listing the intended public symbols:
RailDecision, TransformTarget, TransformSpec, RailOutcome, and
require_rail_outcome. Keep internal implementation details excluded and preserve
the existing definitions and behavior.

Source: Coding guidelines


82-84: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No protection against duplicate transform targets.

__post_init__ only checks non-empty-iff-TRANSFORM; it doesn't reject transforms with repeated TransformTarget entries. transform_text (line 97) builds a dict via comprehension, so a duplicate target silently overwrites the earlier rewrite rather than surfacing an error.

🛡️ Add uniqueness check
     def __post_init__(self) -> None:
         if bool(self.transforms) != (self.decision is RailDecision.TRANSFORM):
             raise ValueError("transforms must be non-empty if and only if decision is TRANSFORM")
+        targets = [spec.target for spec in self.transforms]
+        if len(targets) != len(set(targets)):
+            raise ValueError("transforms must not repeat the same TransformTarget")

Also applies to: 96-97

🤖 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/actions/rail_outcome.py` around lines 82 - 84, Update
RailOutcome.__post_init__ to validate that transforms contains no duplicate
TransformTarget entries, while preserving the existing TRANSFORM/non-empty
consistency check. Reject repeated targets with a ValueError before
transform_text’s dictionary construction can silently overwrite earlier
rewrites.

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

metadata dict is mutable despite "immutable" contract.

The dataclass is frozen=True/slots=True, but metadata is a plain dict. Callers can still do outcome.metadata["k"] = "v" after construction, silently breaking the immutability guarantee this module (and test_outcome_is_immutable) advertises.

🔒 Use a read-only mapping
+from types import MappingProxyType
...
-    metadata: dict[str, Any] = field(default_factory=dict)
+    metadata: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({}))
...
-        return cls(decision=RailDecision.ALLOW, reason=reason, metadata=dict(metadata))
+        return cls(decision=RailDecision.ALLOW, reason=reason, metadata=MappingProxyType(dict(metadata)))

(apply similarly to block and transform)

Also applies to: 99-120

🤖 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/actions/rail_outcome.py` around lines 68 - 80, Update
RailOutcome and the related block/transform outcome dataclasses to store
metadata as a read-only mapping rather than a mutable dict, while preserving the
existing default-empty behavior and metadata access API. Ensure metadata is
defensively copied or wrapped during construction so callers cannot mutate
internal state after creation, including through the default factory.
🤖 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/actions/rail_outcome.py`:
- Around line 29-31: Rewrite the TRANSFORM section of the module docstring in
rail_outcome.py to clearly state that transform outcomes are non-streaming,
while streaming output bypasses them as non-blocking and does not apply
rewrites. Preserve the documented behavior and scope; only correct the grammar
and clarity.

---

Nitpick comments:
In `@nemoguardrails/actions/rail_outcome.py`:
- Around line 1-127: Add an explicit top-level __all__ in rail_outcome.py
listing the intended public symbols: RailDecision, TransformTarget,
TransformSpec, RailOutcome, and require_rail_outcome. Keep internal
implementation details excluded and preserve the existing definitions and
behavior.
- Around line 82-84: Update RailOutcome.__post_init__ to validate that
transforms contains no duplicate TransformTarget entries, while preserving the
existing TRANSFORM/non-empty consistency check. Reject repeated targets with a
ValueError before transform_text’s dictionary construction can silently
overwrite earlier rewrites.
- Around line 68-80: Update RailOutcome and the related block/transform outcome
dataclasses to store metadata as a read-only mapping rather than a mutable dict,
while preserving the existing default-empty behavior and metadata access API.
Ensure metadata is defensively copied or wrapped during construction so callers
cannot mutate internal state after creation, including through the default
factory.
🪄 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: CHILL

Plan: Enterprise

Run ID: 81cdf5e5-d181-4041-9062-1123119e55cf

📥 Commits

Reviewing files that changed from the base of the PR and between 031bfa0 and 8c1b769.

📒 Files selected for processing (2)
  • nemoguardrails/actions/rail_outcome.py
  • tests/test_rail_outcome.py

Comment thread nemoguardrails/actions/rail_outcome.py Outdated
Base automatically changed from pouyanpi/fix-streaming-output-rail-failures to develop July 14, 2026 04:57
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch from 5e1c9d9 to ae7bc3f Compare July 14, 2026 07:10
@github-actions github-actions Bot added size: L and removed size: M labels Jul 14, 2026
Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com>
@Pouyanpi
Pouyanpi force-pushed the pouyanpi/rail-library-stack-1-outcome branch from 55a02d7 to 62d119d Compare July 14, 2026 10:05
Comment thread nemoguardrails/actions/rail_outcome.py
@Pouyanpi
Pouyanpi requested a review from tgasser-nv July 14, 2026 15:43

@tgasser-nv tgasser-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good! I had some pending feedback on replacing **metadata kwargs with a dict and the setitem() breaking deepcopy but these are both address now.

@Pouyanpi
Pouyanpi merged commit c950779 into develop Jul 15, 2026
32 checks passed
@Pouyanpi
Pouyanpi deleted the pouyanpi/rail-library-stack-1-outcome branch July 15, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: L 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.

2 participants