feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379) - #423

Merged
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield
May 27, 2026
Merged

feat(guardrails): Azure Content Safety Prompt Shield — P1 (#379)#423
moonming merged 4 commits into
mainfrom
feat/p1-prompt-shield

Conversation

@moonming

@moonmingmoonming commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

Implements the P1 guardrail kind: Azure AI Content Safety Prompt Shield (kind=azure_content_safety).

The Prompt Shield API detects two attack categories:

  • Direct injection: jailbreak attempts in the user's own prompt
  • Indirect injection: malicious instructions embedded in external documents

DP changes (ai-gateway)

FileChange
aisix-core/src/models/guardrail.rsNew AzureContentSafetyConfig struct (endpoint, api_key, timeout_ms); new GuardrailKind::AzureContentSafety variant (serde tag "azure_content_safety")
aisix-core/src/models/mod.rsExport AzureContentSafetyConfig
aisix-guardrails/src/prompt_shield.rsPromptShieldGuardrail implementing Guardrail — POSTs to /contentsafety/text:shieldPrompt?api-version=2024-09-01; auto-chunks prompts > 10 000 chars on whitespace boundaries; maps 429 → azure_cs_throttled, 5xx/IO → azure_cs_5xx, timeout → azure_cs_timeout with the same fail_open semantics as the Bedrock kind
aisix-guardrails/src/build.rsAzureContentSafety arm in build_one; azure-content-safety feature gate arm
aisix-guardrails/src/lib.rsExport PromptShieldGuardrail; #[cfg(feature = "azure-content-safety")] mod prompt_shield
aisix-guardrails/Cargo.tomlazure-content-safety feature gate (default on), pulls in reqwest from workspace
schemas/resources/guardrail.schema.jsonRegenerated — new AzureContentSafetyConfigoneOf branch

Wire shape (kine → DP)

{
"name": "my-shield",
"kind": "azure_content_safety",
"endpoint": "https://my-resource.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000,
"fail_open": true
}

api_key is decrypted by cp-api before kine write; the DP only ever holds plaintext in memory and never logs the key.

API wire shape

POST {endpoint}/contentsafety/text:shieldPrompt?api-version=2024-09-01
Ocp-Apim-Subscription-Key: {api_key}
{ "userPrompt": "...", "documents": [] }

Response:

{ "userPromptAnalysis": { "attackDetected": bool }, "documentsAnalysis": [] }

Reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference

Behavior matrix

API responsefail_openVerdict
attackDetected=false (all chunks)n/aAllow
attackDetected=true (any chunk)n/aBlock
timeouttrueBypass azure_cs_timeout
timeoutfalseBlock
429 ThrottlingtrueBypass azure_cs_throttled
429 ThrottlingfalseBlock
5xx / IO errortrueBypass azure_cs_5xx
5xx / IO errorfalseBlock

Chunking

Prompts > 10 000 chars are auto-split on whitespace boundaries. Each chunk ≤ 10 000 chars is sent as a separate API call. The first chunk returning attackDetected=true short-circuits the rest.

Test plan

  • 17 unit + wiremock tests in prompt_shield::tests:
    • Bypass-tag contract (wire names must be stable)
    • chunk_text boundary conditions (exact limit, over limit, single oversized word, empty)
    • handle_failure verdict paths (timeout/throttled + both fail_open values)
    • Hook-point gating (output-only skips input; input-only skips output)
    • Wiremock: clean → Allow (+ auth header assertion), attack → Block, 5xx fail_open=true, 5xx fail_open=false, 429 throttled, timeout, long attack prompt, long clean prompt, output check, empty input
  • 2 new aisix-core model tests: parse round-trip, timeout_ms default
  • All 263 existing tests pass (178 aisix-core + 85 aisix-guardrails)
  • E2E test against fakecloud (AISIX-Cloud PR — in progress)

Divergence from reference implementations

  • No reference implementation (LiteLLM / Portkey) wraps Azure CS Prompt Shield today; wire shape derived directly from the Azure CS REST API docs (2024-09-01).
  • documents: [] always sent (required field); we don't have the concept of "retrieved document grounding" at the gateway layer.
  • Timeout handled via tokio::time::timeout (same as our latency_mode=timed Bedrock path) rather than reqwest's built-in timeout — keeps the failure mapping consistent with the rest of the codebase.

Follow-ups

  • AISIX-Cloud CP PR: add (cloud_safety_service, azure, prompt_shield) validation + marshalGuardrailKV + API-key envelope encryption
  • E2E test against fakecloud Azure CS stub
  • Dashboard UI for creating/editing azure_content_safety guardrails

Summary by CodeRabbit

  • New Features

    • Added Azure AI Content Safety guardrail for scanning chat inputs and outputs with chunking for long prompts.
    • New configuration options: endpoint, API key, and timeout (defaults to 5000ms); supports fail-open and fail-closed modes.
  • Tests

    • Added unit and integration tests covering chunking, timeout behavior, success/failure mappings, and gating at hook points.
  • Documentation

    • Schema updated to include the new guardrail kind and timeout default.

Review Change Stack

…ent_safety) — P1
Adds a new guardrail kind that calls the Azure AI Content Safety
Prompt Shield API to detect jailbreak and indirect injection attacks.
Changes:
- `aisix-core`: new `AzureContentSafetyConfig` struct (`endpoint`,
`api_key`, `timeout_ms`) + `GuardrailKind::AzureContentSafety`
variant (serde tag `"azure_content_safety"`); exported from `models/mod.rs`.
- `aisix-guardrails`: new `prompt_shield.rs` implementing the `Guardrail`
trait via `reqwest`; POSTs to `/contentsafety/text:shieldPrompt?api-version=2024-09-01`;
auto-chunks prompts > 10 000 chars on whitespace boundaries;
maps 429 → `azure_cs_throttled`, 5xx/IO → `azure_cs_5xx`,
timeout → `azure_cs_timeout` with the same `fail_open` semantics as
the Bedrock kind.
- `aisix-guardrails/Cargo.toml`: `azure-content-safety` feature gate (default on)
pulling in `reqwest` from the workspace.
- `schemas/resources/guardrail.schema.json`: regenerated (new oneOf branch).
- 17 unit + wiremock tests cover the happy path, all failure modes,
hook-point gating, chunking, and the auth-header contract.
Wire shape (kine → DP):
{ "kind": "azure_content_safety",
"endpoint": "https://<resource>.cognitiveservices.azure.com",
"api_key": "<decrypted-plaintext>",
"timeout_ms": 5000 }
API reference: https://learn.microsoft.com/en-us/azure/ai-services/content-safety/reference
@coderabbitai

