diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e1a97216bd..48185eb742 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1941,6 +1941,7 @@ dependencies = [ "codex-arg0", "codex-config", "codex-core", + "codex-core-plugins", "codex-exec-server", "codex-feedback", "codex-protocol", @@ -2100,6 +2101,7 @@ dependencies = [ "codex-app-server-protocol", "codex-connectors", "codex-core", + "codex-core-plugins", "codex-features", "codex-git-utils", "codex-login", @@ -2489,6 +2491,7 @@ dependencies = [ "codex-arg0", "codex-config", "codex-core", + "codex-core-plugins", "codex-exec-server", "codex-features", "codex-login", diff --git a/codex-rs/app-server-client/Cargo.toml b/codex-rs/app-server-client/Cargo.toml index d9c1ade097..7da86b9188 100644 --- a/codex-rs/app-server-client/Cargo.toml +++ b/codex-rs/app-server-client/Cargo.toml @@ -17,6 +17,7 @@ codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } codex-config = { workspace = true } codex-core = { workspace = true } +codex-core-plugins = { workspace = true } codex-exec-server = { workspace = true } codex-feedback = { workspace = true } codex-protocol = { workspace = true } diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 8165fb3673..86cd1e515e 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -100,7 +100,7 @@ pub mod legacy_core { } pub mod plugins { - pub use codex_core::plugins::PluginsManager; + pub use codex_core_plugins::PluginsManager; } pub mod review_format { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d78d30afab..afbd9564ed 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -271,10 +271,6 @@ use codex_core::find_thread_name_by_id; use codex_core::find_thread_names_by_ids; use codex_core::find_thread_path_by_id_str; use codex_core::path_utils; -use codex_core::plugins::PluginInstallError as CorePluginInstallError; -use codex_core::plugins::PluginInstallRequest; -use codex_core::plugins::PluginReadRequest; -use codex_core::plugins::PluginUninstallError as CorePluginUninstallError; use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::sandboxing::SandboxPermissions; @@ -282,6 +278,10 @@ use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_core::windows_sandbox::WindowsSandboxSetupMode as CoreWindowsSandboxSetupMode; use codex_core::windows_sandbox::WindowsSandboxSetupRequest; use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; +use codex_core_plugins::PluginInstallError as CorePluginInstallError; +use codex_core_plugins::PluginInstallRequest; +use codex_core_plugins::PluginReadRequest; +use codex_core_plugins::PluginUninstallError as CorePluginUninstallError; use codex_core_plugins::loader::load_plugin_apps; use codex_core_plugins::loader::load_plugin_mcp_servers; use codex_core_plugins::manifest::PluginManifestInterface; @@ -6306,9 +6306,7 @@ impl CodexMessageProcessor { plugins_manager .effective_skill_roots_for_layer_stack( &config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), + config.plugin_feature_flags(), ) .await } else { @@ -6391,13 +6389,10 @@ impl CodexMessageProcessor { config.features.enabled(Feature::Plugins) && workspace_codex_plugins_enabled; let plugin_outcome = if plugins_enabled && config.features.enabled(Feature::PluginHooks) { + let mut plugin_feature_flags = config.plugin_feature_flags(); + plugin_feature_flags.plugins_enabled = plugins_enabled; plugins_manager - .plugins_for_layer_stack( - &config.config_layer_stack, - plugins_enabled, - config.features.enabled(Feature::RemotePlugin), - /*plugin_hooks_feature_enabled*/ true, - ) + .plugins_for_layer_stack(&config.config_layer_stack, plugin_feature_flags) .await } else { codex_core::plugins::PluginLoadOutcome::default() diff --git a/codex-rs/app-server/src/config/external_agent_config.rs b/codex-rs/app-server/src/config/external_agent_config.rs index 96ba5b1520..ea34fdc1ce 100644 --- a/codex-rs/app-server/src/config/external_agent_config.rs +++ b/codex-rs/app-server/src/config/external_agent_config.rs @@ -2,8 +2,8 @@ use codex_config::types::PluginConfig; use codex_core::config::Config; use codex_core::config::ConfigBuilder; use codex_core::plugins::PluginId; -use codex_core::plugins::PluginInstallRequest; -use codex_core::plugins::PluginsManager; +use codex_core_plugins::PluginInstallRequest; +use codex_core_plugins::PluginsManager; use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy; use codex_core_plugins::marketplace::find_marketplace_manifest_path; use codex_core_plugins::marketplace_add::MarketplaceAddRequest; diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index 74f9d0eae2..1a327610b9 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -13,6 +13,7 @@ clap = { workspace = true, features = ["derive"] } codex-app-server-protocol = { workspace = true } codex-connectors = { workspace = true } codex-core = { workspace = true } +codex-core-plugins = { workspace = true } codex-features = { workspace = true } codex-git-utils = { workspace = true } codex-login = { workspace = true } diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index d345b22722..8f390b73be 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -17,8 +17,7 @@ pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_o pub use codex_core::connectors::list_cached_accessible_connectors_from_mcp_tools; pub use codex_core::connectors::with_app_enabled_state; use codex_core::plugins::AppConnectorId; -use codex_core::plugins::PluginsManager; -use codex_features::Feature; +use codex_core_plugins::PluginsManager; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::default_client::originator; @@ -138,12 +137,7 @@ fn all_connectors_cache_key(config: &Config, auth: &CodexAuth) -> AllConnectorsC async fn plugin_apps_for_config(config: &Config) -> Vec { PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await .effective_apps() } diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index 9724d8a57f..ce6d109f43 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -4,12 +4,12 @@ use anyhow::bail; use clap::Parser; use codex_core::config::Config; use codex_core::config::find_codex_home; -use codex_core::plugins::PluginMarketplaceUpgradeOutcome; -use codex_core::plugins::PluginsManager; +use codex_core_plugins::PluginsManager; use codex_core_plugins::marketplace_add::MarketplaceAddRequest; use codex_core_plugins::marketplace_add::add_marketplace; use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest; use codex_core_plugins::marketplace_remove::remove_marketplace; +use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome; use codex_utils_cli::CliConfigOverrides; #[derive(Debug, Parser)] diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index c5b4751322..858ef442ae 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -14,7 +14,7 @@ use codex_core::config::Config; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config::find_codex_home; use codex_core::config::load_global_mcp_servers; -use codex_core::plugins::PluginsManager; +use codex_core_plugins::PluginsManager; use codex_mcp::McpOAuthLoginSupport; use codex_mcp::ResolvedMcpOAuthScopes; use codex_mcp::compute_auth_statuses; diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index cf3dd68dc8..10608bb75f 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -14,6 +14,7 @@ mod mcp_types; mod merge; mod overrides; pub mod permissions_toml; +mod plugin_edit; pub mod profile_toml; mod project_root_markers; mod requirements_exec_policy; diff --git a/codex-rs/config/src/mcp_edit.rs b/codex-rs/config/src/mcp_edit.rs index d467502b66..13f65895fe 100644 --- a/codex-rs/config/src/mcp_edit.rs +++ b/codex-rs/config/src/mcp_edit.rs @@ -7,7 +7,6 @@ use std::path::PathBuf; use tokio::task; use toml::Value as TomlValue; use toml_edit::DocumentMut; -use toml_edit::InlineTable; use toml_edit::Item as TomlItem; use toml_edit::Table as TomlTable; use toml_edit::value; @@ -20,6 +19,8 @@ use crate::CONFIG_TOML_FILE; use crate::McpServerConfig; use crate::McpServerEnvVar; use crate::McpServerTransportConfig; +use crate::plugin_edit::PluginConfigEdit; +use crate::plugin_edit::apply_plugin_config_edit; pub async fn load_global_mcp_servers( codex_home: &Path, @@ -69,11 +70,6 @@ pub struct ConfigEditsBuilder { plugin_edits: Vec, } -enum PluginConfigEdit { - SetEnabled { plugin_id: String, enabled: bool }, - Clear { plugin_id: String }, -} - impl ConfigEditsBuilder { pub fn new(codex_home: &Path) -> Self { Self { @@ -89,17 +85,13 @@ impl ConfigEditsBuilder { } pub fn set_plugin_enabled(mut self, plugin_id: &str, enabled: bool) -> Self { - self.plugin_edits.push(PluginConfigEdit::SetEnabled { - plugin_id: plugin_id.to_string(), - enabled, - }); + self.plugin_edits + .push(PluginConfigEdit::set_enabled(plugin_id, enabled)); self } pub fn clear_plugin(mut self, plugin_id: &str) -> Self { - self.plugin_edits.push(PluginConfigEdit::Clear { - plugin_id: plugin_id.to_string(), - }); + self.plugin_edits.push(PluginConfigEdit::clear(plugin_id)); self } @@ -158,97 +150,6 @@ fn replace_mcp_servers(doc: &mut DocumentMut, servers: &BTreeMap bool { - match edit { - PluginConfigEdit::SetEnabled { plugin_id, enabled } => { - set_plugin_enabled(doc, plugin_id, *enabled) - } - PluginConfigEdit::Clear { plugin_id } => clear_plugin(doc, plugin_id), - } -} - -fn set_plugin_enabled(doc: &mut DocumentMut, plugin_id: &str, enabled: bool) -> bool { - let root = doc.as_table_mut(); - let plugins = ensure_table(root, "plugins", /*implicit*/ true); - let plugin = ensure_table(plugins, plugin_id, /*implicit*/ false); - let mut replacement = value(enabled); - if let Some(existing) = plugin.get("enabled") { - preserve_decor(existing, &mut replacement); - } - plugin["enabled"] = replacement; - true -} - -fn clear_plugin(doc: &mut DocumentMut, plugin_id: &str) -> bool { - let root = doc.as_table_mut(); - let Some(item) = root.get_mut("plugins") else { - return false; - }; - match item { - TomlItem::Table(plugins) => plugins.remove(plugin_id).is_some(), - TomlItem::Value(value) => { - let Some(inline) = value.as_inline_table().cloned() else { - return false; - }; - *item = TomlItem::Table(table_from_inline(&inline, /*implicit*/ true)); - if let TomlItem::Table(plugins) = item { - return plugins.remove(plugin_id).is_some(); - } - false - } - _ => false, - } -} - -fn preserve_decor(existing: &TomlItem, replacement: &mut TomlItem) { - if let (TomlItem::Value(existing_value), TomlItem::Value(replacement_value)) = - (existing, replacement) - { - replacement_value - .decor_mut() - .clone_from(existing_value.decor()); - } -} - -fn ensure_table<'a>(parent: &'a mut TomlTable, key: &str, implicit: bool) -> &'a mut TomlTable { - match parent.get_mut(key) { - Some(TomlItem::Table(_)) => {} - Some(item @ TomlItem::Value(_)) => { - if let Some(inline) = item.as_value().and_then(toml_edit::Value::as_inline_table) { - *item = TomlItem::Table(table_from_inline(inline, implicit)); - } else { - *item = TomlItem::Table(new_table(implicit)); - } - } - Some(item) => { - *item = TomlItem::Table(new_table(implicit)); - } - None => { - parent.insert(key, TomlItem::Table(new_table(implicit))); - } - } - let Some(TomlItem::Table(table)) = parent.get_mut(key) else { - unreachable!("inserted value should be a table"); - }; - table -} - -fn new_table(implicit: bool) -> TomlTable { - let mut table = TomlTable::new(); - table.set_implicit(implicit); - table -} - -fn table_from_inline(inline: &InlineTable, implicit: bool) -> TomlTable { - let mut table = new_table(implicit); - for (key, value) in inline.iter() { - let mut value = value.clone(); - value.decor_mut().set_suffix(""); - table.insert(key, TomlItem::Value(value)); - } - table -} - fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { let mut entry = TomlTable::new(); entry.set_implicit(false); diff --git a/codex-rs/config/src/mcp_edit_tests.rs b/codex-rs/config/src/mcp_edit_tests.rs index c37a38be77..cfcd73c3e5 100644 --- a/codex-rs/config/src/mcp_edit_tests.rs +++ b/codex-rs/config/src/mcp_edit_tests.rs @@ -2,8 +2,6 @@ use super::*; use crate::McpServerToolConfig; use pretty_assertions::assert_eq; use std::collections::HashMap; -#[cfg(unix)] -use std::os::unix::fs::symlink; use std::time::SystemTime; use std::time::UNIX_EPOCH; @@ -84,166 +82,3 @@ approval_mode = "approve" Ok(()) } - -#[cfg(unix)] -#[tokio::test] -async fn plugin_edits_write_through_symlink_chain() -> anyhow::Result<()> { - let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let codex_home = std::env::temp_dir().join(format!( - "codex-config-plugin-edit-symlink-test-{}-{unique_suffix}", - std::process::id() - )); - let target_home = std::env::temp_dir().join(format!( - "codex-config-plugin-edit-symlink-target-{}-{unique_suffix}", - std::process::id() - )); - std::fs::create_dir_all(&codex_home)?; - std::fs::create_dir_all(&target_home)?; - let target_path = target_home.join(CONFIG_TOML_FILE); - let link_path = codex_home.join("config-link.toml"); - let config_path = codex_home.join(CONFIG_TOML_FILE); - std::fs::write( - &target_path, - r#"[plugins."linear@openai-curated"] -enabled = false -"#, - )?; - symlink(&target_path, &link_path)?; - symlink("config-link.toml", &config_path)?; - - ConfigEditsBuilder::new(&codex_home) - .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) - .apply() - .await?; - - let meta = std::fs::symlink_metadata(&config_path)?; - assert!(meta.file_type().is_symlink()); - assert_eq!( - std::fs::read_to_string(target_path)?, - r#"[plugins."linear@openai-curated"] -enabled = true -"# - ); - - std::fs::remove_dir_all(&codex_home)?; - std::fs::remove_dir_all(&target_home)?; - - Ok(()) -} - -#[tokio::test] -async fn plugin_edits_set_and_clear_enabled_entries() -> anyhow::Result<()> { - let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let codex_home = std::env::temp_dir().join(format!( - "codex-config-plugin-edit-test-{}-{unique_suffix}", - std::process::id() - )); - std::fs::create_dir_all(&codex_home)?; - std::fs::write( - codex_home.join(CONFIG_TOML_FILE), - r#"[plugins."linear@openai-curated"] -enabled = false - -[plugins."gmail@openai-curated"] -enabled = true -"#, - )?; - - ConfigEditsBuilder::new(&codex_home) - .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) - .clear_plugin("gmail@openai-curated") - .apply() - .await?; - - let serialized = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE))?; - assert_eq!( - serialized, - r#"[plugins."linear@openai-curated"] -enabled = true -"# - ); - - std::fs::remove_dir_all(&codex_home)?; - - Ok(()) -} - -#[tokio::test] -async fn plugin_clear_leaves_malformed_plugins_scalar_unchanged() -> anyhow::Result<()> { - let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let codex_home = std::env::temp_dir().join(format!( - "codex-config-plugin-clear-malformed-test-{}-{unique_suffix}", - std::process::id() - )); - std::fs::create_dir_all(&codex_home)?; - let config_path = codex_home.join(CONFIG_TOML_FILE); - let config = r#"plugins = "bad" -"#; - std::fs::write(&config_path, config)?; - - ConfigEditsBuilder::new(&codex_home) - .clear_plugin("linear@openai-curated") - .apply() - .await?; - - assert_eq!(std::fs::read_to_string(&config_path)?, config); - - std::fs::remove_dir_all(&codex_home)?; - - Ok(()) -} - -#[tokio::test] -async fn plugin_clear_missing_entry_does_not_create_config() -> anyhow::Result<()> { - let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let codex_home = std::env::temp_dir().join(format!( - "codex-config-plugin-clear-missing-test-{}-{unique_suffix}", - std::process::id() - )); - std::fs::create_dir_all(&codex_home)?; - let config_path = codex_home.join(CONFIG_TOML_FILE); - - ConfigEditsBuilder::new(&codex_home) - .clear_plugin("linear@openai-curated") - .apply() - .await?; - - assert!(!config_path.exists()); - - std::fs::remove_dir_all(&codex_home)?; - - Ok(()) -} - -#[tokio::test] -async fn plugin_enabled_update_preserves_existing_value_decor() -> anyhow::Result<()> { - let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let codex_home = std::env::temp_dir().join(format!( - "codex-config-plugin-edit-decor-test-{}-{unique_suffix}", - std::process::id() - )); - std::fs::create_dir_all(&codex_home)?; - let config_path = codex_home.join(CONFIG_TOML_FILE); - std::fs::write( - &config_path, - r#"[plugins."linear@openai-curated"] -enabled = false # keep -"#, - )?; - - ConfigEditsBuilder::new(&codex_home) - .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) - .apply() - .await?; - - assert_eq!( - std::fs::read_to_string(&config_path)?, - r#"[plugins."linear@openai-curated"] -enabled = true # keep -"# - ); - - std::fs::remove_dir_all(&codex_home)?; - - Ok(()) -} diff --git a/codex-rs/config/src/plugin_edit.rs b/codex-rs/config/src/plugin_edit.rs new file mode 100644 index 0000000000..c4a9720f58 --- /dev/null +++ b/codex-rs/config/src/plugin_edit.rs @@ -0,0 +1,120 @@ +use toml_edit::DocumentMut; +use toml_edit::InlineTable; +use toml_edit::Item as TomlItem; +use toml_edit::Table as TomlTable; +use toml_edit::value; + +pub(crate) enum PluginConfigEdit { + SetEnabled { plugin_id: String, enabled: bool }, + Clear { plugin_id: String }, +} + +impl PluginConfigEdit { + pub(crate) fn set_enabled(plugin_id: &str, enabled: bool) -> Self { + Self::SetEnabled { + plugin_id: plugin_id.to_string(), + enabled, + } + } + + pub(crate) fn clear(plugin_id: &str) -> Self { + Self::Clear { + plugin_id: plugin_id.to_string(), + } + } +} + +pub(crate) fn apply_plugin_config_edit(doc: &mut DocumentMut, edit: &PluginConfigEdit) -> bool { + match edit { + PluginConfigEdit::SetEnabled { plugin_id, enabled } => { + set_plugin_enabled(doc, plugin_id, *enabled) + } + PluginConfigEdit::Clear { plugin_id } => clear_plugin(doc, plugin_id), + } +} + +fn set_plugin_enabled(doc: &mut DocumentMut, plugin_id: &str, enabled: bool) -> bool { + let root = doc.as_table_mut(); + let plugins = ensure_table(root, "plugins", /*implicit*/ true); + let plugin = ensure_table(plugins, plugin_id, /*implicit*/ false); + let mut replacement = value(enabled); + if let Some(existing) = plugin.get("enabled") { + preserve_decor(existing, &mut replacement); + } + plugin["enabled"] = replacement; + true +} + +fn clear_plugin(doc: &mut DocumentMut, plugin_id: &str) -> bool { + let root = doc.as_table_mut(); + let Some(item) = root.get_mut("plugins") else { + return false; + }; + match item { + TomlItem::Table(plugins) => plugins.remove(plugin_id).is_some(), + TomlItem::Value(value) => { + let Some(inline) = value.as_inline_table().cloned() else { + return false; + }; + *item = TomlItem::Table(table_from_inline(&inline, /*implicit*/ true)); + if let TomlItem::Table(plugins) = item { + return plugins.remove(plugin_id).is_some(); + } + false + } + _ => false, + } +} + +fn preserve_decor(existing: &TomlItem, replacement: &mut TomlItem) { + if let (TomlItem::Value(existing_value), TomlItem::Value(replacement_value)) = + (existing, replacement) + { + replacement_value + .decor_mut() + .clone_from(existing_value.decor()); + } +} + +fn ensure_table<'a>(parent: &'a mut TomlTable, key: &str, implicit: bool) -> &'a mut TomlTable { + match parent.get_mut(key) { + Some(TomlItem::Table(_)) => {} + Some(item @ TomlItem::Value(_)) => { + if let Some(inline) = item.as_value().and_then(toml_edit::Value::as_inline_table) { + *item = TomlItem::Table(table_from_inline(inline, implicit)); + } else { + *item = TomlItem::Table(new_table(implicit)); + } + } + Some(item) => { + *item = TomlItem::Table(new_table(implicit)); + } + None => { + parent.insert(key, TomlItem::Table(new_table(implicit))); + } + } + let Some(TomlItem::Table(table)) = parent.get_mut(key) else { + unreachable!("inserted value should be a table"); + }; + table +} + +fn new_table(implicit: bool) -> TomlTable { + let mut table = TomlTable::new(); + table.set_implicit(implicit); + table +} + +fn table_from_inline(inline: &InlineTable, implicit: bool) -> TomlTable { + let mut table = new_table(implicit); + for (key, value) in inline.iter() { + let mut value = value.clone(); + value.decor_mut().set_suffix(""); + table.insert(key, TomlItem::Value(value)); + } + table +} + +#[cfg(test)] +#[path = "plugin_edit_tests.rs"] +mod tests; diff --git a/codex-rs/config/src/plugin_edit_tests.rs b/codex-rs/config/src/plugin_edit_tests.rs new file mode 100644 index 0000000000..a66509d683 --- /dev/null +++ b/codex-rs/config/src/plugin_edit_tests.rs @@ -0,0 +1,170 @@ +use crate::CONFIG_TOML_FILE; +use crate::ConfigEditsBuilder; +use pretty_assertions::assert_eq; +#[cfg(unix)] +use std::os::unix::fs::symlink; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +#[cfg(unix)] +#[tokio::test] +async fn plugin_edits_write_through_symlink_chain() -> anyhow::Result<()> { + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let codex_home = std::env::temp_dir().join(format!( + "codex-config-plugin-edit-symlink-test-{}-{unique_suffix}", + std::process::id() + )); + let target_home = std::env::temp_dir().join(format!( + "codex-config-plugin-edit-symlink-target-{}-{unique_suffix}", + std::process::id() + )); + std::fs::create_dir_all(&codex_home)?; + std::fs::create_dir_all(&target_home)?; + let target_path = target_home.join(CONFIG_TOML_FILE); + let link_path = codex_home.join("config-link.toml"); + let config_path = codex_home.join(CONFIG_TOML_FILE); + std::fs::write( + &target_path, + r#"[plugins."linear@openai-curated"] +enabled = false +"#, + )?; + symlink(&target_path, &link_path)?; + symlink("config-link.toml", &config_path)?; + + ConfigEditsBuilder::new(&codex_home) + .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) + .apply() + .await?; + + let meta = std::fs::symlink_metadata(&config_path)?; + assert!(meta.file_type().is_symlink()); + assert_eq!( + std::fs::read_to_string(target_path)?, + r#"[plugins."linear@openai-curated"] +enabled = true +"# + ); + + std::fs::remove_dir_all(&codex_home)?; + std::fs::remove_dir_all(&target_home)?; + + Ok(()) +} + +#[tokio::test] +async fn plugin_edits_set_and_clear_enabled_entries() -> anyhow::Result<()> { + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let codex_home = std::env::temp_dir().join(format!( + "codex-config-plugin-edit-test-{}-{unique_suffix}", + std::process::id() + )); + std::fs::create_dir_all(&codex_home)?; + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[plugins."linear@openai-curated"] +enabled = false + +[plugins."gmail@openai-curated"] +enabled = true +"#, + )?; + + ConfigEditsBuilder::new(&codex_home) + .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) + .clear_plugin("gmail@openai-curated") + .apply() + .await?; + + let serialized = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE))?; + assert_eq!( + serialized, + r#"[plugins."linear@openai-curated"] +enabled = true +"# + ); + + std::fs::remove_dir_all(&codex_home)?; + + Ok(()) +} + +#[tokio::test] +async fn plugin_clear_leaves_malformed_plugins_scalar_unchanged() -> anyhow::Result<()> { + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let codex_home = std::env::temp_dir().join(format!( + "codex-config-plugin-clear-malformed-test-{}-{unique_suffix}", + std::process::id() + )); + std::fs::create_dir_all(&codex_home)?; + let config_path = codex_home.join(CONFIG_TOML_FILE); + let config = r#"plugins = "bad" +"#; + std::fs::write(&config_path, config)?; + + ConfigEditsBuilder::new(&codex_home) + .clear_plugin("linear@openai-curated") + .apply() + .await?; + + assert_eq!(std::fs::read_to_string(&config_path)?, config); + + std::fs::remove_dir_all(&codex_home)?; + + Ok(()) +} + +#[tokio::test] +async fn plugin_clear_missing_entry_does_not_create_config() -> anyhow::Result<()> { + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let codex_home = std::env::temp_dir().join(format!( + "codex-config-plugin-clear-missing-test-{}-{unique_suffix}", + std::process::id() + )); + std::fs::create_dir_all(&codex_home)?; + let config_path = codex_home.join(CONFIG_TOML_FILE); + + ConfigEditsBuilder::new(&codex_home) + .clear_plugin("linear@openai-curated") + .apply() + .await?; + + assert!(!config_path.exists()); + + std::fs::remove_dir_all(&codex_home)?; + + Ok(()) +} + +#[tokio::test] +async fn plugin_enabled_update_preserves_existing_value_decor() -> anyhow::Result<()> { + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let codex_home = std::env::temp_dir().join(format!( + "codex-config-plugin-edit-decor-test-{}-{unique_suffix}", + std::process::id() + )); + std::fs::create_dir_all(&codex_home)?; + let config_path = codex_home.join(CONFIG_TOML_FILE); + std::fs::write( + &config_path, + r#"[plugins."linear@openai-curated"] +enabled = false # keep +"#, + )?; + + ConfigEditsBuilder::new(&codex_home) + .set_plugin_enabled("linear@openai-curated", /*enabled*/ true) + .apply() + .await?; + + assert_eq!( + std::fs::read_to_string(&config_path)?, + r#"[plugins."linear@openai-curated"] +enabled = true # keep +"# + ); + + std::fs::remove_dir_all(&codex_home)?; + + Ok(()) +} diff --git a/codex-rs/core-api/Cargo.toml b/codex-rs/core-api/Cargo.toml index 5f0e24ea72..20e3c3b158 100644 --- a/codex-rs/core-api/Cargo.toml +++ b/codex-rs/core-api/Cargo.toml @@ -17,6 +17,7 @@ codex-arg0 = { workspace = true } codex-analytics = { workspace = true } codex-config = { workspace = true } codex-core = { workspace = true } +codex-core-plugins = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } codex-login = { workspace = true } diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index 00f1abb247..1908863a7f 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -35,9 +35,9 @@ pub use codex_core::config::Permissions; pub use codex_core::config::TerminalResizeReflowConfig; pub use codex_core::config::ThreadStoreConfig; pub use codex_core::config::find_codex_home; -pub use codex_core::plugins::PluginsManager; pub use codex_core::skills::SkillsManager; pub use codex_core::thread_store_from_config; +pub use codex_core_plugins::PluginsManager; pub use codex_exec_server::EnvironmentManager; pub use codex_exec_server::EnvironmentManagerArgs; pub use codex_exec_server::ExecServerRuntimePaths; diff --git a/codex-rs/core-plugins/src/lib.rs b/codex-rs/core-plugins/src/lib.rs index db68963eae..3db93da54a 100644 --- a/codex-rs/core-plugins/src/lib.rs +++ b/codex-rs/core-plugins/src/lib.rs @@ -45,6 +45,7 @@ pub use manager::ConfiguredMarketplaceListOutcome; pub use manager::ConfiguredMarketplacePlugin; pub use manager::PluginDetail; pub use manager::PluginDetailsUnavailableReason; +pub use manager::PluginFeatureFlags; pub use manager::PluginInstallError; pub use manager::PluginInstallOutcome; pub use manager::PluginInstallRequest; diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index cd71c06fe6..4dbc6f8d66 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -77,6 +77,16 @@ static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false); const FEATURED_PLUGIN_IDS_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 3); +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct PluginFeatureFlags { + /// Whether plugin loading is enabled at all. + pub plugins_enabled: bool, + /// Whether remotely installed plugins should be included in local plugin loading. + pub remote_plugins_enabled: bool, + /// Whether plugin-provided hooks should be loaded. + pub plugin_hooks_enabled: bool, +} + #[derive(Clone, PartialEq, Eq)] struct FeaturedPluginIdsCacheKey { chatgpt_base_url: String, @@ -370,8 +380,7 @@ pub struct PluginsManager { #[derive(Clone)] struct CachedPluginLoadOutcome { config_version: String, - remote_plugins_enabled: bool, - plugin_hooks_enabled: bool, + feature_flags: PluginFeatureFlags, outcome: PluginLoadOutcome, } @@ -431,15 +440,11 @@ impl PluginsManager { pub async fn plugins_for_config( &self, config_layer_stack: &ConfigLayerStack, - plugins_enabled: bool, - remote_plugins_enabled: bool, - plugin_hooks_enabled: bool, + feature_flags: PluginFeatureFlags, ) -> PluginLoadOutcome { self.plugins_for_config_with_force_reload( config_layer_stack, - plugins_enabled, - remote_plugins_enabled, - plugin_hooks_enabled, + feature_flags, /*force_reload*/ false, ) .await @@ -448,32 +453,26 @@ impl PluginsManager { pub(crate) async fn plugins_for_config_with_force_reload( &self, config_layer_stack: &ConfigLayerStack, - plugins_enabled: bool, - remote_plugins_enabled: bool, - plugin_hooks_enabled: bool, + feature_flags: PluginFeatureFlags, force_reload: bool, ) -> PluginLoadOutcome { - if !plugins_enabled { + if !feature_flags.plugins_enabled { return PluginLoadOutcome::default(); } let config_version = version_for_toml(&config_layer_stack.effective_config()); if !force_reload - && let Some(outcome) = self.cached_enabled_outcome( - &config_version, - remote_plugins_enabled, - plugin_hooks_enabled, - ) + && let Some(outcome) = self.cached_enabled_outcome(&config_version, feature_flags) { return outcome; } let outcome = load_plugins_from_layer_stack( config_layer_stack, - self.remote_installed_plugin_configs(remote_plugins_enabled), + self.remote_installed_plugin_configs(feature_flags.remote_plugins_enabled), &self.store, self.restriction_product, - plugin_hooks_enabled, + feature_flags.plugin_hooks_enabled, ) .await; log_plugin_load_errors(&outcome); @@ -483,8 +482,7 @@ impl PluginsManager { }; *cache = Some(CachedPluginLoadOutcome { config_version, - remote_plugins_enabled, - plugin_hooks_enabled, + feature_flags, outcome: outcome.clone(), }); outcome @@ -511,19 +509,17 @@ impl PluginsManager { pub async fn plugins_for_layer_stack( &self, config_layer_stack: &ConfigLayerStack, - plugins_enabled: bool, - remote_plugins_enabled: bool, - plugin_hooks_feature_enabled: bool, + feature_flags: PluginFeatureFlags, ) -> PluginLoadOutcome { - if !plugins_enabled { + if !feature_flags.plugins_enabled { return PluginLoadOutcome::default(); } load_plugins_from_layer_stack( config_layer_stack, - self.remote_installed_plugin_configs(remote_plugins_enabled), + self.remote_installed_plugin_configs(feature_flags.remote_plugins_enabled), &self.store, self.restriction_product, - plugin_hooks_feature_enabled, + feature_flags.plugin_hooks_enabled, ) .await } @@ -532,42 +528,30 @@ impl PluginsManager { pub async fn effective_skill_roots_for_layer_stack( &self, config_layer_stack: &ConfigLayerStack, - plugins_enabled: bool, - remote_plugins_enabled: bool, - plugin_hooks_enabled: bool, + feature_flags: PluginFeatureFlags, ) -> Vec { - self.plugins_for_layer_stack( - config_layer_stack, - plugins_enabled, - remote_plugins_enabled, - plugin_hooks_enabled, - ) - .await - .effective_skill_roots() + self.plugins_for_layer_stack(config_layer_stack, feature_flags) + .await + .effective_skill_roots() } fn cached_enabled_outcome( &self, config_version: &str, - remote_plugins_enabled: bool, - plugin_hooks_enabled: bool, + feature_flags: PluginFeatureFlags, ) -> Option { match self.cached_enabled_outcome.read() { Ok(cache) => cache .as_ref() .filter(|cached| { - cached.config_version == config_version - && cached.remote_plugins_enabled == remote_plugins_enabled - && cached.plugin_hooks_enabled == plugin_hooks_enabled + cached.config_version == config_version && cached.feature_flags == feature_flags }) .map(|cached| cached.outcome.clone()), Err(err) => err .into_inner() .as_ref() .filter(|cached| { - cached.config_version == config_version - && cached.remote_plugins_enabled == remote_plugins_enabled - && cached.plugin_hooks_enabled == plugin_hooks_enabled + cached.config_version == config_version && cached.feature_flags == feature_flags }) .map(|cached| cached.outcome.clone()), } diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index aa40c98aa0..bf8f397e25 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -264,6 +264,14 @@ impl TestPluginConfig { chatgpt_base_url: chatgpt_base_url(&effective_config), } } + + fn plugin_feature_flags(&self) -> PluginFeatureFlags { + PluginFeatureFlags { + plugins_enabled: self.plugins_enabled, + remote_plugins_enabled: self.remote_plugins_enabled, + plugin_hooks_enabled: self.plugin_hooks_enabled, + } + } } fn feature_enabled(config: &TomlValue, feature_name: &str) -> bool { @@ -327,9 +335,7 @@ impl PluginsManagerTestConfigExt for PluginsManager { PluginsManager::plugins_for_config( self, &config.config_layer_stack, - config.plugins_enabled, - config.remote_plugins_enabled, - config.plugin_hooks_enabled, + config.plugin_feature_flags(), ) .await } @@ -620,14 +626,11 @@ remote_plugin = true let enabled_outcome = manager.plugins_for_test_config(&config).await; assert_eq!(enabled_outcome.plugins().len(), 1); - let disabled_outcome = PluginsManager::plugins_for_config( - &manager, - &config.config_layer_stack, - config.plugins_enabled, - /*remote_plugins_enabled*/ false, - config.plugin_hooks_enabled, - ) - .await; + let mut feature_flags = config.plugin_feature_flags(); + feature_flags.remote_plugins_enabled = false; + let disabled_outcome = + PluginsManager::plugins_for_config(&manager, &config.config_layer_stack, feature_flags) + .await; assert_eq!(disabled_outcome, PluginLoadOutcome::default()); } @@ -730,9 +733,11 @@ async fn remote_installed_plugins_cache_refresh_clears_stale_cache_when_auth_is_ let outcome = manager .plugins_for_config( &ConfigLayerStack::default(), - /*plugins_enabled*/ true, - /*remote_plugins_enabled*/ true, - /*plugin_hooks_enabled*/ true, + PluginFeatureFlags { + plugins_enabled: true, + remote_plugins_enabled: true, + plugin_hooks_enabled: true, + }, ) .await; assert_eq!(outcome, PluginLoadOutcome::default()); diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index 9d4b7a7ea0..88eaf399eb 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -2,10 +2,9 @@ use super::*; use crate::SkillsManager; use crate::config::CONFIG_TOML_FILE; use crate::config::ConfigBuilder; -use crate::plugins::PluginsManager; use crate::skills_load_input_from_config; use codex_config::ConfigLayerStackOrdering; -use codex_features::Feature; +use codex_core_plugins::PluginsManager; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Verbosity; use codex_protocol::openai_models::ReasoningEffort; @@ -657,12 +656,7 @@ enabled = false let skills_manager = SkillsManager::new(home.path().abs(), /*bundled_skills_enabled*/ true); let plugin_outcome = plugins_manager - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6c39f1f1e9..bf64792d84 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4,7 +4,6 @@ use crate::config::ThreadStoreConfig; use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::config::edit::apply_blocking; -use crate::plugins::PluginsManager; use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; use codex_config::RequirementSource; @@ -53,6 +52,7 @@ use codex_config::types::TuiKeymap; use codex_config::types::TuiNotificationSettings; use codex_config::types::WindowsSandboxModeToml; use codex_config::types::WindowsToml; +use codex_core_plugins::PluginsManager; use codex_exec_server::LOCAL_FS; use codex_features::Feature; use codex_features::FeaturesToml; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9213fef8fd..43b54a0834 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -996,17 +996,20 @@ impl Config { } } + pub fn plugin_feature_flags(&self) -> codex_core_plugins::PluginFeatureFlags { + codex_core_plugins::PluginFeatureFlags { + plugins_enabled: self.features.enabled(Feature::Plugins), + remote_plugins_enabled: self.features.enabled(Feature::RemotePlugin), + plugin_hooks_enabled: self.features.enabled(Feature::PluginHooks), + } + } + pub async fn to_mcp_config( &self, - plugins_manager: &crate::plugins::PluginsManager, + plugins_manager: &codex_core_plugins::PluginsManager, ) -> McpConfig { let loaded_plugins = plugins_manager - .plugins_for_config( - &self.config_layer_stack, - self.features.enabled(Feature::Plugins), - self.features.enabled(Feature::RemotePlugin), - self.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&self.config_layer_stack, self.plugin_feature_flags()) .await; let mut configured_mcp_servers = self.mcp_servers.get().clone(); for plugin in loaded_plugins diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 90851ee074..633bb29559 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -27,12 +27,12 @@ use tracing::warn; use crate::config::Config; use crate::mcp::McpManager; -use crate::plugins::PluginsManager; use crate::session::INITIAL_SUBMIT_ID; use codex_config::AppsRequirementsToml; use codex_config::types::AppToolApproval; use codex_config::types::AppsConfigToml; use codex_config::types::ToolSuggestDiscoverableType; +use codex_core_plugins::PluginsManager; use codex_features::Feature; use codex_login::AuthManager; use codex_login::CodexAuth; @@ -409,12 +409,7 @@ fn write_cached_accessible_connectors( async fn tool_suggest_connector_ids(config: &Config) -> HashSet { let mut connector_ids = PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await .capability_summaries() .iter() diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 0d4c26991d..60e325c4e0 100644 --- a/codex-rs/core/src/mcp.rs +++ b/codex-rs/core/src/mcp.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use crate::config::Config; -use crate::plugins::PluginsManager; use codex_config::McpServerConfig; +use codex_core_plugins::PluginsManager; use codex_login::CodexAuth; use codex_mcp::ToolPluginProvenance; use codex_mcp::configured_mcp_servers; diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index 84a7dd025b..d9969c4610 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -758,9 +758,7 @@ async fn custom_mcp_tool_approval_mode( .plugins_manager .plugins_for_config( &turn_context.config.config_layer_stack, - turn_context.config.features.enabled(Feature::Plugins), - turn_context.config.features.enabled(Feature::RemotePlugin), - turn_context.config.features.enabled(Feature::PluginHooks), + turn_context.config.plugin_feature_flags(), ) .await .plugins() @@ -1810,12 +1808,7 @@ async fn persist_non_app_mcp_tool_approval( let plugin_config_name = sess .services .plugins_manager - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await .plugins() .iter() diff --git a/codex-rs/core/src/mcp_tool_call_tests.rs b/codex-rs/core/src/mcp_tool_call_tests.rs index eabe76a920..c50527fd7e 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -13,7 +13,6 @@ use codex_config::types::ApprovalsReviewer; use codex_config::types::AppsConfigToml; use codex_config::types::McpServerConfig; use codex_config::types::McpServerToolConfig; -use codex_features::Feature; use codex_hooks::Hooks; use codex_hooks::HooksConfig; use codex_model_provider::create_model_provider; @@ -1597,9 +1596,7 @@ enabled = true .plugins_manager .plugins_for_config( &initial_config.config_layer_stack, - initial_config.features.enabled(Feature::Plugins), - initial_config.features.enabled(Feature::RemotePlugin), - initial_config.features.enabled(Feature::PluginHooks), + initial_config.plugin_feature_flags(), ) .await; std::fs::write( diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 44f1685a82..79c2aa4e3e 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -6,22 +6,6 @@ pub(crate) mod test_support; use codex_config::types::McpServerConfig; -pub use codex_core_plugins::ConfiguredMarketplace; -pub use codex_core_plugins::ConfiguredMarketplaceListOutcome; -pub use codex_core_plugins::ConfiguredMarketplacePlugin; -pub use codex_core_plugins::PluginDetail; -pub use codex_core_plugins::PluginDetailsUnavailableReason; -pub use codex_core_plugins::PluginInstallError; -pub use codex_core_plugins::PluginInstallOutcome; -pub use codex_core_plugins::PluginInstallRequest; -pub use codex_core_plugins::PluginReadOutcome; -pub use codex_core_plugins::PluginReadRequest; -pub use codex_core_plugins::PluginRemoteSyncError; -pub use codex_core_plugins::PluginUninstallError; -pub use codex_core_plugins::PluginsManager; -pub use codex_core_plugins::RemotePluginSyncResult; -pub use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeError as PluginMarketplaceUpgradeError; -pub use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome; pub use codex_plugin::AppConnectorId; pub use codex_plugin::EffectiveSkillRoots; pub use codex_plugin::PluginCapabilitySummary; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index bd5b641b4a..db1d84ffc1 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -23,7 +23,6 @@ use codex_config::CloudRequirementsLoader; use codex_config::LoaderOverrides; use codex_config::loader::load_config_layers_state; use codex_exec_server::LOCAL_FS; -use codex_features::Feature; use codex_utils_absolute_path::AbsolutePathBuf; use crate::review_prompts::resolve_review_request; @@ -624,9 +623,7 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec, for let effective_skill_roots = plugins_manager .effective_skill_roots_for_layer_stack( &config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), + config.plugin_feature_flags(), ) .await; let skills_input = crate::SkillsLoadInput::new( diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 67a1437281..eb5c47c536 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -274,7 +274,6 @@ use crate::exec_policy::ExecPolicyUpdateError; use crate::guardian::GuardianReviewSessionManager; use crate::mcp::McpManager; use crate::network_policy_decision::execpolicy_network_rule_amendment; -use crate::plugins::PluginsManager; use crate::rollout::map_session_init_error; use crate::session_startup_prewarm::SessionStartupPrewarmHandle; use crate::shell; @@ -301,6 +300,7 @@ use crate::turn_timing::TurnTimingState; use crate::turn_timing::record_turn_ttfm_metric; use crate::unified_exec::UnifiedExecProcessManager; use crate::windows_sandbox::WindowsSandboxLevelExt; +use codex_core_plugins::PluginsManager; use codex_git_utils::get_git_repo_root; use codex_mcp::compute_auth_statuses; use codex_mcp::with_codex_apps_mcp; @@ -476,12 +476,7 @@ impl Codex { .as_ref() .map(|environment| environment.get_filesystem()); let plugin_outcome = plugins_manager - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); @@ -2675,9 +2670,7 @@ impl Session { .plugins_manager .plugins_for_config( &turn_context.config.config_layer_stack, - turn_context.config.features.enabled(Feature::Plugins), - turn_context.config.features.enabled(Feature::RemotePlugin), - turn_context.config.features.enabled(Feature::PluginHooks), + turn_context.config.plugin_feature_flags(), ) .await; if let Some(plugin_instructions) = @@ -3375,12 +3368,7 @@ async fn build_hooks_for_config( let plugin_hooks_enabled = config.features.enabled(Feature::PluginHooks); let (plugin_hook_sources, plugin_hook_load_warnings) = if plugin_hooks_enabled { let plugin_outcome = plugins_manager - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await; ( plugin_outcome.effective_plugin_hook_sources(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 69efbb0cda..6e3e233f89 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3560,9 +3560,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { .plugins_manager .plugins_for_config( &per_turn_config.config_layer_stack, - per_turn_config.features.enabled(Feature::Plugins), - per_turn_config.features.enabled(Feature::RemotePlugin), - per_turn_config.features.enabled(Feature::PluginHooks), + per_turn_config.plugin_feature_flags(), ) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); @@ -4994,9 +4992,7 @@ where .plugins_manager .plugins_for_config( &per_turn_config.config_layer_stack, - per_turn_config.features.enabled(Feature::Plugins), - per_turn_config.features.enabled(Feature::RemotePlugin), - per_turn_config.features.enabled(Feature::PluginHooks), + per_turn_config.plugin_feature_flags(), ) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 86666c1bf4..fb97a0f73c 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -172,9 +172,7 @@ pub(crate) async fn run_turn( .plugins_manager .plugins_for_config( &turn_context.config.config_layer_stack, - turn_context.config.features.enabled(Feature::Plugins), - turn_context.config.features.enabled(Feature::RemotePlugin), - turn_context.config.features.enabled(Feature::PluginHooks), + turn_context.config.plugin_feature_flags(), ) .await; // Structured plugin:// mentions are resolved from the current session's @@ -1131,9 +1129,7 @@ pub(crate) async fn built_tools( .plugins_manager .plugins_for_config( &turn_context.config.config_layer_stack, - turn_context.config.features.enabled(Feature::Plugins), - turn_context.config.features.enabled(Feature::RemotePlugin), - turn_context.config.features.enabled(Feature::PluginHooks), + turn_context.config.plugin_feature_flags(), ) .await; diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index dce384b239..00ce600657 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -698,9 +698,7 @@ impl Session { .plugins_manager .plugins_for_config( &per_turn_config.config_layer_stack, - per_turn_config.features.enabled(Feature::Plugins), - per_turn_config.features.enabled(Feature::RemotePlugin), - per_turn_config.features.enabled(Feature::PluginHooks), + per_turn_config.plugin_feature_flags(), ) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); diff --git a/codex-rs/core/src/skills_watcher.rs b/codex-rs/core/src/skills_watcher.rs index 194a5d7949..7181b43900 100644 --- a/codex-rs/core/src/skills_watcher.rs +++ b/codex-rs/core/src/skills_watcher.rs @@ -8,8 +8,6 @@ use tokio::runtime::Handle; use tokio::sync::broadcast; use tracing::warn; -use codex_features::Feature; - use crate::SkillsManager; use crate::config::Config; use crate::file_watcher::FileWatcher; @@ -18,8 +16,8 @@ use crate::file_watcher::Receiver; use crate::file_watcher::ThrottledWatchReceiver; use crate::file_watcher::WatchPath; use crate::file_watcher::WatchRegistration; -use crate::plugins::PluginsManager; use crate::skills_load_input_from_config; +use codex_core_plugins::PluginsManager; #[cfg(not(test))] const WATCHER_THROTTLE_INTERVAL: Duration = Duration::from_secs(10); @@ -64,12 +62,7 @@ impl SkillsWatcher { fs: Option>, ) -> WatchRegistration { let plugin_outcome = plugins_manager - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(config, effective_skill_roots); diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index ddf2122172..9cd9e97fbb 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -9,7 +9,6 @@ use crate::exec_policy::ExecPolicyManager; use crate::guardian::GuardianRejection; use crate::guardian::GuardianRejectionCircuitBreaker; use crate::mcp::McpManager; -use crate::plugins::PluginsManager; use crate::skills_watcher::SkillsWatcher; use crate::tools::code_mode::CodeModeService; use crate::tools::network_approval::NetworkApprovalService; @@ -17,6 +16,7 @@ use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use arc_swap::ArcSwap; use codex_analytics::AnalyticsEventsClient; +use codex_core_plugins::PluginsManager; use codex_exec_server::EnvironmentManager; use codex_hooks::Hooks; use codex_login::AuthManager; diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 3587dc937e..33d6e35517 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -8,7 +8,6 @@ use crate::environment_selection::selected_primary_environment; use crate::environment_selection::validate_environment_selections; use crate::file_watcher::FileWatcher; use crate::mcp::McpManager; -use crate::plugins::PluginsManager; use crate::rollout::RolloutRecorder; use crate::rollout::truncation; use crate::session::Codex; @@ -23,6 +22,7 @@ use crate::tasks::interrupted_turn_history_marker; use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::ThreadHistoryBuilder; use codex_app_server_protocol::TurnStatus; +use codex_core_plugins::PluginsManager; use codex_exec_server::EnvironmentManager; use codex_login::AuthManager; use codex_login::CodexAuth; diff --git a/codex-rs/core/src/tools/handlers/tool_suggest.rs b/codex-rs/core/src/tools/handlers/tool_suggest.rs index 4306dec4fb..df96654976 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest.rs @@ -315,7 +315,7 @@ async fn refresh_missing_suggested_connectors( fn verified_plugin_suggestion_completed( tool_id: &str, config: &crate::config::Config, - plugins_manager: &crate::plugins::PluginsManager, + plugins_manager: &codex_core_plugins::PluginsManager, ) -> bool { plugins_manager .list_marketplaces_for_config( diff --git a/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs b/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs index 86cbbc98f5..0f28d3d18f 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs @@ -1,7 +1,5 @@ use super::*; use crate::config::edit::ConfigEditsBuilder; -use crate::plugins::PluginInstallRequest; -use crate::plugins::PluginsManager; use crate::plugins::test_support::load_plugins_config; use crate::plugins::test_support::write_curated_plugin_sha; use crate::plugins::test_support::write_openai_curated_marketplace; @@ -12,6 +10,8 @@ use codex_config::types::ToolSuggestConfig; use codex_config::types::ToolSuggestDisabledTool; use codex_config::types::ToolSuggestDiscoverable; use codex_config::types::ToolSuggestDiscoverableType; +use codex_core_plugins::PluginInstallRequest; +use codex_core_plugins::PluginsManager; use codex_core_plugins::startup_sync::curated_plugins_repo_path; use codex_rmcp_client::ElicitationResponse; use codex_tools::DiscoverablePluginInfo; diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index 63a4fd0657..b597af2105 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -233,12 +233,7 @@ impl App { tokio::spawn(async move { let plugins = PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config( - &config.config_layer_stack, - config.features.enabled(Feature::Plugins), - config.features.enabled(Feature::RemotePlugin), - config.features.enabled(Feature::PluginHooks), - ) + .plugins_for_config(&config.config_layer_stack, config.plugin_feature_flags()) .await .capability_summaries() .to_vec();