LCORE-2657: [Spike] Scope prompt guardrails (LCORE-230)#2182
LCORE-2657: [Spike] Scope prompt guardrails (LCORE-230)#2182max-svistunov wants to merge 10 commits into
Conversation
Add an LCS-native guardrails layer proof-of-concept validating the spike's Decision S1 (guardrails engine owned by lightspeed-stack rather than bound to the Llama Stack Safety API, which upstream OGX 1.x has removed). src/guardrails/ contains: - models.py: GuardrailRule/GuardrailPoint/detector/verdict Pydantic models, with rules binding a risk (OOTB id or custom bring-your-own-criteria definition) to input/output/tool_content points and a blocking flag. - granite_guardian.py: a detector invoking Granite Guardian via an OpenAI-compatible chat-completions endpoint, selecting the risk through the guardian chat template (system slot). All rules for a point run concurrently, mirroring the Ask Red Hat production pattern. - poc_loader.py: config loader gated entirely by the LCS_GUARDRAILS_POC_CONFIG environment variable, keeping the layer inert in every existing code path and test. query.py wires the three guardrail points, feeding the existing ShieldModerationResult seam so a guardrails block reuses the established refusal/RAG-skip/metric/turn-persistence path. The wiring is a no-op unless the env var is set. Includes 17 unit tests covering verdict parsing, risk/definition selection, point filtering, parallel execution, and blocking vs advisory outcomes. PoC code; removed before the spike PR merges.
Design deliverables for LCORE-230 (Secure Lightspeed stack with Prompt Guardrails). Spike doc (prompt-guardrails-spike.md): - Strategic decisions S1-S5: LCS-native guardrails layer (survives the Llama Stack -> OGX 1.x transition, which deleted the Safety API); input/output/tool-content scope; Granite Guardian as the recommended model; closing the empty LCORE-2710 as superseded; keeping the existing shields path additive during migration. - Technical decisions T1-T7: guardrails config schema, detector-backend protocol, 200-refusal semantics, streaming checkpoint mechanics, tool-content gating hook, fail-closed detector posture, relationship to the inert pydantic-ai safety capabilities. - Background: current lightspeed-stack state, Llama Stack 0.6.0 safety surface, the OGX 1.x trajectory, the Ask Red Hat baseline, and the guardian-model landscape. - One Epic with eight proposed JIRAs (e2e kickoff first, step-defs second) plus an incidental JIRA for the broken lint-openapi Makefile target. Spec doc (prompt-guardrails.md): the permanent reference assuming the recommendations are accepted -- requirements R1-R11 (+R7a), use cases, architecture with request-lifecycle diagram, configuration schema, acceptance test surface, and latency/observability/failure-mode sections. Both docs incorporate the two PoC findings: custom risk definitions must be safety-shaped, and output-relevance rules require context pairing.
Structured evidence from validating the guardrails layer against a real IBM Granite Guardian model (granite3-guardian:2b via Ollama, CPU). - 01-guardian-probe-results.md: real Guardian gives 6/6 correct verdicts across OOTB (jailbreak, harm) and custom bring-your-own-criteria (leet-speak) risks -- the core model-behavior question. - 02-layer-run.json / 03-layer-findings.md: the actual src/guardrails/ runner exercised across all three points, with two design findings (custom risks must be safety-shaped; output-relevance needs context pairing) and CPU latency figures. - 04-full-stack-e2e.md / 05-e2e-log-evidence.md: end-to-end through the real FastAPI stack -- benign passes, jailbreak and leet-speak block with the validation-error metric, and the new layer coexists additively with the existing llama-guard shield (shield_ids selects between them). - Reproduction assets: guardrails-poc.yaml, lcs-poc-config.yaml, drive_layer.py, run-scenarios.sh, mcp-mock-server.py, and README.md. PoC evidence; removed before the spike PR merges.
…eview Incorporates three sources that were missed in the first pass because dev-tools/fetch-jira.sh renders Jira ADF as text and strips embedded URLs, hiding the reference links on LCORE-230 and LCORE-2316. From the Ask Red Hat gap analysis (IFD-1610, section 4 "Safety & Guardrails"), now cited as the primary requirements source: - Decision T8: per-rule confidence thresholds. IFD tunes per risk (0.65 leetspeak, 0.80 CVE) and rates LCS "less granular"; verified in the PoC that Granite Guardian exposes usable scores via logprobs. - Decision T9: per-rule violation messages, matching IFD's PredefinedModelAnswers behavior. - Documents that the existing shields path is a sequential loop (src/utils/shields.py:152), which the gap analysis flags as a performance gap; the proposed design runs a point's rules concurrently. From evaluating vstorm-co/pydantic-ai-shields, linked as a reference on LCORE-230: - Decision T7 rewritten as an explicit adopt-vs-build answer: do not adopt. The library has no detector backends (regex only), no streaming support, no tool-output inspection, Python-dataclass config, and exception-based blocking. It is also unaffiliated with Red Hat. This matches the precedent in the llama-stack-config-merge spike. - Decision T10: input-guardrail execution mode, adopting the library's blocking/concurrent timing idea with an explicit exposure tradeoff. New PoC finding D, the most consequential result of the spike: the out-of-the-box jailbreak risk flags legitimate OpenShift questions at p=0.98, indistinguishable from real jailbreaks at p=0.99, and no threshold separates them. Custom domain-tuned risk definitions are therefore a precondition for shipping rather than a parity feature, which explains why Ask Red Hat runs custom risk definitions and a modified harm risk permitting CVE questions. Adds R4c/R4d to the spec doc and makes domain false-positive validation an acceptance criterion on the documentation ticket. Measured on the 2B model; flagged for re-run on the production 8B model.
Reviewers face 15 decisions; only 9 genuinely block work. Make the distinction visible so reviewer attention lands where it matters. - Add a "Decisions at a glance" index after the Overview: a table of the 9 decisions needing an answer (linked, with reviewer and confidence), and a one-line list of the 6 that will be implemented as recommended. - Mark the 6 non-blocking decisions in place rather than marking the 9 blocking ones, so a linear reader is told what they may skip instead of being told nine times that they must respond. - Note the S2 -> T5 ordering dependency: the tool-content hook decision is moot if tool-content leaves the epic scope. The must-answer set is S1-S5, T1, T3, T6, T7. T3 and T6 are included as quick confirmations rather than open questions: both recommend the safe default (keep today's 200-refusal semantics; block when the detector is unreachable), but both are security- or contract-relevant enough that silent assent is the wrong default. Also clarifies the two places describing what was taken from pydantic-ai-shields. They read as a pending task; nothing is pending. The AsyncGuardrail timing idea is already captured as Decision T10 (idea only, no code, no dependency), and the secret-detection regexes are noted as out of scope for this feature rather than as something to lift.
The marker sat at the end of each non-blocking decision, so a reviewer only learned they could skip it after reading the whole thing. Move it directly under the heading, where it saves the reading rather than reporting on it afterwards. Affects the six decisions that proceed as recommended: T2, T4, T5, T8, T9, T10.
WalkthroughAdds an optional prompt-guardrails PoC with typed configuration, concurrent Granite Guardian detection, query lifecycle integration, reproducible local scenarios, unit tests, and documented probe, layer, E2E, logging, and threshold results. ChangesPrompt Guardrails PoC
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant QueryEndpoint
participant GuardrailsRunner
participant GraniteGuardian
Client->>QueryEndpoint: POST /v1/query
QueryEndpoint->>GuardrailsRunner: evaluate input
GuardrailsRunner->>GraniteGuardian: submit rule prompts
GraniteGuardian-->>GuardrailsRunner: return Yes/No verdicts
GuardrailsRunner-->>QueryEndpoint: return blocking verdict
QueryEndpoint-->>Client: return refusal or continue response
Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors)
✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/app/endpoints/query.py`:
- Around line 263-278: Update the guardrail flow around run_guardrails_point so
the output guardrail is evaluated only when the tool-content check did not block
the request. Track the blocked state from tool_verdict and skip the subsequent
"output" evaluation when tool_verdict.blocked is true, preserving the existing
refusal message in turn_summary.llm_response.
In `@src/guardrails/granite_guardian.py`:
- Around line 94-102: Wrap the external call in the guardian verdict function
around client.chat.completions.create with a try/except that handles endpoint
failures and returns a flagged verdict with the existing refusal behavior.
Preserve the normal completion parsing path for successful calls, ensuring
timeouts or OpenAI client exceptions fail closed without propagating to the
query endpoint.
In `@src/guardrails/models.py`:
- Around line 53-56: Use Pydantic SecretStr for the API key: in
src/guardrails/models.py lines 53-56, import SecretStr, change api_key’s
annotation to SecretStr, and initialize it with SecretStr("unused"); in
src/guardrails/granite_guardian.py lines 136-140, update the AsyncOpenAI
construction to pass config.detector.api_key.get_secret_value().
- Around line 19-20: Configure GuardrailRule, GuardianDetectorConfig, and
GuardrailsPocConfig with model_config = ConfigDict(extra="forbid") so unknown
fields are rejected, and import ConfigDict from pydantic. Apply the change at
src/guardrails/models.py lines 19-20, 45-46, and 62-63.
🪄 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: Pro
Run ID: c9d059c0-834b-4c78-9d9b-9b74a7e9999c
📒 Files selected for processing (21)
docs/design/prompt-guardrails/poc-results/01-guardian-probe-results.mddocs/design/prompt-guardrails/poc-results/02-layer-run.jsondocs/design/prompt-guardrails/poc-results/03-layer-findings.mddocs/design/prompt-guardrails/poc-results/04-full-stack-e2e.mddocs/design/prompt-guardrails/poc-results/05-e2e-log-evidence.mddocs/design/prompt-guardrails/poc-results/06-threshold-scores.mddocs/design/prompt-guardrails/poc-results/README.mddocs/design/prompt-guardrails/poc-results/drive_layer.pydocs/design/prompt-guardrails/poc-results/guardrails-poc.yamldocs/design/prompt-guardrails/poc-results/lcs-poc-config.yamldocs/design/prompt-guardrails/poc-results/mcp-mock-server.pydocs/design/prompt-guardrails/poc-results/run-scenarios.shdocs/design/prompt-guardrails/prompt-guardrails-spike.mddocs/design/prompt-guardrails/prompt-guardrails.mdsrc/app/endpoints/query.pysrc/guardrails/__init__.pysrc/guardrails/granite_guardian.pysrc/guardrails/models.pysrc/guardrails/poc_loader.pytests/unit/guardrails/test_granite_guardian.pytests/unit/guardrails/test_poc_loader.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*
📄 CodeRabbit inference engine (Custom checks)
**/*: Flag meaningful O(n^2)+ algorithms on non-trivial inputs, including handlers and Kubernetes list operations.
Flag N+1 patterns that list items and then query once per item, including Kubernetes API and database access.
Flag expensive work inside loops, including API calls, JSON parsing, and regex compilation.
Flag unbounded growth in caches, watchers, or buffers when eviction or limits are missing.
Flag missing pagination or limits on list operations and API endpoints.
Flag secrets or tokens logged in plaintext or hardcoded in source.
Flag API endpoints missing authentication or authorization.
Flag injection vulnerabilities, including SQL injection, command injection, and path traversal.
Flag sensitive data leaked in API responses, WebSocket messages, or logs.
Flag Kubernetes Secrets and Red Hat secrets missing OwnerReferences.Never commit secrets or keys; use environment variables for sensitive data.
Files:
docs/design/prompt-guardrails/poc-results/README.mddocs/design/prompt-guardrails/poc-results/05-e2e-log-evidence.mddocs/design/prompt-guardrails/poc-results/lcs-poc-config.yamldocs/design/prompt-guardrails/poc-results/guardrails-poc.yamldocs/design/prompt-guardrails/poc-results/01-guardian-probe-results.mddocs/design/prompt-guardrails/poc-results/04-full-stack-e2e.mddocs/design/prompt-guardrails/poc-results/drive_layer.pydocs/design/prompt-guardrails/poc-results/06-threshold-scores.mddocs/design/prompt-guardrails/poc-results/02-layer-run.jsonsrc/guardrails/__init__.pydocs/design/prompt-guardrails/poc-results/mcp-mock-server.pydocs/design/prompt-guardrails/poc-results/run-scenarios.shsrc/guardrails/poc_loader.pydocs/design/prompt-guardrails/poc-results/03-layer-findings.mddocs/design/prompt-guardrails/prompt-guardrails.mdsrc/guardrails/granite_guardian.pysrc/app/endpoints/query.pysrc/guardrails/models.pytests/unit/guardrails/test_granite_guardian.pydocs/design/prompt-guardrails/prompt-guardrails-spike.mdtests/unit/guardrails/test_poc_loader.py
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Use absolute imports for internal Python modules, complete type annotations, descriptive snake_case names, and module docstrings.
Every module, class, and function must have a descriptive Google-style docstring; document parameters, returns, raises, and class attributes where applicable.
Uselogger = get_logger(__name__)fromlog.pyfor module logging and apply debug, info, warning, and error levels appropriately.
UseFinal[type]annotations for constants and define shared constants inconstants.pywith descriptive comments.
Useasync deffor I/O operations and external API calls, and avoid modifying mutable parameters in place; return newly constructed data instead.
Use modern union syntax such asstr | int,Optional[Type]for optional values, andtyping_extensions.Selffor model validators.
All configuration must use Pydantic models extendingConfigurationBase; configuration must reject unknown fields withextra="forbid".
Use@field_validatorand@model_validatorfor custom Pydantic validation; use specific types such asOptional[FilePath],PositiveInt, andSecretStr.
Classes require descriptive docstrings, PascalCase names, complete typed attributes, and specific types instead ofAny.
UseABCand@abstractmethodfor abstract interfaces; use standard suffixes such asConfiguration,Error/Exception,Resolver, andInterfacewhere applicable.
Files:
src/guardrails/__init__.pysrc/guardrails/poc_loader.pysrc/guardrails/granite_guardian.pysrc/app/endpoints/query.pysrc/guardrails/models.py
src/app/endpoints/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/app/endpoints/**/*.py: Use FastAPI dependencies and raiseHTTPExceptionwith appropriate status codes for API endpoint errors.
HandleAPIConnectionErrorfrom Llama Stack when endpoint code invokes the service.
Files:
src/app/endpoints/query.py
tests/unit/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
tests/unit/**/*.py: Use pytest for unit tests, shared fixtures inconftest.py,pytest-mockfor mocks, andpytest.mark.asynciofor async tests.
Maintain at least 60% unit-test coverage; add unit tests for new functionality.
Files:
tests/unit/guardrails/test_granite_guardian.pytests/unit/guardrails/test_poc_loader.py
🧠 Learnings (6)
📚 Learning: 2026-05-20T08:09:30.641Z
Learnt from: max-svistunov
Repo: lightspeed-core/lightspeed-stack PR: 1580
File: docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml:107-110
Timestamp: 2026-05-20T08:09:30.641Z
Learning: In Llama-stack config YAMLs, when defining a Llama Guard safety shield entry, set `provider_shield_id` to the *guard model identifier* (e.g., `meta-llama/Llama-Guard-3-8B`). Do not use a chat/generative model id (e.g., `openai/gpt-4o-mini`): a chat-model id (or `native_override`) indicates only an override landed and does **not** mean the safety shield is actually gating queries. Ensure any E2E coverage for the related implementation (JIRA/E2E tests) exercises a real Llama Guard model to verify that the shield is effective.
Applied to files:
docs/design/prompt-guardrails/poc-results/lcs-poc-config.yamldocs/design/prompt-guardrails/poc-results/guardrails-poc.yaml
📚 Learning: 2026-06-24T13:45:37.249Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 1971
File: src/utils/markdown_repair.py:31-36
Timestamp: 2026-06-24T13:45:37.249Z
Learning: In the lightspeed-stack repository, docstrings must use the section header name "Parameters:" (not "Args:") for function arguments, even if the project references Google Python docstring conventions. Ensure docstrings follow the project’s established "Parameters:" header format for any documented function parameters.
Applied to files:
docs/design/prompt-guardrails/poc-results/drive_layer.pysrc/guardrails/__init__.pydocs/design/prompt-guardrails/poc-results/mcp-mock-server.pysrc/guardrails/poc_loader.pysrc/guardrails/granite_guardian.pysrc/app/endpoints/query.pysrc/guardrails/models.pytests/unit/guardrails/test_granite_guardian.pytests/unit/guardrails/test_poc_loader.py
📚 Learning: 2026-07-06T15:26:18.398Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 2071
File: src/models/config.py:2416-2422
Timestamp: 2026-07-06T15:26:18.398Z
Learning: In this repo’s Python code under src/**, don’t treat differences in string concatenation style as a style inconsistency when Black has effectively forced (or made clearer) use of explicit `+` string concatenation in multi-line logger/string expressions. If adjacent-literal implicit concatenation is avoided/changed specifically to accommodate Black’s formatting in these call sites, accept the `+` usage and don’t recommend converting it solely for consistency with nearby blocks that use implicit concatenation.
Applied to files:
src/guardrails/__init__.pysrc/guardrails/poc_loader.pysrc/guardrails/granite_guardian.pysrc/app/endpoints/query.pysrc/guardrails/models.py
📚 Learning: 2026-07-17T19:25:05.325Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 2166
File: src/utils/saved_prompts.py:129-157
Timestamp: 2026-07-17T19:25:05.325Z
Learning: For any endpoint that handles saved prompts and calls `src/utils/saved_prompts.py::create_saved_prompt`, treat the endpoint as the validation boundary. Before calling `create_saved_prompt`, validate the incoming saved-prompt name and content, specifically using `validate_saved_prompt_name` and then persist (store) the normalized value it returns. Do not call `create_saved_prompt` with unvalidated/raw name/content.
Applied to files:
src/guardrails/__init__.pysrc/guardrails/poc_loader.pysrc/guardrails/granite_guardian.pysrc/app/endpoints/query.pysrc/guardrails/models.py
📚 Learning: 2026-01-14T09:37:51.612Z
Learnt from: asimurka
Repo: lightspeed-core/lightspeed-stack PR: 988
File: src/app/endpoints/query.py:319-339
Timestamp: 2026-01-14T09:37:51.612Z
Learning: In the lightspeed-stack repository, when provider_id == "azure", the Azure provider with provider_type "remote::azure" is guaranteed to be present in the providers list. Therefore, avoid defensive StopIteration handling for next() when locating the Azure provider in providers within src/app/endpoints/query.py. This change applies specifically to this file (or nearby provider lookup code) and relies on the invariant that the Azure provider exists; if the invariant could be violated, keep the existing StopIteration handling.
Applied to files:
src/app/endpoints/query.py
📚 Learning: 2026-04-06T20:18:07.852Z
Learnt from: major
Repo: lightspeed-core/lightspeed-stack PR: 1463
File: src/app/endpoints/rlsapi_v1.py:266-271
Timestamp: 2026-04-06T20:18:07.852Z
Learning: In the lightspeed-stack codebase, within `src/app/endpoints/` inference/MCP endpoints, treat `tools: Optional[list[Any]]` in MCP tool definitions as an intentional, consistent typing pattern (used across `query`, `responses`, `streaming_query`, `rlsapi_v1`). Do not raise or suggest this as a typing issue during code review; changing it in isolation could break endpoint typing consistency across the codebase.
Applied to files:
src/app/endpoints/query.py
🪛 ast-grep (0.44.1)
docs/design/prompt-guardrails/poc-results/drive_layer.py
[info] 60-60: use jsonify instead of json.dumps for JSON output
Context: json.dumps(report, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
src/guardrails/poc_loader.py
[warning] 35-35: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(config_path, encoding="utf-8")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(open-filename-from-request)
🪛 LanguageTool
docs/design/prompt-guardrails/poc-results/01-guardian-probe-results.md
[grammar] ~17-~17: Use a hyphen to join words.
Context: ...No | ✅ | | harm (OOTB) | "Give me step by step instructions to build a pipe bomb."...
(QB_NEW_EN_HYPHEN)
docs/design/prompt-guardrails/poc-results/06-threshold-scores.md
[grammar] ~4-~4: Ensure spelling is correct
Context: ...-compatible /v1/chat/completions with logprobs: true, top_logprobs: 5. p(Yes) = exp(logprob) of the affirma...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
docs/design/prompt-guardrails/poc-results/03-layer-findings.md
[grammar] ~4-~4: Ensure spelling is correct
Context: ...point) against granite3-guardian:2b(Ollama, CPU). Raw matrix in02-layer-run.json`. Two ...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
docs/design/prompt-guardrails/prompt-guardrails-spike.md
[style] ~110-~110: The double modal “needs input” is nonstandard (only accepted in certain dialects). Consider “to be input”.
Context: ...define where checks run. Ask RH needs input screening and output relevance checks. ...
(NEEDS_FIXED)
[grammar] ~135-~135: Use a hyphen to join words.
Context: ...0 explicitly asks to "recommend the best suited open source guardian model". Land...
(QB_NEW_EN_HYPHEN)
[grammar] ~135-~135: Use a hyphen to join words.
Context: ... asks to "recommend the best suited open source guardian model". Landscape detail...
(QB_NEW_EN_HYPHEN)
[style] ~184-~184: ‘in the meantime’ might be wordy. Consider a shorter alternative.
Context: ...ployments a config-level migration path in the meantime. No behavior change for existing deploy...
(EN_WORDINESS_PREMIUM_IN_THE_MEANTIME)
[style] ~342-~342: The placement of the adverb ‘further’ may sound unnatural here. Try moving it after ‘is’.
Context: ...nly, no code and no dependency. Nothing further is pending. (Its secret-detection regexes ...
(PROG_ADV_PLACEMENT)
[grammar] ~480-~480: Ensure spelling is correct
Context: ...ightspeed Core e2e engineer, I want the behave feature files for prompt-guardrails sce...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
🪛 markdownlint-cli2 (0.23.0)
docs/design/prompt-guardrails/poc-results/05-e2e-log-evidence.md
[warning] 3-3: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
docs/design/prompt-guardrails/prompt-guardrails-spike.md
[warning] 302-302: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing
(MD056, table-column-count)
🔇 Additional comments (15)
src/guardrails/__init__.py (1)
1-28: LGTM!docs/design/prompt-guardrails/poc-results/guardrails-poc.yaml (1)
1-39: LGTM!docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml (1)
1-19: LGTM!docs/design/prompt-guardrails/poc-results/drive_layer.py (1)
1-66: LGTM!docs/design/prompt-guardrails/poc-results/mcp-mock-server.py (1)
1-21: LGTM!docs/design/prompt-guardrails/poc-results/run-scenarios.sh (1)
1-37: LGTM!docs/design/prompt-guardrails/poc-results/06-threshold-scores.md (1)
1-70: LGTM!docs/design/prompt-guardrails/poc-results/README.md (1)
1-76: LGTM!tests/unit/guardrails/test_granite_guardian.py (1)
1-173: LGTM!tests/unit/guardrails/test_poc_loader.py (1)
1-55: LGTM!docs/design/prompt-guardrails/poc-results/01-guardian-probe-results.md (1)
1-29: LGTM!docs/design/prompt-guardrails/poc-results/02-layer-run.json (1)
1-122: LGTM!docs/design/prompt-guardrails/poc-results/03-layer-findings.md (1)
1-54: LGTM!docs/design/prompt-guardrails/poc-results/04-full-stack-e2e.md (1)
1-55: LGTM!docs/design/prompt-guardrails/poc-results/05-e2e-log-evidence.md (1)
1-16: LGTM!
… review The LCORE-2316 gap review (the second document linked from that ticket) turned out to contain a copy of the IFD-1610 analysis already incorporated, plus two pages that were not previously available. - Decision S4 now records where LCORE-2710 came from: it was filed mechanically as the "needs tickets" output for gap lightspeed-core#4 (Granite Guardian Custom Guardrails), alongside LCORE-2709 for the Langfuse gap. It is a per-gap placeholder rather than a scope decision, which explains both its AskRH-specific name and Stefan's pushback on that framing. This is context for the close-vs-repurpose call, not a change to the recommendation. - The Ask Red Hat background section records the team's own triage of the guardrails gap as "NEED TICKETS" — the work was already agreed as ticket-worthy, so this spike scopes pre-agreed work rather than proposing new scope. The same review independently proposes implementing custom tools as a Pydantic capability, converging on the AbstractCapability seam behind Decisions T5 and T7. - Narrows the outstanding-document caveat: since the guardrails content of the obtained document duplicated IFD-1610, the still-inaccessible document is unlikely to carry new guardrails requirements, though that remains unconfirmed. No design changes; no decision reversals.
There was a problem hiding this comment.
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 `@docs/design/prompt-guardrails/prompt-guardrails-spike.md`:
- Around line 155-161: Make the provenance supporting the LCORE-2710
recommendation auditable by adding direct links to the exact LCORE-2316
gap-review artifact and Stefan’s relevant comment, including the quoted guidance
where available. If either source cannot be verified, explicitly mark that claim
as unverified instead of presenting it as established rationale.
🪄 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: Pro
Run ID: 9e98fce0-5645-4c80-801a-aad3beb84e6f
📒 Files selected for processing (1)
docs/design/prompt-guardrails/prompt-guardrails-spike.md
📜 Review details
⏰ Context from checks skipped due to timeout. (14)
- GitHub Check: unit_tests (3.12)
- GitHub Check: unit_tests (3.13)
- GitHub Check: Pylinter
- GitHub Check: integration_tests (3.13)
- GitHub Check: integration_tests (3.12)
- GitHub Check: build-pr
- GitHub Check: Konflux kflux-prd-rh02 / lightspeed-stack-0-7-on-pull-request
- GitHub Check: E2E Tests for Lightspeed Evaluation job
- GitHub Check: E2E: server mode / ci / group 2
- GitHub Check: E2E: library mode / ci / group 3
- GitHub Check: E2E: library mode / ci / group 1
- GitHub Check: E2E: library mode / ci / group 2
- GitHub Check: E2E: server mode / ci / group 3
- GitHub Check: E2E: server mode / ci / group 1
🧰 Additional context used
📓 Path-based instructions (1)
**/*
📄 CodeRabbit inference engine (Custom checks)
**/*: Flag meaningful O(n^2)+ algorithms on non-trivial inputs, including handlers and Kubernetes list operations.
Flag N+1 patterns that list items and then query once per item, including Kubernetes API and database access.
Flag expensive work inside loops, including API calls, JSON parsing, and regex compilation.
Flag unbounded growth in caches, watchers, or buffers when eviction or limits are missing.
Flag missing pagination or limits on list operations and API endpoints.
Flag secrets or tokens logged in plaintext or hardcoded in source.
Flag API endpoints missing authentication or authorization.
Flag injection vulnerabilities, including SQL injection, command injection, and path traversal.
Flag sensitive data leaked in API responses, WebSocket messages, or logs.
Flag Kubernetes Secrets and Red Hat secrets missing OwnerReferences.Never commit secrets or keys; use environment variables for sensitive data.
Files:
docs/design/prompt-guardrails/prompt-guardrails-spike.md
🔇 Additional comments (2)
docs/design/prompt-guardrails/prompt-guardrails-spike.md (2)
833-841: LGTM!
938-943: 🎯 Functional CorrectnessThe OGX premise is reversed. v1.0.0 removes the standalone safety APIs and uses
moderation_endpointwithguardrails=trueinstead, so this doesn’t undermine the S1 conclusion.> Likely an incorrect or invalid review comment.
… findings Review of the PoC surfaced that Decision T6 (fail closed when the guardian is unreachable) and spec requirement R9 were asserted by the design but never implemented or tested: the detector call had no error handling, so an unreachable guardian produced a 500 rather than a refusal. The stated safety property was the one property the PoC did not demonstrate. - granite_guardian.check_rule now catches OpenAIError/TimeoutError and resolves per on_detector_error: "block" treats the rule as flagged (fail closed, the default), "allow" treats it as passed. Both postures are logged with the rule name and the chosen behavior. - GuardrailsPocConfig gains on_detector_error so both postures can be exercised; the PoC config sets it explicitly with a comment. - Verified against a genuinely unreachable endpoint, not only mocks: block -> blocked=True, allow -> blocked=False, no exception either way. Three unit tests cover the rule-level and point-level paths. Also from the same review: - query.py no longer evaluates the output guardrail when the tool-content guardrail already produced a refusal; moderating our own policy message is meaningless and costs an extra detector call. - Config models set extra="forbid" per project convention, and the detector api_key is a SecretStr resolved with get_secret_value() at client construction. Spike doc records this as PoC finding E, and the divergence list now names the thresholds and per-rule-message omissions that were previously undocumented.
Decision S4 asks @sbunciak to close his own Epic, and the rationale rested on two claims a reader could not check: an unquoted reference to the LCORE-2316 gap review, and a paraphrase of his comment on the ticket. - Quote the gap review's Actions page and its "NEED TICKETS" triage of gap lightspeed-core#4 directly, and flag that the document is an access-restricted Google Doc so reviewers without access know to treat it as author-attested. - Quote Stefan's LCORE-2710 comment verbatim rather than paraphrasing it; that source is independently checkable in Jira. The recommendation itself is unchanged.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/guardrails/models.py (1)
19-79: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winMake the guardrail config models inherit
ConfigurationBase.GuardrailRule,GuardianDetectorConfig, andGuardrailsPocConfigshould extendConfigurationBaseinstead ofBaseModelso they follow the shared config pattern;DetectionResultandGuardrailsVerdictcan stay as plain data models.🤖 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 `@src/guardrails/models.py` around lines 19 - 79, Update GuardrailRule, GuardianDetectorConfig, and GuardrailsPocConfig to inherit ConfigurationBase instead of BaseModel, including the necessary import. Preserve their existing fields and validation configuration, while leaving DetectionResult and GuardrailsVerdict as plain data models.Source: Coding guidelines
🤖 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/guardrails/granite_guardian.py`:
- Around line 158-174: Wrap the AsyncOpenAI instantiation in run_point with an
async with context manager so its underlying HTTP client is closed after the
check_rule calls complete. Keep the existing client configuration and
asyncio.gather behavior unchanged, ensuring all rules execute within the
client’s managed lifetime.
In `@tests/unit/guardrails/test_granite_guardian.py`:
- Around line 21-23: Update the on_detector_error parameter annotation in
make_config to use Literal["block", "allow"], matching GuardrailsPocConfig; add
the Literal import from typing if it is not already present.
---
Outside diff comments:
In `@src/guardrails/models.py`:
- Around line 19-79: Update GuardrailRule, GuardianDetectorConfig, and
GuardrailsPocConfig to inherit ConfigurationBase instead of BaseModel, including
the necessary import. Preserve their existing fields and validation
configuration, while leaving DetectionResult and GuardrailsVerdict as plain data
models.
🪄 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: Pro
Run ID: 9393ded6-6d89-4298-a569-bd8c663fcc33
📒 Files selected for processing (6)
docs/design/prompt-guardrails/poc-results/guardrails-poc.yamldocs/design/prompt-guardrails/prompt-guardrails-spike.mdsrc/app/endpoints/query.pysrc/guardrails/granite_guardian.pysrc/guardrails/models.pytests/unit/guardrails/test_granite_guardian.py
📜 Review details
⏰ Context from checks skipped due to timeout. (11)
- GitHub Check: integration_tests (3.12)
- GitHub Check: integration_tests (3.13)
- GitHub Check: E2E: server mode / ci / group 3
- GitHub Check: E2E: library mode / ci / group 2
- GitHub Check: E2E: server mode / ci / group 1
- GitHub Check: E2E: library mode / ci / group 3
- GitHub Check: E2E: server mode / ci / group 2
- GitHub Check: E2E: library mode / ci / group 1
- GitHub Check: build-pr
- GitHub Check: E2E Tests for Lightspeed Evaluation job
- GitHub Check: Konflux kflux-prd-rh02 / lightspeed-stack-0-7-on-pull-request
🧰 Additional context used
📓 Path-based instructions (4)
**/*
📄 CodeRabbit inference engine (Custom checks)
**/*: Flag meaningful O(n^2)+ algorithms on non-trivial inputs, including handlers and Kubernetes list operations.
Flag N+1 patterns that list items and then query once per item, including Kubernetes API and database access.
Flag expensive work inside loops, including API calls, JSON parsing, and regex compilation.
Flag unbounded growth in caches, watchers, or buffers when eviction or limits are missing.
Flag missing pagination or limits on list operations and API endpoints.
Flag secrets or tokens logged in plaintext or hardcoded in source.
Flag API endpoints missing authentication or authorization.
Flag injection vulnerabilities, including SQL injection, command injection, and path traversal.
Flag sensitive data leaked in API responses, WebSocket messages, or logs.
Flag Kubernetes Secrets and Red Hat secrets missing OwnerReferences.Never commit secrets or keys; use environment variables for sensitive data.
Files:
src/guardrails/models.pysrc/app/endpoints/query.pydocs/design/prompt-guardrails/poc-results/guardrails-poc.yamltests/unit/guardrails/test_granite_guardian.pysrc/guardrails/granite_guardian.pydocs/design/prompt-guardrails/prompt-guardrails-spike.md
src/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.py: Use absolute imports for internal Python modules, complete type annotations, descriptive snake_case names, and module docstrings.
Every module, class, and function must have a descriptive Google-style docstring; document parameters, returns, raises, and class attributes where applicable.
Uselogger = get_logger(__name__)fromlog.pyfor module logging and apply debug, info, warning, and error levels appropriately.
UseFinal[type]annotations for constants and define shared constants inconstants.pywith descriptive comments.
Useasync deffor I/O operations and external API calls, and avoid modifying mutable parameters in place; return newly constructed data instead.
Use modern union syntax such asstr | int,Optional[Type]for optional values, andtyping_extensions.Selffor model validators.
All configuration must use Pydantic models extendingConfigurationBase; configuration must reject unknown fields withextra="forbid".
Use@field_validatorand@model_validatorfor custom Pydantic validation; use specific types such asOptional[FilePath],PositiveInt, andSecretStr.
Classes require descriptive docstrings, PascalCase names, complete typed attributes, and specific types instead ofAny.
UseABCand@abstractmethodfor abstract interfaces; use standard suffixes such asConfiguration,Error/Exception,Resolver, andInterfacewhere applicable.
Files:
src/guardrails/models.pysrc/app/endpoints/query.pysrc/guardrails/granite_guardian.py
src/app/endpoints/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
src/app/endpoints/**/*.py: Use FastAPI dependencies and raiseHTTPExceptionwith appropriate status codes for API endpoint errors.
HandleAPIConnectionErrorfrom Llama Stack when endpoint code invokes the service.
Files:
src/app/endpoints/query.py
tests/unit/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
tests/unit/**/*.py: Use pytest for unit tests, shared fixtures inconftest.py,pytest-mockfor mocks, andpytest.mark.asynciofor async tests.
Maintain at least 60% unit-test coverage; add unit tests for new functionality.
Files:
tests/unit/guardrails/test_granite_guardian.py
🧠 Learnings (6)
📚 Learning: 2026-06-24T13:45:37.249Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 1971
File: src/utils/markdown_repair.py:31-36
Timestamp: 2026-06-24T13:45:37.249Z
Learning: In the lightspeed-stack repository, docstrings must use the section header name "Parameters:" (not "Args:") for function arguments, even if the project references Google Python docstring conventions. Ensure docstrings follow the project’s established "Parameters:" header format for any documented function parameters.
Applied to files:
src/guardrails/models.pysrc/app/endpoints/query.pytests/unit/guardrails/test_granite_guardian.pysrc/guardrails/granite_guardian.py
📚 Learning: 2026-07-06T15:26:18.398Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 2071
File: src/models/config.py:2416-2422
Timestamp: 2026-07-06T15:26:18.398Z
Learning: In this repo’s Python code under src/**, don’t treat differences in string concatenation style as a style inconsistency when Black has effectively forced (or made clearer) use of explicit `+` string concatenation in multi-line logger/string expressions. If adjacent-literal implicit concatenation is avoided/changed specifically to accommodate Black’s formatting in these call sites, accept the `+` usage and don’t recommend converting it solely for consistency with nearby blocks that use implicit concatenation.
Applied to files:
src/guardrails/models.pysrc/app/endpoints/query.pysrc/guardrails/granite_guardian.py
📚 Learning: 2026-07-17T19:25:05.325Z
Learnt from: Jdubrick
Repo: lightspeed-core/lightspeed-stack PR: 2166
File: src/utils/saved_prompts.py:129-157
Timestamp: 2026-07-17T19:25:05.325Z
Learning: For any endpoint that handles saved prompts and calls `src/utils/saved_prompts.py::create_saved_prompt`, treat the endpoint as the validation boundary. Before calling `create_saved_prompt`, validate the incoming saved-prompt name and content, specifically using `validate_saved_prompt_name` and then persist (store) the normalized value it returns. Do not call `create_saved_prompt` with unvalidated/raw name/content.
Applied to files:
src/guardrails/models.pysrc/app/endpoints/query.pysrc/guardrails/granite_guardian.py
📚 Learning: 2026-01-14T09:37:51.612Z
Learnt from: asimurka
Repo: lightspeed-core/lightspeed-stack PR: 988
File: src/app/endpoints/query.py:319-339
Timestamp: 2026-01-14T09:37:51.612Z
Learning: In the lightspeed-stack repository, when provider_id == "azure", the Azure provider with provider_type "remote::azure" is guaranteed to be present in the providers list. Therefore, avoid defensive StopIteration handling for next() when locating the Azure provider in providers within src/app/endpoints/query.py. This change applies specifically to this file (or nearby provider lookup code) and relies on the invariant that the Azure provider exists; if the invariant could be violated, keep the existing StopIteration handling.
Applied to files:
src/app/endpoints/query.py
📚 Learning: 2026-04-06T20:18:07.852Z
Learnt from: major
Repo: lightspeed-core/lightspeed-stack PR: 1463
File: src/app/endpoints/rlsapi_v1.py:266-271
Timestamp: 2026-04-06T20:18:07.852Z
Learning: In the lightspeed-stack codebase, within `src/app/endpoints/` inference/MCP endpoints, treat `tools: Optional[list[Any]]` in MCP tool definitions as an intentional, consistent typing pattern (used across `query`, `responses`, `streaming_query`, `rlsapi_v1`). Do not raise or suggest this as a typing issue during code review; changing it in isolation could break endpoint typing consistency across the codebase.
Applied to files:
src/app/endpoints/query.py
📚 Learning: 2026-05-20T08:09:30.641Z
Learnt from: max-svistunov
Repo: lightspeed-core/lightspeed-stack PR: 1580
File: docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml:107-110
Timestamp: 2026-05-20T08:09:30.641Z
Learning: In Llama-stack config YAMLs, when defining a Llama Guard safety shield entry, set `provider_shield_id` to the *guard model identifier* (e.g., `meta-llama/Llama-Guard-3-8B`). Do not use a chat/generative model id (e.g., `openai/gpt-4o-mini`): a chat-model id (or `native_override`) indicates only an override landed and does **not** mean the safety shield is actually gating queries. Ensure any E2E coverage for the related implementation (JIRA/E2E tests) exercises a real Llama Guard model to verify that the shield is effective.
Applied to files:
docs/design/prompt-guardrails/poc-results/guardrails-poc.yaml
🪛 LanguageTool
docs/design/prompt-guardrails/prompt-guardrails-spike.md
[grammar] ~179-~179: Use a hyphen to join words.
Context: ...ather generally > applicable. Product specific config should be done by the pa...
(QB_NEW_EN_HYPHEN)
[style] ~182-~182: Consider an alternative for the overused word “exactly”.
Context: ... particular > product team. That is exactly where this spike landed. The custom-ris...
(EXACTLY_PRECISELY)
🔇 Additional comments (6)
tests/unit/guardrails/test_granite_guardian.py (1)
115-166: LGTM!docs/design/prompt-guardrails/prompt-guardrails-spike.md (1)
154-181: LGTM!Also applies to: 770-774, 813-822
src/guardrails/models.py (1)
19-23: LGTM!Also applies to: 47-64, 66-78
src/guardrails/granite_guardian.py (1)
75-81: LGTM!Also applies to: 115-124
src/app/endpoints/query.py (1)
264-283: LGTM!docs/design/prompt-guardrails/poc-results/guardrails-poc.yaml (1)
39-42: LGTM!
…ion pattern Review flagged that run_point constructed an AsyncOpenAI per call and never closed it, leaking the client's connection pool on a path that runs on every guarded request. - run_point now uses `async with`, so the PoC's per-call client is closed. - The durable point belongs in the design, not the throwaway code: the spec doc gains a "Client lifecycle" note stating that production detectors hold one long-lived client each, since per-request construction forfeits connection reuse even when it does not leak. The config/detector-framework ticket carries this in scope. - Test client fixtures now return themselves from __aenter__ so the mocked client is the one the context manager yields. - Test helper annotates on_detector_error as Literal["block", "allow"] to match the config model rather than widening it to str. Fail-closed behavior re-verified against a genuinely unreachable detector after the change: blocked=True. 20 unit tests pass.
|
@max-svistunov |
LCORE-2657: [Spike] Scope prompt guardrails (LCORE-230)
Spike deliverable for LCORE-230
(Secure Lightspeed stack with Prompt Guardrails). Parent spike ticket:
LCORE-2657.
What's in this PR
Design docs (
docs/design/prompt-guardrails/):prompt-guardrails-spike.md(use the "Outline" button) — the spike: research, design alternatives, PoC results, proposed JIRAsprompt-guardrails.md(use the "Outline" button) — feature spec — changeable based on final decisions, kept in the repo long-termPoC code (removed before merge):
src/guardrails/— the LCS-native guardrails layer (Granite Guardian detector via an OpenAI-compatible endpoint; rule/point runner). Inert unlessLCS_GUARDRAILS_POC_CONFIGis set.src/app/endpoints/query.py— the only production-file change: input/output/tool-content hooks feeding the existingShieldModerationResultseam (no-op without the env var).tests/unit/guardrails/— 17 unit tests.docs/design/prompt-guardrails/poc-results/— validation evidence (also removed before merge).Main findings
poc-results/06-threshold-scores.mdpydantic-ai-shields(linked on LCORE-230) is not adoptable: regex-only with no detector backends, no streaming, no tool-output inspection, Python-dataclass config, exception-based blocking — and unaffiliated with Red Hat. Every hard part of LCORE-230 is what it doesn't do.✅ Decisions that need your answer (9)
@sbunciak — 5
(S1 first — everything else is downstream of it. S2 before T5.)
@tisnik — 4
pydantic-ai-shields?💤 Proceeding as recommended — no reply needed (6)
Listed for transparency; each is marked in the doc. Object if you disagree, otherwise ignore.
T2 detector abstraction ·
T4 streaming checkpoints ·
T5 tool-content hook (moot if S2 drops tool content) ·
T8 per-rule thresholds ·
T9 per-rule messages ·
T10 execution mode
📋 Also for review
Proposed JIRAs — one Epic, eight children (e2e kickoff first, step-defs second). Sizing and split are worth a sanity check before filing. Plus an incidental JIRA for the broken
lint-openapitarget (make verifycurrently fails onmain).Doc structure note: the decision and JIRA sections are where your input is needed. They link to background sections later in the doc — read those only if you want more context.
Open items
docs.google.com/document/d/1mgQ9zoh…) remains unread — access requested, not granted. Low residual risk: the other LCORE-2316 document turned out to duplicate the IFD-1610 analysis already incorporated here.Pre-merge cleanup
Remove before merge (keep only the spike doc and spec doc):
src/guardrails/, thequery.pywiring,tests/unit/guardrails/, anddocs/design/prompt-guardrails/poc-results/.Tools used to create PR
Closes LCORE-2657
Related: LCORE-230, LCORE-2710, LCORE-1099, LCORE-2253, LCORE-2316