codex_external_agent_migration/
plugins.rs1use 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 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}