mobius_gateway/sandbox/
config.rs1use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10pub struct ExecutionConfig {
11 pub command_timeout_seconds: u64,
13 pub shell_executable: Option<PathBuf>,
15 pub bubblewrap_executable: Option<PathBuf>,
17 pub procfs_mode: mobius::backend::sandbox::ProcfsMode,
19 pub bytes_per_token: f64,
21 pub subagent_max_depth: u8,
23 pub subagent_max_concurrency: usize,
25 pub subagent_max_agents: usize,
27}
28impl Default for ExecutionConfig {
29 fn default() -> Self {
30 let ceilings = mobius::middleware::subagents::SubagentCeilings::default();
31 Self {
32 command_timeout_seconds: mobius::backend::sandbox::default_command_timeout_seconds(),
33 shell_executable: None,
34 bubblewrap_executable: None,
35 procfs_mode: mobius::backend::sandbox::ProcfsMode::default(),
36 bytes_per_token: mobius::middleware::TokenEstimate::default().bytes_per_token(),
37 subagent_max_depth: ceilings.max_depth(),
38 subagent_max_concurrency: ceilings.max_concurrency(),
39 subagent_max_agents: ceilings.max_agents(),
40 }
41 }
42}
43impl ExecutionConfig {
44 pub fn validate(&self) -> Result<()> {
48 crate::config::bounded(
49 "execution.command_timeout_seconds",
50 self.command_timeout_seconds,
51 1..=31_536_000,
52 )?;
53 for path in [&self.shell_executable, &self.bubblewrap_executable]
54 .into_iter()
55 .flatten()
56 {
57 if !path.is_absolute() || !path.is_file() {
58 return Err(Error::Config(
59 "execution executable must be an existing absolute file".into(),
60 ));
61 }
62 }
63 mobius::middleware::TokenEstimate::new(self.bytes_per_token)?;
64 self.subagent_ceilings()?;
65 Ok(())
66 }
67
68 pub fn subagent_ceilings(&self) -> Result<mobius::middleware::subagents::SubagentCeilings> {
72 Ok(mobius::middleware::subagents::SubagentCeilings::new(
73 self.subagent_max_depth,
74 self.subagent_max_concurrency,
75 self.subagent_max_agents,
76 )?)
77 }
78}
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn omitted_execution_fields_reuse_core_defaults_without_recursive_initialization() {
85 let policy: ExecutionConfig = toml::from_str("").expect("defaulted execution policy");
86 assert_eq!(policy, ExecutionConfig::default());
87 assert_eq!(
88 policy.command_timeout_seconds,
89 mobius::backend::sandbox::default_command_timeout_seconds()
90 );
91 assert_eq!(
92 policy.subagent_ceilings().expect("ceilings"),
93 mobius::middleware::subagents::SubagentCeilings::default()
94 );
95 policy.validate().expect("valid defaults");
96 }
97
98 #[test]
99 fn procfs_policy_is_explicit_and_rejects_unknown_modes() {
100 use mobius::backend::sandbox::ProcfsMode;
101
102 assert_eq!(ExecutionConfig::default().procfs_mode, ProcfsMode::Private);
103 let policy: ExecutionConfig =
104 toml::from_str("procfs_mode = 'empty'").expect("empty procfs");
105 assert_eq!(policy.procfs_mode, ProcfsMode::Empty);
106 assert!(toml::from_str::<ExecutionConfig>("procfs_mode = 'automatic'").is_err());
107 }
108
109 #[test]
110 fn trusted_operator_subagent_ceiling_relationships_are_validated() {
111 let mut policy: ExecutionConfig = toml::from_str(
112 "subagent_max_depth=32\nsubagent_max_concurrency=128\nsubagent_max_agents=512",
113 )
114 .expect("operator settings");
115 policy.validate().expect("larger trusted ceilings");
116 policy.subagent_max_agents = 127;
117 assert!(policy.validate().is_err());
118 policy.subagent_max_agents = 512;
119 policy.subagent_max_depth = 0;
120 assert!(policy.validate().is_err());
121 }
122}