use super::*; use crate::config::CONFIG_TOML_FILE; use crate::config::ConfigBuilder; use codex_config::AppRequirementToml; use codex_config::AppToolRequirementToml; use codex_config::AppToolsRequirementsToml; use codex_config::AppsRequirementsToml; use codex_config::CloudRequirementsLoader; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirements; use codex_config::ConfigRequirementsToml; use codex_config::types::AppConfig; use codex_config::types::AppToolConfig; use codex_config::types::AppToolsConfig; use codex_config::types::AppsDefaultConfig; use codex_connectors::merge::plugin_connector_to_app_info; use codex_connectors::metadata::connector_install_url; use codex_connectors::metadata::sanitize_name; use codex_features::Feature; use codex_login::CodexAuth; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::ToolInfo; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use rmcp::model::JsonObject; use rmcp::model::Tool; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; use tempfile::tempdir; fn annotations(destructive_hint: Option, open_world_hint: Option) -> ToolAnnotations { ToolAnnotations::from_raw( /*title*/ None, /*read_only_hint*/ None, destructive_hint, /*idempotent_hint*/ None, open_world_hint, ) } fn app(id: &str) -> AppInfo { AppInfo { id: id.to_string(), name: id.to_string(), description: None, logo_url: None, logo_url_dark: None, distribution_channel: None, install_url: None, branding: None, app_metadata: None, labels: None, is_accessible: false, is_enabled: true, plugin_display_names: Vec::new(), } } fn plugin_names(names: &[&str]) -> Vec { names.iter().map(ToString::to_string).collect() } fn test_tool_definition(tool_name: &str) -> Tool { Tool::new_with_raw(tool_name.to_string(), None, Arc::new(JsonObject::default())) } fn codex_app_tool( tool_name: &str, connector_id: &str, connector_name: Option<&str>, plugin_display_names: &[&str], ) -> ToolInfo { let tool_namespace = connector_name .map(sanitize_name) .map(|connector_name| format!("mcp__{CODEX_APPS_MCP_SERVER_NAME}__{connector_name}")) .unwrap_or_else(|| CODEX_APPS_MCP_SERVER_NAME.to_string()); ToolInfo { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), supports_parallel_tool_calls: false, server_origin: None, callable_name: tool_name.to_string(), callable_namespace: tool_namespace, namespace_description: None, tool: test_tool_definition(tool_name), connector_id: Some(connector_id.to_string()), connector_name: connector_name.map(ToOwned::to_owned), plugin_display_names: plugin_names(plugin_display_names), } } fn with_accessible_connectors_cache_cleared(f: impl FnOnce() -> R) -> R { let previous = { let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); cache_guard.take() }; let result = f(); let mut cache_guard = ACCESSIBLE_CONNECTORS_CACHE .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); *cache_guard = previous; result } #[test] fn accessible_connectors_from_mcp_tools_carries_plugin_display_names() { let tools = vec![ codex_app_tool( "calendar_list_events", "calendar", /*connector_name*/ None, &["sample", "sample"], ), codex_app_tool( "calendar_create_event", "calendar", Some("Google Calendar"), &["beta", "sample"], ), ToolInfo { server_name: "sample".to_string(), supports_parallel_tool_calls: false, server_origin: None, callable_name: "echo".to_string(), callable_namespace: "sample".to_string(), namespace_description: None, tool: test_tool_definition("echo"), connector_id: None, connector_name: None, plugin_display_names: plugin_names(&["ignored"]), }, ]; let connectors = accessible_connectors_from_mcp_tools(&tools); assert_eq!( connectors, vec![AppInfo { id: "calendar".to_string(), name: "Google Calendar".to_string(), description: None, logo_url: None, logo_url_dark: None, distribution_channel: None, install_url: Some(connector_install_url("Google Calendar", "calendar")), branding: None, app_metadata: None, labels: None, is_accessible: true, is_enabled: true, plugin_display_names: plugin_names(&["beta", "sample"]), }] ); } #[tokio::test] async fn refresh_accessible_connectors_cache_from_mcp_tools_writes_latest_installed_apps() { let codex_home = tempdir().expect("tempdir should succeed"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("config should load"); let _ = config.features.set_enabled(Feature::Apps, /*enabled*/ true); let cache_key = accessible_connectors_cache_key(&config, /*auth*/ None); let tools = vec![ codex_app_tool( "calendar_list_events", "calendar", Some("Google Calendar"), &["calendar-plugin"], ), codex_app_tool( "openai_hidden", "connector_openai_hidden", Some("Hidden"), &[], ), ]; let cached = with_accessible_connectors_cache_cleared(|| { refresh_accessible_connectors_cache_from_mcp_tools(&config, /*auth*/ None, &tools); read_cached_accessible_connectors(&cache_key).expect("cache should be populated") }); assert_eq!( cached, vec![ AppInfo { id: "calendar".to_string(), name: "Google Calendar".to_string(), description: None, logo_url: None, logo_url_dark: None, distribution_channel: None, install_url: Some(connector_install_url("Google Calendar", "calendar")), branding: None, app_metadata: None, labels: None, is_accessible: true, is_enabled: true, plugin_display_names: plugin_names(&["calendar-plugin"]), }, AppInfo { id: "connector_openai_hidden".to_string(), name: "Hidden".to_string(), description: None, logo_url: None, logo_url_dark: None, distribution_channel: None, install_url: Some(connector_install_url("Hidden", "connector_openai_hidden")), branding: None, app_metadata: None, labels: None, is_accessible: true, is_enabled: true, plugin_display_names: Vec::new(), } ] ); } #[test] fn accessible_connectors_from_mcp_tools_preserves_description() { let mcp_tools = vec![ToolInfo { server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(), supports_parallel_tool_calls: false, server_origin: None, callable_name: "calendar_create_event".to_string(), callable_namespace: "mcp__codex_apps__calendar".to_string(), namespace_description: Some("Plan events".to_string()), tool: Tool::new( "calendar_create_event", "Create a calendar event", Arc::new(JsonObject::default()), ), connector_id: Some("calendar".to_string()), connector_name: Some("Calendar".to_string()), plugin_display_names: Vec::new(), }]; assert_eq!( accessible_connectors_from_mcp_tools(&mcp_tools), vec![AppInfo { id: "calendar".to_string(), name: "Calendar".to_string(), description: Some("Plan events".to_string()), logo_url: None, logo_url_dark: None, distribution_channel: None, branding: None, app_metadata: None, labels: None, install_url: Some(connector_install_url("Calendar", "calendar")), is_accessible: true, is_enabled: true, plugin_display_names: Vec::new(), }] ); } #[test] fn app_tool_policy_uses_global_defaults_for_destructive_hints() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: false, open_world_enabled: true, }), apps: HashMap::new(), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(&annotations(Some(true), /*open_world_hint*/ None)), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_defaults_missing_destructive_hint_to_true() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: false, open_world_enabled: true, }), apps: HashMap::new(), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(&annotations(/*destructive_hint*/ None, Some(false))), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_defaults_missing_open_world_hint_to_true() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: true, destructive_enabled: true, open_world_enabled: false, }), apps: HashMap::new(), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(&annotations(Some(false), /*open_world_hint*/ None)), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn app_is_enabled_uses_default_for_unconfigured_apps() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, open_world_enabled: true, }), apps: HashMap::new(), }; assert!(!app_is_enabled(&apps_config, Some("calendar"))); assert!(!app_is_enabled(&apps_config, /*connector_id*/ None)); } #[test] fn app_is_enabled_prefers_per_app_override_over_default() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, open_world_enabled: true, }), apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: None, open_world_enabled: None, default_tools_approval_mode: None, default_tools_enabled: None, tools: None, }, )]), }; assert!(app_is_enabled(&apps_config, Some("calendar"))); assert!(!app_is_enabled(&apps_config, Some("drive"))); } #[test] fn requirements_disabled_connector_overrides_enabled_connector() { let mut effective_apps = AppsConfigToml { default: None, apps: HashMap::from([( "connector_123123".to_string(), AppConfig { enabled: true, ..Default::default() }, )]), }; let requirements_apps = AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }; apply_requirements_apps_constraints(&mut effective_apps, Some(&requirements_apps)); assert_eq!( effective_apps .apps .get("connector_123123") .map(|app| app.enabled), Some(false) ); } #[test] fn requirements_enabled_does_not_override_disabled_connector() { let mut effective_apps = AppsConfigToml { default: None, apps: HashMap::from([( "connector_123123".to_string(), AppConfig { enabled: false, ..Default::default() }, )]), }; let requirements_apps = AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(true), tools: None, }, )]), }; apply_requirements_apps_constraints(&mut effective_apps, Some(&requirements_apps)); assert_eq!( effective_apps .apps .get("connector_123123") .map(|app| app.enabled), Some(false) ); } #[tokio::test] async fn cloud_requirements_disable_connector_overrides_user_apps_config() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), r#" [apps.connector_123123] enabled = true "#, ) .expect("write config"); let requirements = ConfigRequirementsToml { apps: Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }), ..Default::default() }; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cloud_requirements(CloudRequirementsLoader::new(async move { Ok(Some(requirements)) })) .build() .await .expect("config should build"); let policy = app_tool_policy( &config, Some("connector_123123"), "events.list", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[tokio::test] async fn cloud_requirements_disable_connector_applies_without_user_apps_table() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write(codex_home.path().join(CONFIG_TOML_FILE), "").expect("write config"); let requirements = ConfigRequirementsToml { apps: Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }), ..Default::default() }; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cloud_requirements(CloudRequirementsLoader::new(async move { Ok(Some(requirements)) })) .build() .await .expect("config should build"); let policy = app_tool_policy( &config, Some("connector_123123"), "events.list", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[tokio::test] async fn local_requirements_disable_connector_overrides_user_apps_config() { let codex_home = tempdir().expect("tempdir should succeed"); let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)).expect("abs path"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await .expect("config should build"); let requirements = ConfigRequirementsToml { apps: Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }), ..Default::default() }; config.config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("requirements stack") .with_user_config( &config_toml_path, toml::from_str::( r#" [apps.connector_123123] enabled = true "#, ) .expect("apps config"), ); let policy = app_tool_policy( &config, Some("connector_123123"), "events.list", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[tokio::test] async fn local_requirements_disable_connector_applies_without_user_apps_table() { let codex_home = tempdir().expect("tempdir should succeed"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await .expect("config should build"); let requirements = ConfigRequirementsToml { apps: Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_123123".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }), ..Default::default() }; config.config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("requirements stack"); let policy = app_tool_policy( &config, Some("connector_123123"), "events.list", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[tokio::test] async fn with_app_enabled_state_preserves_unrelated_disabled_connector() { let codex_home = tempdir().expect("tempdir should succeed"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await .expect("config should build"); let requirements = ConfigRequirementsToml { apps: Some(AppsRequirementsToml { apps: BTreeMap::from([( "connector_drive".to_string(), AppRequirementToml { enabled: Some(false), tools: None, }, )]), }), ..Default::default() }; config.config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("requirements stack"); let mut slack = app("connector_slack"); slack.is_enabled = false; let mut drive = app("connector_drive"); drive.is_enabled = false; assert_eq!( with_app_enabled_state(vec![slack.clone(), app("connector_drive")], &config), vec![slack, drive] ); } #[test] fn app_tool_policy_honors_default_app_enabled_false() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, open_world_enabled: true, }), apps: HashMap::new(), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, Some(&annotations( /*destructive_hint*/ None, /*open_world_hint*/ None, )), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_uses_managed_approval_without_apps_config() { let policy = app_tool_policy_from_apps_config( /*apps_config*/ None, Some("calendar"), "events/list", /*tool_title*/ None, /*annotations*/ None, Some(AppToolApproval::Approve), ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } fn app_tool_requirements( app_id: &str, tool_name: &str, approval_mode: AppToolApproval, ) -> AppsRequirementsToml { AppsRequirementsToml { apps: BTreeMap::from([( app_id.to_string(), AppRequirementToml { enabled: None, tools: Some(AppToolsRequirementsToml { tools: BTreeMap::from([( tool_name.to_string(), AppToolRequirementToml { approval_mode: Some(approval_mode), }, )]), }), }, )]), } } #[test] fn managed_app_tool_approval_uses_raw_tool_name() { let requirements_apps = app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, ); assert_eq!( managed_app_tool_approval( Some(&requirements_apps), Some("connector_123123"), "calendar/list_events", ), Some(AppToolApproval::Approve) ); assert_eq!( managed_app_tool_approval( Some(&requirements_apps), Some("connector_123123"), "calendar/create_event", ), None ); } #[tokio::test] async fn cloud_requirements_tool_approval_overrides_user_apps_config() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), r#" [apps.connector_123123.tools."calendar/list_events"] approval_mode = "prompt" "#, ) .expect("write config"); let requirements = ConfigRequirementsToml { apps: Some(app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, )), ..Default::default() }; let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .cloud_requirements(CloudRequirementsLoader::new(async move { Ok(Some(requirements)) })) .build() .await .expect("config should build"); let policy = app_tool_policy( &config, Some("connector_123123"), "calendar/list_events", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } #[tokio::test] async fn local_requirements_tool_approval_overrides_user_apps_config() { let codex_home = tempdir().expect("tempdir should succeed"); let config_toml_path = AbsolutePathBuf::try_from(codex_home.path().join(CONFIG_TOML_FILE)).expect("abs path"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await .expect("config should build"); let requirements = ConfigRequirementsToml { apps: Some(app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, )), ..Default::default() }; config.config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("requirements stack") .with_user_config( &config_toml_path, toml::from_str::( r#" [apps.connector_123123.tools."calendar/list_events"] approval_mode = "prompt" "#, ) .expect("apps config"), ); let policy = app_tool_policy( &config, Some("connector_123123"), "calendar/list_events", /*tool_title*/ None, /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } #[tokio::test] async fn local_requirements_tool_approval_does_not_match_tool_title() { let codex_home = tempdir().expect("tempdir should succeed"); let mut config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .fallback_cwd(Some(codex_home.path().to_path_buf())) .build() .await .expect("config should build"); let requirements = ConfigRequirementsToml { apps: Some(app_tool_requirements( "connector_123123", "calendar/list_events", AppToolApproval::Approve, )), ..Default::default() }; config.config_layer_stack = ConfigLayerStack::new(Vec::new(), ConfigRequirements::default(), requirements) .expect("requirements stack"); let policy = app_tool_policy( &config, Some("connector_123123"), "calendar/create_event", Some("calendar/list_events"), /*annotations*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_allows_per_app_enable_when_default_is_disabled() { let apps_config = AppsConfigToml { default: Some(AppsDefaultConfig { enabled: false, destructive_enabled: true, open_world_enabled: true, }), apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: None, open_world_enabled: None, default_tools_approval_mode: None, default_tools_enabled: None, tools: None, }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, Some(&annotations( /*destructive_hint*/ None, /*open_world_hint*/ None, )), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_per_tool_enabled_true_overrides_app_level_disable_flags() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), default_tools_approval_mode: None, default_tools_enabled: None, tools: Some(AppToolsConfig { tools: HashMap::from([( "events/create".to_string(), AppToolConfig { enabled: Some(true), approval_mode: None, }, )]), }), }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(&annotations(Some(true), Some(true))), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_default_tools_enabled_true_overrides_app_level_tool_hints() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), default_tools_approval_mode: None, default_tools_enabled: Some(true), tools: None, }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/create", /*tool_title*/ None, Some(&annotations(Some(true), Some(true))), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Auto, } ); } #[test] fn app_tool_policy_default_tools_enabled_false_overrides_app_level_tool_hints() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(true), open_world_enabled: Some(true), default_tools_approval_mode: Some(AppToolApproval::Approve), default_tools_enabled: Some(false), tools: None, }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, Some(&annotations( /*destructive_hint*/ None, /*open_world_hint*/ None, )), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: false, approval: AppToolApproval::Approve, } ); } #[test] fn app_tool_policy_uses_default_tools_approval_mode() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: None, open_world_enabled: None, default_tools_approval_mode: Some(AppToolApproval::Prompt), default_tools_enabled: None, tools: Some(AppToolsConfig { tools: HashMap::new(), }), }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "events/list", /*tool_title*/ None, Some(&annotations( /*destructive_hint*/ None, /*open_world_hint*/ None, )), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Prompt, } ); } #[test] fn app_tool_policy_matches_prefix_stripped_tool_name_for_tool_config() { let apps_config = AppsConfigToml { default: None, apps: HashMap::from([( "calendar".to_string(), AppConfig { enabled: true, destructive_enabled: Some(false), open_world_enabled: Some(false), default_tools_approval_mode: Some(AppToolApproval::Auto), default_tools_enabled: Some(false), tools: Some(AppToolsConfig { tools: HashMap::from([( "events/create".to_string(), AppToolConfig { enabled: Some(true), approval_mode: Some(AppToolApproval::Approve), }, )]), }), }, )]), }; let policy = app_tool_policy_from_apps_config( Some(&apps_config), Some("calendar"), "calendar_events/create", Some("events/create"), Some(&annotations(Some(true), Some(true))), /*managed_approval*/ None, ); assert_eq!( policy, AppToolPolicy { enabled: true, approval: AppToolApproval::Approve, } ); } #[tokio::test] async fn tool_suggest_connector_ids_include_configured_tool_suggest_discoverables() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), r#" [tool_suggest] discoverables = [ { type = "connector", id = "connector_2128aebfecb84f64a069897515042a44" }, { type = "plugin", id = "slack@openai-curated" }, { type = "connector", id = " " } ] "#, ) .expect("write config"); let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("config should load"); assert_eq!( tool_suggest_connector_ids(&config).await, HashSet::from(["connector_2128aebfecb84f64a069897515042a44".to_string()]) ); } #[tokio::test] async fn tool_suggest_connector_ids_exclude_disabled_tool_suggestions() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), r#" [tool_suggest] discoverables = [ { type = "connector", id = "connector_calendar" }, { type = "connector", id = "connector_gmail" } ] disabled_tools = [ { type = "connector", id = "connector_calendar" } ] "#, ) .expect("write config"); let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("config should load"); assert_eq!( tool_suggest_connector_ids(&config).await, HashSet::from(["connector_gmail".to_string()]) ); } #[tokio::test] async fn tool_suggest_uses_connector_id_fallback_when_directory_cache_is_empty() { let codex_home = tempdir().expect("tempdir should succeed"); std::fs::write( codex_home.path().join(CONFIG_TOML_FILE), r#" [features] apps = true [tool_suggest] discoverables = [ { type = "connector", id = "connector_gmail" } ] "#, ) .expect("write config"); let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .build() .await .expect("config should load"); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth(&config, Some(&auth), &[]) .await .expect("discoverable tools should load"); assert_eq!( discoverable_tools, vec![DiscoverableTool::from(plugin_connector_to_app_info( "connector_gmail".to_string(), ))] ); }