Skip to main content

codex_external_agent_migration/
plugins.rs

1use codex_analytics::PluginInstallSource;
2use codex_core::config::ConfigBuilder;
3use codex_core_plugins::PluginInstallError;
4use codex_core_plugins::PluginInstallRequest;
5use codex_core_plugins::PluginsManager;
6use codex_core_plugins::marketplace::MarketplaceError;
7use codex_core_plugins::marketplace::find_marketplace_manifest_path;
8use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
9use codex_core_plugins::marketplace_add::add_marketplace;
10use codex_core_plugins::marketplace_add::is_local_marketplace_source;
11use std::collections::BTreeMap;
12use std::io;
13use std::path::Path;
14
15use crate::migration_source::MarketplaceImportSource;
16use crate::model::MigrationDetails;
17use crate::model::PluginImportOutcome;
18use crate::reporting::plugin_import_raw_error;
19use crate::reporting::record_plugin_import_errors;
20use crate::scope::MigrationScope;
21use crate::service::ExternalAgentConfigService;
22use crate::utils::invalid_data_error;
23
24impl ExternalAgentConfigService {
25    fn marketplace_import_sources(
26        &self,
27        cwd: Option<&Path>,
28    ) -> io::Result<BTreeMap<String, MarketplaceImportSource>> {
29        let Some(scope) = MigrationScope::from_cwd(cwd)? else {
30            return Ok(BTreeMap::new());
31        };
32        let source_root = scope
33            .repo_root()
34            .unwrap_or(self.external_agent_home.as_path());
35        let source_settings = self.source_settings(&scope);
36        self.source.marketplace_import_sources(
37            self.external_agent_home.as_path(),
38            source_root,
39            &source_settings,
40        )
41    }
42
43    pub(super) fn partition_plugin_migration_details(
44        &self,
45        cwd: Option<&Path>,
46        details: MigrationDetails,
47    ) -> io::Result<(Option<MigrationDetails>, Option<MigrationDetails>)> {
48        let import_sources = self.marketplace_import_sources(cwd)?;
49
50        let mut local_plugins = Vec::new();
51        let mut remote_plugins = Vec::new();
52        for plugin_group in details.plugins {
53            let is_local = import_sources
54                .get(&plugin_group.marketplace_name)
55                .and_then(|import_source| {
56                    is_local_marketplace_source(
57                        &import_source.source,
58                        import_source.ref_name.clone(),
59                    )
60                    .ok()
61                })
62                .unwrap_or(false);
63
64            if is_local {
65                local_plugins.push(plugin_group);
66            } else {
67                remote_plugins.push(plugin_group);
68            }
69        }
70
71        let local_details = (!local_plugins.is_empty()).then_some(MigrationDetails {
72            plugins: local_plugins,
73            ..Default::default()
74        });
75        let remote_details = (!remote_plugins.is_empty()).then_some(MigrationDetails {
76            plugins: remote_plugins,
77            ..Default::default()
78        });
79
80        Ok((local_details, remote_details))
81    }
82
83    pub async fn import_plugins(
84        &self,
85        cwd: Option<&Path>,
86        details: Option<MigrationDetails>,
87    ) -> io::Result<PluginImportOutcome> {
88        let Some(MigrationDetails { plugins, .. }) = details else {
89            return Err(invalid_data_error(
90                "plugins migration item is missing details".to_string(),
91            ));
92        };
93        let config = ConfigBuilder::default()
94            .codex_home(self.codex_home.clone())
95            .fallback_cwd(Some(
96                cwd.map(Path::to_path_buf)
97                    .unwrap_or_else(|| self.codex_home.clone()),
98            ))
99            .build()
100            .await
101            .map_err(|err| io::Error::other(format!("failed to load config: {err}")))?;
102        let requirements = config.config_layer_stack.requirements().clone();
103        let mut outcome = PluginImportOutcome::default();
104        let plugins_manager = PluginsManager::new(self.codex_home.clone())
105            .with_plugin_install_source(PluginInstallSource::ExternalAgentMigration);
106        if let Some(analytics_events_client) = self.analytics_events_client.clone() {
107            plugins_manager.set_analytics_events_client(analytics_events_client);
108        }
109        let configured_marketplace_paths = plugins_manager
110            .list_marketplaces_for_config(
111                &config.plugins_config_input(),
112                &[],
113                /*include_openai_curated*/ true,
114            )
115            .map_err(|err| {
116                invalid_data_error(format!("failed to list configured marketplaces: {err}"))
117            })?
118            .marketplaces
119            .into_iter()
120            .map(|marketplace| (marketplace.name, marketplace.path))
121            .collect::<BTreeMap<_, _>>();
122        let import_sources = self.marketplace_import_sources(cwd)?;
123        for plugin_group in plugins {
124            let marketplace_name = plugin_group.marketplace_name.clone();
125            let plugin_names = plugin_group.plugin_names;
126            let plugin_ids = plugin_names
127                .iter()
128                .map(|plugin_name| format!("{plugin_name}@{marketplace_name}"))
129                .collect::<Vec<_>>();
130            let marketplace_path = if let Some(marketplace_path) =
131                configured_marketplace_paths.get(&marketplace_name)
132            {
133                outcome
134                    .succeeded_marketplaces
135                    .push(marketplace_name.clone());
136                marketplace_path.clone()
137            } else {
138                let Some(import_source) = import_sources.get(&marketplace_name).cloned() else {
139                    let message = format!(
140                        "external agent plugin marketplace source was not found: {marketplace_name}"
141                    );
142                    record_plugin_import_errors(
143                        &mut outcome,
144                        cwd,
145                        &plugin_ids,
146                        "plugin_import",
147                        message,
148                    );
149                    outcome.failed_marketplaces.push(marketplace_name);
150                    outcome.failed_plugin_ids.extend(plugin_ids);
151                    continue;
152                };
153                let request = MarketplaceAddRequest {
154                    source: import_source.source,
155                    ref_name: import_source.ref_name,
156                    sparse_paths: Vec::new(),
157                };
158                match add_marketplace(self.codex_home.clone(), requirements.clone(), request).await
159                {
160                    Ok(add_marketplace_outcome) => {
161                        let Some(marketplace_path) = find_marketplace_manifest_path(
162                            add_marketplace_outcome.installed_root.as_path(),
163                        ) else {
164                            let message = format!(
165                                "plugin marketplace manifest was not found after install: {marketplace_name}"
166                            );
167                            record_plugin_import_errors(
168                                &mut outcome,
169                                cwd,
170                                &plugin_ids,
171                                "plugin_import",
172                                message,
173                            );
174                            outcome.failed_marketplaces.push(marketplace_name);
175                            outcome.failed_plugin_ids.extend(plugin_ids);
176                            continue;
177                        };
178                        outcome
179                            .succeeded_marketplaces
180                            .push(marketplace_name.clone());
181                        marketplace_path
182                    }
183                    Err(err) => {
184                        record_plugin_import_errors(
185                            &mut outcome,
186                            cwd,
187                            &plugin_ids,
188                            "plugin_import",
189                            err.to_string(),
190                        );
191                        outcome.failed_marketplaces.push(marketplace_name);
192                        outcome.failed_plugin_ids.extend(plugin_ids);
193                        continue;
194                    }
195                }
196            };
197            let install_config = match ConfigBuilder::default()
198                .codex_home(self.codex_home.clone())
199                .fallback_cwd(Some(
200                    cwd.map(Path::to_path_buf)
201                        .unwrap_or_else(|| self.codex_home.clone()),
202                ))
203                .build()
204                .await
205            {
206                Ok(config) => config,
207                Err(err) => {
208                    record_plugin_import_errors(
209                        &mut outcome,
210                        cwd,
211                        &plugin_ids,
212                        "plugin_import",
213                        format!("failed to reload config after adding marketplace: {err}"),
214                    );
215                    outcome.failed_plugin_ids.extend(plugin_ids);
216                    continue;
217                }
218            };
219            for plugin_name in plugin_names {
220                match plugins_manager
221                    .install_plugin(
222                        &install_config.config_layer_stack,
223                        PluginInstallRequest {
224                            plugin_name: plugin_name.clone(),
225                            marketplace_path: marketplace_path.clone(),
226                        },
227                    )
228                    .await
229                {
230                    Ok(_) => outcome
231                        .succeeded_plugin_ids
232                        .push(format!("{plugin_name}@{marketplace_name}")),
233                    Err(err) => {
234                        let plugin_id = format!("{plugin_name}@{marketplace_name}");
235                        outcome.failed_plugin_ids.push(plugin_id.clone());
236                        let sub_error_type = err.sub_error_type();
237                        let mut raw_error = plugin_import_raw_error(
238                            cwd,
239                            "plugin_import",
240                            err.to_string(),
241                            Some(plugin_id),
242                        );
243                        raw_error.sub_error_type = sub_error_type;
244                        if matches!(
245                            err,
246                            PluginInstallError::Marketplace(
247                                MarketplaceError::PluginNotFound { .. }
248                            )
249                        ) {
250                            raw_error.error_type = Some("plugin_not_found".to_string());
251                        }
252                        outcome.raw_errors.push(raw_error);
253                    }
254                }
255            }
256        }
257
258        Ok(outcome)
259    }
260}