Skip to content

LCORE-1259: Safety Shield configuration for query & streaming query#1100

Open
JslYoon wants to merge 4 commits intolightspeed-core:mainfrom
JslYoon:JslYoon-safety-shield-config
Open

LCORE-1259: Safety Shield configuration for query & streaming query#1100
JslYoon wants to merge 4 commits intolightspeed-core:mainfrom
JslYoon:JslYoon-safety-shield-config

Conversation

@JslYoon
Copy link
Contributor

@JslYoon JslYoon commented Feb 3, 2026

Description

As a developer using Lightspeed Core Stack, I want to specify which safety shields to apply per query, so that I can use different shield configurations for different use cases without restarting the service.

Updated query v1, query v2, and streaming query to take in an extra parameter to identify which shields to apply. If the request parameter is not provided, it will apply all safety shields (same as before).

Type of change

  • Refactor
  • New feature
  • Bug fix
  • CVE fix
  • Optimization
  • Documentation Update
  • Configuration Update
  • Bump-up service version
  • Bump-up dependent library
  • Bump-up library or tool used for development (does not change the final image)
  • CI configuration change
  • Konflux configuration change
  • Unit tests improvement
  • Integration tests improvement
  • End to end tests improvement

Tools used to create PR

Identify any AI code assistants used in this PR (for transparency and review context)

  • Assisted-by: Claude Code

Related Tickets & Documents

Checklist before requesting a review

  • I have performed a self-review of my code.
  • PR has passed all pre-merge test jobs.
  • If it is a core feature, I have added thorough tests.

Testing

  • Please provide detailed steps to perform tests related to this code change.
  • How were the fix/results from this change verified? Please provide relevant screenshots or results.

Summary by CodeRabbit

  • New Features

    • Added optional shield_ids parameter to query endpoints, enabling users to selectively apply specific safety shields or disable all shields for individual requests.
    • Added configuration option to restrict shield customization when needed.
  • Documentation

    • Updated documentation with shield customization guidance and configuration examples.

Signed-off-by: Lucas <lyoon@redhat.com>
Signed-off-by: Lucas <lyoon@redhat.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 3, 2026

Walkthrough

This PR adds support for overriding which safety shields are applied to query requests. It introduces a new optional shield_ids field to QueryRequest, implements validation and filtering logic, and integrates the capability across query endpoints with a configuration flag to disable the feature.

Changes

Cohort / File(s) Summary
Model & Configuration Updates
src/models/requests.py, src/models/config.py
Added shield_ids: Optional[list[str]] field to QueryRequest model with behavior documentation; added disable_shield_ids_override: bool = False flag to Customization config.
Core Shield Logic
src/utils/shields.py
Implemented validate_shield_ids_override() for enforcing configuration-based restrictions; modified run_shield_moderation() to filter shields based on provided shield_ids, with empty list skipping all shields and None using all configured shields. Added validation for invalid shield IDs with detailed error messaging.
Endpoint Integration
src/app/endpoints/query.py, src/app/endpoints/streaming_query.py, src/app/endpoints/a2a.py
Threaded shield_ids parameter from request through endpoint handlers to moderation logic; added validation calls in query endpoints to enforce override restrictions.
Test Coverage
tests/unit/utils/test_shields.py
Added 8 new async tests validating shield_ids filtering behavior (empty list, missing shields, specific shield selection) and validate_shield_ids_override() enforcement under various configuration states.
Documentation
README.md
Updated documentation with new shield_ids override configuration option and usage examples.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Handler as Endpoint Handler
    participant Validator as validate_shield_ids_override
    participant Moderation as run_shield_moderation
    participant ShieldAPI as Shield Service

    Client->>Handler: POST /query with shield_ids
    Handler->>Validator: validate_shield_ids_override(request, config)
    alt Override disabled & shield_ids provided
        Validator-->>Handler: HTTPException 422
        Handler-->>Client: Error Response
    else Override allowed or shield_ids not provided
        Validator-->>Handler: Valid
    end
    Handler->>Moderation: run_shield_moderation(..., shield_ids)
    alt shield_ids is empty list
        Moderation-->>Handler: Return (blocked=false)
    else shield_ids specified
        Moderation->>Moderation: Filter shields_to_run by shield_ids
        alt Invalid shield_ids
            Moderation-->>Handler: HTTPException 422
        else Valid shields found
            Moderation->>ShieldAPI: Apply filtered shields
            ShieldAPI-->>Moderation: Moderation results
            Moderation-->>Handler: ShieldModerationResult
        end
    else shield_ids is None
        Moderation->>ShieldAPI: Apply all configured shields
        ShieldAPI-->>Moderation: Moderation results
        Moderation-->>Handler: ShieldModerationResult
    end
    Handler-->>Client: Response with moderation applied
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • lightspeed-stack#979: Modifies shields moderation flow with changes to run_shield_moderation() and endpoint integrations, directly related to this shield override implementation.

Suggested reviewers

  • tisnik
  • manstis
🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (17 files):

