Skip to main content

mobius_gateway/sandbox/
config.rs

1//! Operator-selected command execution policy.
2
3use crate::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7/// Host command policy independent of Bot permissions.
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10pub struct ExecutionConfig {
11    /// Deadline for each agent command.
12    pub command_timeout_seconds: u64,
13    /// Optional absolute location of a shell accepting `-c` scripts.
14    pub shell_executable: Option<PathBuf>,
15    /// Optional absolute location of Bubblewrap on Linux.
16    pub bubblewrap_executable: Option<PathBuf>,
17    /// Linux procfs policy; empty procfs requires host-provided PID isolation.
18    pub procfs_mode: mobius::backend::sandbox::ProcfsMode,
19    /// Configurable fallback estimate when measured token usage is unavailable.
20    pub bytes_per_token: f64,
21    /// Host ceiling for each Bot's subagent nesting depth.
22    pub subagent_max_depth: u8,
23    /// Host ceiling for concurrent agents, including the root.
24    pub subagent_max_concurrency: usize,
25    /// Host ceiling for retained agents, including the root.
26    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    /// Validates execution policy before building a sandbox.
45    /// # Errors
46    /// Returns an error for invalid durations, paths or token estimates.
47    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    /// Returns validated host ceilings independent of frontend-writable Bot settings.
69    /// # Errors
70    /// Returns an error for invalid depth, concurrency, or agent relationships.
71    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}