Skip to main content

systemprompt_ai/services/gateway/
spec.rs

1//! Declarative gateway-policy specification.
2//!
3//! Spec payload of `ai_gateway_policies` rows, shared with the YAML schema in
4//! `services/gateway/policies.yaml`. Carries quota windows and safety
5//! configuration.
6//!
7//! Model exposure lives on the profile's gateway catalog, not here — see
8//! `GatewayConfig::is_model_exposed`.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct QuotaWindow {
18    pub window_seconds: i32,
19    /// Which subject the bucket is keyed by: `user` (the default), or any
20    /// subject-attribute dimension registered by an extension (for example
21    /// `organization`). A window whose subject cannot be resolved for the
22    /// requesting user is skipped.
23    #[serde(default = "default_subject")]
24    pub subject: String,
25    pub max_requests: Option<i64>,
26    pub max_input_tokens: Option<i64>,
27    pub max_output_tokens: Option<i64>,
28    /// Spend ceiling for the window. Enforced one request late: cost is known
29    /// only after the response, so the request that crosses the ceiling
30    /// completes and subsequent requests are denied.
31    #[serde(default)]
32    pub max_cost_microdollars: Option<i64>,
33}
34
35impl Default for QuotaWindow {
36    fn default() -> Self {
37        Self {
38            window_seconds: 0,
39            subject: default_subject(),
40            max_requests: None,
41            max_input_tokens: None,
42            max_output_tokens: None,
43            max_cost_microdollars: None,
44        }
45    }
46}
47
48fn default_subject() -> String {
49    "user".to_owned()
50}
51
52pub const USER_QUOTA_SUBJECT: &str = "user";
53
54/// How far back into a conversation the request-phase scanners look.
55///
56/// A request carries the whole conversation, so scanning all of it re-reads
57/// every earlier turn on every turn: one finding would deny the rest of the
58/// conversation, and each turn would persist the same finding again.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
60#[serde(rename_all = "lowercase")]
61pub enum SafetyHistoryMode {
62    /// Judge only the newest user turn.
63    #[default]
64    Off,
65    /// Also scan earlier turns, recording findings at phase `request_history`
66    /// without denying the request.
67    Audit,
68    /// Scan earlier turns and let their findings deny the request.
69    Block,
70}
71
72/// Phrase-list tuning for the builtin `heuristic` scanner. Ignored when an
73/// extension registers its own scanner under the name `heuristic`, which
74/// shadows the builtin wholesale.
75#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76#[serde(deny_unknown_fields)]
77pub struct HeuristicConfig {
78    /// Replaces the builtin phrase list entirely when set.
79    #[serde(default)]
80    pub phrases: Option<Vec<String>>,
81    /// Appended to whichever base list is in effect.
82    #[serde(default)]
83    pub extra_phrases: Vec<String>,
84    /// Drops the builtin list, leaving only `phrases`/`extra_phrases`.
85    #[serde(default)]
86    pub disable_builtin: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90#[serde(deny_unknown_fields)]
91pub struct SafetyConfig {
92    #[serde(default)]
93    pub scanners: Vec<String>,
94    #[serde(default)]
95    pub heuristic: HeuristicConfig,
96    #[serde(default)]
97    pub block_categories: Vec<String>,
98    /// Response-phase categories that deny the reply instead of merely
99    /// recording it. Buffered replies only: a streamed reply is already on the
100    /// wire by the time the scan can run, so listing a category here has no
101    /// effect on streaming and the gateway does not pretend otherwise.
102    #[serde(default)]
103    pub block_response_categories: Vec<String>,
104    #[serde(default)]
105    pub history: SafetyHistoryMode,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109#[serde(deny_unknown_fields)]
110pub struct GatewayPolicySpec {
111    #[serde(default)]
112    pub quota_windows: Vec<QuotaWindow>,
113    #[serde(default)]
114    pub safety: SafetyConfig,
115}
116
117impl GatewayPolicySpec {
118    #[must_use]
119    pub fn permissive() -> Self {
120        Self::default()
121    }
122}