Skip to content

fix(guardrails): restore PII masking and contain output failures - #358

Merged
rapids-bot[bot] merged 7 commits into
NVIDIA-AI-Blueprints:release/2.2from
tanleach:fix/aiq-015-pii-guardrails
Jul 17, 2026
Merged

rapids-bot[bot] merged 7 commits into
NVIDIA-AI-Blueprints:release/2.2from
tanleach:fix/aiq-015-pii-guardrails

Conversation

@tanleach

@tanleach tanleach commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

Restores supported PII masking in the AI-Q 2.2 release image and makes output guardrail failures fail closed instead of escaping as generic internal errors.

The release runtime previously included NeMo Guardrails without the Presidio, spaCy, and language-model dependencies required by sensitive_data_detection. Because those imports are lazy, the service could start healthy but fail every response once output PII masking ran. Output-rail exceptions could also escape the middleware boundary.

What changed

  • Add a dedicated pii runtime extra containing the NAT Security defense dependencies and pinned en-core-web-lg==3.8.0 model.
  • Install the pii extra in the release image and execute the public NeMo mask_sensitive_data action during the Docker build.
  • Contain output-rail binding, traversal, evaluation, rewriting, and terminal-synchronization failures at the shared middleware boundary.
  • Return traversal-independent, schema-compatible emergency refusals for workflow, shallow-agent, and deep-agent outputs.
  • Sanitize guardrail exception logs so they include safe boundary context and exception type without arbitrary exception text or protected output.
  • Buffer and guard string, structured, and mixed string/structured streams as one logical output before emission.
  • Synchronize every rewritten structured chunk's workflow_outcome.result so later chunks cannot retain unfiltered content.
  • Keep workflow-specific terminal synchronization in the workflow middleware rather than the generic guardrails mixin.
  • Keep immutable application assets root-owned while making /app/data writable by runtime UID 1000.
  • Document stream behavior and the optional PII runtime dependency contract.

DCO sign-off for the squash commit

Signed-off-by: Tanner Leach tleach@nvidia.com

Validation

  • uv run ruff check .
  • uv run ruff format --check . — 370 files
  • uv run pytest -q tests/aiq_agent/guardrails — 99 passed
  • uv run pytest -q — 1788 passed, 13 skipped
  • uv run nat --help and uv run aiq-research --help
  • Sphinx documentation build with warnings treated as errors
  • Release Docker build
  • In-image public NeMo mask_sensitive_data action returned Contact <EMAIL_ADDRESS>
  • Runtime UID 1000 could read/execute application assets and create/read/delete data under /app/data
  • Local release-equivalent deployment returned HTTP 200 for health and chat
  • Benign output passed unchanged in both the public choice and terminal workflow outcome
  • Email output was masked in both fields; the serialized response contained no raw email
  • Forced rail failures returned boundary-compatible refusals in deterministic regular and streaming tests
  • Captured middleware exception logs excluded protected output and internal exception messages
  • Commits are GPG-signed and DCO-signed as Tanner Leach <tleach@nvidia.com>
  • Documentation updated for user-facing behavior and installation requirements
  • Confirmed the PR contains no secrets or credentials

Formal QA cases 26.07_GUARD_001 and 26.07_GUARD_002 remain to be run against a published corrected image.

Where should reviewers start?

  1. src/aiq_agent/guardrails/interface/middleware.py — shared exception containment and buffered stream handling.
  2. src/aiq_agent/guardrails/workflow/middleware.py — workflow-shaped emergency refusals and terminal-outcome synchronization.
  3. tests/aiq_agent/guardrails/test_workflow_guardrails.py — multi-chunk, mixed-order stream, synchronization-failure, no-target, and PII-safe logging regressions.
  4. pyproject.toml and deploy/Dockerfile — PII extra and release-image verification.

Related Issues

  • Relates to AIQ-3572 / NVBug 6454040

Summary by CodeRabbit

  • New Features
    • Added an optional PII dependency bundle (Presidio defense) with a compatible spaCy model.
    • Improved guardrails to emit schema-safe emergency refusal states during blocked/error scenarios.
  • Bug Fixes
    • Prevent sensitive/unfiltered content from being returned when input/output guardrails evaluation fails.
    • Improved workflow and streaming output sanitization, including synchronization across mixed text and structured responses.
  • Documentation
    • Expanded guardrails failure behavior and PII runtime requirements.
  • Tests
    • Added/extended coverage for deep, shallow, and workflow guardrails refusal and masking paths.

@tanleach
tanleach requested a review from ericevans-nv July 16, 2026 15:51
@copy-pr-bot

copy-pr-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 88efdc50-2b6c-4287-898c-406aba90aaee

📥 Commits

Reviewing files that changed from the base of the PR and between 414c721 and 58652b7.

⛔ Files ignored due to path filters (1)
  • mcp/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • mcp/tests/test_config_and_packaging.py
📜 Recent review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Run Harbor skill eval
  • GitHub Check: Pytest and Coverage
  • GitHub Check: Lint and Hooks
  • GitHub Check: Script Validation
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run ruff check and ruff format validation for Python code changes

**/*.py: Python code must be linted and formatted with Ruff using line length 120, target Python 3.11, rule sets E,F,W,I,PL,UP, and isort force-single-line configuration
Never commit secrets, tokens, or environment-specific hostnames; use environment variables and SecretStr instead, resolving API keys at runtime
Never print or log secret values, including in tool output or error messages
Missing-secret paths must degrade gracefully (stub/skip), not crash or leak
Do not hand-reformat unrelated code when making changes; match the existing import and formatting style

Files:

  • mcp/tests/test_config_and_packaging.py
**/*test*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run pytest for all behavior changes in Python code

Files:

  • mcp/tests/test_config_and_packaging.py
