Skip to content

feat(library): add F5 Guardrails integration#2105

Open
ciarancourtney wants to merge 3 commits into
NVIDIA-NeMo:developfrom
ciarancourtney:feat/f5-ai-guardrails
Open

feat(library): add F5 Guardrails integration#2105
ciarancourtney wants to merge 3 commits into
NVIDIA-NeMo:developfrom
ciarancourtney:feat/f5-ai-guardrails

Conversation

@ciarancourtney

@ciarancourtney ciarancourtney commented Jun 30, 2026

Copy link
Copy Markdown

Description

Adds a new community integration for F5 AI Guardrails

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 support for integrating F5 AI Guardrails into guardrail flows for both incoming and outgoing text.
    • Added a configurable fail-open option for handling scan service errors.
  • Documentation

    • Added setup and configuration docs for the new F5 guardrails integration.
    • Added example READMEs and usage instructions for trying the new configurations.
  • Tests

    • Added coverage for allowed, blocked, and fail-open guardrail behavior.

@github-actions github-actions Bot added size: L status: needs triage New issues that have not yet been reviewed or categorized. needs: signing labels Jun 30, 2026
Signed-off-by: Ciaran Courtney <c.courtney@f5.com>
@ciarancourtney ciarancourtney force-pushed the feat/f5-ai-guardrails branch from e63e72d to d3b3870 Compare June 30, 2026 14:23
@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 1, 2026
@Pouyanpi Pouyanpi added this to the v0.24.0 milestone Jul 1, 2026
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a community integration for F5 AI Guardrails, supporting both Colang 1.x and 2.x runtimes with fail-open handling for HTTP errors and timeouts. All three issues flagged in the previous review round (double-logging of RuntimeError, missing repeat=True in the fail-open test, and unhandled asyncio.TimeoutError) have been addressed in this version.

  • Adds nemoguardrails/library/f5/actions.py with async HTTP scanning, explicit asyncio.TimeoutError handling, and targeted aiohttp.ClientError catching so that deliberate RuntimeError raises propagate cleanly.
  • Provides Colang 1.x (flows.v1.co) and 2.x (flows.co) flow definitions, example configs for both versions, documentation, and a test suite covering cleared, blocked, fail-open, and timeout scenarios.

Confidence Score: 5/5

Safe to merge; the integration follows established patterns and all three issues from the previous review round have been properly fixed.

The exception handling hierarchy is now correct (asyncio.TimeoutError caught before aiohttp.ClientError, no broad except-Exception to re-catch deliberate RuntimeError raises). The fail-open test uses repeat=True so both rail invocations are properly mocked. The flow logic, output_mapping, and test coverage are consistent with other community integrations in this codebase.

No files require special attention. actions.py has a minor inconsistency where fail_open does not apply to a missing API key, but this is a low-risk design choice.

Important Files Changed

Filename Overview
nemoguardrails/library/f5/actions.py Core action implementation; exception hierarchy is now correct (TimeoutError → ClientError) and RuntimeError from HTTP errors no longer gets re-caught. Minor: fail_open silently doesn't apply to ValueError raised for missing API key.
tests/test_f5_guardrails.py Good coverage of blocked/cleared/fail-open/timeout cases. fail_open test now correctly uses repeat=True. Dedicated async tests for timeout paths are well structured.
nemoguardrails/library/f5/flows.co Colang 2.x flows for input/output rails; consistent with other library integrations and correctly uses dot-notation dict access.
nemoguardrails/library/f5/flows.v1.co Colang 1.x subflow definitions; correctly uses execute keyword and stop directive.
docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.md Clear setup docs covering env vars and customization; code fences are properly closed.
examples/configs/f5_guardrails/config.yml Colang 1.x example config; correctly references both input and output rail flows.
examples/configs/f5_guardrails_v2/config.yml Colang 2.x example config; correctly sets colang_version and delegates rail logic to rails.co.
examples/configs/f5_guardrails_v2/rails.co Colang 2.x rails definition; correctly imports library and wires input/output rail flows.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant NeMo as NeMo Guardrails
    participant Flow as f5 guardrails scan input/output
    participant Action as f5_guardrails_scan()
    participant F5 as F5 AI Guardrails API

    User->>NeMo: Send message
    NeMo->>Flow: Execute input rail flow
    Flow->>Action: "execute/await f5_guardrails_scan(text=$user_message)"
    Action->>F5: POST /backend/v1/scans
    alt HTTP 200 outcome cleared
        F5-->>Action: result outcome cleared
        Action-->>Flow: raw dict
        Flow->>NeMo: continue
        NeMo->>NeMo: Generate LLM response
        NeMo->>Flow: Execute output rail flow
        Flow->>Action: "execute/await f5_guardrails_scan(text=$bot_message)"
        Action->>F5: POST /backend/v1/scans
        F5-->>Action: result outcome cleared
        Action-->>Flow: raw dict
        Flow->>NeMo: continue
        NeMo-->>User: Bot response
    else HTTP error or timeout with fail_open true
        Action-->>Flow: result outcome cleared
        Flow->>NeMo: continue
        NeMo-->>User: Bot response
    else HTTP error or timeout with fail_open false
        Action-->>NeMo: raise RuntimeError
        NeMo-->>User: Internal error message
    else outcome flagged
        F5-->>Action: result outcome flagged
        Flow->>NeMo: bot refuse to respond and abort
        NeMo-->>User: I'm sorry, I can't respond to that.
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant NeMo as NeMo Guardrails
    participant Flow as f5 guardrails scan input/output
    participant Action as f5_guardrails_scan()
    participant F5 as F5 AI Guardrails API

    User->>NeMo: Send message
    NeMo->>Flow: Execute input rail flow
    Flow->>Action: "execute/await f5_guardrails_scan(text=$user_message)"
    Action->>F5: POST /backend/v1/scans
    alt HTTP 200 outcome cleared
        F5-->>Action: result outcome cleared
        Action-->>Flow: raw dict
        Flow->>NeMo: continue
        NeMo->>NeMo: Generate LLM response
        NeMo->>Flow: Execute output rail flow
        Flow->>Action: "execute/await f5_guardrails_scan(text=$bot_message)"
        Action->>F5: POST /backend/v1/scans
        F5-->>Action: result outcome cleared
        Action-->>Flow: raw dict
        Flow->>NeMo: continue
        NeMo-->>User: Bot response
    else HTTP error or timeout with fail_open true
        Action-->>Flow: result outcome cleared
        Flow->>NeMo: continue
        NeMo-->>User: Bot response
    else HTTP error or timeout with fail_open false
        Action-->>NeMo: raise RuntimeError
        NeMo-->>User: Internal error message
    else outcome flagged
        F5-->>Action: result outcome flagged
        Flow->>NeMo: bot refuse to respond and abort
        NeMo-->>User: I'm sorry, I can't respond to that.
    end
Loading

Reviews (3): Last reviewed commit: "fixup! catch TimeoutError" | Re-trigger Greptile

Comment thread nemoguardrails/library/f5/actions.py Outdated
Comment on lines +74 to +98
async with session.post(endpoint, headers=headers, json=payload) as response:
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")

if fail_open:
log.warning(
"F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

raise RuntimeError(f"F5 Guardrails API error: {response.status}")

result = await response.json()
return result
except Exception as e:
log.error(f"Error connecting to F5 Guardrails API: {str(e)}")

if fail_open:
log.warning("F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

if isinstance(e, aiohttp.ClientError):
raise RuntimeError(f"Connection error to F5 Guardrails API: {str(e)}") from e

raise e

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 RuntimeError re-caught causes double-logging and misleading messages

When response.status != 200 and fail_open=False, the RuntimeError raised on line 84 escapes the try block and is immediately caught by the outer except Exception as e on line 88. This causes two log entries: the first correct one ("F5 Guardrails API call failed: {status} - {detail}") and a second, misleading one saying "Error connecting to F5 Guardrails API: F5 Guardrails API error: {status}" — using "connecting" language for what is an HTTP status error. The fix is to use a narrower except aiohttp.ClientError instead of the broad except Exception, so that deliberate RuntimeError raises from the HTTP-error path propagate cleanly without being re-caught.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/f5/actions.py
Line: 74-98

Comment:
**`RuntimeError` re-caught causes double-logging and misleading messages**

When `response.status != 200` and `fail_open=False`, the `RuntimeError` raised on line 84 escapes the `try` block and is immediately caught by the outer `except Exception as e` on line 88. This causes two log entries: the first correct one (`"F5 Guardrails API call failed: {status} - {detail}"`) and a second, misleading one saying `"Error connecting to F5 Guardrails API: F5 Guardrails API error: {status}"` — using "connecting" language for what is an HTTP status error. The fix is to use a narrower `except aiohttp.ClientError` instead of the broad `except Exception`, so that deliberate `RuntimeError` raises from the HTTP-error path propagate cleanly without being re-caught.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +110 to +130
def test_f5_guardrails_fail_open(config, monkeypatch):
monkeypatch.setenv("F5_GUARDRAILS_API_KEY", "test-key")
monkeypatch.setenv("F5_GUARDRAILS_FAIL_OPEN", "true")
chat = TestChat(
config,
llm_completions=[
" express greeting",
"Hello! How can I assist you today?",
],
)

with aioresponses() as m:
# Simulate an API error (e.g., 500)
m.post(
"https://us1.calypsoai.app/backend/v1/scans",
status=500,
)

chat >> "Hello!"
chat << "express greeting"
chat << "Hello! How can I assist you today?"

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 fail_open test only mocks one of two required API calls

test_f5_guardrails_fail_open registers a single mock (status=500, no repeat=True), but the configured rails call the scan endpoint twice — once for the input rail and once for the output rail. The second call hits an unregistered URL in aioresponses, which raises an aiohttp.ClientConnectionError. This happens to be swallowed by the fail_open branch, so the test passes by accident rather than by design. Adding repeat=True (or registering a second mock) makes the test's intent explicit and avoids relying on this side-effect.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/test_f5_guardrails.py
Line: 110-130

Comment:
**`fail_open` test only mocks one of two required API calls**

`test_f5_guardrails_fail_open` registers a single mock (status=500, no `repeat=True`), but the configured rails call the scan endpoint twice — once for the input rail and once for the output rail. The second call hits an unregistered URL in `aioresponses`, which raises an `aiohttp.ClientConnectionError`. This happens to be swallowed by the `fail_open` branch, so the test passes by accident rather than by design. Adding `repeat=True` (or registering a second mock) makes the test's intent explicit and avoids relying on this side-effect.

How can I resolve this? If you propose a fix, please make it concise.

@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.50000% with 7 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
nemoguardrails/library/f5/actions.py 82.50% 7 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds F5 AI Guardrails integration: a new f5_guardrails_scan async action that calls the F5 Guardrails API, Colang v1/v2 input/output flows that refuse responses when scans are not "cleared", example configurations, guardrail catalog documentation, and pytest coverage for cleared, flagged, and fail-open scenarios.

Changes

F5 AI Guardrails Integration

Layer / File(s) Summary
F5 guardrails scan action
nemoguardrails/library/f5/__init__.py, nemoguardrails/library/f5/actions.py
Adds license header and implements f5_guardrails_scan_mapping and the async f5_guardrails_scan system action that posts to the F5 API, handles timeouts, errors, and a fail-open mode.
Input/output scan flows
nemoguardrails/library/f5/flows.co, nemoguardrails/library/f5/flows.v1.co
Defines input/output scan subflows in both default and v1 Colang syntax, refusing to respond and stopping when scan outcome is not "cleared".
Example configurations
examples/configs/f5_guardrails/*, examples/configs/f5_guardrails_v2/*
Adds runnable example configs and READMEs wiring the F5 scan flows into config.yml, main.co, and rails.co for Colang 1.x and 2.x setups.
Guardrail catalog documentation
docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.md
Documents the integration, required environment variables, and a customization example overriding the default scan subflow.
Test coverage
tests/test_f5_guardrails.py
Adds a config fixture and tests for cleared input, flagged input, flagged output, and fail-open behavior on API errors.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Rails as NeMo Guardrails
  participant Action as f5_guardrails_scan
  participant F5API as F5 Guardrails API
  participant LLM

  User->>Rails: send message
  Rails->>Action: scan input (user_message)
  Action->>F5API: POST /backend/v1/scans
  F5API-->>Action: outcome result
  alt outcome not cleared
    Action-->>Rails: blocked
    Rails-->>User: refuse to respond
  else outcome cleared
    Rails->>LLM: generate response
    LLM-->>Rails: bot_message
    Rails->>Action: scan output (bot_message)
    Action->>F5API: POST /backend/v1/scans
    F5API-->>Action: outcome result
    alt outcome not cleared
      Action-->>Rails: blocked
      Rails-->>User: refuse to respond
    else outcome cleared
      Rails-->>User: deliver response
    end
  end
Loading

Suggested labels: documentation

Suggested reviewers: cparisien, Pouyanpi, tgasser-nv

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Results For Major Changes ⚠️ Warning Major new integration, but the PR body shows only checklist marks and no actual test results or verification notes. Add a brief testing/verification section to the PR description with commands run, outcomes, and any checks not executed.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding an F5 Guardrails integration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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: 4

Caution

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

⚠️ Outside diff range comments (1)
nemoguardrails/library/f5/flows.v1.co (1)

1-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix trailing newline (CI lint failure).

The end-of-file-fixer pre-commit hook reports this file was modified; the file needs a proper trailing newline fixed before merge.

🤖 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/f5/flows.v1.co` around lines 1 - 17, The file needs a
proper trailing newline to satisfy the end-of-file-fixer lint check. Update the
`nemoguardrails/library/f5/flows.v1.co` content so it ends with a single newline
character after the last `stop` in the `f5 guardrails scan output` subflow,
keeping the `define subflow` blocks unchanged.

Source: Pipeline failures

♻️ Duplicate comments (1)
nemoguardrails/library/f5/flows.v1.co (1)

1-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Same unhandled-exception risk as the Colang v2 flows.

Duplicates the same issue flagged in nemoguardrails/library/f5/flows.co: f5_guardrails_scan (via execute) can raise RuntimeError when fail-open is disabled and the API call fails, and this subflow has no handling for that case, so it would propagate as an unhandled error rather than triggering bot refuse to respond / stop.

🤖 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/f5/flows.v1.co` around lines 1 - 15, The F5 scan
subflows in f5 guardrails scan input and f5 guardrails scan output call execute
f5_guardrails_scan without handling RuntimeError when fail-open is disabled and
the API call fails. Wrap the execute calls in these subflows with the same error
handling used in the Colang v2 flow, so failures fall back to bot refuse to
respond and stop instead of propagating as unhandled exceptions. Use the
existing subflow names and the f5_guardrails_scan invocation points to place the
fix.
🤖 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 `@docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.md`:
- Around line 20-24: The F5 Guardrails configuration docs are missing the
fail-open environment variable, so update the config variables list in the F5
Guardrails section to include F5_GUARDRAILS_FAIL_OPEN alongside
F5_GUARDRAILS_API_KEY and F5_GUARDRAILS_API_URL. Make sure the new entry clearly
states that it enables allow-through behavior when the API errors, using the
existing integration wording so readers can discover and set it correctly.

In `@nemoguardrails/library/f5/actions.py`:
- Around line 75-77: The F5 API failure logging in actions.py is writing the
full response body from response.text() into logs, which can expose sensitive
user or request data. Update the error handling in the F5 Guardrails API call
path to avoid logging raw error bodies: in the block around response.status and
error_detail, truncate, redact, or otherwise sanitize the logged detail before
passing it to log.error. Keep the log message useful for debugging, but ensure
any response content is safely limited and non-sensitive.
- Around line 73-98: The `send_to_f5_guardrails` flow is catching its own
deliberate `RuntimeError` from the non-200 response path and logging it as a
connection failure. Narrow the `except` in `actions.py` so it only handles
actual request/network errors from `session.post` (for example, the aiohttp
client exception path), and let the HTTP-status `RuntimeError` raised after the
`response.status != 200` check propagate without being re-caught; keep the
existing non-200 log in place and update the exception logging to use `e!s`
instead of `str(e)`.

In `@tests/test_f5_guardrails.py`:
- Around line 121-126: The mocked POST for the scans API is only consumed once,
so the second call in this test can fall through and hit an unmatched-route
error instead of the intended 500. Update the aioresponses stub in the test
around the scans POST so it repeats for both input and output scan calls by
enabling repeat behavior on that mock.

---

Outside diff comments:
In `@nemoguardrails/library/f5/flows.v1.co`:
- Around line 1-17: The file needs a proper trailing newline to satisfy the
end-of-file-fixer lint check. Update the `nemoguardrails/library/f5/flows.v1.co`
content so it ends with a single newline character after the last `stop` in the
`f5 guardrails scan output` subflow, keeping the `define subflow` blocks
unchanged.

---

Duplicate comments:
In `@nemoguardrails/library/f5/flows.v1.co`:
- Around line 1-15: The F5 scan subflows in f5 guardrails scan input and f5
guardrails scan output call execute f5_guardrails_scan without handling
RuntimeError when fail-open is disabled and the API call fails. Wrap the execute
calls in these subflows with the same error handling used in the Colang v2 flow,
so failures fall back to bot refuse to respond and stop instead of propagating
as unhandled exceptions. Use the existing subflow names and the
f5_guardrails_scan invocation points to place the fix.
🪄 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: 0ba8018f-b2f3-4b96-8d43-8f967f4d6465

📥 Commits

Reviewing files that changed from the base of the PR and between 79236fc and d3b3870.

📒 Files selected for processing (12)
  • docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.md
  • examples/configs/f5_guardrails/README.md
  • examples/configs/f5_guardrails/config.yml
  • examples/configs/f5_guardrails_v2/README.md
  • examples/configs/f5_guardrails_v2/config.yml
  • examples/configs/f5_guardrails_v2/main.co
  • examples/configs/f5_guardrails_v2/rails.co
  • nemoguardrails/library/f5/__init__.py
  • nemoguardrails/library/f5/actions.py
  • nemoguardrails/library/f5/flows.co
  • nemoguardrails/library/f5/flows.v1.co
  • tests/test_f5_guardrails.py

Comment on lines +20 to +24
The following environment variables can be used to configure the integration:

- `F5_GUARDRAILS_API_KEY`: The API key for the F5 Guardrails API.
- `F5_GUARDRAILS_API_URL`: The base URL for the F5 Guardrails API (defaults to https://us1.calypsoai.app).

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the fail-open toggle.

F5_GUARDRAILS_FAIL_OPEN is used by the integration but isn't documented here, so readers won't know how to enable the allow-through behavior on API errors.

Update the config variables list
- `F5_GUARDRAILS_API_URL`: The base URL for the F5 Guardrails API (defaults to https://us1.calypsoai.app).
+ `F5_GUARDRAILS_API_URL`: The base URL for the F5 Guardrails API (defaults to `https://us1.calypsoai.app`).
+ - `F5_GUARDRAILS_FAIL_OPEN`: When `true`, API failures are treated as cleared and the content is allowed through.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The following environment variables can be used to configure the integration:
- `F5_GUARDRAILS_API_KEY`: The API key for the F5 Guardrails API.
- `F5_GUARDRAILS_API_URL`: The base URL for the F5 Guardrails API (defaults to https://us1.calypsoai.app).
The following environment variables can be used to configure the integration:
- `F5_GUARDRAILS_API_KEY`: The API key for the F5 Guardrails API.
- `F5_GUARDRAILS_API_URL`: The base URL for the F5 Guardrails API (defaults to `https://us1.calypsoai.app`).
- `F5_GUARDRAILS_FAIL_OPEN`: When `true`, API failures are treated as cleared and the content is allowed through.
🤖 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 `@docs/configure-rails/guardrail-catalog/community/f5-ai-guardrails.md` around
lines 20 - 24, The F5 Guardrails configuration docs are missing the fail-open
environment variable, so update the config variables list in the F5 Guardrails
section to include F5_GUARDRAILS_FAIL_OPEN alongside F5_GUARDRAILS_API_KEY and
F5_GUARDRAILS_API_URL. Make sure the new entry clearly states that it enables
allow-through behavior when the API errors, using the existing integration
wording so readers can discover and set it correctly.

Comment thread nemoguardrails/library/f5/actions.py Outdated
Comment on lines +73 to +98
try:
async with session.post(endpoint, headers=headers, json=payload) as response:
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")

if fail_open:
log.warning(
"F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

raise RuntimeError(f"F5 Guardrails API error: {response.status}")

result = await response.json()
return result
except Exception as e:
log.error(f"Error connecting to F5 Guardrails API: {str(e)}")

if fail_open:
log.warning("F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

if isinstance(e, aiohttp.ClientError):
raise RuntimeError(f"Connection error to F5 Guardrails API: {str(e)}") from e

raise e

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Deliberate RuntimeError (non-200 status) gets re-caught and mis-logged as a connection error.

The raise RuntimeError(...) at line 84 is raised inside the same try block guarded by except Exception as e (line 88). When fail_open is False and the API returns a non-200 status, this RuntimeError propagates into the except handler, which logs it as "Error connecting to F5 Guardrails API: ..." (line 89) — duplicating the earlier, more accurate log at line 77 and mischaracterizing an HTTP error response as a connection failure. Final behavior is unchanged (the error is re-raised via raise e), but the log output is confusing/misleading and complicates debugging. Also addressing Ruff RUF010: use {e!s} instead of {str(e)}.

🐛 Proposed fix: scope the except to actual network errors
     timeout = aiohttp.ClientTimeout(total=30)
-    async with aiohttp.ClientSession(timeout=timeout) as session:
-        try:
+    try:
+        async with aiohttp.ClientSession(timeout=timeout) as session:
             async with session.post(endpoint, headers=headers, json=payload) as response:
                 if response.status != 200:
                     error_detail = await response.text()
                     log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")
 
                     if fail_open:
                         log.warning(
                             "F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
                         return {"result": {"outcome": "cleared"}}
 
                     raise RuntimeError(f"F5 Guardrails API error: {response.status}")
 
                 result = await response.json()
                 return result
-        except Exception as e:
-            log.error(f"Error connecting to F5 Guardrails API: {str(e)}")
-
-            if fail_open:
-                log.warning("F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
-                return {"result": {"outcome": "cleared"}}
-
-            if isinstance(e, aiohttp.ClientError):
-                raise RuntimeError(f"Connection error to F5 Guardrails API: {str(e)}") from e
-
-            raise e
+    except (aiohttp.ClientError, asyncio.TimeoutError) as e:
+        log.error(f"Error connecting to F5 Guardrails API: {e!s}")
+
+        if fail_open:
+            log.warning("F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
+            return {"result": {"outcome": "cleared"}}
+
+        raise RuntimeError(f"Connection error to F5 Guardrails API: {e!s}") from e

Requires import asyncio at the top of the file.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 89-89: Use explicit conversion flag

Replace with conversion flag

(RUF010)


[warning] 96-96: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🤖 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/f5/actions.py` around lines 73 - 98, The
`send_to_f5_guardrails` flow is catching its own deliberate `RuntimeError` from
the non-200 response path and logging it as a connection failure. Narrow the
`except` in `actions.py` so it only handles actual request/network errors from
`session.post` (for example, the aiohttp client exception path), and let the
HTTP-status `RuntimeError` raised after the `response.status != 200` check
propagate without being re-caught; keep the existing non-200 log in place and
update the exception logging to use `e!s` instead of `str(e)`.

Source: Linters/SAST tools

Comment thread nemoguardrails/library/f5/actions.py Outdated
Comment on lines +75 to +77
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Logging raw API error body may leak sensitive content.

error_detail = await response.text() is logged verbatim. If the F5 API echoes the submitted text (or account/request metadata) in its error response, this writes potentially sensitive user content into application logs.

🛡️ Proposed fix: truncate/redact logged error detail
             if response.status != 200:
                 error_detail = await response.text()
-                log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")
+                log.error(f"F5 Guardrails API call failed with status {response.status}")
+                log.debug(f"F5 Guardrails API error detail: {error_detail[:200]}")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail}")
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed with status {response.status}")
log.debug(f"F5 Guardrails API error detail: {error_detail[:200]}")
🤖 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/f5/actions.py` around lines 75 - 77, The F5 API
failure logging in actions.py is writing the full response body from
response.text() into logs, which can expose sensitive user or request data.
Update the error handling in the F5 Guardrails API call path to avoid logging
raw error bodies: in the block around response.status and error_detail,
truncate, redact, or otherwise sanitize the logged detail before passing it to
log.error. Keep the log message useful for debugging, but ensure any response
content is safely limited and non-sensitive.

Comment on lines +121 to +126
with aioresponses() as m:
# Simulate an API error (e.g., 500)
m.post(
"https://us1.calypsoai.app/backend/v1/scans",
status=500,
)

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

# Locate the test file and nearby related tests
git ls-files tests/test_f5_guardrails.py

# Show the relevant section with line numbers
sed -n '1,220p' tests/test_f5_guardrails.py | cat -n

# Search for related scan mock patterns in the same file
rg -n "repeat=True|backend/v1/scans|aioresponses|fail-open|500" tests/test_f5_guardrails.py

Repository: NVIDIA-NeMo/Guardrails

Length of output: 5400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the F5 guardrails implementation and the test helper used here.
rg -n "f5 guardrails scan input|f5 guardrails scan output|backend/v1/scans|fail_open|F5_GUARDRAILS_FAIL_OPEN" -S nemoguardrails tests

# Map the relevant test helper and implementation files before reading them.
fd -a "test_chat.py|f5|guardrails" nemoguardrails tests

# If aioresponses is installed, inspect its route-matching semantics directly.
python3 - <<'PY'
try:
    import aioresponses
    import inspect
    from aioresponses import core
    print("aioresponses module:", aioresponses.__file__)
    print("core module:", core.__file__)
    print("aioresponses.Aioresponses source excerpt:")
    src = inspect.getsource(aioresponses.aioresponses)
    for i, line in enumerate(src.splitlines(), 1):
        if "repeat" in line or "match" in line or "assert" in line:
            print(f"{i}: {line}")
except Exception as e:
    print("IMPORT_ERROR:", repr(e))
PY

Repository: NVIDIA-NeMo/Guardrails

Length of output: 7399


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the F5 action implementation with line numbers.
sed -n '1,220p' nemoguardrails/library/f5/actions.py | cat -n

# Inspect the related flow definitions to confirm both input and output scan hooks.
sed -n '1,120p' nemoguardrails/library/f5/flows.co | cat -n
sed -n '1,120p' nemoguardrails/library/f5/flows.v1.co | cat -n

# Check whether the repo pins aioresponses and, if so, the version.
rg -n "aioresponses" pyproject.toml poetry.lock requirements*.txt setup.cfg setup.py

Repository: NVIDIA-NeMo/Guardrails

Length of output: 6211


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the TestChat helper.
fd -a "utils.py" tests
rg -n "class TestChat|def __rshift__|def __lshift__|send_message|llm_completions" tests/utils.py tests -S

# Read the helper file if present.
sed -n '1,260p' tests/utils.py | cat -n

Repository: NVIDIA-NeMo/Guardrails

Length of output: 47526


🌐 Web query:

aioresponses default behavior single mocked request consumed once repeat=False unmatched route raises connection error documentation

💡 Result:

In aioresponses, the default behavior for a mocked request is to be consumed exactly once [1][2]. This is controlled by the repeat parameter, which defaults to False (equivalent to repeat=1) [1][3]. If you need to mock a request that should be handled multiple times, you must explicitly set the repeat argument [1][4]: * repeat=n: Repeats the response n times [1][3]. * repeat=True: Repeats the response indefinitely [1][3]. Regarding unmatched routes, aioresponses defaults to raising an aiohttp.client_exceptions.ClientConnectionError when a request is made that does not match any configured mock [5]. To change this behavior and allow unmatched requests to proceed, you can initialize aioresponses with the passthrough_unmatched=True flag [6]: @aioresponses(passthrough_unmatched=True) def test_passthrough(m): # Requests not matching defined mocks will now be allowed to pass through... Alternatively, you can provide a list of URLs to the passthrough argument if you only want to allow specific requests to bypass the mocking system [6].

Citations:


Make the 500 mock repeatable here

This test hits both input and output scans, but aioresponses consumes the mocked POST only once by default. The second scan can fall through to an unmatched-route connection error, so the fail-open path may pass without actually exercising a second 500. Add repeat=True to keep both calls on the intended response.

🤖 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/test_f5_guardrails.py` around lines 121 - 126, The mocked POST for the
scans API is only consumed once, so the second call in this test can fall
through and hit an unmatched-route error instead of the intended 500. Update the
aioresponses stub in the test around the scans POST so it repeats for both input
and output scan calls by enabling repeat behavior on that mock.

Comment on lines +71 to +95
timeout = aiohttp.ClientTimeout(total=30)
async with aiohttp.ClientSession(timeout=timeout) as session:
try:
async with session.post(endpoint, headers=headers, json=payload) as response:
if response.status != 200:
error_detail = await response.text()
log.error(f"F5 Guardrails API call failed: {response.status} - {error_detail[:200]}")

if fail_open:
log.warning(
"F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

raise RuntimeError(f"F5 Guardrails API error: {response.status}")

result = await response.json()
return result
except aiohttp.ClientError as e:
log.error(f"Error connecting to F5 Guardrails API: {str(e)}")

if fail_open:
log.warning("F5 Guardrails API call failed, but F5_GUARDRAILS_FAIL_OPEN is enabled, allowing content.")
return {"result": {"outcome": "cleared"}}

raise RuntimeError(f"Connection error to F5 Guardrails API: {str(e)}") from e

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 Timeout not caught — fail_open silently fails on request timeout

aiohttp raises asyncio.TimeoutError (not a subclass of aiohttp.ClientError) when the ClientTimeout.total of 30 seconds is exceeded. The except aiohttp.ClientError block therefore does not catch it, so a slow API will cause the exception to propagate uncaught regardless of whether fail_open=True was set. Another integration in this codebase — nemoguardrails/library/jailbreak_detection/request.py line 146 — catches asyncio.TimeoutError explicitly for exactly this reason. Add a separate except asyncio.TimeoutError handler with the same fail-open / raise logic applied to aiohttp.ClientError.

Prompt To Fix With AI
This is a comment left during a code review.
Path: nemoguardrails/library/f5/actions.py
Line: 71-95

Comment:
**Timeout not caught — `fail_open` silently fails on request timeout**

`aiohttp` raises `asyncio.TimeoutError` (not a subclass of `aiohttp.ClientError`) when the `ClientTimeout.total` of 30 seconds is exceeded. The `except aiohttp.ClientError` block therefore does not catch it, so a slow API will cause the exception to propagate uncaught regardless of whether `fail_open=True` was set. Another integration in this codebase — `nemoguardrails/library/jailbreak_detection/request.py` line 146 — catches `asyncio.TimeoutError` explicitly for exactly this reason. Add a separate `except asyncio.TimeoutError` handler with the same fail-open / raise logic applied to `aiohttp.ClientError`.

How can I resolve this? If you propose a fix, please make it concise.

NVIDIA-NeMo#2105 (comment)

```
<a href="#"><img alt="P1" src="https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=9" align="top"></a> **Timeout not caught — `fail_open` silently fails on request timeout**

`aiohttp` raises `asyncio.TimeoutError` (not a subclass of `aiohttp.ClientError`) when the `ClientTimeout.total` of 30 seconds is exceeded. The `except aiohttp.ClientError` block therefore does not catch it, so a slow API will cause the exception to propagate uncaught regardless of whether `fail_open=True` was set. Another integration in this codebase — `nemoguardrails/library/jailbreak_detection/request.py` line 146 — catches `asyncio.TimeoutError` explicitly for exactly this reason. Add a separate `except asyncio.TimeoutError` handler with the same fail-open / raise logic applied to `aiohttp.ClientError`.

<details><summary>Prompt To Fix With AI</summary>

`````markdown
This is a comment left during a code review.
Path: nemoguardrails/library/f5/actions.py
Line: 71-95

Comment:
**Timeout not caught — `fail_open` silently fails on request timeout**

`aiohttp` raises `asyncio.TimeoutError` (not a subclass of `aiohttp.ClientError`) when the `ClientTimeout.total` of 30 seconds is exceeded. The `except aiohttp.ClientError` block therefore does not catch it, so a slow API will cause the exception to propagate uncaught regardless of whether `fail_open=True` was set. Another integration in this codebase — `nemoguardrails/library/jailbreak_detection/request.py` line 146 — catches `asyncio.TimeoutError` explicitly for exactly this reason. Add a separate `except asyncio.TimeoutError` handler with the same fail-open / raise logic applied to `aiohttp.ClientError`.

How can I resolve this? If you propose a fix, please make it concise.
`````
</details>
```
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