⚔️ README.md (content)
⚔️ src/app/endpoints/a2a.py (content)
⚔️ src/app/endpoints/query.py (content)
⚔️ src/app/endpoints/streaming_query.py (content)
⚔️ src/authentication/rh_identity.py (content)
⚔️ src/constants.py (content)
⚔️ src/models/config.py (content)
⚔️ src/models/requests.py (content)
⚔️ src/models/responses.py (content)
⚔️ src/utils/mcp_headers.py (content)
⚔️ src/utils/responses.py (content)
⚔️ src/utils/shields.py (content)
⚔️ tests/benchmarks/test_app_database.py (content)
⚔️ tests/unit/app/endpoints/test_streaming_query.py (content)
⚔️ tests/unit/authentication/test_rh_identity.py (content)
⚔️ tests/unit/test_configuration.py (content)
⚔️ tests/unit/utils/test_shields.py (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main feature: adding safety shield configuration for query and streaming query endpoints, which aligns with the changeset's core objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch JslYoon-safety-shield-config
  • Post resolved changes as copyable diffs in a comment

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
src/models/config.py (1)

1277-1277: Consider adding a Field() descriptor for consistency with other config classes.

Most configuration fields in this file use Field() with title and description for OpenAPI/schema documentation. This field (and the adjacent disable_query_system_prompt) omit them. Not blocking, but adding metadata would improve discoverability and self-documentation.

Suggested improvement
-    disable_shield_ids_override: bool = False
+    disable_shield_ids_override: bool = Field(
+        False,
+        title="Disable shield IDs override",
+        description="When set to true, clients cannot specify shield_ids in query requests. "
+        "All configured shields will always be applied.",
+    )
src/utils/shields.py (2)

125-131: Unnecessary client.shields.list() call when shield_ids=[].

When shield_ids is an empty list, the intent is to skip all moderation. However, all_shields = await client.shields.list() on line 125 is always invoked before the early return on line 131. Consider moving the empty-list check before the API call to avoid an unnecessary round-trip.

♻️ Proposed fix
+    # Early return if shield_ids is explicitly empty (skip all shields)
+    if shield_ids is not None and len(shield_ids) == 0:
+        logger.info("shield_ids=[] provided, skipping all shields")
+        return ShieldModerationResult(blocked=False)
+
     all_shields = await client.shields.list()
 
     # Filter shields based on shield_ids parameter
     if shield_ids is not None:
-        if len(shield_ids) == 0:
-            logger.info("shield_ids=[] provided, skipping all shields")
-            return ShieldModerationResult(blocked=False)
-
         shields_to_run = [s for s in all_shields if s.identifier in shield_ids]

133-148: Consider using a set for shield_ids lookup to improve filtering performance.

Line 133 uses s.identifier in shield_ids where shield_ids is a list, making each lookup O(n). If the list could be large, converting to a set first would be more efficient. Minor optimization, but aligns with the set() already used on line 136-137.

♻️ Proposed fix
-        shields_to_run = [s for s in all_shields if s.identifier in shield_ids]
-
-        # Log warning if requested shield not found
-        requested = set(shield_ids)
+        requested = set(shield_ids)
+        shields_to_run = [s for s in all_shields if s.identifier in requested]
+
+        # Log warning if requested shield not found
         available = {s.identifier for s in shields_to_run}

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Contributor

@tisnik tisnik left a comment

Choose a reason for hiding this comment

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

Thank you for your PR. It seems ok, but we'd also need to introduce new configuration parameter that will enable or disable this behavior. Because let's say this is ok for your purposes, but other systems want not to allow any overrides like this. Please look at existing configuration option disable_query_system_prompt. Would you be so kind to add something similar to our PR? Thank you in advance.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/utils/shields.py`:
- Line 12: Remove the trailing whitespace after the import symbol
NotFoundResponse in src/utils/shields.py (the line containing
"NotFoundResponse,") so the line ends immediately after the comma; then re-run
formatting/linting (Black and pylint) to ensure C0303 is resolved and the file
is correctly formatted.
🧹 Nitpick comments (3)
src/utils/shields.py (3)

89-95: Avoid unnecessary API call when shield_ids is an empty list.

client.shields.list() is called on every invocation, even when shield_ids == [] leads to an immediate early return. Move the empty-list guard before the network call.

♻️ Suggested reorder
+    # Early exit: empty list means skip all shields
+    if shield_ids is not None and len(shield_ids) == 0:
+        logger.info("shield_ids=[] provided, skipping all shields")
+        return ShieldModerationResult(blocked=False)
+
     all_shields = await client.shields.list()
 
     # Filter shields based on shield_ids parameter
     if shield_ids is not None:
-        if len(shield_ids) == 0:
-            logger.info("shield_ids=[] provided, skipping all shields")
-            return ShieldModerationResult(blocked=False)
-
         shields_to_run = [s for s in all_shields if s.identifier in shield_ids]

97-97: Consider converting shield_ids to a set for O(1) membership lookups.

s.identifier in shield_ids performs a linear scan on a list. While the list is likely small, using a set is both idiomatic and future-proof.

-        shields_to_run = [s for s in all_shields if s.identifier in shield_ids]
+        shield_id_set = set(shield_ids)
+        shields_to_run = [s for s in all_shields if s.identifier in shield_id_set]

86-88: Docstring Raises section is incomplete — missing the 422 case.

The new code path on Lines 107-112 raises HTTPException (422) when none of the requested shield_ids are found. Document this alongside the existing 404 case.

… config

Signed-off-by: Lucas <lyoon@redhat.com>
@JslYoon
Copy link
Contributor Author

JslYoon commented Feb 13, 2026

Thank you for your PR. It seems ok, but we'd also need to introduce new configuration parameter that will enable or disable this behavior. Because let's say this is ok for your purposes, but other systems want not to allow any overrides like this. Please look at existing configuration option disable_query_system_prompt. Would you be so kind to add something similar to our PR? Thank you in advance.

Thank you for the great feedback @tisnik! I just made the changes, please let me know what you think.

@JslYoon JslYoon requested a review from tisnik February 14, 2026 00:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants