Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 64 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ lz4_flex = "0.11"
# (api7/aisix#860).
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "rustls-tls-native-roots", "gzip", "multipart"] }

# kind=custom guardrail: the sandboxed engine that runs operator-supplied
# screening scripts. Vendors quickjs-ng; `futures` makes Rust futures
# awaitable from the script (that is how `fetch` works) and `parallel`
# makes the runtime Send so a hook can run on any worker thread.
rquickjs = { version = "0.12", features = ["futures", "parallel"] }

# TLS. The root-store crates are direct dependencies because the
# outbound paths that are not reqwest — the Realtime WebSocket — have to
# assemble the same trust store by hand (aisix-gateway::upstream_tls).
Expand Down
97 changes: 97 additions & 0 deletions crates/aisix-core/src/models/guardrail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
//! detection-only block.
//! * `presidio` — self-hosted Presidio analyze→anonymize; per-entity
//! `mask`/`block` + selectable anonymize operator.
//! * `custom` — operator-supplied script run in a sandboxed engine in the
//! DP process; reaches a screening service that speaks its own protocol
//! without a separate adapter deployment. Detection-only.
//!
//! See `aisix-guardrails/src/keyword.rs` for the runtime semantics
//! the snapshot is parsed into.
Expand Down Expand Up @@ -828,6 +831,94 @@ fn default_semantic_text_source() -> String {
"user_messages".to_owned()
}

/// Config block for `kind: "custom"`. Runs an operator-supplied script in a
/// sandboxed engine inside the gateway, so a screening service that speaks
/// its own protocol can be reached without deploying a separate adapter.
///
/// The script is an ES module exporting `checkInput` and/or `checkOutput`.
/// Each receives a context object and returns a verdict:
///
/// ```js
/// export async function checkInput(ctx) {
/// const resp = await fetch("https://screening.internal/scan", {
/// method: "POST",
/// headers: { "content-type": "application/json" },
/// body: JSON.stringify({ text: ctx.text }),
/// });
/// const result = await resp.json();
/// return result.verdict === "deny"
/// ? { action: "block", reason_code: "policy" }
/// : { action: "none" };
/// }
/// ```
///
/// This guardrail is detection-only and never rewrites content: rewriting is
/// a synchronous path, and a script whose purpose is to call an external
/// service cannot participate in it. Applies on input, output, or both,
/// including streamed output.
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema, PartialEq)]
pub struct CustomConfig {
/// The script source, as an ES module exporting `checkInput` and/or
/// `checkOutput`. A hook whose function the module does not export is
/// skipped, so a script may cover one direction only.
#[schemars(length(min = 1))]
pub script: String,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
/// Values the script reads as `ctx.secrets.<NAME>`, for credentials the
/// screening service requires. Stored encrypted and decrypted before
/// projection; plaintext is held in memory only and is never logged.
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub secrets: std::collections::BTreeMap<String, String>,
/// Wall-clock budget for one hook invocation, in milliseconds, covering
/// the script's own execution and every call it makes. `fail_open` and
/// `output_fail_open` govern the verdict when it elapses. Per-call
/// timeouts within the budget are the script's own to set.
#[serde(default = "default_custom_timeout_ms")]
#[schemars(range(min = 1, max = 300_000))]
pub timeout_ms: u32,
/// Memory ceiling for the script engine, in bytes. A script that exceeds
/// it is terminated and the hook's fail-open policy applies.
#[serde(default = "default_custom_max_memory_bytes")]
#[schemars(range(min = 1_048_576, max = 536_870_912u64))]
pub max_memory_bytes: u64,

// --- streaming-output controls (consumed by aisix-proxy build_sse_stream) ---
/// Streaming output moderation mode: sliding-window incremental release
/// or whole-response hold-back.
#[serde(default = "default_custom_stream_processing_mode")]
pub stream_processing_mode: String,
/// Sliding-window size in characters for window mode.
#[serde(default = "default_acs_window_size")]
#[schemars(range(min = 1, max = 10_000))]
pub window_size: u32,
/// Chars carried between windows so a span split across a boundary is still caught.
#[serde(default = "default_acs_window_overlap_size")]
pub window_overlap_size: u32,
/// Max bytes buffered in `buffer_full` mode before `on_buffer_exceeded` applies.
#[serde(default = "default_acs_max_buffer_bytes")]
#[schemars(range(min = 1))]
pub max_buffer_bytes: u64,
/// 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 (the default), a
/// script failure blocks model output instead of releasing unscanned
/// content. The input hook uses the top-level `fail_open` policy.
#[serde(default)]
pub output_fail_open: bool,
}