coderabbitaiBot commented May 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@moonming, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 1b05770c-e246-47f3-a7bc-626207ab9710

📥 Commits

Reviewing files that changed from the base of the PR and between f17c713 and f52054a.

📒 Files selected for processing (1)
  • crates/aisix-guardrails/src/prompt_shield.rs
📝 Walkthrough

Walkthrough

This PR adds an optional Azure AI Content Safety guardrail: new AzureContentSafetyConfig and GuardrailKind::AzureContentSafety, JSON schema support, crate feature/dependency for azure-content-safety (reqwest), builder wiring to construct PromptShieldGuardrail, and the PromptShield implementation with chunking, API calls, timeout/failure handling, and tests.

Changes

Azure Content Safety Guardrail

Layer / File(s)Summary
Data model and schema contracts
crates/aisix-core/src/models/guardrail.rs, crates/aisix-core/src/models/mod.rs, schemas/resources/guardrail.schema.json
AzureContentSafetyConfig with endpoint, api_key, and timeout_ms (defaults to 5000ms); GuardrailKind::AzureContentSafety variant added and serderename_all = "snake_case" applied; re-export formatting adjusted; JSON Schema adds azure_content_safety variant; unit tests for explicit and default timeout deserialization.
Feature flag and dependency setup
crates/aisix-guardrails/Cargo.toml, crates/aisix-guardrails/src/lib.rs
Adds optional reqwest dependency, defines azure-content-safety feature (enables dep:reqwest), includes it in default features, conditionally declares prompt_shield module and re-exports PromptShieldGuardrail behind the feature flag.
Builder integration
crates/aisix-guardrails/src/build.rs
build_one matches GuardrailKind::AzureContentSafety and constructs PromptShieldGuardrail when feature enabled; when disabled returns BuildError::FeatureDisabled("azure-content-safety"); BuildError::FeatureDisabled made unconditionally available for multi-feature arms.
PromptShield implementation
crates/aisix-guardrails/src/prompt_shield.rs
Adds PromptShieldGuardrail with HTTP client, endpoint normalization, stored API key, configurable timeout, and fail_open behavior; shield() chunks text and posts each chunk to Azure shieldPrompt, returning Block on any attackDetected=true; call_api() enforces timeout, maps 429/5xx/4xx/timeouts to AcsFailure; handle_failure() maps failures to Bypass (when fail_open) or Block (when not); includes serde request/response shapes, Guardrail trait impl gating by hook point, chunking helpers, and comprehensive tests (unit + tokio/wiremock integration).

