Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions nemoguardrails/http/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def __init__(
policy: RetryPolicy | None = None,
*,
sleep: Callable[[float], Awaitable[None]] = asyncio.sleep,
random_value: Callable[[], float] = random.random,
random_value: Callable[[], float] | None = None,
now: Callable[[], datetime] | None = None,
):
"""Initialize a retrying client.
Expand All @@ -143,7 +143,7 @@ def __init__(
self._client = client
self._policy = policy or RetryPolicy()
self._sleep = sleep
self._random_value = random_value
self._random_value = random_value if random_value is not None else random.random
self._now = now or (lambda: datetime.now(timezone.utc))
self._closed = False

Expand Down
4 changes: 4 additions & 0 deletions nemoguardrails/library/clavata/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,10 @@ async def clavata_check(
try:
labels = get_labels(clavata_config, labels=labels, rail=rail)
except ClavataPluginValueError:
log.debug(
"No labels resolved for rail %r; falling back to whole-policy matching.",
rail,
)
labels = None

result = await evaluate_with_policy(text, str(policy_id), clavata_config, http_client=http_client)
Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/library/clavata/flows.co
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ flow clavata check for $text $policy $labels=""
if $is_match.is_blocked
if $system.config.enable_rails_exceptions
global $msg
$msg = "Interaction blocked by clavata check with policy={$policy} and text={$text}"
$msg = "Interaction blocked by clavata check with policy={$policy}"
send ClavataPolicyMatchException(message=$msg)
else
bot refuse to respond
Expand Down
6 changes: 2 additions & 4 deletions nemoguardrails/library/clavata/flows.v1.co
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ define flow clavata check input

if $is_match.is_blocked
if $config.enable_rails_exceptions
$msg = "Interaction blocked by clavata check with policy={$policy} and text={$text}"
create event ClavataPolicyMatchException(message=$msg)
create event ClavataPolicyMatchException(message="Interaction blocked by clavata check on input.")
else
bot refuse to respond
stop
Expand All @@ -21,8 +20,7 @@ define flow clavata check output

if $is_match.is_blocked
if $config.enable_rails_exceptions
$msg = "Interaction blocked by clavata check with policy={$policy} and text={$text}"
create event ClavataPolicyMatchException(message=$msg)
create event ClavataPolicyMatchException(message="Interaction blocked by clavata check on output.")
else
bot refuse to respond
stop
16 changes: 7 additions & 9 deletions nemoguardrails/library/clavata/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
log = logging.getLogger(__name__)


_CLAVATA_API_KEY = os.environ.get("CLAVATA_API_KEY")
_CLAVATA_RETRY_POLICY = RetryPolicy(
max_attempts=3,
retryable_methods=frozenset({"POST"}),
Expand All @@ -64,7 +63,7 @@ def to_headers(self) -> Dict[str, str]:
"""
Converts the auth token into request headers.
"""
api_key = self.api_key or _CLAVATA_API_KEY
api_key = self.api_key or os.environ.get("CLAVATA_API_KEY")

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not cache the environment key in ClavataClient.

AuthHeader.to_headers now reads CLAVATA_API_KEY, but ClavataClient.__init__ still stores the environment value in self.api_key at Line 160. _get_headers passes that cached value at Line 171, so the fallback at Line 66 is not reached after a key rotation. A reused client sends the old credential.

Store only an explicitly supplied api_key in ClavataClient. Let AuthHeader.to_headers resolve the environment variable for each request. This conflicts with the PR objective that the key is read at call time.

Proposed fix
-        self.api_key = api_key or os.environ.get("CLAVATA_API_KEY")
-        if self.api_key is None:
-            raise ClavataPluginConfigurationError(...)
+        self.api_key = api_key
🤖 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/library/clavata/request.py` at line 66, Update
ClavataClient.__init__ to store only an explicitly provided api_key and stop
copying CLAVATA_API_KEY into self.api_key. Ensure _get_headers passes that value
to AuthHeader.to_headers so the method resolves the current environment key on
every request, including after key rotation.

if api_key is None:
raise ClavataPluginConfigurationError(
"CLAVATA_API_KEY environment variable is not set. "
Expand Down Expand Up @@ -203,27 +202,26 @@ async def _make_request(
)

if response.status_code != 200:
raise ClavataPluginAPIError(
f"Clavata call failed with status code {response.status_code}.\nDetails: {response.text}"
)
raise ClavataPluginAPIError(f"Clavata call failed with status code {response.status_code}.")

try:
parsed_response = response.json()
except HTTPResponseDecodeError as e:
raise ClavataPluginValueError(
f"Failed to parse Clavata response as JSON. Status: {response.status_code}, "
f"Content: {response.text}"
f"Failed to parse Clavata response as JSON. Status: {response.status_code}"
) from e

try:
return response_model.model_validate(parsed_response)
except ValidationError as e:
raise ClavataPluginValueError(f"Invalid response format from Clavata API. Details: {e}") from e
raise ClavataPluginValueError(
f"Invalid response format from Clavata API. Validation errors: {e.error_count()}"
) from e

except ClavataPluginError:
raise
except Exception as e:
raise ClavataPluginAPIError(f"Failed to make Clavata API request. Error: {e}") from e
raise ClavataPluginAPIError(f"Failed to make Clavata API request. Error: {type(e).__name__}") from e

async def create_job(self, text: str, policy_id: str) -> Job:
"""
Expand Down
2 changes: 1 addition & 1 deletion nemoguardrails/library/content_safety/flows.co
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ flow content safety check output $model