fn default_custom_timeout_ms() -> u32 {
5_000
}

fn default_custom_max_memory_bytes() -> u64 {
16 * 1024 * 1024
}

fn default_custom_stream_processing_mode() -> String {
"window".to_owned()
}

/// Provider discriminator. The kind drives which `*_config` block is
/// expected. Serde's `tag = "kind"` keeps us honest at parse time.
///
Expand Down Expand Up @@ -891,6 +982,11 @@ pub enum GuardrailKind {
/// Detection-only — never rewrites content. Applies on input,
/// output, or both, including buffered streaming output.
Semantic(SemanticConfig),
/// Screening by an operator-supplied script the gateway runs in a
/// sandboxed engine, for a screening service that speaks its own
/// protocol. Detection-only — never rewrites content. Applies on input,
/// output, or both, including streaming output.
Custom(CustomConfig),
}

impl GuardrailKind {
Expand All @@ -911,6 +1007,7 @@ impl GuardrailKind {
GuardrailKind::OpenaiModeration(_) => "openai_moderation",
GuardrailKind::Presidio(_) => "presidio",
GuardrailKind::Semantic(_) => "semantic",
GuardrailKind::Custom(_) => "custom",
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions crates/aisix-core/src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ pub use ensemble::{EnsembleConfig, Judge, PanelMember};
pub use guardrail::{
AliyunAiGuardrailConfig, AliyunTextModerationConfig, AppliedGuardrail,
AzureContentSafetyConfig, AzureContentSafetyTextModerationConfig, BedrockAWSCredentials,
BedrockConfig, BedrockLatencyMode, Guardrail, GuardrailAttachment, GuardrailEnforcedHit,
GuardrailExecution, GuardrailHookPoint, GuardrailKind, GuardrailMetricsSink,
GuardrailMonitorHit, GuardrailScopeType, KeywordConfig, KeywordPattern, LakeraConfig,
OpenaiModerationConfig, PiiConfig, PiiCustomPattern, PiiDetectorConfig, PresidioConfig,
PresidioEntityConfig, SemanticConfig,
BedrockConfig, BedrockLatencyMode, CustomConfig, Guardrail, GuardrailAttachment,
GuardrailEnforcedHit, GuardrailExecution, GuardrailHookPoint, GuardrailKind,
GuardrailMetricsSink, GuardrailMonitorHit, GuardrailScopeType, KeywordConfig, KeywordPattern,
LakeraConfig, OpenaiModerationConfig, PiiConfig, PiiCustomPattern, PiiDetectorConfig,
PresidioConfig, PresidioEntityConfig, SemanticConfig,
};
pub use mcp_auth_settings::McpAuthSettings;
pub use mcp_policy::{McpAccess, McpPolicy, McpPolicyScope};
Expand Down
18 changes: 18 additions & 0 deletions crates/aisix-core/src/models/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1388,6 +1388,19 @@ pub fn guardrail_root_schema() -> Value {
}
}
}
"custom" => {
set_property_enum(
b,
"stream_processing_mode",
json!(["window", "buffer_full"]),
);
set_property_enum(b, "on_buffer_exceeded", json!(["fail_closed", "fail_open"]));
set_property_additional_properties_description(
b,
"secrets",
"Value the script reads as ctx.secrets under this name.",
);
}
_ => {}
}
}
Expand Down Expand Up @@ -1497,6 +1510,10 @@ fn guardrail_kind_description(kind: &str) -> Option<&'static str> {
"Guardrail provider type for embedding-similarity screening against \
example texts, using an embedding-kind Model.",
),
"custom" => Some(
"Guardrail provider type for screening by an operator-supplied \
script the gateway runs in a sandboxed engine.",
),
_ => None,
}
}
Expand Down Expand Up @@ -4624,6 +4641,7 @@ mod tests {
"azure_content_safety",
"azure_content_safety_text_moderation",
"bedrock",
"custom",
"keyword",
"lakera",
"openai_moderation",
Expand Down
Loading
Loading