Skip to main content

uni_common/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4use std::path::{Path, PathBuf};
5use std::thread;
6use std::time::Duration;
7
8#[derive(Clone, Debug)]
9pub struct CompactionConfig {
10    /// Enable background compaction (default: true)
11    pub enabled: bool,
12
13    /// Max L1 runs before triggering compaction (default: 4)
14    pub max_l1_runs: usize,
15
16    /// Max L1 size in bytes before compaction (default: 256MB)
17    pub max_l1_size_bytes: u64,
18
19    /// Max age of oldest L1 run before compaction (default: 1 hour)
20    pub max_l1_age: Duration,
21
22    /// Background check interval (default: 30s)
23    pub check_interval: Duration,
24
25    /// Number of compaction worker threads (default: 1)
26    pub worker_threads: usize,
27}
28
29impl Default for CompactionConfig {
30    fn default() -> Self {
31        Self {
32            enabled: true,
33            max_l1_runs: 4,
34            max_l1_size_bytes: 256 * 1024 * 1024,
35            max_l1_age: Duration::from_secs(3600),
36            check_interval: Duration::from_secs(30),
37            worker_threads: 1,
38        }
39    }
40}
41
42/// Configuration for background index rebuilding.
43#[derive(Clone, Debug)]
44pub struct IndexRebuildConfig {
45    /// Maximum number of retry attempts for failed index builds (default: 3).
46    pub max_retries: u32,
47
48    /// Delay between retry attempts (default: 60s).
49    pub retry_delay: Duration,
50
51    /// How often to check for pending index rebuild tasks (default: 5s).
52    pub worker_check_interval: Duration,
53
54    /// Row growth ratio to trigger rebuild (default: 0.5 = 50%). Set 0.0 to disable.
55    pub growth_trigger_ratio: f64,
56
57    /// Max index age before rebuild. `None` disables the time-based trigger.
58    pub max_index_age: Option<Duration>,
59
60    /// Enable post-flush automatic rebuild scheduling (default: false).
61    pub auto_rebuild_enabled: bool,
62}
63
64impl Default for IndexRebuildConfig {
65    fn default() -> Self {
66        Self {
67            max_retries: 3,
68            retry_delay: Duration::from_secs(60),
69            worker_check_interval: Duration::from_secs(5),
70            growth_trigger_ratio: 0.5,
71            max_index_age: None,
72            auto_rebuild_enabled: false,
73        }
74    }
75}
76
77#[derive(Clone, Copy, Debug)]
78pub struct WriteThrottleConfig {
79    /// L1 run count to start throttling (default: 8)
80    pub soft_limit: usize,
81
82    /// L1 run count to stop writes entirely (default: 16)
83    pub hard_limit: usize,
84
85    /// Base delay when throttling (default: 10ms)
86    pub base_delay: Duration,
87}
88
89impl Default for WriteThrottleConfig {
90    fn default() -> Self {
91        Self {
92            soft_limit: 8,
93            hard_limit: 16,
94            base_delay: Duration::from_millis(10),
95        }
96    }
97}
98
99#[derive(Clone, Debug)]
100pub struct ObjectStoreConfig {
101    pub connect_timeout: Duration,
102    pub read_timeout: Duration,
103    pub write_timeout: Duration,
104    pub max_retries: u32,
105    pub retry_backoff_base: Duration,
106    pub retry_backoff_max: Duration,
107}
108
109impl Default for ObjectStoreConfig {
110    fn default() -> Self {
111        Self {
112            connect_timeout: Duration::from_secs(10),
113            read_timeout: Duration::from_secs(30),
114            write_timeout: Duration::from_secs(60),
115            max_retries: 3,
116            retry_backoff_base: Duration::from_millis(100),
117            retry_backoff_max: Duration::from_secs(10),
118        }
119    }
120}
121
122/// Security configuration for file system operations.
123/// Controls which paths can be accessed by BACKUP, COPY, and EXPORT commands.
124///
125/// Disabled by default for backward compatibility in embedded mode.
126/// MUST be enabled for server mode with untrusted clients.
127#[derive(Clone, Debug, Default)]
128pub struct FileSandboxConfig {
129    /// If true, file operations are restricted to allowed_paths.
130    /// If false, all paths are allowed (NOT RECOMMENDED for server mode).
131    pub enabled: bool,
132
133    /// List of allowed base directories for file operations.
134    /// Paths must be absolute and canonical.
135    /// File operations are only allowed within these directories.
136    pub allowed_paths: Vec<PathBuf>,
137}
138
139/// Deployment mode for the database.
140///
141/// Used to determine appropriate security defaults.
142#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
143pub enum DeploymentMode {
144    /// Embedded/library mode where the host application controls access.
145    /// File sandbox is disabled by default for backward compatibility.
146    #[default]
147    Embedded,
148    /// Server mode with untrusted clients.
149    /// File sandbox is enabled by default with restricted paths.
150    Server,
151}
152
153/// HTTP server configuration.
154///
155/// Controls CORS, authentication, and other HTTP-related security settings.
156///
157/// # Security
158///
159/// **CWE-942 (Overly Permissive CORS)**, **CWE-306 (Missing Authentication)**:
160/// Production deployments should configure explicit `allowed_origins` and
161/// enable API key authentication.
162#[derive(Clone, Debug)]
163pub struct ServerConfig {
164    /// Allowed CORS origins.
165    ///
166    /// - Empty vector: No CORS headers (most restrictive)
167    /// - `["*"]`: Allow all origins (NOT RECOMMENDED for production)
168    /// - Explicit list: Only allow specified origins (RECOMMENDED)
169    ///
170    /// # Security
171    ///
172    /// **CWE-942**: Using `["*"]` allows any website to make requests to
173    /// your server, potentially exposing sensitive data.
174    pub allowed_origins: Vec<String>,
175
176    /// Optional API key for request authentication.
177    ///
178    /// When set, all API requests must include the header:
179    /// `X-API-Key: <key>`
180    ///
181    /// # Security
182    ///
183    /// **CWE-306**: Without authentication, any client can execute queries.
184    /// Enable this for any deployment accessible beyond localhost.
185    pub api_key: Option<String>,
186
187    /// Whether to require API key for metrics endpoint.
188    ///
189    /// Default: false (metrics are public for observability tooling)
190    pub require_auth_for_metrics: bool,
191}
192
193impl Default for ServerConfig {
194    fn default() -> Self {
195        Self {
196            // Default to localhost-only origin for development safety
197            allowed_origins: vec!["http://localhost:3000".to_string()],
198            api_key: None,
199            require_auth_for_metrics: false,
200        }
201    }
202}
203
204impl ServerConfig {
205    /// Create a permissive config for local development only.
206    ///
207    /// # Security
208    ///
209    /// **WARNING**: Do not use in production. This config allows all CORS origins
210    /// and has no authentication.
211    #[must_use]
212    pub fn development() -> Self {
213        Self {
214            allowed_origins: vec!["*".to_string()],
215            api_key: None,
216            require_auth_for_metrics: false,
217        }
218    }
219
220    /// Create a production config with explicit origins and required API key.
221    ///
222    /// # Panics
223    ///
224    /// Panics if `api_key` is empty.
225    #[must_use]
226    pub fn production(allowed_origins: Vec<String>, api_key: String) -> Self {
227        assert!(
228            !api_key.is_empty(),
229            "API key must not be empty for production"
230        );
231        Self {
232            allowed_origins,
233            api_key: Some(api_key),
234            require_auth_for_metrics: true,
235        }
236    }
237
238    /// Returns a security warning if the config is insecure.
239    pub fn security_warning(&self) -> Option<&'static str> {
240        if self.allowed_origins.contains(&"*".to_string()) && self.api_key.is_none() {
241            Some(
242                "Server config has permissive CORS (allow all origins) and no API key. \
243                 This is insecure for production deployments.",
244            )
245        } else if self.allowed_origins.contains(&"*".to_string()) {
246            Some(
247                "Server config has permissive CORS (allow all origins). \
248                 Consider restricting to specific origins for production.",
249            )
250        } else if self.api_key.is_none() {
251            Some(
252                "Server config has no API key authentication. \
253                 Enable api_key for production deployments.",
254            )
255        } else {
256            None
257        }
258    }
259}
260
261impl FileSandboxConfig {
262    /// Creates a sandboxed config that only allows operations in the specified directories.
263    pub fn sandboxed(paths: Vec<PathBuf>) -> Self {
264        Self {
265            enabled: true,
266            allowed_paths: paths,
267        }
268    }
269
270    /// Creates a config with appropriate defaults for the deployment mode.
271    ///
272    /// # Security
273    ///
274    /// - **Embedded mode**: Sandbox disabled (host application controls access)
275    /// - **Server mode**: Sandbox enabled with default paths `/var/lib/uni/data` and
276    ///   `/var/lib/uni/backups`
277    ///
278    /// **CWE-22 (Path Traversal)**: Server deployments MUST enable the sandbox to
279    /// prevent arbitrary file read/write via BACKUP, COPY, and EXPORT commands.
280    pub fn default_for_mode(mode: DeploymentMode) -> Self {
281        match mode {
282            DeploymentMode::Embedded => Self {
283                enabled: false,
284                allowed_paths: vec![],
285            },
286            DeploymentMode::Server => Self {
287                enabled: true,
288                allowed_paths: vec![
289                    PathBuf::from("/var/lib/uni/data"),
290                    PathBuf::from("/var/lib/uni/backups"),
291                ],
292            },
293        }
294    }
295
296    /// Returns a security warning message if the sandbox is disabled.
297    ///
298    /// Call this at startup to alert administrators about potential security risks.
299    /// Returns `Some(message)` if a warning should be displayed, `None` otherwise.
300    ///
301    /// # Security
302    ///
303    /// **CWE-22 (Path Traversal)**, **CWE-73 (External Control of File Name)**:
304    /// Disabled sandbox allows unrestricted filesystem access for BACKUP, COPY,
305    /// and EXPORT commands, which can lead to:
306    /// - Arbitrary file read/write in server deployments
307    /// - Data exfiltration to attacker-controlled paths
308    /// - Potential privilege escalation via file overwrites
309    ///
310    /// # Example
311    ///
312    /// ```ignore
313    /// if let Some(warning) = config.file_sandbox.security_warning() {
314    ///     tracing::warn!(target: "uni_db::security", "{}", warning);
315    /// }
316    /// ```
317    pub fn security_warning(&self) -> Option<&'static str> {
318        if !self.enabled {
319            Some(
320                "File sandbox is DISABLED. This allows unrestricted filesystem access \
321                 for BACKUP, COPY, and EXPORT commands. Enable sandbox for server \
322                 deployments: file_sandbox.enabled = true",
323            )
324        } else {
325            None
326        }
327    }
328
329    /// Returns whether the sandbox is in a potentially insecure state.
330    ///
331    /// Returns `true` if the sandbox is disabled or enabled with no allowed paths.
332    pub fn is_potentially_insecure(&self) -> bool {
333        !self.enabled || self.allowed_paths.is_empty()
334    }
335
336    /// Validate that a path is within the allowed sandbox.
337    /// Returns Ok(canonical_path) if allowed, Err if not.
338    pub fn validate_path(&self, path: &str) -> Result<PathBuf, String> {
339        if !self.enabled {
340            // Sandbox disabled - allow all paths
341            return Ok(PathBuf::from(path));
342        }
343
344        if self.allowed_paths.is_empty() {
345            return Err("File sandbox is enabled but no allowed paths configured".to_string());
346        }
347
348        // Resolve the path to canonical form to prevent traversal attacks
349        let input_path = Path::new(path);
350
351        // For paths that don't exist yet (e.g., export destinations), we need to
352        // check their parent directory exists and is within allowed paths
353        let canonical = if input_path.exists() {
354            input_path
355                .canonicalize()
356                .map_err(|e| format!("Failed to canonicalize path: {}", e))?
357        } else {
358            // Path doesn't exist - check parent
359            let parent = input_path
360                .parent()
361                .ok_or_else(|| "Invalid path: no parent directory".to_string())?;
362            if !parent.exists() {
363                return Err(format!(
364                    "Parent directory does not exist: {}",
365                    parent.display()
366                ));
367            }
368            let canonical_parent = parent
369                .canonicalize()
370                .map_err(|e| format!("Failed to canonicalize parent: {}", e))?;
371            // Reconstruct with canonical parent + original filename
372            let filename = input_path
373                .file_name()
374                .ok_or_else(|| "Invalid path: no filename".to_string())?;
375            canonical_parent.join(filename)
376        };
377
378        // Check if the canonical path is within any allowed directory
379        for allowed in &self.allowed_paths {
380            // Ensure allowed path is canonical too
381            let canonical_allowed = if allowed.exists() {
382                allowed.canonicalize().unwrap_or_else(|_| allowed.clone())
383            } else {
384                allowed.clone()
385            };
386
387            if canonical.starts_with(&canonical_allowed) {
388                return Ok(canonical);
389            }
390        }
391
392        Err(format!(
393            "Path '{}' is outside allowed sandbox directories. Allowed: {:?}",
394            path, self.allowed_paths
395        ))
396    }
397}
398
399#[derive(Clone, Debug)]
400pub struct UniConfig {
401    /// Maximum adjacency cache size in bytes (default: 1GB)
402    pub cache_size: usize,
403
404    /// Number of worker threads for parallel execution
405    pub parallelism: usize,
406
407    /// Size of each data morsel/batch (number of rows)
408    pub batch_size: usize,
409
410    /// Maximum size of traversal frontier before pruning
411    pub max_frontier_size: usize,
412
413    /// Auto-flush threshold for L0 buffer (default: 10_000 mutations)
414    pub auto_flush_threshold: usize,
415
416    /// Auto-flush interval for L0 buffer (default: 5 seconds).
417    /// Flush triggers if time elapsed AND mutation count >= auto_flush_min_mutations.
418    /// Set to None to disable time-based flush.
419    pub auto_flush_interval: Option<Duration>,
420
421    /// Minimum mutations required before time-based flush triggers (default: 1).
422    /// Prevents unnecessary flushes when there's minimal activity.
423    pub auto_flush_min_mutations: usize,
424
425    /// Enable write-ahead logging (default: true)
426    pub wal_enabled: bool,
427
428    /// Compaction configuration
429    pub compaction: CompactionConfig,
430
431    /// Write throttling configuration
432    pub throttle: WriteThrottleConfig,
433
434    /// File sandbox configuration for BACKUP/COPY/EXPORT commands.
435    /// MUST be enabled with allowed paths in server mode to prevent arbitrary file access.
436    pub file_sandbox: FileSandboxConfig,
437
438    /// Default query execution timeout (default: 30s)
439    pub query_timeout: Duration,
440
441    /// Default maximum memory per query (default: 1GB)
442    pub max_query_memory: usize,
443
444    /// Maximum transaction buffer memory in bytes (default: 1GB).
445    /// Limits memory usage during transactions to prevent OOM.
446    pub max_transaction_memory: usize,
447
448    /// Maximum rows for in-memory compaction (default: 5M, ~725MB at 145 bytes/row).
449    /// Configurable OOM guard to prevent memory exhaustion during compaction.
450    pub max_compaction_rows: usize,
451
452    /// Enable in-memory VID-to-labels index for O(1) lookups (default: true).
453    /// Memory cost: ~42 bytes per vertex (1M vertices ≈ 42MB).
454    pub enable_vid_labels_index: bool,
455
456    /// Maximum iterations for recursive CTE evaluation (default: 1000).
457    pub max_recursive_cte_iterations: usize,
458
459    /// Object store resilience configuration
460    pub object_store: ObjectStoreConfig,
461
462    /// Background index rebuild configuration
463    pub index_rebuild: IndexRebuildConfig,
464}
465
466impl Default for UniConfig {
467    fn default() -> Self {
468        let parallelism = thread::available_parallelism()
469            .map(|n| n.get())
470            .unwrap_or(4);
471
472        Self {
473            cache_size: 1024 * 1024 * 1024, // 1GB
474            parallelism,
475            batch_size: 1024, // Default morsel size
476            max_frontier_size: 1_000_000,
477            auto_flush_threshold: 10_000,
478            auto_flush_interval: Some(Duration::from_secs(5)),
479            auto_flush_min_mutations: 1,
480            wal_enabled: true,
481            compaction: CompactionConfig::default(),
482            throttle: WriteThrottleConfig::default(),
483            file_sandbox: FileSandboxConfig::default(),
484            query_timeout: Duration::from_secs(30),
485            max_query_memory: 1024 * 1024 * 1024,       // 1GB
486            max_transaction_memory: 1024 * 1024 * 1024, // 1GB
487            max_compaction_rows: 5_000_000,             // 5M rows
488            enable_vid_labels_index: true,              // Enable by default
489            max_recursive_cte_iterations: 1000,
490            object_store: ObjectStoreConfig::default(),
491            index_rebuild: IndexRebuildConfig::default(),
492        }
493    }
494}
495
496/// Cloud storage backend configuration.
497///
498/// Supports Amazon S3, Google Cloud Storage, and Azure Blob Storage.
499/// Each variant contains the credentials and connection parameters for
500/// its respective cloud provider.
501///
502/// # Examples
503///
504/// ```ignore
505/// // Create S3 configuration from environment variables
506/// let config = CloudStorageConfig::s3_from_env("my-bucket");
507///
508/// // Create explicit S3 configuration for LocalStack testing
509/// let config = CloudStorageConfig::S3 {
510///     bucket: "test-bucket".to_string(),
511///     region: Some("us-east-1".to_string()),
512///     endpoint: Some("http://localhost:4566".to_string()),
513///     access_key_id: Some("test".to_string()),
514///     secret_access_key: Some("test".to_string()),
515///     session_token: None,
516///     virtual_hosted_style: false,
517/// };
518/// ```
519#[derive(Clone, Debug)]
520pub enum CloudStorageConfig {
521    /// Amazon S3 storage configuration.
522    S3 {
523        /// S3 bucket name.
524        bucket: String,
525        /// AWS region (e.g., "us-east-1"). Uses AWS_REGION env var if None.
526        region: Option<String>,
527        /// Custom endpoint URL for S3-compatible services (MinIO, LocalStack).
528        endpoint: Option<String>,
529        /// AWS access key ID. Uses AWS_ACCESS_KEY_ID env var if None.
530        access_key_id: Option<String>,
531        /// AWS secret access key. Uses AWS_SECRET_ACCESS_KEY env var if None.
532        secret_access_key: Option<String>,
533        /// AWS session token for temporary credentials.
534        session_token: Option<String>,
535        /// Use virtual-hosted-style requests (bucket.s3.region.amazonaws.com).
536        virtual_hosted_style: bool,
537    },
538    /// Google Cloud Storage configuration.
539    Gcs {
540        /// GCS bucket name.
541        bucket: String,
542        /// Path to service account JSON key file.
543        service_account_path: Option<String>,
544        /// Service account JSON key content (alternative to path).
545        service_account_key: Option<String>,
546    },
547    /// Azure Blob Storage configuration.
548    Azure {
549        /// Azure container name.
550        container: String,
551        /// Azure storage account name.
552        account: String,
553        /// Azure storage account access key.
554        access_key: Option<String>,
555        /// Azure SAS token for limited access.
556        sas_token: Option<String>,
557    },
558}
559
560impl CloudStorageConfig {
561    /// Creates an S3 configuration using environment variables.
562    ///
563    /// Reads credentials from standard AWS environment variables:
564    /// - `AWS_ACCESS_KEY_ID`
565    /// - `AWS_SECRET_ACCESS_KEY`
566    /// - `AWS_SESSION_TOKEN` (optional)
567    /// - `AWS_REGION` or `AWS_DEFAULT_REGION`
568    /// - `AWS_ENDPOINT_URL` (optional, for S3-compatible services)
569    #[must_use]
570    pub fn s3_from_env(bucket: &str) -> Self {
571        Self::S3 {
572            bucket: bucket.to_string(),
573            region: std::env::var("AWS_REGION")
574                .or_else(|_| std::env::var("AWS_DEFAULT_REGION"))
575                .ok(),
576            endpoint: std::env::var("AWS_ENDPOINT_URL").ok(),
577            access_key_id: std::env::var("AWS_ACCESS_KEY_ID").ok(),
578            secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").ok(),
579            session_token: std::env::var("AWS_SESSION_TOKEN").ok(),
580            virtual_hosted_style: false,
581        }
582    }
583
584    /// Creates a GCS configuration using environment variables.
585    ///
586    /// Reads service account path from `GOOGLE_APPLICATION_CREDENTIALS`.
587    #[must_use]
588    pub fn gcs_from_env(bucket: &str) -> Self {
589        Self::Gcs {
590            bucket: bucket.to_string(),
591            service_account_path: std::env::var("GOOGLE_APPLICATION_CREDENTIALS").ok(),
592            service_account_key: None,
593        }
594    }
595
596    /// Creates an Azure configuration using environment variables.
597    ///
598    /// Reads credentials from Azure environment variables:
599    /// - `AZURE_STORAGE_ACCOUNT`
600    /// - `AZURE_STORAGE_ACCESS_KEY` (optional)
601    /// - `AZURE_STORAGE_SAS_TOKEN` (optional)
602    ///
603    /// # Panics
604    ///
605    /// Panics if `AZURE_STORAGE_ACCOUNT` is not set.
606    #[must_use]
607    pub fn azure_from_env(container: &str) -> Self {
608        Self::Azure {
609            container: container.to_string(),
610            account: std::env::var("AZURE_STORAGE_ACCOUNT")
611                .expect("AZURE_STORAGE_ACCOUNT environment variable required"),
612            access_key: std::env::var("AZURE_STORAGE_ACCESS_KEY").ok(),
613            sas_token: std::env::var("AZURE_STORAGE_SAS_TOKEN").ok(),
614        }
615    }
616
617    /// Returns the bucket/container name for this configuration.
618    #[must_use]
619    pub fn bucket_name(&self) -> &str {
620        match self {
621            Self::S3 { bucket, .. } => bucket,
622            Self::Gcs { bucket, .. } => bucket,
623            Self::Azure { container, .. } => container,
624        }
625    }
626
627    /// Returns a URL-style identifier for this storage location.
628    #[must_use]
629    pub fn to_url(&self) -> String {
630        match self {
631            Self::S3 { bucket, .. } => format!("s3://{bucket}"),
632            Self::Gcs { bucket, .. } => format!("gs://{bucket}"),
633            Self::Azure {
634                container, account, ..
635            } => format!("az://{account}/{container}"),
636        }
637    }
638}
639
640#[cfg(test)]
641mod security_tests {
642    use super::*;
643
644    /// Tests for CWE-22 (Path Traversal) prevention in file sandbox.
645    mod file_sandbox {
646        use super::*;
647
648        #[test]
649        fn test_sandbox_disabled_allows_all_paths() {
650            let config = FileSandboxConfig::default();
651            assert!(!config.enabled);
652            // When disabled, all paths are allowed
653            assert!(config.validate_path("/tmp/test").is_ok());
654        }
655
656        #[test]
657        fn test_sandbox_enabled_with_no_paths_rejects() {
658            let config = FileSandboxConfig {
659                enabled: true,
660                allowed_paths: vec![],
661            };
662            let result = config.validate_path("/tmp/test");
663            assert!(result.is_err());
664            assert!(result.unwrap_err().contains("no allowed paths configured"));
665        }
666
667        #[test]
668        fn test_sandbox_rejects_outside_path() {
669            let config = FileSandboxConfig {
670                enabled: true,
671                allowed_paths: vec![PathBuf::from("/var/lib/uni")],
672            };
673            let result = config.validate_path("/etc/passwd");
674            assert!(result.is_err());
675            assert!(result.unwrap_err().contains("outside allowed sandbox"));
676        }
677
678        #[test]
679        fn test_is_potentially_insecure() {
680            // Disabled is insecure
681            let disabled = FileSandboxConfig::default();
682            assert!(disabled.is_potentially_insecure());
683
684            // Enabled with no paths is insecure
685            let no_paths = FileSandboxConfig {
686                enabled: true,
687                allowed_paths: vec![],
688            };
689            assert!(no_paths.is_potentially_insecure());
690
691            // Enabled with paths is secure
692            let secure = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
693            assert!(!secure.is_potentially_insecure());
694        }
695
696        #[test]
697        fn test_security_warning_when_disabled() {
698            let disabled = FileSandboxConfig::default();
699            assert!(disabled.security_warning().is_some());
700
701            let enabled = FileSandboxConfig::sandboxed(vec![PathBuf::from("/data")]);
702            assert!(enabled.security_warning().is_none());
703        }
704
705        #[test]
706        fn test_deployment_mode_defaults() {
707            let embedded = FileSandboxConfig::default_for_mode(DeploymentMode::Embedded);
708            assert!(!embedded.enabled);
709
710            let server = FileSandboxConfig::default_for_mode(DeploymentMode::Server);
711            assert!(server.enabled);
712            assert!(!server.allowed_paths.is_empty());
713        }
714    }
715}