Skip to main content

pmcp_code_mode/
config.rs

1//! Code Mode configuration.
2
3use crate::types::RiskLevel;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::collections::HashSet;
7
8/// Configuration for Code Mode.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct CodeModeConfig {
11    /// Whether Code Mode is enabled for this server
12    #[serde(default)]
13    pub enabled: bool,
14
15    // ========================================================================
16    // GraphQL-specific settings
17    // ========================================================================
18    /// Whether to allow mutations (MVP: false)
19    #[serde(default)]
20    pub allow_mutations: bool,
21
22    /// Allowed mutation names (whitelist). If empty and allow_mutations=true, all are allowed.
23    #[serde(default)]
24    pub allowed_mutations: HashSet<String>,
25
26    /// Blocked mutation names (blacklist). Always blocked even if allow_mutations=true.
27    #[serde(default)]
28    pub blocked_mutations: HashSet<String>,
29
30    /// Whether to allow introspection queries
31    #[serde(default)]
32    pub allow_introspection: bool,
33
34    /// Fields that should never be returned (Type.field format) - GraphQL
35    #[serde(default)]
36    pub blocked_fields: HashSet<String>,
37
38    /// Allowed query names (whitelist). If empty and mode is allowlist, none are allowed.
39    #[serde(default)]
40    pub allowed_queries: HashSet<String>,
41
42    /// Blocked query names (blocklist). Always blocked even if reads enabled.
43    #[serde(default)]
44    pub blocked_queries: HashSet<String>,
45
46    // ========================================================================
47    // OpenAPI-specific settings
48    // ========================================================================
49    /// Whether read operations (GET) are enabled (default: true)
50    #[serde(default = "default_true")]
51    pub openapi_reads_enabled: bool,
52
53    /// Whether write operations (POST, PUT, PATCH) are allowed globally
54    #[serde(default)]
55    pub openapi_allow_writes: bool,
56
57    /// Allowed write operations (operationId or "METHOD /path")
58    #[serde(default)]
59    pub openapi_allowed_writes: HashSet<String>,
60
61    /// Blocked write operations
62    #[serde(default)]
63    pub openapi_blocked_writes: HashSet<String>,
64
65    /// Whether delete operations (DELETE) are allowed globally
66    #[serde(default)]
67    pub openapi_allow_deletes: bool,
68
69    /// Allowed delete operations (operationId or "METHOD /path")
70    #[serde(default)]
71    pub openapi_allowed_deletes: HashSet<String>,
72
73    /// Blocked paths (glob patterns like "/admin/*")
74    #[serde(default)]
75    pub openapi_blocked_paths: HashSet<String>,
76
77    /// Fields that are stripped from API responses entirely (no access)
78    #[serde(default)]
79    pub openapi_internal_blocked_fields: HashSet<String>,
80
81    /// Fields that can be used internally but not in script output
82    #[serde(default)]
83    pub openapi_output_blocked_fields: HashSet<String>,
84
85    /// Whether scripts must declare their return type with @returns
86    #[serde(default)]
87    pub openapi_require_output_declaration: bool,
88
89    // ========================================================================
90    // Common settings
91    // ========================================================================
92    /// Action tags to override inferred actions for specific operations.
93    #[serde(default)]
94    pub action_tags: HashMap<String, String>,
95
96    /// Maximum query depth
97    #[serde(default = "default_max_depth")]
98    pub max_depth: u32,
99
100    /// Maximum field count per query
101    #[serde(default = "default_max_field_count")]
102    pub max_field_count: u32,
103
104    /// Maximum estimated query cost
105    #[serde(default = "default_max_cost")]
106    pub max_cost: u32,
107
108    /// Allowed sensitive data categories
109    #[serde(default)]
110    pub allowed_sensitive_categories: HashSet<String>,
111
112    /// Token time-to-live in seconds
113    #[serde(default = "default_token_ttl")]
114    pub token_ttl_seconds: i64,
115
116    /// Risk levels that can be auto-approved without human confirmation
117    #[serde(default = "default_auto_approve_levels")]
118    pub auto_approve_levels: Vec<RiskLevel>,
119
120    /// Maximum query length in characters
121    #[serde(default = "default_max_query_length")]
122    pub max_query_length: usize,
123
124    /// Maximum result rows to return
125    #[serde(default = "default_max_result_rows")]
126    pub max_result_rows: usize,
127
128    /// Query execution timeout in seconds
129    #[serde(default = "default_query_timeout")]
130    pub query_timeout_seconds: u32,
131
132    /// Server ID for token generation
133    #[serde(default)]
134    pub server_id: Option<String>,
135
136    // ========================================================================
137    // SDK-backed settings
138    // ========================================================================
139    /// Allowed SDK operation names for SDK-backed Code Mode.
140    /// When non-empty, Code Mode uses SDK dispatch instead of HTTP.
141    /// Operations are validated at compile time — unlisted names are rejected.
142    #[serde(default)]
143    pub sdk_operations: HashSet<String>,
144}
145
146impl Default for CodeModeConfig {
147    fn default() -> Self {
148        Self {
149            enabled: false,
150            // GraphQL
151            allow_mutations: false,
152            allowed_mutations: HashSet::new(),
153            blocked_mutations: HashSet::new(),
154            allow_introspection: false,
155            blocked_fields: HashSet::new(),
156            allowed_queries: HashSet::new(),
157            blocked_queries: HashSet::new(),
158            // OpenAPI
159            openapi_reads_enabled: true,
160            openapi_allow_writes: false,
161            openapi_allowed_writes: HashSet::new(),
162            openapi_blocked_writes: HashSet::new(),
163            openapi_allow_deletes: false,
164            openapi_allowed_deletes: HashSet::new(),
165            openapi_blocked_paths: HashSet::new(),
166            openapi_internal_blocked_fields: HashSet::new(),
167            openapi_output_blocked_fields: HashSet::new(),
168            openapi_require_output_declaration: false,
169            // Common
170            action_tags: HashMap::new(),
171            max_depth: default_max_depth(),
172            max_field_count: default_max_field_count(),
173            max_cost: default_max_cost(),
174            allowed_sensitive_categories: HashSet::new(),
175            token_ttl_seconds: default_token_ttl(),
176            auto_approve_levels: default_auto_approve_levels(),
177            max_query_length: default_max_query_length(),
178            max_result_rows: default_max_result_rows(),
179            query_timeout_seconds: default_query_timeout(),
180            server_id: None,
181            // SDK
182            sdk_operations: HashSet::new(),
183        }
184    }
185}
186
187impl CodeModeConfig {
188    /// Create a new config with Code Mode enabled.
189    pub fn enabled() -> Self {
190        Self {
191            enabled: true,
192            ..Default::default()
193        }
194    }
195
196    /// Returns true if this config enables SDK-backed Code Mode.
197    pub fn is_sdk_mode(&self) -> bool {
198        !self.sdk_operations.is_empty()
199    }
200
201    /// Check if a risk level should be auto-approved.
202    pub fn should_auto_approve(&self, risk_level: RiskLevel) -> bool {
203        self.auto_approve_levels.contains(&risk_level)
204    }
205
206    /// Get the server ID, falling back to a default.
207    pub fn server_id(&self) -> &str {
208        self.server_id.as_deref().unwrap_or("unknown")
209    }
210
211    /// Convert to ServerConfigEntity for policy evaluation.
212    pub fn to_server_config_entity(&self) -> crate::policy::ServerConfigEntity {
213        crate::policy::ServerConfigEntity {
214            server_id: self.server_id().to_string(),
215            server_type: "graphql".to_string(),
216            allow_write: self.allow_mutations,
217            allow_delete: self.allow_mutations,
218            allow_admin: self.allow_introspection,
219            allowed_operations: self.allowed_mutations.clone(),
220            blocked_operations: self.blocked_mutations.clone(),
221            max_depth: self.max_depth,
222            max_field_count: self.max_field_count,
223            max_cost: self.max_cost,
224            max_api_calls: 50,
225            blocked_fields: self.blocked_fields.clone(),
226            allowed_sensitive_categories: self.allowed_sensitive_categories.clone(),
227        }
228    }
229
230    /// Convert to OpenAPIServerEntity for policy evaluation (OpenAPI Code Mode).
231    #[cfg(feature = "openapi-code-mode")]
232    pub fn to_openapi_server_entity(&self) -> crate::policy::OpenAPIServerEntity {
233        let mut allowed_operations = self.openapi_allowed_writes.clone();
234        allowed_operations.extend(self.openapi_allowed_deletes.clone());
235
236        let write_mode = if !self.openapi_allow_writes {
237            "deny_all"
238        } else if !self.openapi_allowed_writes.is_empty() {
239            "allowlist"
240        } else if !self.openapi_blocked_writes.is_empty() {
241            "blocklist"
242        } else {
243            "allow_all"
244        };
245
246        crate::policy::OpenAPIServerEntity {
247            server_id: self.server_id().to_string(),
248            server_type: "openapi".to_string(),
249            allow_write: self.openapi_allow_writes,
250            allow_delete: self.openapi_allow_deletes,
251            allow_admin: false,
252            write_mode: write_mode.to_string(),
253            max_depth: self.max_depth,
254            max_cost: self.max_cost,
255            max_api_calls: 50,
256            max_loop_iterations: 100,
257            max_script_length: self.max_query_length as u32,
258            max_nesting_depth: self.max_depth,
259            execution_timeout_seconds: self.query_timeout_seconds,
260            allowed_operations,
261            blocked_operations: self.openapi_blocked_writes.clone(),
262            allowed_methods: HashSet::new(),
263            blocked_methods: HashSet::new(),
264            allowed_path_patterns: HashSet::new(),
265            blocked_path_patterns: self.openapi_blocked_paths.clone(),
266            sensitive_path_patterns: self.openapi_blocked_paths.clone(),
267            auto_approve_read_only: self.openapi_reads_enabled,
268            max_api_calls_for_auto_approve: 10,
269            internal_blocked_fields: self.openapi_internal_blocked_fields.clone(),
270            output_blocked_fields: self.openapi_output_blocked_fields.clone(),
271            require_output_declaration: self.openapi_require_output_declaration,
272        }
273    }
274}
275
276fn default_true() -> bool {
277    true
278}
279
280fn default_token_ttl() -> i64 {
281    300 // 5 minutes
282}
283
284fn default_auto_approve_levels() -> Vec<RiskLevel> {
285    vec![RiskLevel::Low]
286}
287
288fn default_max_query_length() -> usize {
289    10000
290}
291
292fn default_max_result_rows() -> usize {
293    10000
294}
295
296fn default_query_timeout() -> u32 {
297    30
298}
299
300fn default_max_depth() -> u32 {
301    10
302}
303
304fn default_max_field_count() -> u32 {
305    100
306}
307
308fn default_max_cost() -> u32 {
309    1000
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_default_config() {
318        let config = CodeModeConfig::default();
319        assert!(!config.enabled);
320        assert!(!config.allow_mutations);
321        assert_eq!(config.token_ttl_seconds, 300);
322        assert_eq!(config.auto_approve_levels, vec![RiskLevel::Low]);
323    }
324
325    #[test]
326    fn test_enabled_config() {
327        let config = CodeModeConfig::enabled();
328        assert!(config.enabled);
329    }
330
331    #[test]
332    fn test_auto_approve() {
333        let config = CodeModeConfig::default();
334        assert!(config.should_auto_approve(RiskLevel::Low));
335        assert!(!config.should_auto_approve(RiskLevel::Medium));
336        assert!(!config.should_auto_approve(RiskLevel::High));
337        assert!(!config.should_auto_approve(RiskLevel::Critical));
338    }
339}