**/*config*.py

📄 CodeRabbit inference engine (AGENTS.md)

Config schemas must inherit from FunctionBaseConfig and YAML _type names must come from the registered config class

Files:

  • mcp/tests/test_config_and_packaging.py
🔇 Additional comments (1)
mcp/tests/test_config_and_packaging.py (1)

153-162: LGTM!


Walkthrough

Guardrails middleware now converts execution, traversal, adaptation, and streaming failures into safe refusals while synchronizing guarded workflow outputs. The project also adds PII dependencies, Docker validation, packaging checks, documentation, and regression tests.

Changes

Guardrails failure safety

Layer / File(s) Summary
Middleware failure and streaming paths
src/aiq_agent/guardrails/interface/middleware.py
Guardrails execution catches failures, logs exception types, builds emergency refusals, handles mixed buffered chunks, and synchronizes guarded outputs.
Agent and workflow refusal adapters
src/aiq_agent/guardrails/{deep_agent,shallow_agent,workflow}/middleware.py
Agent-specific refusal states and workflow terminal-output synchronization preserve response schemas while preventing protected-output leakage.
Failure and streaming regression coverage
tests/aiq_agent/guardrails/*, docs/source/customization/guardrails.md
Tests cover refusal, traversal, logging, terminal synchronization, mixed streaming, and raw-output suppression; documentation records the runtime behavior.

PII runtime dependency validation

Layer / File(s) Summary
PII extra and image validation
pyproject.toml, deploy/Dockerfile, docs/source/customization/guardrails.md, mcp/tests/test_config_and_packaging.py
The pii extra adds the security package and spaCy model; Docker installs and validates masking and model availability, while documentation and packaging tests describe and verify the direct model dependency.

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

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant GuardrailsMixin
  participant OutputRails
  participant WorkflowOutput
  Agent->>GuardrailsMixin: invoke or stream output
  GuardrailsMixin->>OutputRails: evaluate selected content
  OutputRails-->>GuardrailsMixin: masked, blocked, or failed result
  GuardrailsMixin->>WorkflowOutput: synchronize buffered and terminal text
  GuardrailsMixin-->>Agent: sanitized output or refusal-only response
Loading

Possibly related PRs

  • NVIDIA-AI-Blueprints/aiq#292: Modifies the same Guardrails middleware boundaries for blocked, failed, and traversal-related output handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits and accurately summarizes the main guardrails and PII masking changes.
Description check ✅ Passed The description covers overview, DCO sign-off, validation, reviewer guidance, and related issues, matching the template closely.
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.

tanleach added 3 commits July 16, 2026 15:46
Signed-off-by: Tanner Leach <tleach@nvidia.com>
Signed-off-by: Tanner Leach <tleach@nvidia.com>
Signed-off-by: Tanner Leach <tleach@nvidia.com>
@tanleach
tanleach force-pushed the fix/aiq-015-pii-guardrails branch from 23076e8 to 6f22960 Compare July 16, 2026 19:59
@copy-pr-bot

copy-pr-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@tanleach
tanleach marked this pull request as ready for review July 16, 2026 19:59
@tanleach
tanleach requested a review from a team July 16, 2026 19:59
Signed-off-by: Tanner Leach <tleach@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/aiq_agent/guardrails/interface/middleware.py`:
- Around line 126-137: Move consumption of call_next into the same try block as
_apply_output_rails_to_structured_stream by buffering it with an explicit async
for, so producer exceptions—including failures after protected chunks—enter the
refusal path. When refusing, use the first buffered chunk when available and
handle an empty buffer safely while preserving the existing logging and
response-yield behavior.

In `@src/aiq_agent/guardrails/workflow/middleware.py`:
- Around line 168-175: Change the input-target extraction flow used by
pre_invoke so exceptions from traversing or reading the input are handled by its
existing failure boundary and propagate as _GUARDRAILS_FAILURE_REFUSAL, rather
than being caught and converted to an empty target list. Remove the local
swallowing behavior around the extraction logic, and add a regression test
covering an input property that raises.
🪄 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: ASSERTIVE

Plan: Enterprise

Run ID: 892e5e29-4c40-447e-baa5-4a3b716832ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1dd1a57 and 6f22960.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • deploy/Dockerfile
  • docs/source/customization/guardrails.md
  • pyproject.toml
  • src/aiq_agent/guardrails/deep_agent/middleware.py
  • src/aiq_agent/guardrails/interface/middleware.py
  • src/aiq_agent/guardrails/shallow_agent/middleware.py
  • src/aiq_agent/guardrails/workflow/middleware.py
  • tests/aiq_agent/guardrails/test_deep_agent_guardrails.py
  • tests/aiq_agent/guardrails/test_shallow_agent_guardrails.py
  • tests/aiq_agent/guardrails/test_workflow_guardrails.py
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Run Harbor skill eval
  • GitHub Check: Lint and Hooks
  • GitHub Check: Script Validation
  • GitHub Check: Pytest and Coverage
🧰 Additional context used
📓 Path-based instructions (7)
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run ruff check and ruff format validation for Python code changes

**/*.py: Python code must be linted and formatted with Ruff using line length 120, target Python 3.11, rule sets E,F,W,I,PL,UP, and isort force-single-line configuration
Never commit secrets, tokens, or environment-specific hostnames; use environment variables and SecretStr instead, resolving API keys at runtime
Never print or log secret values, including in tool output or error messages
Missing-secret paths must degrade gracefully (stub/skip), not crash or leak
Do not hand-reformat unrelated code when making changes; match the existing import and formatting style

Files:

  • src/aiq_agent/guardrails/shallow_agent/middleware.py
  • tests/aiq_agent/guardrails/test_shallow_agent_guardrails.py
  • src/aiq_agent/guardrails/deep_agent/middleware.py
  • src/aiq_agent/guardrails/workflow/middleware.py
  • tests/aiq_agent/guardrails/test_deep_agent_guardrails.py
  • tests/aiq_agent/guardrails/test_workflow_guardrails.py
  • src/aiq_agent/guardrails/interface/middleware.py
