use codex_config::AppsRequirementsToml; use codex_config::ConfigLayerStack; use codex_config::types::AppToolApproval; use codex_config::types::AppsConfigToml; use serde::Deserialize; use crate::AppInfo; /// The effective enablement and approval policy for one app tool. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AppToolPolicy { pub enabled: bool, pub approval: AppToolApproval, } impl Default for AppToolPolicy { fn default() -> Self { Self { enabled: true, approval: AppToolApproval::Auto, } } } /// App and account metadata used to evaluate one tool. #[derive(Debug, Clone, Copy)] pub struct AppToolPolicyInput<'a> { pub connector_id: Option<&'a str>, pub link_id: Option<&'a str>, pub tool_name: &'a str, pub tool_title: Option<&'a str>, pub destructive_hint: Option, pub open_world_hint: Option, } /// Resolves app tool policy against one immutable config snapshot. /// /// Callers should construct one evaluator and reuse it for every tool in the /// same exposure build so config layers are merged and decoded only once. pub struct AppToolPolicyEvaluator<'a> { apps_config: Option, requirements_apps_config: Option<&'a AppsRequirementsToml>, } impl<'a> AppToolPolicyEvaluator<'a> { pub fn new(config_layer_stack: &'a ConfigLayerStack) -> Self { let apps_config = apps_config_from_layer_stack(config_layer_stack); let requirements_apps_config = config_layer_stack.requirements_toml().apps.as_ref(); Self::from_parts(apps_config, requirements_apps_config) } pub fn policy(&self, input: AppToolPolicyInput<'_>) -> AppToolPolicy { let managed_approval = managed_app_tool_approval( self.requirements_apps_config, input.connector_id, input.tool_name, ); app_tool_policy_from_apps_config(self.apps_config.as_ref(), input, managed_approval) } /// Returns the effective local and managed enablement for one connector. pub fn app_enabled(&self, connector_id: &str) -> bool { self.apps_config .as_ref() .map(|apps_config| app_is_enabled(apps_config, Some(connector_id))) .unwrap_or(true) } /// Applies app policy without overriding source state for unconfigured apps. pub fn apply_app_enabled_state(&self, mut apps: Vec) -> Vec { let Some(apps_config) = self.apps_config.as_ref() else { return apps; }; for app in &mut apps { if apps_config.default.is_some() || apps_config.apps.contains_key(app.id.as_str()) { app.is_enabled = self.app_enabled(app.id.as_str()); } } apps } fn from_parts( apps_config: Option, requirements_apps_config: Option<&'a AppsRequirementsToml>, ) -> Self { Self { apps_config: effective_apps_config(apps_config, requirements_apps_config), requirements_apps_config, } } } /// Reads the merged, unmanaged Apps configuration from a config-layer stack. pub fn apps_config_from_layer_stack( config_layer_stack: &ConfigLayerStack, ) -> Option { config_layer_stack .effective_config() .as_table() .and_then(|table| table.get("apps")) .cloned() .and_then(|value| AppsConfigToml::deserialize(value).ok()) } pub fn app_is_enabled(apps_config: &AppsConfigToml, connector_id: Option<&str>) -> bool { let default_enabled = apps_config .default .as_ref() .map(|defaults| defaults.enabled) .unwrap_or(true); connector_id .and_then(|connector_id| apps_config.apps.get(connector_id)) .map(|app| app.enabled) .unwrap_or(default_enabled) } fn effective_apps_config( apps_config: Option, requirements_apps_config: Option<&AppsRequirementsToml>, ) -> Option { let had_apps_config = apps_config.is_some(); let mut apps_config = apps_config.unwrap_or_default(); apply_requirements_apps_constraints(&mut apps_config, requirements_apps_config); if had_apps_config || apps_config.default.is_some() || !apps_config.apps.is_empty() { Some(apps_config) } else { None } } fn apply_requirements_apps_constraints( apps_config: &mut AppsConfigToml, requirements_apps_config: Option<&AppsRequirementsToml>, ) { let Some(requirements_apps_config) = requirements_apps_config else { return; }; for (app_id, requirement) in &requirements_apps_config.apps { if requirement.enabled == Some(false) { let app = apps_config.apps.entry(app_id.clone()).or_default(); app.enabled = false; } } } fn managed_app_tool_approval( requirements_apps_config: Option<&AppsRequirementsToml>, connector_id: Option<&str>, tool_name: &str, ) -> Option { let connector_id = connector_id?; requirements_apps_config? .apps .get(connector_id)? .tools .as_ref()? .tools .get(tool_name)? .approval_mode } fn app_tool_policy_from_apps_config( apps_config: Option<&AppsConfigToml>, input: AppToolPolicyInput<'_>, managed_approval: Option, ) -> AppToolPolicy { let Some(apps_config) = apps_config else { return AppToolPolicy { approval: managed_approval.unwrap_or(AppToolApproval::Auto), ..Default::default() }; }; let app = input .connector_id .and_then(|connector_id| apps_config.apps.get(connector_id)); let tools = app.and_then(|app| app.tools.as_ref()); let tool_config = tools.and_then(|tools| { tools .tools .get(input.tool_name) .or_else(|| input.tool_title.and_then(|title| tools.tools.get(title))) }); let approval = managed_approval .or_else(|| tool_config.and_then(|tool| tool.approval_mode)) .or_else(|| { input .link_id .and_then(|link_id| app?.links.as_ref()?.links.get(link_id)) .and_then(|link| link.default_tools_approval_mode) }) .or_else(|| app.and_then(|app| app.default_tools_approval_mode)) .or_else(|| { input .connector_id .and(apps_config.default.as_ref()) .and_then(|defaults| defaults.default_tools_approval_mode) }) .unwrap_or(AppToolApproval::Auto); if !app_is_enabled(apps_config, input.connector_id) { return AppToolPolicy { enabled: false, approval, }; } if let Some(enabled) = tool_config.and_then(|tool| tool.enabled) { return AppToolPolicy { enabled, approval }; } if let Some(enabled) = app.and_then(|app| app.default_tools_enabled) { return AppToolPolicy { enabled, approval }; } let app_defaults = apps_config.default.as_ref(); let destructive_enabled = app .and_then(|app| app.destructive_enabled) .unwrap_or_else(|| { app_defaults .map(|defaults| defaults.destructive_enabled) .unwrap_or(true) }); let open_world_enabled = app .and_then(|app| app.open_world_enabled) .unwrap_or_else(|| { app_defaults .map(|defaults| defaults.open_world_enabled) .unwrap_or(true) }); let destructive_hint = input.destructive_hint.unwrap_or(true); let open_world_hint = input.open_world_hint.unwrap_or(true); let enabled = (destructive_enabled || !destructive_hint) && (open_world_enabled || !open_world_hint); AppToolPolicy { enabled, approval } } #[cfg(test)] #[path = "app_tool_policy_tests.rs"] mod tests;