Sequence Diagram

sequenceDiagram
participant Guardrail as check_input/output
participant PromptShield as PromptShieldGuardrail::shield
participant Chunker as chunk_text
participant AzureAPI as Azure shieldPrompt
participant FailureHandler as handle_failure
Guardrail->>PromptShield: collected text + hook_point
PromptShield->>Chunker: split into ~10k chunks
loop for each chunk
PromptShield->>AzureAPI: POST shieldPrompt (Ocp-Apim-Subscription-Key)
AzureAPI-->>PromptShield: response (attackDetected, analyses) or error/status
alt attackDetected == true
PromptShield-->>Guardrail: Block
else error/timeout
PromptShield->>FailureHandler: AcsFailure
FailureHandler-->>PromptShield: Bypass tag or Block (per fail_open)
end
end
PromptShield-->>Guardrail: Allow or Block or Bypass verdict
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

MEDIUM-1: timeout_ms=0 doc contradicted actual behavior
- Fix doc comment in AzureContentSafetyConfig.timeout_ms: "0 = no timeout"
was wrong; Duration::ZERO fires on the first poll. Correct guidance:
use u32::MAX for an effectively unlimited timeout. Regenerate schema.
MEDIUM-2: happy-path test did not assert request body shape
- Add body_json + content-type matchers to
clean_input_returns_allow_and_sends_auth_header so a rename of
ShieldRequest.user_prompt or ShieldRequest.documents would catch a
real break rather than silently passing.
MEDIUM-3: 4xx (non-429) errors mislabeled as azure_cs_5xx
- Add AcsFailure::ConfigError + AcsFailure::ServerError, retiring the
ambiguous Other variant. 4xx gets bypass_tag "azure_cs_config_error";
5xx keeps "azure_cs_5xx". Log 4xx at error level (not warn) since
with fail_open=true a wrong api_key silently bypasses every request.
- Add two wiremock tests (HTTP 401 fail_open=true/false) to pin the
new ConfigError → azure_cs_config_error path.
- Extend bypass_tags_match_wire_contract to cover ServerError and
ConfigError variants.
- Update module-level behavior matrix table.
LOW-1: Fix inaccurate "disable pool timeout" comment in new()
LOW-2: chunk_text("") now returns [] not [""]; strengthen test assertion
…2 (api-version pin)
New MEDIUM from re-audit: ConfigError path fired tracing::error! in
call_api() then tracing::warn! in handle_failure() for the same event,
producing two log lines at different levels per request. Suppress the
generic warn when the failure is ConfigError.
LOW-2: pin api-version query parameter in the contract test so a version
bump in SHIELD_PATH would be caught immediately. Add query_param matcher
alongside the existing body_json and content-type assertions.
LOW-1 (schema minimum=0) is intentional: the CP serializes timeout_ms
with omitempty, so timeout_ms=0 is never forwarded to the DP — the DP
defaults to 5000. The schema minimum=0 is correct for the CP→DP flow.
Sign up for freeto 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.

1 participant

@moonming