Uh oh!
There was an error while loading. Please reload this page.
feat(guardrails): Aliyun AI Guardrails kind (MultiModalGuard) with suggestion verdicts and mask write-back - #799
Conversation
… MultiModalGuard) Adds a new guardrail kind for Aliyun's AI Guardrails product (AI 安全护栏) — a different Aliyun product from the existing aliyun_text_moderation (TextModerationPlus / Content Moderation): separately activated (commodity lvwang_guardrail_public_cn), separately billed, policy-configured in its own console, and — the point of the feature — its calls appear in that console's records, which TextModerationPlus calls never do (AISIX-Cloud#1070). - Action=MultiModalGuard on the same green-cip endpoint and RPC v1 signature (signing helpers shared with the TextModerationPlus dispatcher). - Input hook: query_security_check_pro (query_security_check at service_level: basic); output hook: response_security_check_pro / response_security_check. - Verdict follows the returned Data.Suggestion (computed by Aliyun from the console-configured policy): block blocks; pass/watch/mask release. No local risk threshold. Full per-dimension detail (Type/Level/Suggestion, both level vocabularies incl. sensitiveData S0–S3) lands in logs; only detected labels are logged, and matched content (Ext) has no deserialized path to a log (#153). - Streaming output: windowed like the TMP kind; each window carries the stream-stable sessionId AND chatId so Aliyun stitches one console record per response. done is not sent (optional per docs; the trait has no end-of-stream signal). - Code 408 (commodity not activated) logs a fix-naming error, the most common first-run failure. - Upstream RequestId diagnostics follow the #781 pattern (x-acs-request-id header first, body fallback). Ref AISIX-Cloud#1070
Completes the aliyun_ai_guardrail verdict semantics for Suggestion=mask (Aliyun-side desensitization policies), following the Bedrock pattern: - Segment path (non-streaming bodies, held-back tail of a streamed response): moderates_segments()=true; a mask suggestion rewrites the body with Aliyun's Ext.Desensitization text. A single segment (the common case) reuses the verdict call's rewrite directly; multiple segments are re-checked one call per segment because MultiModalGuard takes one content string per call — the re-calls keep masked[i] aligned with texts[i] by construction. mask with no usable Desensitization fails closed. A clipped (>2000 chars) segment gets its unscanned tail re-attached on write-back. - Blob path (check_input/check_output — the mid-stream windows): mask maps to Block, since there is no write-back channel and releasing un-masked content would defeat the operator's policy. - Ext is parsed tolerantly (object, or a JSON document nested in a string — Aliyun types it inconsistently); the desensitized text travels outside AigDiagnostics so the diagnostics type stays structurally content-free (#153). - The Desensitization accumulation assumption (last non-empty rewrite wins across Result[] entries) is doc-derived and flagged for live re-verification during #1070 QA. - Test-side: cross-module lock for log-capturing tests — a concurrently dropped capture subscriber recomputes tracing's GLOBAL max-level hint and can fast-path away another thread's events mid-capture. Ref AISIX-Cloud#1070
📝 WalkthroughWalkthroughAdds an ChangesAliyun AI Guardrails
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AISIX
participant AliyunMultiModalGuard
participant Upstream
Client->>AISIX: send input or generate output
AISIX->>AliyunMultiModalGuard: signed moderation request
AliyunMultiModalGuard-->>AISIX: suggestion, details, request ID
AISIX->>Upstream: forward permitted request
Upstream-->>AISIX: generated response
AISIX-->>Client: allow, block, stream error, or masked response
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Checkov (3.3.8)schemas/resources/guardrail.schema.jsonTraceback (most recent call last): Comment |
…uardrail # Conflicts: # crates/aisix-core/src/models/mod.rs
… live endpoint #1070 QA against the real green-cip endpoint (commodity now activated) verified the block path end to end and corrected the one mask assumption that was doc-derived: - Real sensitiveData mask contract: the dimension returns the COMPLETE desensitized text (every span already masked) in exactly ONE Result's Ext.Desensitization; a sibling Result for a second matched sub-type carries only Ext.SensitiveData (the original values — a leak vector) with no Desensitization. Not the per-item accumulation the old comment assumed. desensitization()'s scan-for-the-carrier is correct either way; comment rewritten to the observed contract and a unit test added with the exact captured multi-Result body (landline + two mobiles → one full-text carrier + one metadata-only sibling). - Ext is a JSON object live (confirmed); the string-encoded path stays as a defensive tolerance. - live_smoke now exercises the real mask + the full segment write-back path (moderate_input_segments), asserting the rewrite carries the mask token and never the original value. Benign assertion relaxed to pass|watch: with the sensitive-data module's action set to observe, even a no-match prompt bubbles a top-level watch (both release). Also confirmed live and unchanged: block on top-level Suggestion, per-dimension diagnostics (S0–S3 seen for sensitiveData), header request_id, and that matched content in Ext (RiskWords/SensitiveData) never reaches a log or the write-back. Ref AISIX-Cloud#1070
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/aisix-core/src/models/guardrail.rs`:
- Around line 361-363: Update the public API documentation comment near the
guardrail model’s access_key_secret field: replace “kine projection” with “kind
projection” and revise the parenthetical so it forms clear, grammatically
separated sentences. Preserve the documented secrecy and logging behavior while
avoiding internal shorthand.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 7a31e51f-660d-4854-a499-b50854308daa
📒 Files selected for processing (10)
crates/aisix-admin/src/openapi.rscrates/aisix-core/src/models/guardrail.rscrates/aisix-core/src/models/mod.rscrates/aisix-guardrails/src/aliyun.rscrates/aisix-guardrails/src/aliyun_ai_guardrail.rscrates/aisix-guardrails/src/build.rscrates/aisix-guardrails/src/lib.rscrates/aisix-server/src/heartbeat.rsschemas/resources/guardrail.schema.jsontests/e2e/src/cases/guardrail-aliyun-ai-guardrail-e2e.test.ts
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
`add_variant_titles` still assigns titles by position, and that is deliberate — but it leaves one edit uncovered, so cover it where it matters. Positional title assignment is safe for the edits that actually happen. `title_schema_variants` bails on a length mismatch, so adding or removing a kind makes every title in that family disappear and `openapi_titles_schema_variants_for_redoc_tabs` fail loudly; replacing a variant in place edits the same line the title sits beside. The record bears this out: #799 inserted a guardrail kind and updated the titles correctly while silently breaking the descriptions (#1037), and #997 swapped `weighted` for `consistent_hash` with the titles staying right. The descriptions had no such forcing function, which is the whole reason the two tables diverged. That leaves a length-preserving reorder as the only silent edit. The two families that grow now pin their titles by discriminator in tests — the guardrail kinds already did; the exporter kinds do now. Verified by reordering two exporter titles: the new assertion names the offending kind while `openapi_titles_schema_variants_for_redoc_tabs` stays green, which is exactly the window being closed. The remaining families are small, stable string enums, plus `Model`, whose branches are mutual-exclusion shapes carrying no discriminator to key on at all. Converting those would split `add_variant_titles` into two mechanisms for a reorder that has never happened, and whose result is self-contradictory on sight — the tab would read `Lakera Guard` over a branch whose `kind` says `openai_moderation`, since kinds and descriptions no longer travel by position.
The generated Admin API reference documented the `aliyun_ai_guardrail` guardrail with the `pii` branch's sentence — "in-process sensitive-data detection and redaction" — telling readers that a cloud-backed, policy-driven moderation service runs inside the gateway and redacts content, and giving them no way to tell the two kinds apart. Dispatch always routed the kinds correctly; only the reference was wrong. Two sources produced these sentences and they disagreed. `guardrail_kind_description` keys on the kind value and had no `aliyun_ai_guardrail` arm, leaving that branch undescribed. `add_missing_property_descriptions` then backfilled it from a hardcoded positional list of `/Guardrail/oneOf/0..8` pointers written when the enum had nine variants; `aliyun_ai_guardrail` was later inserted at position six, so `/oneOf/5` no longer pointed at `pii`. The backfill only writes where a description is absent, so every other kind kept its correct text and the stale entries stayed silent — the one hole in the kind-keyed function was exactly where the stale list landed. A positional pointer can only mislabel, never fail. Add the missing arm, and remove positional addressing from the file rather than just the branch that had broken. Of the twenty pointers in `add_missing_property_descriptions`, sixteen were dead — the guardrail, Bedrock and keyword-pattern families all describe their variants beside the type in `aisix-core`. The four live ones, the observability exporter kinds, had no other source and carried the identical latent defect, as did the `sample_rate` default in `add_schema_defaults`, where an index shift lands on a branch without that field and silently drops a documented default. Both now select the branch by its `kind` discriminator through a new `variant_property_mut` helper. Regenerating the OpenAPI proves the sweep behavior-preserving: flattened to leaf pointers, the document differs from the base in exactly two values, both on the `aliyun_ai_guardrail` branch. That branch's own description was also silent on what the guardrail does to traffic, while every sibling states its side — `pii`, `presidio` and `lakera` say they mask, `openai_moderation` and `semantic` say they never rewrite content. This one does rewrite: on a `mask` suggestion the caller-visible content is replaced with the provider's desensitized text, and blocked when none comes back. That is now stated, and a non-English product name and a regional endpoint template the `region` field already documents are gone. Four tests cover a binding nothing checked before. The pre-existing checks only assert that metadata is present, not that it belongs to the branch carrying it, which is why the defect shipped. The guardrail test pins the source rather than the text — each description must equal `guardrail_kind_description(kind)`, so a second source writing these fails however plausible its sentence — plus the kind set and non-empty text. Two OpenAPI-side tests check both carriers of a branch's identity by discriminator, since the site of the original defect is in `aisix-admin` where an `aisix-core` test cannot see it. `add_variant_titles` stays positional, deliberately. It has a forcing function the descriptions lacked: `title_schema_variants` bails on a length mismatch, so adding or removing a kind makes every title in the family vanish and fails loudly. That asymmetry is why #799 updated the titles correctly while silently breaking the descriptions. Only a length-preserving reorder slips through, so the two families that grow pin their titles by discriminator in tests. Two older assertions that reached for `ObservabilityExporter.oneOf[0]` are converted too. `sample_rate` exists only on `otlp_http`, so a reorder failed them while the document was entirely correct — the mirror image of this bug, reporting a defect that is not there. A reorder should fail exactly one test, the one checking metadata still belongs to its branch. Closes#1037
Adds guardrail kind
aliyun_ai_guardrail, integrating Aliyun's AI Guardrails product (AI 安全护栏, actionMultiModalGuard). This is a different Aliyun product from the existingaliyun_text_moderation(TextModerationPlus / Content Moderation): separately activated (commoditylvwang_guardrail_public_cn), separately billed, policy-configured in its own console — and its calls appear in that console's records, which TextModerationPlus calls never do. That console visibility is the core ask of the issue.What it does
MultiModalGuardongreen-cip.<region>.aliyuncs.com(verified live: the endpoint routes both actions), RPC v1 signature shared with the TextModerationPlus dispatcher. Config carriesregion/endpoint/access_key_id/access_key_secret/timeout_msplusservice_level: pro|basic—pro(default) callsquery_security_check_pro/response_security_check_pro,basicthe non-Pro codes.Data.Suggestion, which Aliyun computes from the console-configured policies — no local risk threshold.block→ 422;pass/watch→ release; full per-dimension detail (Detail[].Type/Level/Suggestion, including sensitiveData'sS0–S3vocabulary) goes to logs/telemetry as opaque strings, so an Aliyun vocabulary or dimension extension can't break a request.maskhonored via segment write-back (Bedrock pattern): on the segment path (non-streaming bodies, held-back tail of a streamed response) amasksuggestion rewrites the body with Aliyun'sExt.Desensitizationtext — single segment reuses the verdict call's rewrite, multiple segments are re-checked per segment somasked[i]stays aligned withtexts[i](MultiModalGuard takes onecontentstring per call).maskwith no usable rewrite fails closed. On the blob path (mid-stream windows — no write-back channel)maskmaps to Block so un-masked content is never released.sessionIdandchatIdso Aliyun stitches one console record per response.doneis not sent (optional per Aliyun docs; the trait has no end-of-stream signal).RequestIdon every path (x-acs-request-idheader first, body fallback — the feat(guardrails): preserve Aliyun's upstream RequestId and correlate it to the gateway request #781 pattern); businessCode408 (commodity not activated) logs an error naming the activation fix; 4xx bodies are never echoed (only the symbolicCode), and matched content (Ext, risk words) has no deserialized path into a log.Testing
Ext+ missing-rewrite fail-closed), service-tier codes, session/chat ids, 408/4xx/5xx/timeout/malformed paths, no-content-leak log assertions, clipped-tail re-attach.tests/e2e, realaisix+ etcd + mock green-cip): input block before upstream, output block after upstream, watch release, mask write-back visible to the caller, streaming SSE error with stablesessionId/chatIdacross windows, basic-tier service codes.--ignoredlive smoke (live_smoke_real_endpoint) exercises the real endpoint including a sensitive-data mask probe; theExt.Desensitizationaccumulation assumption (doc-derived) is flagged there for live re-verification.LiteLLM has no Aliyun guardrail integration (only dashscope LLM providers), so there is no baseline to compare against.
CP exposure (kind enum, schema descriptor, dashboard) ships separately per the cross-plane workflow.
Fixes api7/AISIX-Cloud#1070
Summary by CodeRabbit