src/aiq_agent/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/aiq_agent/**/*.py: Respect authenticated data sources by honoring requires_auth, per-user token pass-through, and backend token validators; apply owner guardrails before loading protected report or artifact context into an agent
Do not weaken or bypass AuthMiddleware, validators, or auth gating without a prior design discussion

Files:

  • src/aiq_agent/guardrails/shallow_agent/middleware.py
  • src/aiq_agent/guardrails/deep_agent/middleware.py
  • src/aiq_agent/guardrails/workflow/middleware.py
  • src/aiq_agent/guardrails/interface/middleware.py
{.github/**,ci/**,.pre-commit-config.yaml,pyproject.toml,uv.lock,mcp/pyproject.toml,mcp/uv.lock}

⚙️ CodeRabbit configuration file

{.github/**,ci/**,.pre-commit-config.yaml,pyproject.toml,uv.lock,mcp/pyproject.toml,mcp/uv.lock}: Review automation and packaging changes for least-privilege permissions, pinned versions where appropriate,
copy-pr-bot pull-request/ branch behavior, reproducible uv/npm setup, secret handling, and consistency with
the documented validation matrix.

Files:

  • pyproject.toml
docs/source/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Update the docs under docs/source/ when behavior, configuration, or workflows change

Files:

  • docs/source/customization/guardrails.md
{docs/**,README.md,CONTRIBUTING.md,SECURITY.md,CODE-OF-CONDUCT.md}

⚙️ CodeRabbit configuration file

{docs/**,README.md,CONTRIBUTING.md,SECURITY.md,CODE-OF-CONDUCT.md}: Review documentation for command accuracy, branch-name consistency, current CI and copy-pr-bot behavior, public
vs internal boundary clarity, stale examples, and links that no longer match the repository layout.

Files:

  • docs/source/customization/guardrails.md
**/*test*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run pytest for all behavior changes in Python code

Files:

  • tests/aiq_agent/guardrails/test_shallow_agent_guardrails.py
  • tests/aiq_agent/guardrails/test_deep_agent_guardrails.py
  • tests/aiq_agent/guardrails/test_workflow_guardrails.py
{deploy/**,configs/**}

⚙️ CodeRabbit configuration file

{deploy/**,configs/**}: Review deployment and config changes for secret separation, safe defaults, local-vs-production behavior, Helm and
Docker portability, and documentation parity. Flag committed credentials, environment-specific NVIDIA internals in
public defaults, and changes that make examples diverge from CI-tested paths.

Files:

  • deploy/Dockerfile
🪛 ast-grep (0.44.1)
tests/aiq_agent/guardrails/test_workflow_guardrails.py

[info] 941-941: use jsonify instead of json.dumps for JSON output
Context: json.dumps([item.model_dump(mode="json") for item in results])
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)


[info] 1077-1077: use jsonify instead of json.dumps for JSON output
Context: json.dumps([item if isinstance(item, str) else item.model_dump(mode="json") for item in results])
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🪛 Trivy (0.72.0)
deploy/Dockerfile

[error] 79-82: 'apt-get' missing '--no-install-recommends'

'--no-install-recommends' flag is missed: 'apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*'

Rule: DS-0029

Learn more

(IaC/Dockerfile)

🔇 Additional comments (11)
pyproject.toml (1)

56-59: LGTM!

deploy/Dockerfile (2)

79-79: LGTM!


93-99: LGTM!

src/aiq_agent/guardrails/interface/middleware.py (1)

52-56: LGTM!

Also applies to: 73-86, 164-214, 228-256

src/aiq_agent/guardrails/deep_agent/middleware.py (1)

28-28: LGTM!

Also applies to: 138-145

src/aiq_agent/guardrails/shallow_agent/middleware.py (1)

27-27: LGTM!

Also applies to: 138-145

src/aiq_agent/guardrails/workflow/middleware.py (1)

29-33: LGTM!

Also applies to: 103-152

tests/aiq_agent/guardrails/test_deep_agent_guardrails.py (1)

22-26: LGTM!

Also applies to: 385-442

tests/aiq_agent/guardrails/test_shallow_agent_guardrails.py (1)

22-26: LGTM!

Also applies to: 397-454

tests/aiq_agent/guardrails/test_workflow_guardrails.py (1)

29-34: LGTM!

Also applies to: 76-80, 643-853, 900-1090, 1138-1232

docs/source/customization/guardrails.md (1)

37-38: LGTM!

Also applies to: 45-50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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 `@mcp/tests/test_config_and_packaging.py`:
- Around line 153-161: The URL-source branch in the lockfile test should also
validate the package version. Within the existing assertions in
test_config_and_packaging, add an assertion that package["version"] equals
"3.8.0", while preserving the current package name and URL checks.
🪄 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: ASSERTIVE

Plan: Enterprise

Run ID: 067e6328-1ecb-4ef9-856d-ca6f4ffb1ac7

📥 Commits

Reviewing files that changed from the base of the PR and between 6f22960 and 6546522.

⛔ Files ignored due to path filters (1)
  • mcp/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • mcp/tests/test_config_and_packaging.py
📜 Review details
⏰ Context from checks skipped due to timeout. (4)
  • GitHub Check: Validate skill-eval specs
  • GitHub Check: Script Validation
  • GitHub Check: Pytest and Coverage
  • GitHub Check: Lint and Hooks
🧰 Additional context used
📓 Path-based instructions (3)
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run ruff check and ruff format validation for Python code changes

**/*.py: Python code must be linted and formatted with Ruff using line length 120, target Python 3.11, rule sets E,F,W,I,PL,UP, and isort force-single-line configuration
Never commit secrets, tokens, or environment-specific hostnames; use environment variables and SecretStr instead, resolving API keys at runtime
Never print or log secret values, including in tool output or error messages
Missing-secret paths must degrade gracefully (stub/skip), not crash or leak
Do not hand-reformat unrelated code when making changes; match the existing import and formatting style

Files:

  • mcp/tests/test_config_and_packaging.py
**/*test*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run pytest for all behavior changes in Python code

Files:

  • mcp/tests/test_config_and_packaging.py
**/*config*.py

📄 CodeRabbit inference engine (AGENTS.md)

Config schemas must inherit from FunctionBaseConfig and YAML _type names must come from the registered config class

Files:

  • mcp/tests/test_config_and_packaging.py

Comment thread mcp/tests/test_config_and_packaging.py Resolved

@KyleZheng1284 KyleZheng1284 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.

left a few comments otherwise LGTM

tanleach and others added 3 commits July 17, 2026 16:04
Move workflow input-target extraction into the existing fail-closed boundary so unexpected traversal errors cannot bypass input rails. Add regression coverage proving the workflow and rail backend remain uninvoked and sensitive exception details stay out of logs.

Signed-off-by: Tanner Leach <tleach@nvidia.com>
Verify the root lock records en-core-web-lg at version 3.8.0 in addition to checking its package name and exact wheel URL.

Signed-off-by: Tanner Leach <tleach@nvidia.com>
@tanleach
tanleach removed the request for review from AjayThorve July 17, 2026 20:21
@tanleach

Copy link
Copy Markdown
Collaborator Author

/merge

@rapids-bot
rapids-bot Bot merged commit a4223b2 into NVIDIA-AI-Blueprints:release/2.2 Jul 17, 2026
11 checks passed
@tanleach
tanleach deleted the fix/aiq-015-pii-guardrails branch July 17, 2026 20:24
@AjayThorve AjayThorve added this to the v2.2 milestone Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants