use crate::{
config::{CliConfigOverrides, EffectiveConfig, McPaths, Settings},
instructions::{InstructionFile, discover_agents_with_additional_markdown},
mcp::manager::McpManager,
sessions::SessionManager,
skills::{SkillDiscovery, SkillRoots, discover_skills, filter_enabled_skills},
};
use anyhow::Result;
use std::{
env,
path::PathBuf,
sync::{Arc, Mutex},
};
#[derive(Clone)]
pub(super) struct ServiceRuntime {
pub(super) config: EffectiveConfig,
pub(super) settings: Settings,
pub(super) instructions: Vec<InstructionFile>,
pub(super) skills: SkillDiscovery,
pub(super) session_manager: SessionManager,
pub(super) cwd: PathBuf,
pub(super) mcp: Option<Arc<Mutex<McpManager>>>,
}
impl ServiceRuntime {
pub(super) fn load() -> Result<Self> {
Self::load_for_profile(false)
}
pub(super) fn load_persistent() -> Result<Self> {
Self::load_for_profile(true)
}
#[cfg(any(unix, test))]
pub(super) fn load_unix(workspace: PathBuf, state_root: PathBuf) -> Result<Self> {
anyhow::ensure!(
workspace.is_absolute() && state_root.is_absolute(),
"absolute service paths required"
);
let cwd = workspace.canonicalize()?;
anyhow::ensure!(cwd.is_dir(), "workspace must be a directory");
let paths = McPaths::from_root_and_project_dir(state_root, cwd.clone());
paths.ensure_runtime_dirs()?;
let paths = McPaths::from_root_and_project_dir(paths.root.canonicalize()?, cwd.clone());
Self::load_with_paths(cwd, paths)
}
fn load_for_profile(canonical_paths: bool) -> Result<Self> {
let cwd = env::current_dir()?;
let paths = McPaths::resolve()?;
paths.ensure_runtime_dirs()?;
let (cwd, paths) = if canonical_paths {
let cwd = cwd.canonicalize()?;
let paths = McPaths::from_root_and_project_dir(paths.root.canonicalize()?, cwd.clone());
(cwd, paths)
} else {
(cwd, paths)
};
Self::load_with_paths(cwd, paths)
}
fn load_with_paths(cwd: PathBuf, paths: McPaths) -> Result<Self> {
let (config, settings, _, _) = crate::config::load_startup_config_with_settings(
paths.clone(),
CliConfigOverrides::default(),
)?;
let instructions = discover_agents_with_additional_markdown(
&paths.user_agents,
&cwd,
&settings.instructions.additional_markdown_paths,
)?;
let mut skill_roots = SkillRoots::from_paths(&paths.root, &cwd);
skill_roots.additional_paths = settings.skills.additional_paths.clone();
let discovered_skills = discover_skills(&skill_roots);
let disabled_skills = crate::config::disabled_skill_names_from_settings(&settings);
let skills = filter_enabled_skills(&discovered_skills, &disabled_skills);
let mcp = (!settings.mcp_servers.is_empty()).then(|| {
Arc::new(Mutex::new(McpManager::from_settings_with_paths(
&settings.mcp_servers,
Some(&paths.root),
)))
});
Ok(Self {
config,
settings,
instructions,
skills,
session_manager: SessionManager::new(paths.sessions.clone()),
cwd,
mcp,
})
}
pub(super) fn current_provider_auth_ready(&self) -> Result<bool> {
self.reloaded()?.config.current_store_provider_auth_ready()
}
pub(super) fn reloaded(&self) -> Result<Self> {
self.with_frontend_settings(crate::config::read_settings(&self.config.paths)?)
}
pub(super) fn capture_turn_settings(&self) -> Result<Self> {
self.with_frontend_settings(crate::config::read_settings_locked(&self.config.paths)?)
}
fn with_frontend_settings(&self, settings: Settings) -> Result<Self> {
let mut runtime = self.clone();
runtime.settings.selected_model = settings.selected_model;
runtime.settings.fast = settings.fast;
runtime.config = EffectiveConfig::from_loaded_settings(
self.config.paths.clone(),
CliConfigOverrides::default(),
runtime.settings.clone(),
)?;
Ok(runtime)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn explicit_runtime_paths_are_canonical_and_do_not_change_process_context() {
let temp = tempfile::tempdir().unwrap();
let cwd = env::current_dir().unwrap();
let home = env::var_os("MC_HOME");
let workspace = temp.path().join("workspace");
std::fs::create_dir(&workspace).unwrap();
let root = temp.path().join("state");
let runtime = ServiceRuntime::load_unix(workspace.join("."), root.clone()).unwrap();
assert_eq!(runtime.cwd, workspace.canonicalize().unwrap());
assert_eq!(runtime.config.paths.root, root.canonicalize().unwrap());
assert_eq!(env::current_dir().unwrap(), cwd);
assert_eq!(env::var_os("MC_HOME"), home);
assert!(ServiceRuntime::load_unix(PathBuf::from("relative"), root).is_err());
}
}