//! Agent bootstrap and lifecycle hooks. //! //! [`bootstrap`] runs the full init sequence (config resolution, process //! singletons, model catalog) and returns a resolved config + `ModelsManager`. use std::sync::Arc; use indexmap::IndexMap; use crate::agent::config::{self, Config as AgentConfig, ModelEntry}; use crate::agent::models::ModelsManager; use crate::auth::AuthManager; use crate::config::StorageMode; /// Resolve config, init process singletons, build the model catalog. /// /// The `ModelsManager` is `Clone + Send`, so callers that need a handle /// for the config watcher can clone it before passing it to /// `MvpAgent::with_models`. pub fn bootstrap( cfg: &AgentConfig, auth_manager: &Arc, prefetched: Option>, ) -> Result<(AgentConfig, ModelsManager), String> { // Fail closed before any policy is read: a tampered managed policy must not run unmanaged. crate::managed_config::managed_policy_gate()?; let cfg = resolve_config(cfg, auth_manager); cfg.validate_model_filters()?; init_process(&cfg, auth_manager); let models_manager = ModelsManager::from_config(&cfg, prefetched, auth_manager.clone())?; // Refresh on every auth refresh — the FSEvents watcher can silently die after // macOS sleep, stranding the catalog on bundled defaults. models_manager.start_auth_refresh_watcher(auth_manager.refresh_notifier()); Ok((cfg, models_manager)) } /// Print a `bootstrap`/`MvpAgent::new` config error and exit (process boundary). /// /// Restores native stderr first: a managed-policy refusal on the ACP/server path reaches here /// while fd 2 may still point at the `/dev/null` the TUI's `redirect_native_stderr()` set, which /// would swallow the message. No-op when stderr was never redirected (headless). pub(crate) fn exit_on_config_error(e: String) -> T { kigi_tty_utils::restore_native_stderr(); eprintln!("\nConfiguration error:\n\n {e}\n"); std::process::exit(1); } /// Config transform: apply managed settings, fetch remote settings, /// resolve storage mode. fn resolve_config(cfg: &AgentConfig, auth_manager: &AuthManager) -> AgentConfig { let mut cfg = cfg.clone(); if let Ok(layers) = crate::config::ConfigLayers::load() && layers.has_managed() { let origins = crate::config::config_origins(&layers); let managed_keys: Vec<&str> = origins .iter() .filter(|(_, s)| matches!(s, config::ConfigSource::ManagedConfig)) .map(|(k, _)| k.as_str()) .collect(); if !managed_keys.is_empty() { tracing::info!(keys = ?managed_keys, "managed_config.toml fields"); } } let managed_enforced = crate::config::apply_managed_settings_features(&mut cfg); let requirements_enforced = crate::config::apply_requirements(&mut cfg); for e in managed_enforced.iter().chain(&requirements_enforced) { tracing::info!(field = %e.path, value = %e.value, source = %e.source, "policy override"); } crate::util::config::sync_campaign_fields(&mut cfg); crate::agent::config::apply_remote_settings_side_effects(cfg.remote_settings.as_ref()); // env var > remote settings > Local. Skip remote settings for Generic (kigi -p, subagents). if cfg.storage_mode == StorageMode::Local && cfg.mode != crate::agent::config::AgentMode::Generic { cfg.storage_mode = StorageMode::resolve(None, cfg.remote_settings.as_ref()); } // Writeback talks to the code backend; requires a Kimi Code session. if cfg.storage_mode == StorageMode::Writeback && !auth_manager.current().is_some_and(|a| a.is_session_auth()) { tracing::info!("Writeback is disabled: requires auth with kimi.com"); cfg.storage_mode = StorageMode::Local; } if let Some(rs) = cfg.remote_settings.as_ref() && let Some(v) = rs.path_not_found_hints { cfg.path_not_found_hints = v; } cfg } /// Initialize process-level singletons (deployment sync, bundled files). /// `Once`-guarded: only the first call takes effect. fn init_process(cfg: &AgentConfig, auth_manager: &AuthManager) { use std::sync::Once; static INIT: Once = Once::new(); INIT.call_once(|| { if !cfg!(test) { // Clear a logged-out team's files before the background sync runs. crate::managed_config::clear_orphan(); crate::managed_config::spawn_sync(tokio_util::sync::CancellationToken::new()); } let kigi_home = crate::util::kigi_home::kigi_home(); crate::builtin::extract_bundled_files(&kigi_home); let feedback = cfg.resolve_feedback(); let feedback_url = cfg.endpoints.resolve_feedback_base_url(); tracing::info!( feedback = %feedback, feedback_url = %feedback_url, feedback_url_custom = cfg.endpoints.feedback_base_url.is_some(), "data capture config resolved", ); }); }