use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ExecutionConfig {
pub command_timeout_seconds: u64,
pub shell_executable: Option<PathBuf>,
pub bubblewrap_executable: Option<PathBuf>,
pub procfs_mode: mobius::backend::sandbox::ProcfsMode,
pub bytes_per_token: f64,
pub subagent_max_depth: u8,
pub subagent_max_concurrency: usize,
pub subagent_max_agents: usize,
}
impl Default for ExecutionConfig {
fn default() -> Self {
let ceilings = mobius::middleware::subagents::SubagentCeilings::default();
Self {
command_timeout_seconds: mobius::backend::sandbox::default_command_timeout_seconds(),
shell_executable: None,
bubblewrap_executable: None,
procfs_mode: mobius::backend::sandbox::ProcfsMode::default(),
bytes_per_token: mobius::middleware::TokenEstimate::default().bytes_per_token(),
subagent_max_depth: ceilings.max_depth(),
subagent_max_concurrency: ceilings.max_concurrency(),
subagent_max_agents: ceilings.max_agents(),
}
}
}
impl ExecutionConfig {
pub fn validate(&self) -> Result<()> {
crate::config::bounded(
"execution.command_timeout_seconds",
self.command_timeout_seconds,
1..=31_536_000,
)?;
for path in [&self.shell_executable, &self.bubblewrap_executable]
.into_iter()
.flatten()
{
if !path.is_absolute() || !path.is_file() {
return Err(Error::Config(
"execution executable must be an existing absolute file".into(),
));
}
}
mobius::middleware::TokenEstimate::new(self.bytes_per_token)?;
self.subagent_ceilings()?;
Ok(())
}
pub fn subagent_ceilings(&self) -> Result<mobius::middleware::subagents::SubagentCeilings> {
Ok(mobius::middleware::subagents::SubagentCeilings::new(
self.subagent_max_depth,
self.subagent_max_concurrency,
self.subagent_max_agents,
)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn omitted_execution_fields_reuse_core_defaults_without_recursive_initialization() {
let policy: ExecutionConfig = toml::from_str("").expect("defaulted execution policy");
assert_eq!(policy, ExecutionConfig::default());
assert_eq!(
policy.command_timeout_seconds,
mobius::backend::sandbox::default_command_timeout_seconds()
);
assert_eq!(
policy.subagent_ceilings().expect("ceilings"),
mobius::middleware::subagents::SubagentCeilings::default()
);
policy.validate().expect("valid defaults");
}
#[test]
fn procfs_policy_is_explicit_and_rejects_unknown_modes() {
use mobius::backend::sandbox::ProcfsMode;
assert_eq!(ExecutionConfig::default().procfs_mode, ProcfsMode::Private);
let policy: ExecutionConfig =
toml::from_str("procfs_mode = 'empty'").expect("empty procfs");
assert_eq!(policy.procfs_mode, ProcfsMode::Empty);
assert!(toml::from_str::<ExecutionConfig>("procfs_mode = 'automatic'").is_err());
}
#[test]
fn trusted_operator_subagent_ceiling_relationships_are_validated() {
let mut policy: ExecutionConfig = toml::from_str(
"subagent_max_depth=32\nsubagent_max_concurrency=128\nsubagent_max_agents=512",
)
.expect("operator settings");
policy.validate().expect("larger trusted ceilings");
policy.subagent_max_agents = 127;
assert!(policy.validate().is_err());
policy.subagent_max_agents = 512;
policy.subagent_max_depth = 0;
assert!(policy.validate().is_err());
}
}