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) discovered_skill_names: Vec<String>,
pub(super) session_manager: SessionManager,
pub(super) cwd: PathBuf,
pub(super) mcp: Option<Arc<Mutex<McpManager>>>,
pub(super) application: Option<(
super::application::CapturedTurn,
super::application::SharedApplicationState,
)>,
}
impl ServiceRuntime {
pub(super) fn load_persistent() -> Result<Self> {
let cwd = env::current_dir()?.canonicalize()?;
let paths = McPaths::resolve()?;
paths.ensure_runtime_dirs()?;
let paths = McPaths::from_root_and_project_dir(paths.root.canonicalize()?, cwd.clone());
Self::load_with_paths(cwd, paths)
}
#[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_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 discovered_skill_names = discovered_skills.skills.keys().cloned().collect();
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,
discovered_skill_names,
session_manager: SessionManager::new(paths.sessions.clone()),
cwd,
mcp,
application: None,
})
}
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 startup_catalog_keeps_disabled_skill_names_without_enabling_instructions() {
let temp = tempfile::tempdir().unwrap();
let workspace = temp.path().join("workspace");
let skill = workspace.join(".agents/skills/reserved_skill");
std::fs::create_dir_all(&skill).unwrap();
std::fs::write(
skill.join("SKILL.md"),
"---\ndescription: Reserved\n---\ninstructions",
)
.unwrap();
let paths =
McPaths::from_root_and_project_dir(temp.path().join("state"), workspace.clone());
paths.ensure_runtime_dirs().unwrap();
let mut settings = Settings::default();
settings.skills.disabled = vec!["reserved_skill".into()];
crate::config::write_settings(&paths, &settings).unwrap();
let runtime = ServiceRuntime::load_unix(workspace, paths.root).unwrap();
assert!(!runtime.skills.skills.contains_key("reserved_skill"));
assert!(
runtime
.discovered_skill_names
.iter()
.any(|name| name == "reserved_skill")
);
}
#[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());
}
}