if not $allowed
if $system.config.enable_rails_exceptions
send ContentSafetyCheckOuputException(message="Output not allowed. The output was blocked by the 'content safety check output $model='{$model}'' flow.")
send ContentSafetyCheckOutputException(message="Output not allowed. The output was blocked by the 'content safety check output $model='{$model}'' flow.")
else
if $system.config.rails.config.content_safety.multilingual.enabled
$lang_result = await DetectLanguageAction()
Expand Down
4 changes: 2 additions & 2 deletions nemoguardrails/library/content_safety/flows.v1.co
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ define flow content safety check input

if not $allowed
if $config.enable_rails_exceptions
create event ContentSafetyCheckInputException(message="Input not allowed. The input was blocked by the 'content safety check input $model='{$model}'' flow.")
create event ContentSafetyCheckInputException(message="Input not allowed. The input was blocked by the 'content safety check input' flow.")
else
if $config.rails.config.content_safety.multilingual.enabled
$lang_result = execute detect_language
Expand All @@ -26,7 +26,7 @@ define flow content safety check output

if not $allowed
if $config.enable_rails_exceptions
create event ContentSafetyCheckOuputException(message="Output not allowed. The output was blocked by the 'content safety check output $model='{$model}'' flow.")
create event ContentSafetyCheckOutputException(message="Output not allowed. The output was blocked by the 'content safety check output' flow.")
else
if $config.rails.config.content_safety.multilingual.enabled
$lang_result = execute detect_language
Expand Down
6 changes: 6 additions & 0 deletions nemoguardrails/library/content_safety/rail.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
ActionRef,
Binding,
ConfigSpecRef,
ModelRequirement,
RailActions,
RailConfigSchema,
RailDirection,
RailFlows,
RailManifest,
RailMetadata,
RailPrivacy,
RailRequirements,
RailSpec,
RailSurface,
)
Expand Down Expand Up @@ -79,6 +81,10 @@
bindings=(Binding.surface_param("model_name", "model"),),
),
),
requirements=RailRequirements(
models=(ModelRequirement(type="content_safety", required=True),),
extras=("multilingual",),
),
privacy=RailPrivacy(sends_user_text=True, sends_bot_text=True),
),
)
1 change: 0 additions & 1 deletion nemoguardrails/library/f5/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ def _retrying_http_client(client: HTTPClient, f5_config: F5GuardrailsRailConfig)
client,
_retry_policy(f5_config),
sleep=asyncio.sleep,
random_value=lambda: 1.0,
)


Expand Down
11 changes: 8 additions & 3 deletions nemoguardrails/library/injection_detection/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@ def _injection_detection_outcome(
action_option: str,
original_text: str,
) -> RailOutcome:
metadata = dict(result)
metadata["action"] = action_option
# the checked text is deliberately excluded: outcome metadata reaches
# processing logs and tracing exporters
metadata = {
"is_injection": result["is_injection"],
"detections": result["detections"],
"action": action_option,
}
Comment on lines +62 to +68

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 3 'metadata.*text|_injection_detection_outcome|_regex_outcome' nemoguardrails tests --glob '*.py'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 31707


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository guidance ---'
find nemoguardrails -name AGENTS.md -print -exec cat {} \;

printf '%s\n' '--- injection detection implementation and tests ---'
sed -n '1,125p' nemoguardrails/library/injection_detection/actions.py
sed -n '1,145p' tests/test_injection_detection.py

printf '%s\n' '--- regex implementation and related tests/docs ---'
sed -n '1,125p' nemoguardrails/library/regex/actions.py
rg -n -C 4 'regex_detection|_regex_outcome|is_match|detections.*source|source.*detections' nemoguardrails tests docs README.md --glob '*.py' --glob '*.md' 2>/dev/null || true

printf '%s\n' '--- exact metadata text consumers ---'
rg -n -C 3 'metadata\s*\[\s*["'\'']text["'\'']\s*\]|metadata\.get\(\s*["'\'']text["'\'']|["'\'']text["'\'']\s*:\s*.*metadata|metadata.*text' nemoguardrails tests --glob '*.py' || true

printf '%s\n' '--- current diff for the two implementations ---'
git diff -- nemoguardrails/library/injection_detection/actions.py nemoguardrails/library/regex/actions.py

Repository: NVIDIA-NeMo/Guardrails

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- injection action documentation ---'
rg -n -A35 -B5 'def (detect|check).*injection|InjectionDetectionResult|injection detection' \
  nemoguardrails/library/injection_detection/actions.py

printf '%s\n' '--- regex tests and metadata assertions ---'
rg -n -C 5 'detect_regex_pattern|metadata|is_match|detections|source' \
  tests/test_regex_detection.py tests/test_runtime_flow_gate_equivalence.py \
  tests/recorded --glob '*.py' | head -n 260

printf '%s\n' '--- focused diff status ---'
git diff --stat -- nemoguardrails/library/injection_detection/actions.py nemoguardrails/library/regex/actions.py tests
git diff --unified=15 -- nemoguardrails/library/injection_detection/actions.py nemoguardrails/library/regex/actions.py

printf '%s\n' '--- AST-based metadata consumer and documentation scan ---'
python3 - <<'PY'
import ast
from pathlib import Path

roots = [Path("nemoguardrails"), Path("tests")]
for root in roots:
    for path in root.rglob("*.py"):
        try:
            tree = ast.parse(path.read_text())
        except Exception:
            continue
        for node in ast.walk(tree):
            if isinstance(node, ast.FunctionDef) and node.name in {
                "detect_regex_pattern",
                "_injection_detection_outcome",
                "_regex_outcome",
            }:
                print(f"{path}:{node.lineno} {node.name}")
                print("  docstring:", repr(ast.get_docstring(node)))
            if isinstance(node, ast.Subscript):
                target = node.value
                if isinstance(target, ast.Name) and target.id == "metadata":
                    sl = node.slice
                    if isinstance(sl, ast.Constant) and sl.value == "text":
                        print(f"{path}:{node.lineno} metadata['text'] read")
            if isinstance(node, ast.Call):
                for kw in node.keywords:
                    if kw.arg == "metadata" and isinstance(kw.value, ast.Dict):
                        keys = [
                            k.value for k in kw.value.keys
                            if isinstance(k, ast.Constant) and isinstance(k.value, str)
                        ]
                        if "text" in keys:
                            print(f"{path}:{node.lineno} metadata literal keys={keys}")
PY

Repository: NVIDIA-NeMo/Guardrails

Length of output: 32083


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- public action docstrings ---'
sed -n '300,350p' nemoguardrails/library/injection_detection/actions.py
sed -n '42,66p' nemoguardrails/library/regex/actions.py
rg -n -F 'The original text that was checked' . --glob '*.py' --glob '*.md' || true

printf '%s\n' '--- equivalence helper and injection/regex cases ---'
sed -n '840,930p' tests/test_runtime_flow_gate_equivalence.py
sed -n '1140,1180p' tests/test_runtime_flow_gate_equivalence.py
sed -n '1428,1490p' tests/test_runtime_flow_gate_equivalence.py
rg -n -A35 -B10 'assert .*metadata|expected_outcome|actual_outcome|RailOutcome' \
  tests/test_runtime_flow_gate_equivalence.py | tail -n 180

printf '%s\n' '--- metadata reads in runtime and tests ---'
python3 - <<'PY'
import ast
from pathlib import Path

for path in [*Path("nemoguardrails").rglob("*.py"), *Path("tests").rglob("*.py")]:
    try:
        tree = ast.parse(path.read_text())
    except Exception:
        continue
    for node in ast.walk(tree):
        if not isinstance(node, ast.Subscript):
            continue
        key = node.slice
        is_text = isinstance(key, ast.Constant) and key.value == "text"
        if not is_text:
            continue
        base = ast.unparse(node.value)
        if "metadata" in base or "outcome" in base or "result" in base:
            print(f"{path}:{node.lineno}: {base}['text']")
PY

Repository: NVIDIA-NeMo/Guardrails

Length of output: 16631


Update the RailOutcome.metadata documentation.

Remove text from the injection_detection and detect_regex_pattern return documentation. Document the actual fields: is_injection, detections, and action for injection detection; is_match, detections, and source for regex detection. No downstream metadata["text"] consumer requires a code change.

📍 Affects 2 files
  • nemoguardrails/library/injection_detection/actions.py#L62-L68 (this comment)
  • nemoguardrails/library/regex/actions.py#L33-L35
🤖 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/library/injection_detection/actions.py` around lines 62 - 68,
Update the RailOutcome.metadata documentation in
nemoguardrails/library/injection_detection/actions.py (lines 62-68) to remove
text and document is_injection, detections, and action; update
nemoguardrails/library/regex/actions.py (lines 33-35) to remove text and
document is_match, detections, and source. No downstream metadata consumer
changes are needed.

Source: Coding guidelines

if action_option == "reject" and result["is_injection"]:
return RailOutcome.block(metadata=metadata)
if result["text"] != original_text:
Expand Down Expand Up @@ -193,7 +198,7 @@ def _load_rules(
except yara.SyntaxError as e:
msg = f"Failed to initialize injection detection due to configuration or YARA rule error: YARA compilation failed: {e}"
log.error(msg)
return None
raise ValueError(msg) from e
Comment on lines 198 to +201

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 3 'yara\.SyntaxError|_load_rules|injection_detection' nemoguardrails tests --glob '*.py'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 49424


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- AGENTS.md files ---'
find nemoguardrails -name AGENTS.md -print -exec cat {} \;
printf '%s\n' '--- actions.py documentation and exception flow ---'
sed -n '70,215p' nemoguardrails/library/injection_detection/actions.py
sed -n '315,355p' nemoguardrails/library/injection_detection/actions.py
printf '%s\n' '--- exception handling and documentation references ---'
rg -n -C 4 'except (ValueError|yara\.SyntaxError)|raises:|Raises:|YARA compilation|_load_rules\(' nemoguardrails tests --glob '*.py'

Repository: NVIDIA-NeMo/Guardrails

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- focused injection tests ---'
sed -n '700,770p' tests/test_injection_detection.py
printf '%s\n' '--- action-dispatch files ---'
fd -t f -i 'action.*dispatcher|dispatcher.*action' nemoguardrails
printf '%s\n' '--- injection action references outside its implementation ---'
rg -n 'library\.injection_detection\.actions|actions:injection_detection|_load_rules|yara\.SyntaxError' nemoguardrails tests --glob '*.py' \
  | rg -v 'nemoguardrails/library/injection_detection/actions\.py'
printf '%s\n' '--- targeted exception handlers in the runtime ---'
rg -n -C 3 'execute_action|run_action|dispatch.*action|action.*exception|except Exception|except ValueError' nemoguardrails/actions nemoguardrails/colang nemoguardrails/rails --glob '*.py' \
  | head -n 240
printf '%s\n' '--- AST-based documentation and raise verifier ---'
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("nemoguardrails/library/injection_detection/actions.py")
tree = ast.parse(path.read_text())
functions = {
    node.name: node
    for node in ast.walk(tree)
    if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
}
for name in ("_load_rules", "injection_detection"):
    node = functions[name]
    doc = ast.get_docstring(node) or ""
    raises = [n for n in ast.walk(node) if isinstance(n, ast.Raise)]
    print(f"{name}: documented_yara_syntax_error={'yara.SyntaxError' in doc}, "
          f"documented_value_error={'ValueError' in doc}, raise_count={len(raises)}")
load = functions["_load_rules"]
for node in ast.walk(load):
    if isinstance(node, ast.ExceptHandler):
        print("load_rules_handler:", ast.unparse(node.type) if node.type else "bare")
        for child in ast.walk(node):
            if isinstance(child, ast.Raise):
                print("load_rules_raise:", ast.unparse(child.exc) if child.exc else "bare")
PY

Repository: NVIDIA-NeMo/Guardrails

Length of output: 23636


Document YARA compilation failures as ValueError.

_load_rules converts yara.SyntaxError to ValueError, but its Raises section still lists yara.SyntaxError. Clarify that injection_detection raises ValueError for invalid YARA rules. Callers handle the failure generically and do not require yara.SyntaxError handling.

🤖 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/library/injection_detection/actions.py` around lines 198 -
201, Update the Raises documentation for _load_rules to state that invalid YARA
rules and compilation failures are raised as ValueError, replacing the outdated
yara.SyntaxError entry. Keep the existing exception conversion and caller
behavior unchanged.

Source: Coding guidelines

return rules


Expand Down
5 changes: 3 additions & 2 deletions nemoguardrails/library/regex/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ class RegexDetectionResult(TypedDict):


def _regex_outcome(source: str, result: RegexDetectionResult) -> RailOutcome:
metadata = dict(result)
metadata["source"] = source
# the checked text is deliberately excluded: outcome metadata reaches
# processing logs and tracing exporters
metadata = {"is_match": result["is_match"], "detections": result["detections"], "source": source}

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.

P2 Metadata docs retain removed text

The regex and injection-detection outcome helpers now omit checked text, but their action docstrings still advertise metadata["text"]; custom consumers following that documented return shape can access a nonexistent field, and generated documentation continues to promise sensitive data that is deliberately no longer returned.

Knowledge Base Used: Library Rails

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/regex/actions.py
Line: 35

Comment:
**Metadata docs retain removed text**

The regex and injection-detection outcome helpers now omit checked text, but their action docstrings still advertise `metadata["text"]`; custom consumers following that documented return shape can access a nonexistent field, and generated documentation continues to promise sensitive data that is deliberately no longer returned.

**Knowledge Base Used:** [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.

if result["is_match"] and source == "retrieval":
return RailOutcome.transform([(TransformTarget.RELEVANT_CHUNKS, "")], metadata=metadata)
if result["is_match"]:
Expand Down
5 changes: 4 additions & 1 deletion tests/test_f5_guardrails.py
Original file line number Diff line number Diff line change
Expand Up @@ -678,8 +678,11 @@ async def test_f5_guardrails_429_exhausted_fail_closed(config_no_backoff, monkey

@pytest.mark.asyncio
async def test_f5_guardrails_429_no_retry_after_uses_backoff(monkeypatch):
"""When Retry-After is missing, retry_backoff_seconds * 2**attempt is used."""
"""When Retry-After is missing, retry_backoff_seconds * 2**attempt is the jitter cap."""
monkeypatch.setenv("F5_GUARDRAILS_API_KEY", "test-key")
# backoff is full jitter, a uniform draw in [0, cap]; pin the draw to the
# cap so the delays below are deterministic
monkeypatch.setattr("nemoguardrails.http.retry.random.random", lambda: 1.0)

cfg = RailsConfig.from_content(
yaml_content="""
Expand Down
18 changes: 9 additions & 9 deletions tests/test_injection_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,23 +90,21 @@ def match(self, data=None, **kwargs):
{"is_injection": False, "text": "normal", "detections": []},
"reject",
"normal",
RailOutcome.allow(metadata={"is_injection": False, "text": "normal", "detections": [], "action": "reject"}),
RailOutcome.allow(metadata={"is_injection": False, "detections": [], "action": "reject"}),
),
(
{"is_injection": True, "text": "normal", "detections": ["sqli"]},
"reject",
"normal",
RailOutcome.block(
metadata={"is_injection": True, "text": "normal", "detections": ["sqli"], "action": "reject"}
),
RailOutcome.block(metadata={"is_injection": True, "detections": ["sqli"], "action": "reject"}),
),
(
{"is_injection": True, "text": "omitted", "detections": ["sqli"]},
"omit",
"normal",
RailOutcome.transform(
[(TransformTarget.BOT_MESSAGE, "omitted")],
metadata={"is_injection": True, "text": "omitted", "detections": ["sqli"], "action": "omit"},
metadata={"is_injection": True, "detections": ["sqli"], "action": "omit"},
),
),
],
Expand Down Expand Up @@ -711,8 +709,8 @@ async def test_omit_action_with_exceptions_enabled():


@pytest.mark.asyncio
async def test_malformed_inline_yara_rule_fails_gracefully(caplog):
"""Test that a malformed inline YARA rule leads to graceful failure (detection becomes no-op)."""
async def test_malformed_inline_yara_rule_fails_closed(caplog):
"""Test that a malformed inline YARA rule fails closed rather than disabling detection."""

inline_rule_name = "malformed_rule"
# this rule is malformed: missing { after rule name
Expand Down Expand Up @@ -750,8 +748,10 @@ async def test_malformed_inline_yara_rule_fails_gracefully(caplog):

result = await rails.generate_async(messages=[{"role": "user", "content": "trigger detection"}])

# check that no exception was raised
assert result.get("role") != "exception", f"Expected no exception, but got {result}"
# a rule that cannot compile must not silently disable detection: the
# unchecked model output must never reach the caller
assert some_text_that_would_be_injection not in result["content"]
assert "internal error" in result["content"]

# verify the error log was created with the expected content
assert any(
Expand Down
6 changes: 2 additions & 4 deletions tests/test_regex_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,10 +687,8 @@ async def test_regex_action_accepts_extra_kwargs():
def test_regex_output_verdict_blocks_on_match():
from nemoguardrails.actions.rail_outcome import RailOutcome

matched = RailOutcome.block(
metadata={"is_match": True, "text": "fight club", "detections": ["\\bfight\\s+club\\b"]}
)
no_match = RailOutcome.allow(metadata={"is_match": False, "text": "hello", "detections": []})
matched = RailOutcome.block(metadata={"is_match": True, "detections": ["\\bfight\\s+club\\b"]})
no_match = RailOutcome.allow(metadata={"is_match": False, "detections": []})

assert matched.is_blocked is True
assert no_match.is_blocked is False