Skip to main content

systemprompt_models/profile/
server.rs

1//! Server configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
9#[serde(deny_unknown_fields)]
10pub struct ServerConfig {
11    pub host: String,
12
13    pub port: u16,
14
15    pub api_server_url: String,
16
17    pub api_internal_url: String,
18
19    pub api_external_url: String,
20
21    #[serde(default)]
22    pub use_https: bool,
23
24    #[serde(default)]
25    pub cors_allowed_origins: Vec<String>,
26
27    #[serde(default)]
28    pub content_negotiation: ContentNegotiationConfig,
29
30    #[serde(default)]
31    pub security_headers: SecurityHeadersConfig,
32
33    /// Stable identifier for this replica. Empty/unset resolves to the
34    /// OS hostname (or a generated short id) at config build time.
35    #[serde(default)]
36    pub instance_id: Option<String>,
37
38    /// Global cap on concurrent A2A SSE streams for this replica.
39    #[serde(default = "default_max_concurrent_streams")]
40    pub max_concurrent_streams: usize,
41
42    /// CIDR ranges whose immediate-peer requests are allowed to set
43    /// `X-Forwarded-For`, `X-Real-IP`, and `CF-Connecting-IP`. Empty
44    /// means the platform treats every connection as direct and ignores
45    /// those headers — the only safe default behind no proxy. Each entry
46    /// is a CIDR string (e.g. `10.0.0.0/8`, `192.168.1.0/24`,
47    /// `2001:db8::/32`). Single addresses without a `/` are accepted as
48    /// `/32` (IPv4) or `/128` (IPv6).
49    #[serde(default)]
50    pub trusted_proxies: Vec<String>,
51}
52
53const fn default_max_concurrent_streams() -> usize {
54    crate::config::DEFAULT_MAX_CONCURRENT_STREAMS
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
58#[serde(deny_unknown_fields)]
59pub struct ContentNegotiationConfig {
60    #[serde(default)]
61    pub enabled: bool,
62
63    #[serde(default = "default_markdown_suffix")]
64    pub markdown_suffix: String,
65}
66
67impl Default for ContentNegotiationConfig {
68    fn default() -> Self {
69        Self {
70            enabled: false,
71            markdown_suffix: default_markdown_suffix(),
72        }
73    }
74}
75
76fn default_markdown_suffix() -> String {
77    ".md".to_owned()
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
81#[serde(deny_unknown_fields)]
82pub struct SecurityHeadersConfig {
83    #[serde(default = "default_enabled")]
84    pub enabled: bool,
85
86    #[serde(default = "default_hsts")]
87    pub hsts: String,
88
89    #[serde(default = "default_frame_options")]
90    pub frame_options: String,
91
92    #[serde(default = "default_content_type_options")]
93    pub content_type_options: String,
94
95    #[serde(default = "default_referrer_policy")]
96    pub referrer_policy: String,
97
98    #[serde(default = "default_permissions_policy")]
99    pub permissions_policy: String,
100
101    #[serde(default)]
102    pub content_security_policy: Option<String>,
103}
104
105impl Default for SecurityHeadersConfig {
106    fn default() -> Self {
107        Self {
108            enabled: true,
109            hsts: default_hsts(),
110            frame_options: default_frame_options(),
111            content_type_options: default_content_type_options(),
112            referrer_policy: default_referrer_policy(),
113            permissions_policy: default_permissions_policy(),
114            content_security_policy: None,
115        }
116    }
117}
118
119const fn default_enabled() -> bool {
120    true
121}
122
123fn default_hsts() -> String {
124    "max-age=63072000; includeSubDomains; preload".to_owned()
125}
126
127fn default_frame_options() -> String {
128    "DENY".to_owned()
129}
130
131fn default_content_type_options() -> String {
132    "nosniff".to_owned()
133}
134
135fn default_referrer_policy() -> String {
136    "strict-origin-when-cross-origin".to_owned()
137}
138
139fn default_permissions_policy() -> String {
140    "camera=(), microphone=(), geolocation=()".to_owned()
141}