use super::*; use codex_config::Constrained; use codex_config::types::AppToolApproval; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; use codex_protocol::models::ManagedFileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; use std::path::PathBuf; fn test_mcp_config(codex_home: PathBuf) -> McpConfig { McpConfig { chatgpt_base_url: "https://chatgpt.com".to_string(), apps_mcp_path_override: None, apps_mcp_product_sku: None, codex_home, mcp_oauth_credentials_store_mode: OAuthCredentialsStoreMode::default(), mcp_oauth_callback_port: None, mcp_oauth_callback_url: None, skill_mcp_dependency_install_enabled: true, approval_policy: Constrained::allow_any(AskForApproval::OnFailure), codex_linux_sandbox_exe: None, use_legacy_landlock: false, apps_enabled: false, legacy_apps_mcp_loader_enabled: true, prefix_mcp_tool_names: true, client_elicitation_capability: ElicitationCapability::default(), configured_mcp_servers: HashMap::new(), plugin_ids_by_mcp_server_name: HashMap::new(), plugin_capability_summaries: Vec::new(), } } #[test] fn qualified_mcp_tool_name_prefix_sanitizes_server_names_without_lowercasing() { assert_eq!( qualified_mcp_tool_name_prefix("Some-Server"), "mcp__Some_Server__".to_string() ); } #[test] fn mcp_prompt_auto_approval_honors_unrestricted_managed_profiles() { assert!(mcp_permission_prompt_is_auto_approved( AskForApproval::Never, &PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Unrestricted, network: NetworkSandboxPolicy::Enabled, }, McpPermissionPromptAutoApproveContext::default(), )); assert!(mcp_permission_prompt_is_auto_approved( AskForApproval::Never, &PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Unrestricted, network: NetworkSandboxPolicy::Restricted, }, McpPermissionPromptAutoApproveContext::default(), )); assert!(!mcp_permission_prompt_is_auto_approved( AskForApproval::Never, &PermissionProfile::read_only(), McpPermissionPromptAutoApproveContext::default(), )); assert!(!mcp_permission_prompt_is_auto_approved( AskForApproval::OnRequest, &PermissionProfile::Managed { file_system: ManagedFileSystemPermissions::Unrestricted, network: NetworkSandboxPolicy::Enabled, }, McpPermissionPromptAutoApproveContext::default(), )); } #[test] fn mcp_prompt_auto_approval_honors_approved_tools_in_all_permission_modes() { for approval_policy in [ AskForApproval::UnlessTrusted, AskForApproval::OnFailure, AskForApproval::OnRequest, AskForApproval::Granular(GranularApprovalConfig { sandbox_approval: true, rules: true, skill_approval: true, request_permissions: true, mcp_elicitations: true, }), AskForApproval::Never, ] { assert!(mcp_permission_prompt_is_auto_approved( approval_policy, &PermissionProfile::read_only(), McpPermissionPromptAutoApproveContext { tool_approval_mode: Some(AppToolApproval::Approve), }, )); } assert!(!mcp_permission_prompt_is_auto_approved( AskForApproval::OnRequest, &PermissionProfile::read_only(), McpPermissionPromptAutoApproveContext { tool_approval_mode: Some(AppToolApproval::Auto), }, )); } #[test] fn mcp_prompt_auto_approval_rejects_auto_mode_in_default_permission_mode() { assert!(!mcp_permission_prompt_is_auto_approved( AskForApproval::OnRequest, &PermissionProfile::read_only(), McpPermissionPromptAutoApproveContext { tool_approval_mode: Some(AppToolApproval::Auto), }, )); } #[test] fn tool_plugin_provenance_collects_app_and_mcp_sources() { let mut config = test_mcp_config(PathBuf::new()); config.plugin_ids_by_mcp_server_name = HashMap::from([("alpha".to_string(), "alpha@test".to_string())]); config.plugin_capability_summaries = vec![ PluginCapabilitySummary { display_name: "alpha-plugin".to_string(), app_connector_ids: vec![AppConnectorId("connector_example".to_string())], mcp_server_names: vec!["alpha".to_string()], ..PluginCapabilitySummary::default() }, PluginCapabilitySummary { display_name: "beta-plugin".to_string(), app_connector_ids: vec![ AppConnectorId("connector_example".to_string()), AppConnectorId("connector_gmail".to_string()), ], mcp_server_names: vec!["beta".to_string()], ..PluginCapabilitySummary::default() }, ]; let provenance = tool_plugin_provenance(&config); assert_eq!( provenance, ToolPluginProvenance { plugin_display_names_by_connector_id: HashMap::from([ ( "connector_example".to_string(), vec!["alpha-plugin".to_string(), "beta-plugin".to_string()], ), ( "connector_gmail".to_string(), vec!["beta-plugin".to_string()], ), ]), plugin_display_names_by_mcp_server_name: HashMap::from([ ("alpha".to_string(), vec!["alpha-plugin".to_string()]), ("beta".to_string(), vec!["beta-plugin".to_string()]), ]), plugin_ids_by_mcp_server_name: HashMap::from([( "alpha".to_string(), "alpha@test".to_string(), )]), } ); assert_eq!( provenance.plugin_id_for_mcp_server_name("alpha"), Some("alpha@test") ); assert_eq!(provenance.plugin_id_for_mcp_server_name("beta"), None); } #[test] fn codex_apps_mcp_url_for_base_url_keeps_existing_paths() { assert_eq!( codex_apps_mcp_url_for_base_url( "https://chatgpt.com/backend-api", /*apps_mcp_path_override*/ None, ), "https://chatgpt.com/backend-api/wham/apps" ); assert_eq!( codex_apps_mcp_url_for_base_url( "https://chat.openai.com", /*apps_mcp_path_override*/ None, ), "https://chat.openai.com/backend-api/wham/apps" ); assert_eq!( codex_apps_mcp_url_for_base_url( "http://localhost:8080/api/codex", /*apps_mcp_path_override*/ None, ), "http://localhost:8080/api/codex/apps" ); assert_eq!( codex_apps_mcp_url_for_base_url( "http://localhost:8080", /*apps_mcp_path_override*/ None, ), "http://localhost:8080/api/codex/apps" ); } #[test] fn codex_apps_server_config_uses_legacy_codex_apps_path() { let mut config = test_mcp_config(PathBuf::from("/tmp")); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let mut servers = with_codex_apps_mcp(HashMap::new(), /*auth*/ None, &config); assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME)); config.apps_enabled = true; servers = with_codex_apps_mcp(servers, Some(&auth), &config); let server = servers .get(CODEX_APPS_MCP_SERVER_NAME) .expect("codex apps should be present when apps is enabled"); let config = server .configured_config() .expect("codex apps should use configured transport"); let url = match &config.transport { McpServerTransportConfig::StreamableHttp { url, .. } => url, _ => panic!("expected streamable http transport for codex apps"), }; assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps"); } #[test] fn codex_apps_server_config_uses_configured_apps_mcp_path_override() { let mut config = test_mcp_config(PathBuf::from("/tmp")); config.apps_mcp_path_override = Some("/custom/mcp".to_string()); config.apps_enabled = true; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let servers = with_codex_apps_mcp(HashMap::new(), Some(&auth), &config); let server = servers .get(CODEX_APPS_MCP_SERVER_NAME) .expect("codex apps should be present when apps is enabled"); let config = server .configured_config() .expect("codex apps should use configured transport"); let url = match &config.transport { McpServerTransportConfig::StreamableHttp { url, .. } => url, _ => panic!("expected streamable http transport for codex apps"), }; assert_eq!(url, "https://chatgpt.com/backend-api/custom/mcp"); } #[test] fn codex_apps_server_config_forwards_configured_product_sku_header() { let mut config = test_mcp_config(PathBuf::from("/tmp")); config.apps_mcp_product_sku = Some("tpp".to_string()); config.apps_enabled = true; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let servers = with_codex_apps_mcp(HashMap::new(), Some(&auth), &config); let server = servers .get(CODEX_APPS_MCP_SERVER_NAME) .expect("codex apps should be present when apps is enabled"); let config = server .configured_config() .expect("codex apps should use configured transport"); match &config.transport { McpServerTransportConfig::StreamableHttp { http_headers, env_http_headers, .. } => { assert_eq!( http_headers, &Some(HashMap::from([( "X-OpenAI-Product-Sku".to_string(), "tpp".to_string(), )])) ); assert!(env_http_headers.is_none()); } other => panic!("expected streamable http transport, got {other:?}"), } } #[tokio::test] async fn effective_mcp_servers_preserve_user_servers_and_add_codex_apps() { let codex_home = tempfile::tempdir().expect("tempdir"); let mut config = test_mcp_config(codex_home.path().to_path_buf()); config.apps_enabled = true; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); config.configured_mcp_servers.insert( "sample".to_string(), McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url: "https://user.example/mcp".to_string(), bearer_token_env_var: None, http_headers: None, env_http_headers: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); config.configured_mcp_servers.insert( "docs".to_string(), McpServerConfig { transport: McpServerTransportConfig::StreamableHttp { url: "https://docs.example/mcp".to_string(), bearer_token_env_var: None, http_headers: None, env_http_headers: None, }, environment_id: codex_config::DEFAULT_MCP_SERVER_ENVIRONMENT_ID.to_string(), enabled: true, required: false, supports_parallel_tool_calls: false, disabled_reason: None, startup_timeout_sec: None, tool_timeout_sec: None, default_tools_approval_mode: None, enabled_tools: None, disabled_tools: None, scopes: None, oauth: None, oauth_resource: None, tools: HashMap::new(), }, ); let effective = effective_mcp_servers(&config, Some(&auth)); let sample = effective.get("sample").expect("user server should exist"); let docs = effective .get("docs") .expect("configured server should exist"); let codex_apps = effective .get(CODEX_APPS_MCP_SERVER_NAME) .expect("codex apps server should exist"); let sample = sample .configured_config() .expect("configured server should retain transport"); let docs = docs .configured_config() .expect("configured server should retain transport"); let codex_apps = codex_apps .configured_config() .expect("codex apps should use configured transport"); match &sample.transport { McpServerTransportConfig::StreamableHttp { url, .. } => { assert_eq!(url, "https://user.example/mcp"); } other => panic!("expected streamable http transport, got {other:?}"), } match &docs.transport { McpServerTransportConfig::StreamableHttp { url, .. } => { assert_eq!(url, "https://docs.example/mcp"); } other => panic!("expected streamable http transport, got {other:?}"), } match &codex_apps.transport { McpServerTransportConfig::StreamableHttp { url, .. } => { assert_eq!(url, "https://chatgpt.com/backend-api/wham/apps"); } other => panic!("expected streamable http transport, got {other:?}"), } }