diff --git a/crates/aisix-admin/src/openapi.rs b/crates/aisix-admin/src/openapi.rs index 7f77632d4..22e87c1af 100644 --- a/crates/aisix-admin/src/openapi.rs +++ b/crates/aisix-admin/src/openapi.rs @@ -4161,7 +4161,7 @@ fn add_variant_titles(doc: &mut Value) { "Azure AI Content Safety Prompt Shield", "Azure AI Content Safety Text Moderation", "Aliyun Text Moderation", - "PII Detection & Redaction", + "PII Detection and Redaction", "Lakera Guard", "OpenAI Moderation", "Microsoft Presidio", @@ -4259,59 +4259,59 @@ fn add_missing_property_descriptions(doc: &mut Value) { for (pointer, description) in [ ( "/components/schemas/BedrockAWSCredentials/oneOf/0/properties/kind", - "Credential mode. Use `static` for explicitly configured AWS access keys.", + "Credential mode for explicitly configured AWS access keys.", ), ( "/components/schemas/BedrockLatencyMode/oneOf/0/properties/kind", - "Latency mode. Use `serial` to wait for the Bedrock guardrail response.", + "Latency mode that waits for the Bedrock guardrail response.", ), ( "/components/schemas/BedrockLatencyMode/oneOf/1/properties/kind", - "Latency mode. Use `timed` to stop waiting after `timeout_ms`.", + "Latency mode that stops waiting after `timeout_ms`.", ), ( "/components/schemas/Guardrail/oneOf/0/properties/kind", - "Guardrail provider type. Use `keyword` for literal and regular expression matching.", + "Guardrail provider type for literal and regular expression matching.", ), ( "/components/schemas/Guardrail/oneOf/1/properties/kind", - "Guardrail provider type. Use `bedrock` for Amazon Bedrock Guardrails.", + "Guardrail provider type for Amazon Bedrock Guardrails.", ), ( "/components/schemas/Guardrail/oneOf/2/properties/kind", - "Guardrail provider type. Use `azure_content_safety` for Azure Prompt Shield.", + "Guardrail provider type for Azure Prompt Shield.", ), ( "/components/schemas/Guardrail/oneOf/3/properties/kind", - "Guardrail provider type. Use `azure_content_safety_text_moderation` for Azure text moderation.", + "Guardrail provider type for Azure text moderation.", ), ( "/components/schemas/Guardrail/oneOf/4/properties/kind", - "Guardrail provider type. Use `aliyun_text_moderation` for Aliyun text moderation.", + "Guardrail provider type for Aliyun text moderation.", ), ( "/components/schemas/Guardrail/oneOf/5/properties/kind", - "Guardrail provider type. Use `pii` for in-process sensitive-data detection and redaction.", + "Guardrail provider type for in-process sensitive-data detection and redaction.", ), ( "/components/schemas/Guardrail/oneOf/6/properties/kind", - "Guardrail provider type. Use `lakera` for Lakera Guard screening.", + "Guardrail provider type for Lakera Guard screening.", ), ( "/components/schemas/Guardrail/oneOf/7/properties/kind", - "Guardrail provider type. Use `openai_moderation` for the OpenAI Moderation API.", + "Guardrail provider type for the OpenAI Moderation API.", ), ( "/components/schemas/Guardrail/oneOf/8/properties/kind", - "Guardrail provider type. Use `presidio` for self-hosted Microsoft Presidio PII detection and anonymization.", + "Guardrail provider type for self-hosted Microsoft Presidio PII detection and anonymization.", ), ( "/components/schemas/KeywordPattern/oneOf/0/properties/kind", - "Pattern type. Use `literal` to match the value as plain text.", + "Pattern type for matching the value as plain text.", ), ( "/components/schemas/KeywordPattern/oneOf/1/properties/kind", - "Pattern type. Use `regex` to match the value as a regular expression.", + "Pattern type for matching the value as a regular expression.", ), ( "/components/schemas/KeywordPattern/oneOf/0/properties/value", diff --git a/crates/aisix-core/src/models/guardrail.rs b/crates/aisix-core/src/models/guardrail.rs index b2435ed27..bb94be2e1 100644 --- a/crates/aisix-core/src/models/guardrail.rs +++ b/crates/aisix-core/src/models/guardrail.rs @@ -34,15 +34,15 @@ //! * `aliyun_text_moderation` — calls Aliyun's content-safety //! guardrail (`TextModerationPlus` on `green-cip..aliyuncs.com`). //! Risk-level moderation on input (`llm_query_moderation`) and output -//! (`llm_response_moderation`). #603. +//! (`llm_response_moderation`). //! * `pii` — in-process sensitive-data detection + redaction -//! (built-in detectors + custom regex, `mask`/`block`). #932. +//! (built-in detectors + custom regex, `mask`/`block`). //! * `lakera` — calls Lakera Guard `/v2/guard`; injection/jailbreak -//! blocks, PII-only detections mask via returned offsets. #52. +//! blocks, PII-only detections mask via returned offsets. //! * `openai_moderation` — calls the OpenAI Moderation API; -//! detection-only block. #52. +//! detection-only block. //! * `presidio` — self-hosted Presidio analyze→anonymize; per-entity -//! `mask`/`block` + selectable anonymize operator. #52. +//! `mask`/`block` + selectable anonymize operator. //! //! See `aisix-guardrails/src/keyword.rs` for the runtime semantics //! the snapshot is parsed into. @@ -101,7 +101,9 @@ pub enum BedrockAWSCredentials { }, } -/// Per-guardrail latency policy for `kind: "bedrock"`. `serial` waits for the guardrail response. `timed` aborts at `timeout_ms` and applies `fail_open`. +/// Per-guardrail latency policy for `kind: "bedrock"`. Serial mode waits +/// for the guardrail response; timed mode aborts at `timeout_ms` and applies +/// `fail_open`. #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)] #[serde(tag = "kind", rename_all = "lowercase")] pub enum BedrockLatencyMode { @@ -175,7 +177,9 @@ pub struct AzureContentSafetyTextModerationConfig { pub timeout_ms: u32, // --- moderation parameters --- - /// Severity scale. Use `FourSeverityLevels` for 0, 2, 4, and 6, or `EightSeverityLevels` for 0 through 7. + /// Severity scale used for Azure category scores. The four-level scale + /// returns even severities; the eight-level scale returns every integer + /// severity. #[serde(default = "default_acs_output_type")] pub output_type: String, /// Categories to analyze. @@ -194,13 +198,14 @@ pub struct AzureContentSafetyTextModerationConfig { /// Forwarded to Azure's `haltOnBlocklistHit`. #[serde(default)] pub halt_on_blocklist_hit: bool, - /// Input-hook text selection. Use `concatenate_all_content` to include all message content. Ignored on the output hook. + /// Input-hook text selection. The default scans user messages only; the + /// alternate mode includes all message content. Ignored on the output hook. #[serde(default = "default_acs_text_source")] pub text_source: String, // --- streaming-output controls (consumed by aisix-proxy build_sse_stream) --- - /// `window` for sliding-window incremental release or `buffer_full` - /// for whole-response hold-back. + /// Streaming output moderation mode: sliding-window incremental release + /// or whole-response hold-back. #[serde(default = "default_acs_stream_processing_mode")] pub stream_processing_mode: String, /// Sliding-window size in characters for window mode. @@ -214,7 +219,7 @@ pub struct AzureContentSafetyTextModerationConfig { #[serde(default = "default_acs_max_buffer_bytes")] #[schemars(range(min = 1))] pub max_buffer_bytes: u64, - /// Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit. + /// Buffer-overflow policy for streamed output when the buffer cap is hit. #[serde(default = "default_acs_on_buffer_exceeded")] pub on_buffer_exceeded: String, /// Fail-open policy for the output hook. When disabled, an Azure outage does not release unscanned model output. @@ -295,7 +300,8 @@ pub struct AliyunTextModerationConfig { /// in memory only and is not logged. Used to sign the request. #[schemars(length(min = 1))] pub access_key_secret: String, - /// Minimum risk level that triggers a block: `low`, `medium`, or `high`. A returned level at or above this blocks. + /// Minimum risk level that triggers a block. A returned level at or above + /// this threshold blocks. #[serde(default = "default_aliyun_risk_level_threshold")] pub risk_level_threshold: String, /// HTTP call timeout in milliseconds. `fail_open` and `output_fail_open` @@ -309,8 +315,8 @@ pub struct AliyunTextModerationConfig { pub output_fail_open: bool, // --- streaming-output controls (consumed by aisix-proxy build_sse_stream) --- - /// `window` for sliding-window incremental release or `buffer_full` - /// for whole-response hold-back. + /// Streaming output moderation mode: sliding-window incremental release + /// or whole-response hold-back. #[serde(default = "default_acs_stream_processing_mode")] pub stream_processing_mode: String, /// Sliding-window size in characters when window mode is used. Aliyun limits each `llm_response_moderation` call to 2,000 characters. @@ -324,7 +330,7 @@ pub struct AliyunTextModerationConfig { #[serde(default = "default_acs_max_buffer_bytes")] #[schemars(range(min = 1))] pub max_buffer_bytes: u64, - /// Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit. + /// Buffer-overflow policy for streamed output when the buffer cap is hit. #[serde(default = "default_acs_on_buffer_exceeded")] pub on_buffer_exceeded: String, } @@ -339,13 +345,12 @@ fn default_aliyun_risk_level_threshold() -> String { #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct PiiDetectorConfig { - /// Built-in detector id: `email`, `china_mobile`, `china_id_card`, - /// `bank_card`, `us_ssn`, `ip_address`, `api_key`, `jwt`, `private_key`. + /// Built-in detector to enable for this PII guardrail entry. #[serde(rename = "type")] #[schemars(length(min = 1))] pub detector_type: String, - /// Per-detector action override: `mask` or `block`. Falls back to the - /// guardrail's `default_action` when omitted. + /// Per-detector action override. Falls back to the guardrail's + /// `default_action` when omitted. #[serde(default, skip_serializing_if = "Option::is_none")] pub action: Option, } @@ -362,16 +367,17 @@ pub struct PiiCustomPattern { /// An invalid pattern makes AISIX log and skip the guardrail. #[schemars(length(min = 1))] pub regex: String, - /// Per-pattern action override: `mask` or `block`. Falls back to the - /// guardrail's `default_action` when omitted. + /// Per-pattern action override. Falls back to the guardrail's + /// `default_action` when omitted. #[serde(default, skip_serializing_if = "Option::is_none")] pub action: Option, } -/// Config block for `kind: "pii"`. In-process sensitive-data detection and -/// redaction (#932): built-in detectors + custom regex patterns, each with a -/// `mask` (redact-and-continue) or `block` action, applied to request and/or -/// response text per the row's `hook_point`. +/// Config block for `kind: "pii"`. Built-in sensitive-data detection and +/// redaction. AISIX evaluates built-in detectors and custom regex patterns +/// inside the gateway. Each detector can mask the matched span and let traffic +/// continue, or block the request or response according to the row's +/// `hook_point`. /// /// `mask` rewrites each matched span to `[_REDACTED]` and lets the /// request/response continue; `block` rejects with the standard 422 @@ -380,16 +386,14 @@ pub struct PiiCustomPattern { #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)] #[serde(deny_unknown_fields)] pub struct PiiConfig { - /// Built-in detectors to enable. Unknown detector ids are rejected when - /// AISIX builds the guardrail chain, so a typo cannot silently disable - /// the policy. + /// Built-in detectors to enable. The resource schema rejects unknown + /// detector ids, so a typo cannot silently disable the policy. #[serde(default)] pub detectors: Vec, /// Operator-supplied regex detectors, evaluated after the built-ins. #[serde(default)] pub custom_patterns: Vec, - /// Action for detectors that don't set their own: `mask` (default) or - /// `block`. + /// Action for detectors that do not set their own override. #[serde(default = "default_pii_action")] pub default_action: String, @@ -402,9 +406,7 @@ pub struct PiiConfig { #[serde(default = "default_acs_max_buffer_bytes")] #[schemars(range(min = 1))] pub max_buffer_bytes: u64, - /// Buffer-overflow policy. Use `fail_open` to release output unscanned - /// (and unmasked) when the buffer cap is hit; the default `fail_closed` - /// blocks the response instead. + /// Buffer-overflow policy for streamed output when the buffer cap is hit. #[serde(default = "default_acs_on_buffer_exceeded")] pub on_buffer_exceeded: String, } @@ -438,7 +440,7 @@ pub struct BedrockConfig { pub region: String, /// IAM credentials for Bedrock requests. pub aws_credentials: BedrockAWSCredentials, - /// Bedrock guardrail latency policy. Use `timed` with `timeout_ms` to cap wait time. + /// Bedrock guardrail latency policy. Timed mode caps wait time with `timeout_ms`. pub latency_mode: BedrockLatencyMode, /// Fail-open policy for the output hook. When disabled (the default), a /// Bedrock outage blocks model output instead of releasing unscanned content. @@ -447,13 +449,11 @@ pub struct BedrockConfig { pub output_fail_open: bool, } -/// Config block for `kind: "lakera"` (#52). Calls Lakera Guard -/// (`POST {endpoint}/v2/guard`) with the conversation text and translates -/// the screening result into a verdict: `flagged` with any non-PII -/// detector (prompt injection, jailbreak, moderated content) blocks; -/// `flagged` with ONLY `pii/*` detectors masks the detected spans using -/// the offsets Lakera returns and lets the request continue (LiteLLM -/// `lakera_ai_v2` behavior). +/// Config block for `kind: "lakera"`. Calls Lakera Guard +/// (`POST {endpoint}/v2/guard`) with conversation text and translates the +/// screening result into a verdict. Prompt-injection, jailbreak, and content +/// detections block traffic. Detections that involve only PII mask the detected +/// spans using the offsets Lakera returns and let traffic continue. /// /// The `api_key` is stored encrypted and decrypted only when the /// configuration is applied; the plaintext is held in memory only and is @@ -495,17 +495,16 @@ pub struct LakeraConfig { #[serde(default = "default_acs_max_buffer_bytes")] #[schemars(range(min = 1))] pub max_buffer_bytes: u64, - /// Buffer-overflow policy. Use `fail_open` to release output unscanned - /// when the buffer cap is hit; the default `fail_closed` blocks the - /// response instead. + /// Buffer-overflow policy for streamed output when the buffer cap is hit. #[serde(default = "default_acs_on_buffer_exceeded")] pub on_buffer_exceeded: String, } -/// Config block for `kind: "openai_moderation"` (#52). Calls the OpenAI -/// Moderation API (`POST {endpoint}/moderations`, free) and blocks when the -/// result is flagged. Detection-only — it never rewrites content. -/// Monitor-before-enforce comes from the row's `enforcement_mode`. +/// Config block for `kind: "openai_moderation"`. Calls the OpenAI Moderation +/// API (`POST {endpoint}/moderations`) and blocks when the provider flags +/// content or when configured category thresholds are reached. This guardrail +/// is detection-only and never rewrites content. Monitor-before-enforce comes +/// from the row's `enforcement_mode`. /// /// The `api_key` is stored encrypted and decrypted only when the /// configuration is applied; the plaintext is held in memory only and is @@ -522,8 +521,9 @@ pub struct OpenaiModerationConfig { #[serde(default)] #[schemars(length(min = 1))] pub endpoint: Option, - /// Moderation model. `omni-moderation-latest` (default) or - /// `text-moderation-latest`. + /// Moderation model sent to the provider. The default is + /// `omni-moderation-latest`; provider model names are not restricted by + /// AISIX. #[serde(default = "default_openai_moderation_model")] #[schemars(length(min = 1))] pub model: String, @@ -561,24 +561,21 @@ pub struct PresidioEntityConfig { #[serde(rename = "type")] #[schemars(length(min = 1))] pub entity_type: String, - /// Per-entity action override: `mask` or `block`. Falls back to the - /// guardrail's `default_action` when omitted. + /// Per-entity action override. Falls back to the guardrail's + /// `default_action` when omitted. #[serde(default, skip_serializing_if = "Option::is_none")] pub action: Option, } -/// Config block for `kind: "presidio"` (#52). Self-hosted Microsoft -/// Presidio PII detection + anonymization: `POST {analyzer_url}/analyze` -/// finds entities; when the effective action is `mask`, -/// `POST {anonymizer_url}/anonymize` rewrites the text and the request/ -/// response continues; `block` rejects with the standard 422 -/// content-filter envelope. +/// Config block for `kind: "presidio"`. Calls a self-hosted Microsoft +/// Presidio analyzer to find PII entities and, when the effective action is +/// `mask`, calls the anonymizer to rewrite text before traffic continues. +/// `block` rejects with the standard 422 content-filter envelope. /// /// vs. the built-in `kind: "pii"`: Presidio adds NER/ML entities a regex /// cannot express (`PERSON`, `LOCATION`, `NRP`, …), a self-hosted -/// compliance posture, and selectable anonymize operators (`replace`, -/// `mask`, `hash`, `redact`). No vendor secret — both URLs point at -/// customer-run containers. +/// compliance posture, and selectable anonymize operators. No vendor secret: +/// both URLs point at customer-run containers. #[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq)] #[serde(deny_unknown_fields)] pub struct PresidioConfig { @@ -595,13 +592,10 @@ pub struct PresidioConfig { /// full recognizer set and applies `default_action` to every hit. #[serde(default)] pub entities: Vec, - /// Action for entities that don't set their own: `mask` (default) or - /// `block`. + /// Action for entities that do not set their own override. #[serde(default = "default_pii_action")] pub default_action: String, - /// Anonymize operator applied to masked entities: `replace` (default — - /// Presidio substitutes ``), `mask` (asterisks), `hash` - /// (SHA-256 hex), or `redact` (span removed). + /// Anonymize operator applied to masked entities. #[serde(default = "default_presidio_operator")] pub operator: String, /// Analyzer language code. @@ -634,9 +628,7 @@ pub struct PresidioConfig { #[serde(default = "default_acs_max_buffer_bytes")] #[schemars(range(min = 1))] pub max_buffer_bytes: u64, - /// Buffer-overflow policy. Use `fail_open` to release output unscanned - /// (and unmasked) when the buffer cap is hit; the default `fail_closed` - /// blocks the response instead. + /// Buffer-overflow policy for streamed output when the buffer cap is hit. #[serde(default = "default_acs_on_buffer_exceeded")] pub on_buffer_exceeded: String, } @@ -670,23 +662,25 @@ pub enum GuardrailKind { /// `TextModerationPlus` action on `green-cip..aliyuncs.com`, on /// input and/or output, including streaming output. AliyunTextModeration(AliyunTextModerationConfig), - /// In-process sensitive-data detection + redaction (#932): built-in - /// detectors + custom regex, per-detector `mask`/`block` actions, on - /// input and/or output, including streaming output. Always available. + /// Built-in sensitive-data detection and redaction inside AISIX. Built-in + /// detectors and custom regex patterns can mask matched spans or block + /// traffic on input, output, or both, including buffered streaming output. + /// Always available and does not call an external service. Pii(PiiConfig), - /// Lakera Guard screening via `POST /v2/guard` (#52): prompt-injection / - /// jailbreak / content detection blocks; PII-only detections mask via - /// the returned offsets, on input and/or output, including streaming - /// output. + /// Lakera Guard screening via `POST /v2/guard`. Prompt-injection, + /// jailbreak, and content-policy detections block traffic. Detections + /// that involve only PII mask spans using the returned offsets. Applies + /// on input, output, or both, including buffered streaming output. Lakera(LakeraConfig), - /// OpenAI Moderation API (#52): category content moderation via - /// `POST /moderations`, detection-only (block, never rewrite), on input - /// and/or output, including streaming output. + /// OpenAI Moderation API category screening via `POST /moderations`. + /// Flagged content or configured category thresholds block traffic. This + /// guardrail is detection-only and never rewrites content. Applies on + /// input, output, or both, including buffered streaming output. OpenaiModeration(OpenaiModerationConfig), - /// Self-hosted Microsoft Presidio PII detection + anonymization (#52): - /// analyzer entities with per-entity `mask`/`block` actions and a - /// selectable anonymize operator, on input and/or output, including - /// streaming output. + /// Self-hosted Microsoft Presidio PII detection and anonymization. + /// Analyzer entities can mask or block per entity, and masked entities + /// use the selected anonymize operator. Applies on input, output, or both, + /// including buffered streaming output. Presidio(PresidioConfig), } @@ -788,9 +782,9 @@ pub struct Guardrail { #[serde(flatten)] pub config: GuardrailKind, - /// How AISIX handles matching content. `block` enforces the guardrail. - /// `monitor` records what would have happened without blocking or - /// redacting the caller-visible response. + /// How AISIX handles matching content. Enforcing mode applies the + /// guardrail verdict; monitor mode records what would have happened + /// without blocking or redacting the caller-visible response. #[serde(default = "default_enforcement_mode")] pub enforcement_mode: String, @@ -801,8 +795,8 @@ pub struct Guardrail { #[serde(default)] pub mandatory: bool, - /// Attachment direction hint: `input`, `output`, or `both`. Guardrail - /// execution still follows `hook_point`. + /// Attachment direction hint. Guardrail execution still follows + /// `hook_point`. #[serde(default = "default_direction")] pub direction: String, diff --git a/crates/aisix-core/src/models/schema.rs b/crates/aisix-core/src/models/schema.rs index dabd2efcc..c5ab60789 100644 --- a/crates/aisix-core/src/models/schema.rs +++ b/crates/aisix-core/src/models/schema.rs @@ -286,8 +286,10 @@ fn title_single_value_enum_variants( /// schema enforced via `enum`. They stay `String` on the struct (their /// values flow through `aisix-guardrails` as strings; converting them to /// Rust enums would churn that crate's processing), so the closed set is -/// injected here into the relevant kind branch. -/// 3. `created_at` republishes its `date-time` format (annotation-only). +/// injected here into the relevant property. +/// 3. `schemars` leaves discriminator tag fields and collection item schemas +/// without descriptions, so the public schema fills those gaps. +/// 4. `created_at` republishes its `date-time` format (annotation-only). pub fn guardrail_root_schema() -> Value { let mut schema = struct_root_schema::(false); let obj = schema @@ -310,6 +312,85 @@ pub fn guardrail_root_schema() -> Value { } } } + + set_definition_property_enum( + defs, + "PiiDetectorConfig", + "type", + json!([ + "email", + "china_mobile", + "china_id_card", + "bank_card", + "us_ssn", + "ip_address", + "api_key", + "jwt", + "private_key" + ]), + ); + set_definition_property_enum( + defs, + "PiiDetectorConfig", + "action", + json!(["mask", "block"]), + ); + set_definition_property_enum(defs, "PiiCustomPattern", "action", json!(["mask", "block"])); + set_definition_property_enum( + defs, + "PresidioEntityConfig", + "action", + json!(["mask", "block"]), + ); + set_definition_variant_property_description( + defs, + "BedrockAWSCredentials", + "static", + "kind", + "Credential mode for explicitly configured AWS access keys.", + ); + set_definition_variant_property_description( + defs, + "BedrockLatencyMode", + "serial", + "kind", + "Latency mode that waits for the Bedrock guardrail response.", + ); + set_definition_variant_property_description( + defs, + "BedrockLatencyMode", + "timed", + "kind", + "Latency mode that stops waiting after `timeout_ms`.", + ); + set_definition_variant_property_description( + defs, + "KeywordPattern", + "literal", + "kind", + "Pattern type for matching the value as plain text.", + ); + set_definition_variant_property_description( + defs, + "KeywordPattern", + "regex", + "kind", + "Pattern type for matching the value as a regular expression.", + ); + set_definition_variant_property_description( + defs, + "KeywordPattern", + "literal", + "value", + "Literal string to match.", + ); + set_definition_variant_property_description( + defs, + "KeywordPattern", + "regex", + "value", + "Regular expression pattern to match.", + ); } if let Some(Value::Array(branches)) = obj.get_mut("oneOf") { @@ -317,8 +398,14 @@ pub fn guardrail_root_schema() -> Value { let Some(b) = branch.as_object_mut() else { continue; }; - match branch_kind(b) { - Some("azure_content_safety_text_moderation") => { + let Some(kind) = branch_kind(b).map(str::to_owned) else { + continue; + }; + if let Some(description) = guardrail_kind_description(&kind) { + set_property_description(b, "kind", description); + } + match kind.as_str() { + "azure_content_safety_text_moderation" => { set_property_enum( b, "output_type", @@ -334,21 +421,48 @@ pub fn guardrail_root_schema() -> Value { "stream_processing_mode", json!(["window", "buffer_full"]), ); - set_property_enum(b, "on_buffer_exceeded", json!(["fail_closed", "fail_open"])); set_property_items_enum( b, "categories", json!(["Hate", "Sexual", "SelfHarm", "Violence"]), ); + set_property_items_description( + b, + "categories", + "Azure content category to analyze.", + ); + set_property_items_description( + b, + "blocklist_names", + "Azure blocklist name to match against.", + ); + set_property_additional_properties_description( + b, + "severity_threshold_by_category", + "Severity threshold for the category key.", + ); } - Some("aliyun_text_moderation") => { + "aliyun_text_moderation" => { set_property_enum(b, "risk_level_threshold", json!(["low", "medium", "high"])); set_property_enum( b, "stream_processing_mode", json!(["window", "buffer_full"]), ); - set_property_enum(b, "on_buffer_exceeded", json!(["fail_closed", "fail_open"])); + } + "pii" => { + set_property_enum(b, "default_action", json!(["mask", "block"])); + } + "openai_moderation" => { + set_property_additional_properties_description( + b, + "category_thresholds", + "Score threshold for the category key.", + ); + } + "presidio" => { + set_property_enum(b, "default_action", json!(["mask", "block"])); + set_property_enum(b, "operator", json!(["replace", "mask", "hash", "redact"])); } _ => {} } @@ -369,12 +483,85 @@ pub fn guardrail_root_schema() -> Value { /// Set a closed `enum` on a oneOf branch's property (for stringly-typed fields /// whose closed set lives only in the schema, not the Rust type). fn set_property_enum(branch: &mut serde_json::Map, field: &str, values: Value) { + if let Some(Value::Object(properties)) = branch.get_mut("properties") { + set_enum(properties, field, values); + } +} + +fn set_definition_property_enum( + defs: &mut serde_json::Map, + definition: &str, + field: &str, + values: Value, +) { + if let Some(Value::Object(properties)) = defs + .get_mut(definition) + .and_then(|d| d.get_mut("properties")) + { + set_enum(properties, field, values); + } +} + +fn set_definition_variant_property_description( + defs: &mut serde_json::Map, + definition: &str, + variant_kind: &str, + field: &str, + description: &str, +) { + let Some(Value::Array(branches)) = defs.get_mut(definition).and_then(|d| d.get_mut("oneOf")) + else { + return; + }; + for branch in branches { + let Some(branch) = branch.as_object_mut() else { + continue; + }; + if branch_kind(branch) == Some(variant_kind) { + set_property_description(branch, field, description); + } + } +} + +fn guardrail_kind_description(kind: &str) -> Option<&'static str> { + match kind { + "keyword" => Some("Guardrail provider type for literal and regular expression matching."), + "bedrock" => Some("Guardrail provider type for Amazon Bedrock Guardrails."), + "azure_content_safety" => Some("Guardrail provider type for Azure Prompt Shield."), + "azure_content_safety_text_moderation" => { + Some("Guardrail provider type for Azure text moderation.") + } + "aliyun_text_moderation" => Some("Guardrail provider type for Aliyun text moderation."), + "pii" => { + Some("Guardrail provider type for in-process sensitive-data detection and redaction.") + } + "lakera" => Some("Guardrail provider type for Lakera Guard screening."), + "openai_moderation" => Some("Guardrail provider type for the OpenAI Moderation API."), + "presidio" => { + Some("Guardrail provider type for self-hosted Microsoft Presidio PII detection and anonymization.") + } + _ => None, + } +} + +fn set_enum(properties: &mut serde_json::Map, field: &str, values: Value) { + if let Some(prop) = properties.get_mut(field).and_then(Value::as_object_mut) { + prop.insert("enum".to_string(), values); + } +} + +fn set_property_description( + branch: &mut serde_json::Map, + field: &str, + description: &str, +) { if let Some(prop) = branch .get_mut("properties") .and_then(|p| p.get_mut(field)) .and_then(Value::as_object_mut) { - prop.insert("enum".to_string(), values); + prop.entry("description".to_string()) + .or_insert_with(|| Value::String(description.to_string())); } } @@ -394,6 +581,40 @@ fn set_property_items_enum( } } +fn set_property_items_description( + branch: &mut serde_json::Map, + field: &str, + description: &str, +) { + if let Some(items) = branch + .get_mut("properties") + .and_then(|p| p.get_mut(field)) + .and_then(|f| f.get_mut("items")) + .and_then(Value::as_object_mut) + { + items + .entry("description".to_string()) + .or_insert_with(|| Value::String(description.to_string())); + } +} + +fn set_property_additional_properties_description( + branch: &mut serde_json::Map, + field: &str, + description: &str, +) { + if let Some(additional_properties) = branch + .get_mut("properties") + .and_then(|p| p.get_mut(field)) + .and_then(|f| f.get_mut("additionalProperties")) + .and_then(Value::as_object_mut) + { + additional_properties + .entry("description".to_string()) + .or_insert_with(|| Value::String(description.to_string())); + } +} + /// Canonical JSON Schema for the `cache_policy` resource, derived from the /// [`CachePolicy`](crate::models::CachePolicy) struct. The struct intentionally /// has no `deny_unknown_fields`, so the schema omits `additionalProperties` @@ -1509,6 +1730,155 @@ mod tests { assert!(validate_guardrail(&v).is_err()); } + #[test] + fn guardrail_rejects_invalid_string_enums() { + let cases = [ + json!({ + "name": "g", + "kind": "azure_content_safety_text_moderation", + "endpoint": "https://example.cognitiveservices.azure.com", + "api_key": "key", + "output_type": "TwoSeverityLevels" + }), + json!({ + "name": "g", + "kind": "azure_content_safety_text_moderation", + "endpoint": "https://example.cognitiveservices.azure.com", + "api_key": "key", + "categories": ["Hate", "Spam"] + }), + json!({ + "name": "g", + "kind": "azure_content_safety_text_moderation", + "endpoint": "https://example.cognitiveservices.azure.com", + "api_key": "key", + "text_source": "assistant_only" + }), + json!({ + "name": "g", + "kind": "azure_content_safety_text_moderation", + "endpoint": "https://example.cognitiveservices.azure.com", + "api_key": "key", + "stream_processing_mode": "chunked" + }), + json!({ + "name": "g", + "kind": "pii", + "detectors": [{ "type": "email", "action": "redact" }] + }), + json!({ + "name": "g", + "kind": "pii", + "custom_patterns": [{ + "name": "employee_id", + "regex": "\\bEMP-\\d+\\b", + "action": "redact" + }] + }), + json!({ + "name": "g", + "kind": "pii", + "default_action": "redact", + "detectors": [{ "type": "email" }] + }), + json!({ + "name": "g", + "kind": "pii", + "detectors": [{ "type": "driver_license" }] + }), + json!({ + "name": "g", + "kind": "presidio", + "analyzer_url": "http://presidio-analyzer:3000", + "anonymizer_url": "http://presidio-anonymizer:3000", + "default_action": "redact" + }), + json!({ + "name": "g", + "kind": "presidio", + "analyzer_url": "http://presidio-analyzer:3000", + "anonymizer_url": "http://presidio-anonymizer:3000", + "entities": [{ "type": "EMAIL_ADDRESS", "action": "redact" }] + }), + json!({ + "name": "g", + "kind": "presidio", + "analyzer_url": "http://presidio-analyzer:3000", + "anonymizer_url": "http://presidio-anonymizer:3000", + "operator": "tokenize" + }), + ]; + + for value in cases { + assert!( + validate_guardrail(&value).is_err(), + "guardrail schema accepted invalid enum value: {value}", + ); + } + } + + #[test] + fn guardrail_fail_safe_fields_stay_schema_open() { + let cases = [ + json!({ + "name": "g", + "kind": "keyword", + "patterns": [], + "enforcement_mode": "audit", + "direction": "sideways" + }), + json!({ + "name": "g", + "kind": "azure_content_safety_text_moderation", + "endpoint": "https://example.cognitiveservices.azure.com", + "api_key": "key", + "on_buffer_exceeded": "drop" + }), + json!({ + "name": "g", + "kind": "aliyun_text_moderation", + "region": "cn-shanghai", + "access_key_id": "id", + "access_key_secret": "s", + "on_buffer_exceeded": "drop" + }), + json!({ + "name": "g", + "kind": "pii", + "detectors": [{ "type": "email" }], + "on_buffer_exceeded": "drop" + }), + json!({ + "name": "g", + "kind": "lakera", + "api_key": "key", + "on_buffer_exceeded": "drop" + }), + json!({ + "name": "g", + "kind": "presidio", + "analyzer_url": "http://presidio-analyzer:3000", + "anonymizer_url": "http://presidio-anonymizer:3000", + "on_buffer_exceeded": "drop" + }), + ]; + + for value in cases { + validate_guardrail(&value).unwrap(); + } + } + + #[test] + fn guardrail_openai_moderation_model_stays_open() { + let v = json!({ + "name": "openai-mod", + "kind": "openai_moderation", + "api_key": "plaintext-key", + "model": "future-moderation-model" + }); + validate_guardrail(&v).unwrap(); + } + // ---- observability_exporter schema tests ---- #[test] diff --git a/schemas/resources/guardrail.schema.json b/schemas/resources/guardrail.schema.json index da80bdff5..f35caf178 100644 --- a/schemas/resources/guardrail.schema.json +++ b/schemas/resources/guardrail.schema.json @@ -13,6 +13,7 @@ "type": "string" }, "kind": { + "description": "Credential mode for explicitly configured AWS access keys.", "enum": [ "static" ], @@ -34,12 +35,13 @@ ] }, "BedrockLatencyMode": { - "description": "Per-guardrail latency policy for `kind: \"bedrock\"`. `serial` waits for the guardrail response. `timed` aborts at `timeout_ms` and applies `fail_open`.", + "description": "Per-guardrail latency policy for `kind: \"bedrock\"`. Serial mode waits for the guardrail response; timed mode aborts at `timeout_ms` and applies `fail_open`.", "oneOf": [ { "additionalProperties": false, "properties": { "kind": { + "description": "Latency mode that waits for the Bedrock guardrail response.", "enum": [ "serial" ], @@ -55,6 +57,7 @@ "additionalProperties": false, "properties": { "kind": { + "description": "Latency mode that stops waiting after `timeout_ms`.", "enum": [ "timed" ], @@ -110,12 +113,14 @@ "description": "Literal string to match.", "properties": { "kind": { + "description": "Pattern type for matching the value as plain text.", "enum": [ "literal" ], "type": "string" }, "value": { + "description": "Literal string to match.", "minLength": 1, "type": "string" } @@ -131,12 +136,14 @@ "description": "Regular expression pattern to match.", "properties": { "kind": { + "description": "Pattern type for matching the value as a regular expression.", "enum": [ "regex" ], "type": "string" }, "value": { + "description": "Regular expression pattern to match.", "minLength": 1, "type": "string" } @@ -154,7 +161,11 @@ "description": "One operator-supplied regex detector for `kind: \"pii\"`.", "properties": { "action": { - "description": "Per-pattern action override: `mask` or `block`. Falls back to the guardrail's `default_action` when omitted.", + "description": "Per-pattern action override. Falls back to the guardrail's `default_action` when omitted.", + "enum": [ + "mask", + "block" + ], "type": "string" }, "name": { @@ -180,11 +191,26 @@ "description": "One built-in detector selection for `kind: \"pii\"`. The `type` names the detector to enable; `action` optionally overrides the guardrail-level `default_action` for this detector only.", "properties": { "action": { - "description": "Per-detector action override: `mask` or `block`. Falls back to the guardrail's `default_action` when omitted.", + "description": "Per-detector action override. Falls back to the guardrail's `default_action` when omitted.", + "enum": [ + "mask", + "block" + ], "type": "string" }, "type": { - "description": "Built-in detector id: `email`, `china_mobile`, `china_id_card`, `bank_card`, `us_ssn`, `ip_address`, `api_key`, `jwt`, `private_key`.", + "description": "Built-in detector to enable for this PII guardrail entry.", + "enum": [ + "email", + "china_mobile", + "china_id_card", + "bank_card", + "us_ssn", + "ip_address", + "api_key", + "jwt", + "private_key" + ], "minLength": 1, "type": "string" } @@ -199,7 +225,11 @@ "description": "One entity selection for `kind: \"presidio\"`. The `type` names a Presidio entity (`EMAIL_ADDRESS`, `PHONE_NUMBER`, `PERSON`, `CREDIT_CARD`, …); `action` optionally overrides the guardrail-level `default_action` for this entity only — the same per-detector shape as `kind: \"pii\"`.", "properties": { "action": { - "description": "Per-entity action override: `mask` or `block`. Falls back to the guardrail's `default_action` when omitted.", + "description": "Per-entity action override. Falls back to the guardrail's `default_action` when omitted.", + "enum": [ + "mask", + "block" + ], "type": "string" }, "type": { @@ -220,6 +250,7 @@ "description": "In-process literal/regex blocklist. Always available.", "properties": { "kind": { + "description": "Guardrail provider type for literal and regular expression matching.", "enum": [ "keyword" ], @@ -263,6 +294,7 @@ "type": "string" }, "kind": { + "description": "Guardrail provider type for Amazon Bedrock Guardrails.", "enum": [ "bedrock" ], @@ -274,7 +306,7 @@ "$ref": "#/definitions/BedrockLatencyMode" } ], - "description": "Bedrock guardrail latency policy. Use `timed` with `timeout_ms` to cap wait time." + "description": "Bedrock guardrail latency policy. Timed mode caps wait time with `timeout_ms`." }, "output_fail_open": { "default": false, @@ -311,6 +343,7 @@ "type": "string" }, "kind": { + "description": "Guardrail provider type for Azure Prompt Shield.", "enum": [ "azure_content_safety" ], @@ -349,6 +382,7 @@ "default": [], "description": "Azure CS blocklist names to match against.", "items": { + "description": "Azure blocklist name to match against.", "type": "string" }, "type": "array" @@ -362,6 +396,7 @@ ], "description": "Categories to analyze.", "items": { + "description": "Azure content category to analyze.", "enum": [ "Hate", "Sexual", @@ -383,6 +418,7 @@ "type": "boolean" }, "kind": { + "description": "Guardrail provider type for Azure text moderation.", "enum": [ "azure_content_safety_text_moderation" ], @@ -397,11 +433,7 @@ }, "on_buffer_exceeded": { "default": "fail_closed", - "description": "Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit.", - "enum": [ - "fail_closed", - "fail_open" - ], + "description": "Buffer-overflow policy for streamed output when the buffer cap is hit.", "type": "string" }, "output_fail_open": { @@ -411,7 +443,7 @@ }, "output_type": { "default": "FourSeverityLevels", - "description": "Severity scale. Use `FourSeverityLevels` for 0, 2, 4, and 6, or `EightSeverityLevels` for 0 through 7.", + "description": "Severity scale used for Azure category scores. The four-level scale returns even severities; the eight-level scale returns every integer severity.", "enum": [ "FourSeverityLevels", "EightSeverityLevels" @@ -428,6 +460,7 @@ }, "severity_threshold_by_category": { "additionalProperties": { + "description": "Severity threshold for the category key.", "format": "uint8", "minimum": 0.0, "type": "integer" @@ -438,7 +471,7 @@ }, "stream_processing_mode": { "default": "window", - "description": "`window` for sliding-window incremental release or `buffer_full` for whole-response hold-back.", + "description": "Streaming output moderation mode: sliding-window incremental release or whole-response hold-back.", "enum": [ "window", "buffer_full" @@ -447,7 +480,7 @@ }, "text_source": { "default": "concatenate_user_content", - "description": "Input-hook text selection. Use `concatenate_all_content` to include all message content. Ignored on the output hook.", + "description": "Input-hook text selection. The default scans user messages only; the alternate mode includes all message content. Ignored on the output hook.", "enum": [ "concatenate_user_content", "concatenate_all_content" @@ -505,6 +538,7 @@ "type": "string" }, "kind": { + "description": "Guardrail provider type for Aliyun text moderation.", "enum": [ "aliyun_text_moderation" ], @@ -519,11 +553,7 @@ }, "on_buffer_exceeded": { "default": "fail_closed", - "description": "Buffer-overflow policy. Use `fail_open` to allow output when the buffer cap is hit.", - "enum": [ - "fail_closed", - "fail_open" - ], + "description": "Buffer-overflow policy for streamed output when the buffer cap is hit.", "type": "string" }, "output_fail_open": { @@ -538,7 +568,7 @@ }, "risk_level_threshold": { "default": "high", - "description": "Minimum risk level that triggers a block: `low`, `medium`, or `high`. A returned level at or above this blocks.", + "description": "Minimum risk level that triggers a block. A returned level at or above this threshold blocks.", "enum": [ "low", "medium", @@ -548,7 +578,7 @@ }, "stream_processing_mode": { "default": "window", - "description": "`window` for sliding-window incremental release or `buffer_full` for whole-response hold-back.", + "description": "Streaming output moderation mode: sliding-window incremental release or whole-response hold-back.", "enum": [ "window", "buffer_full" @@ -588,7 +618,7 @@ "type": "object" }, { - "description": "In-process sensitive-data detection + redaction (#932): built-in detectors + custom regex, per-detector `mask`/`block` actions, on input and/or output, including streaming output. Always available.", + "description": "Built-in sensitive-data detection and redaction inside AISIX. Built-in detectors and custom regex patterns can mask matched spans or block traffic on input, output, or both, including buffered streaming output. Always available and does not call an external service.", "properties": { "custom_patterns": { "default": [], @@ -600,18 +630,23 @@ }, "default_action": { "default": "mask", - "description": "Action for detectors that don't set their own: `mask` (default) or `block`.", + "description": "Action for detectors that do not set their own override.", + "enum": [ + "mask", + "block" + ], "type": "string" }, "detectors": { "default": [], - "description": "Built-in detectors to enable. Unknown detector ids are rejected when AISIX builds the guardrail chain, so a typo cannot silently disable the policy.", + "description": "Built-in detectors to enable. The resource schema rejects unknown detector ids, so a typo cannot silently disable the policy.", "items": { "$ref": "#/definitions/PiiDetectorConfig" }, "type": "array" }, "kind": { + "description": "Guardrail provider type for in-process sensitive-data detection and redaction.", "enum": [ "pii" ], @@ -626,7 +661,7 @@ }, "on_buffer_exceeded": { "default": "fail_closed", - "description": "Buffer-overflow policy. Use `fail_open` to release output unscanned (and unmasked) when the buffer cap is hit; the default `fail_closed` blocks the response instead.", + "description": "Buffer-overflow policy for streamed output when the buffer cap is hit.", "type": "string" } }, @@ -636,7 +671,7 @@ "type": "object" }, { - "description": "Lakera Guard screening via `POST /v2/guard` (#52): prompt-injection / jailbreak / content detection blocks; PII-only detections mask via the returned offsets, on input and/or output, including streaming output.", + "description": "Lakera Guard screening via `POST /v2/guard`. Prompt-injection, jailbreak, and content-policy detections block traffic. Detections that involve only PII mask spans using the returned offsets. Applies on input, output, or both, including buffered streaming output.", "properties": { "api_key": { "description": "Lakera API key sent as a `Authorization: Bearer` header. Decrypted before projection. Plaintext is held in memory only and is not logged.", @@ -650,6 +685,7 @@ "type": "string" }, "kind": { + "description": "Guardrail provider type for Lakera Guard screening.", "enum": [ "lakera" ], @@ -664,7 +700,7 @@ }, "on_buffer_exceeded": { "default": "fail_closed", - "description": "Buffer-overflow policy. Use `fail_open` to release output unscanned when the buffer cap is hit; the default `fail_closed` blocks the response instead.", + "description": "Buffer-overflow policy for streamed output when the buffer cap is hit.", "type": "string" }, "output_fail_open": { @@ -693,7 +729,7 @@ "type": "object" }, { - "description": "OpenAI Moderation API (#52): category content moderation via `POST /moderations`, detection-only (block, never rewrite), on input and/or output, including streaming output.", + "description": "OpenAI Moderation API category screening via `POST /moderations`. Flagged content or configured category thresholds block traffic. This guardrail is detection-only and never rewrites content. Applies on input, output, or both, including buffered streaming output.", "properties": { "api_key": { "description": "OpenAI API key sent as a `Authorization: Bearer` header. Decrypted before projection. Plaintext is held in memory only and is not logged.", @@ -702,6 +738,7 @@ }, "category_thresholds": { "additionalProperties": { + "description": "Score threshold for the category key.", "format": "double", "type": "number" }, @@ -716,6 +753,7 @@ "type": "string" }, "kind": { + "description": "Guardrail provider type for the OpenAI Moderation API.", "enum": [ "openai_moderation" ], @@ -723,7 +761,7 @@ }, "model": { "default": "omni-moderation-latest", - "description": "Moderation model. `omni-moderation-latest` (default) or `text-moderation-latest`.", + "description": "Moderation model sent to the provider. The default is `omni-moderation-latest`; provider model names are not restricted by AISIX.", "minLength": 1, "type": "string" }, @@ -748,7 +786,7 @@ "type": "object" }, { - "description": "Self-hosted Microsoft Presidio PII detection + anonymization (#52): analyzer entities with per-entity `mask`/`block` actions and a selectable anonymize operator, on input and/or output, including streaming output.", + "description": "Self-hosted Microsoft Presidio PII detection and anonymization. Analyzer entities can mask or block per entity, and masked entities use the selected anonymize operator. Applies on input, output, or both, including buffered streaming output.", "properties": { "analyzer_url": { "description": "Presidio analyzer base URL, e.g. `http://presidio-analyzer:3000`. The data plane appends `/analyze`.", @@ -762,7 +800,11 @@ }, "default_action": { "default": "mask", - "description": "Action for entities that don't set their own: `mask` (default) or `block`.", + "description": "Action for entities that do not set their own override.", + "enum": [ + "mask", + "block" + ], "type": "string" }, "entities": { @@ -774,6 +816,7 @@ "type": "array" }, "kind": { + "description": "Guardrail provider type for self-hosted Microsoft Presidio PII detection and anonymization.", "enum": [ "presidio" ], @@ -794,12 +837,18 @@ }, "on_buffer_exceeded": { "default": "fail_closed", - "description": "Buffer-overflow policy. Use `fail_open` to release output unscanned (and unmasked) when the buffer cap is hit; the default `fail_closed` blocks the response instead.", + "description": "Buffer-overflow policy for streamed output when the buffer cap is hit.", "type": "string" }, "operator": { "default": "replace", - "description": "Anonymize operator applied to masked entities: `replace` (default — Presidio substitutes ``), `mask` (asterisks), `hash` (SHA-256 hex), or `redact` (span removed).", + "description": "Anonymize operator applied to masked entities.", + "enum": [ + "replace", + "mask", + "hash", + "redact" + ], "type": "string" }, "output_fail_open": { @@ -839,7 +888,7 @@ }, "direction": { "default": "both", - "description": "Attachment direction hint: `input`, `output`, or `both`. Guardrail execution still follows `hook_point`.", + "description": "Attachment direction hint. Guardrail execution still follows `hook_point`.", "type": "string" }, "enabled": { @@ -849,7 +898,7 @@ }, "enforcement_mode": { "default": "block", - "description": "How AISIX handles matching content. `block` enforces the guardrail. `monitor` records what would have happened without blocking or redacting the caller-visible response.", + "description": "How AISIX handles matching content. Enforcing mode applies the guardrail verdict; monitor mode records what would have happened without blocking or redacting the caller-visible response.", "type": "string" }, "fail_open": {