From 7b3de6304154ea7cbc4575f9e7d9b96a617d2a3d Mon Sep 17 00:00:00 2001 From: xl-openai Date: Thu, 30 Apr 2026 14:26:14 -0700 Subject: [PATCH] Move plugin out of core. (#20348) --- codex-rs/Cargo.lock | 4 + codex-rs/app-server-client/src/lib.rs | 4 - codex-rs/app-server/Cargo.toml | 1 + .../app-server/src/codex_message_processor.rs | 24 +- .../plugin_app_helpers.rs | 4 +- .../src/codex_message_processor/plugins.rs | 20 +- .../src/config/external_agent_config.rs | 9 +- codex-rs/app-server/src/config_api.rs | 2 +- codex-rs/app-server/src/message_processor.rs | 2 +- codex-rs/chatgpt/Cargo.toml | 2 + codex-rs/chatgpt/src/connectors.rs | 11 +- codex-rs/cli/src/marketplace_cmd.rs | 7 +- codex-rs/cli/src/mcp_cmd.rs | 2 +- codex-rs/config/src/lib.rs | 5 + codex-rs/config/src/plugin_edit.rs | 307 ++++++++++++++++++ codex-rs/core-api/src/lib.rs | 1 - codex-rs/core-plugins/Cargo.toml | 3 +- codex-rs/core-plugins/src/lib.rs | 25 ++ .../plugins => core-plugins/src}/manager.rs | 282 ++++++++-------- .../src}/manager_tests.rs | 70 ++-- .../src/startup_remote_sync.rs} | 10 +- .../src/startup_remote_sync_tests.rs} | 17 +- codex-rs/core-plugins/src/test_support.rs | 139 ++++++++ codex-rs/core/src/agent/role_tests.rs | 5 +- codex-rs/core/src/config/config_tests.rs | 2 +- codex-rs/core/src/config/mod.rs | 17 +- codex-rs/core/src/connectors.rs | 5 +- codex-rs/core/src/lib.rs | 2 +- codex-rs/core/src/mcp.rs | 2 +- codex-rs/core/src/mcp_tool_call.rs | 4 +- codex-rs/core/src/mcp_tool_call_tests.rs | 2 +- codex-rs/core/src/plugins/discoverable.rs | 11 +- .../core/src/plugins/discoverable_tests.rs | 2 +- codex-rs/core/src/plugins/mod.rs | 31 +- codex-rs/core/src/plugins/test_support.rs | 1 - codex-rs/core/src/session/handlers.rs | 3 +- codex-rs/core/src/session/mod.rs | 10 +- codex-rs/core/src/session/tests.rs | 4 +- codex-rs/core/src/session/turn.rs | 4 +- codex-rs/core/src/session/turn_context.rs | 2 +- codex-rs/core/src/skills_watcher.rs | 5 +- codex-rs/core/src/state/service.rs | 2 +- codex-rs/core/src/thread_manager.rs | 2 +- .../core/src/tools/handlers/tool_suggest.rs | 5 +- .../src/tools/handlers/tool_suggest_tests.rs | 4 +- codex-rs/tui/src/app.rs | 2 +- codex-rs/tui/src/app/background_requests.rs | 3 +- 47 files changed, 786 insertions(+), 295 deletions(-) create mode 100644 codex-rs/config/src/plugin_edit.rs rename codex-rs/{core/src/plugins => core-plugins/src}/manager.rs (90%) rename codex-rs/{core/src/plugins => core-plugins/src}/manager_tests.rs (98%) rename codex-rs/{core/src/plugins/startup_sync.rs => core-plugins/src/startup_remote_sync.rs} (93%) rename codex-rs/{core/src/plugins/startup_sync_tests.rs => core-plugins/src/startup_remote_sync_tests.rs} (84%) create mode 100644 codex-rs/core-plugins/src/test_support.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 404ada1ee..056bae406 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1882,6 +1882,7 @@ dependencies = [ "codex-model-provider-info", "codex-models-manager", "codex-otel", + "codex-plugin", "codex-protocol", "codex-rmcp-client", "codex-rollout", @@ -2100,9 +2101,11 @@ dependencies = [ "codex-app-server-protocol", "codex-connectors", "codex-core", + "codex-core-plugins", "codex-git-utils", "codex-login", "codex-model-provider", + "codex-plugin", "codex-utils-cargo-bin", "codex-utils-cli", "pretty_assertions", @@ -2504,6 +2507,7 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", + "codex-analytics", "codex-app-server-protocol", "codex-config", "codex-core-skills", diff --git a/codex-rs/app-server-client/src/lib.rs b/codex-rs/app-server-client/src/lib.rs index 8165fb367..cafb696c7 100644 --- a/codex-rs/app-server-client/src/lib.rs +++ b/codex-rs/app-server-client/src/lib.rs @@ -99,10 +99,6 @@ pub mod legacy_core { pub use codex_core::personality_migration::*; } - pub mod plugins { - pub use codex_core::plugins::PluginsManager; - } - pub mod review_format { pub use codex_core::review_format::*; } diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 9771466cd..5d73f97c2 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -44,6 +44,7 @@ codex-features = { workspace = true } codex-git-utils = { workspace = true } codex-hooks = { workspace = true } codex-otel = { workspace = true } +codex-plugin = { workspace = true } codex-shell-command = { workspace = true } codex-utils-cli = { workspace = true } codex-utils-pty = { workspace = true } diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index b74225208..62d250dc0 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -270,10 +270,6 @@ use codex_core::find_archived_thread_path_by_id_str; use codex_core::find_thread_name_by_id; 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; @@ -281,6 +277,11 @@ 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::PluginLoadOutcome; +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; @@ -6321,8 +6322,9 @@ impl CodexMessageProcessor { .get(&cwd) .map_or(&[][..], std::vec::Vec::as_slice); let effective_skill_roots = if workspace_codex_plugins_enabled { + let plugins_input = config.plugins_config_input(); plugins_manager - .effective_skill_roots_for_layer_stack(&config_layer_stack, &config) + .effective_skill_roots_for_layer_stack(&config_layer_stack, &plugins_input) .await } else { Vec::new() @@ -6404,15 +6406,16 @@ impl CodexMessageProcessor { config.features.enabled(Feature::Plugins) && workspace_codex_plugins_enabled; let plugin_outcome = if plugins_enabled && config.features.enabled(Feature::PluginHooks) { + let plugins_input = config.plugins_config_input(); plugins_manager .plugins_for_layer_stack( &config.config_layer_stack, - &config, + &plugins_input, /*plugin_hooks_feature_enabled*/ true, ) .await } else { - codex_core::plugins::PluginLoadOutcome::default() + PluginLoadOutcome::default() }; let hooks = codex_hooks::list_hooks(codex_hooks::HooksConfig { feature_enabled: config.features.enabled(Feature::CodexHooks), @@ -6470,10 +6473,13 @@ impl CodexMessageProcessor { let config = self.load_latest_config(/*fallback_cwd*/ None).await?; let plugins_manager = self.thread_manager.plugins_manager(); let MarketplaceUpgradeParams { marketplace_name } = params; + let plugins_input = config.plugins_config_input(); let outcome = tokio::task::spawn_blocking(move || { - plugins_manager - .upgrade_configured_marketplaces_for_config(&config, marketplace_name.as_deref()) + plugins_manager.upgrade_configured_marketplaces_for_config( + &plugins_input, + marketplace_name.as_deref(), + ) }) .await .map_err(|err| internal_error(format!("failed to upgrade marketplaces: {err}")))? diff --git a/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs index ad5875608..7a409d4ce 100644 --- a/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs +++ b/codex-rs/app-server/src/codex_message_processor/plugin_app_helpers.rs @@ -4,8 +4,8 @@ use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppSummary; use codex_chatgpt::connectors; use codex_core::config::Config; -use codex_core::plugins::AppConnectorId; use codex_exec_server::EnvironmentManager; +use codex_plugin::AppConnectorId; use tracing::warn; pub(super) async fn load_plugin_app_summaries( @@ -113,7 +113,7 @@ pub(super) fn plugin_apps_needing_auth( #[cfg(test)] mod tests { use codex_app_server_protocol::AppInfo; - use codex_core::plugins::AppConnectorId; + use codex_plugin::AppConnectorId; use pretty_assertions::assert_eq; use super::plugin_apps_needing_auth; diff --git a/codex-rs/app-server/src/codex_message_processor/plugins.rs b/codex-rs/app-server/src/codex_message_processor/plugins.rs index 654b942c3..0e08106d1 100644 --- a/codex-rs/app-server/src/codex_message_processor/plugins.rs +++ b/codex-rs/app-server/src/codex_message_processor/plugins.rs @@ -37,14 +37,15 @@ impl CodexMessageProcessor { { return Ok(empty_response()); } + let plugins_input = config.plugins_config_input(); plugins_manager.maybe_start_plugin_list_background_tasks_for_config( - &config, + &plugins_input, auth.clone(), &roots, Some(self.effective_plugins_changed_callback(config.clone())), ); - let config_for_marketplace_listing = config.clone(); + let config_for_marketplace_listing = plugins_input.clone(); let plugins_manager_for_marketplace_listing = plugins_manager.clone(); let (mut data, marketplace_load_errors) = match tokio::task::spawn_blocking(move || { let outcome = plugins_manager_for_marketplace_listing @@ -145,7 +146,7 @@ impl CodexMessageProcessor { .any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME) { match plugins_manager - .featured_plugin_ids_for_config(&config, auth.as_ref()) + .featured_plugin_ids_for_config(&plugins_input, auth.as_ref()) .await { Ok(featured_plugin_ids) => featured_plugin_ids, @@ -201,6 +202,7 @@ impl CodexMessageProcessor { }); let config = self.load_latest_config(config_cwd).await?; + let plugins_input = config.plugins_config_input(); let plugin = match read_source { Ok(marketplace_path) => { @@ -209,7 +211,7 @@ impl CodexMessageProcessor { marketplace_path, }; let outcome = plugins_manager - .read_plugin_for_config(&config, &request) + .read_plugin_for_config(&plugins_input, &request) .await .map_err(|err| Self::marketplace_error(err, "read plugin details"))?; let environment_manager = self.thread_manager.environment_manager(); @@ -279,7 +281,7 @@ impl CodexMessageProcessor { .app_ids .iter() .cloned() - .map(codex_core::plugins::AppConnectorId) + .map(codex_plugin::AppConnectorId) .collect::>(); let environment_manager = self.thread_manager.environment_manager(); let app_summaries = plugin_app_helpers::load_plugin_app_summaries( @@ -570,7 +572,7 @@ impl CodexMessageProcessor { self.thread_manager .plugins_manager() .maybe_start_remote_installed_plugins_cache_refresh_after_mutation( - &config, + &config.plugins_config_input(), auth.clone(), Some(self.effective_plugins_changed_callback(config.clone())), ); @@ -602,7 +604,7 @@ impl CodexMessageProcessor { config: &Config, is_chatgpt_auth: bool, plugin_id: &str, - plugin_apps: &[codex_core::plugins::AppConnectorId], + plugin_apps: &[codex_plugin::AppConnectorId], ) -> Vec { if plugin_apps.is_empty() || !config.features.apps_enabled_for_auth(is_chatgpt_auth) { return Vec::new(); @@ -675,7 +677,7 @@ impl CodexMessageProcessor { params: PluginUninstallParams, ) -> Result { let PluginUninstallParams { plugin_id } = params; - if codex_core::plugins::PluginId::parse(&plugin_id).is_err() + if codex_plugin::PluginId::parse(&plugin_id).is_err() && (plugin_id.is_empty() || !is_valid_remote_plugin_id(&plugin_id)) { return Err(invalid_request( @@ -798,7 +800,7 @@ impl CodexMessageProcessor { self.on_effective_plugins_changed(config.clone()); } plugins_manager.maybe_start_remote_installed_plugins_cache_refresh_after_mutation( - &config, + &config.plugins_config_input(), auth.clone(), Some(self.effective_plugins_changed_callback(config.clone())), ); 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 20030dfe2..9de2c184b 100644 --- a/codex-rs/app-server/src/config/external_agent_config.rs +++ b/codex-rs/app-server/src/config/external_agent_config.rs @@ -1,9 +1,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; @@ -20,6 +19,7 @@ use codex_external_agent_migration::missing_command_names; use codex_external_agent_migration::missing_subagent_names; use codex_external_agent_sessions::ExternalAgentSessionMigration; use codex_external_agent_sessions::detect_recent_sessions; +use codex_plugin::PluginId; use codex_protocol::protocol::Product; use serde_json::Value as JsonValue; use std::collections::BTreeMap; @@ -1146,8 +1146,9 @@ fn configured_marketplace_plugins( config: &Config, plugins_manager: &PluginsManager, ) -> io::Result>> { + let plugins_input = config.plugins_config_input(); let marketplaces = plugins_manager - .list_marketplaces_for_config(config, &[]) + .list_marketplaces_for_config(&plugins_input, &[]) .map_err(|err| { invalid_data_error(format!("failed to list configured marketplaces: {err}")) })?; diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 0de3e2df4..4b6cbdd19 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -32,11 +32,11 @@ use codex_config::ResidencyRequirement as CoreResidencyRequirement; use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement; use codex_core::ThreadManager; use codex_core::config::Config; -use codex_core::plugins::PluginId; use codex_core_plugins::loader::installed_plugin_telemetry_metadata; use codex_core_plugins::toggles::collect_plugin_enabled_candidates; use codex_features::canonical_feature_for_key; use codex_features::feature_for_key; +use codex_plugin::PluginId; use codex_protocol::config_types::WebSearchMode; use codex_protocol::protocol::Op; use serde_json::json; diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index b79bf7732..0480aba8c 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -314,7 +314,7 @@ impl MessageProcessor { thread_manager .plugins_manager() .maybe_start_plugin_startup_tasks_for_config( - &config, + &config.plugins_config_input(), auth_manager.clone(), Some(on_effective_plugins_changed), ); diff --git a/codex-rs/chatgpt/Cargo.toml b/codex-rs/chatgpt/Cargo.toml index ce9aa627d..62cb56a02 100644 --- a/codex-rs/chatgpt/Cargo.toml +++ b/codex-rs/chatgpt/Cargo.toml @@ -13,9 +13,11 @@ 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-git-utils = { workspace = true } codex-login = { workspace = true } codex-model-provider = { workspace = true } +codex-plugin = { workspace = true } codex-utils-cli = { workspace = true } serde = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["full"] } diff --git a/codex-rs/chatgpt/src/connectors.rs b/codex-rs/chatgpt/src/connectors.rs index 9dba71ce3..cbeb4fd1b 100644 --- a/codex-rs/chatgpt/src/connectors.rs +++ b/codex-rs/chatgpt/src/connectors.rs @@ -16,11 +16,11 @@ pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_o pub use codex_core::connectors::list_accessible_connectors_from_mcp_tools_with_options_and_status; 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_core_plugins::PluginsManager; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_login::default_client::originator; +use codex_plugin::AppConnectorId; const DIRECTORY_CONNECTORS_TIMEOUT: Duration = Duration::from_secs(60); @@ -135,9 +135,10 @@ fn all_connectors_cache_key(config: &Config, auth: &CodexAuth) -> AllConnectorsC ) } -async fn plugin_apps_for_config(config: &Config) -> Vec { +async fn plugin_apps_for_config(config: &Config) -> Vec { + let plugins_input = config.plugins_config_input(); PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config(config) + .plugins_for_config(&plugins_input) .await .effective_apps() } @@ -188,7 +189,7 @@ pub fn merge_connectors_with_accessible( mod tests { use super::*; use codex_connectors::metadata::connector_install_url; - use codex_core::plugins::AppConnectorId; + use codex_plugin::AppConnectorId; use pretty_assertions::assert_eq; fn app(id: &str) -> AppInfo { diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index d8756c263..fcd9049d5 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -4,8 +4,8 @@ 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::PluginMarketplaceUpgradeOutcome; +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; @@ -128,8 +128,9 @@ async fn run_upgrade( .context("failed to load configuration")?; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let manager = PluginsManager::new(codex_home.to_path_buf()); + let plugins_input = config.plugins_config_input(); let outcome = manager - .upgrade_configured_marketplaces_for_config(&config, marketplace_name.as_deref()) + .upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref()) .map_err(anyhow::Error::msg)?; print_upgrade_outcome(&outcome, marketplace_name.as_deref()) } diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index c5b475132..858ef442a 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 cf3dd68dc..e88c736db 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; @@ -95,6 +96,10 @@ pub use mcp_types::McpServerTransportConfig; pub use mcp_types::RawMcpServerConfig; pub use merge::merge_toml_values; pub use overrides::build_cli_overrides_layer; +pub use plugin_edit::PluginConfigEdit; +pub use plugin_edit::apply_user_plugin_config_edits; +pub use plugin_edit::clear_user_plugin; +pub use plugin_edit::set_user_plugin_enabled; pub use project_root_markers::default_project_root_markers; pub use project_root_markers::project_root_markers_from_config; pub use requirements_exec_policy::RequirementsExecPolicy; diff --git a/codex-rs/config/src/plugin_edit.rs b/codex-rs/config/src/plugin_edit.rs new file mode 100644 index 000000000..63795cc6c --- /dev/null +++ b/codex-rs/config/src/plugin_edit.rs @@ -0,0 +1,307 @@ +use std::fs; +use std::io::ErrorKind; +use std::path::Path; + +use codex_utils_path::resolve_symlink_write_paths; +use codex_utils_path::write_atomically; +use tokio::task; +use toml_edit::DocumentMut; +use toml_edit::Item as TomlItem; +use toml_edit::Table as TomlTable; +use toml_edit::value; + +use crate::CONFIG_TOML_FILE; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PluginConfigEdit { + SetEnabled { plugin_key: String, enabled: bool }, + Clear { plugin_key: String }, +} + +pub async fn set_user_plugin_enabled( + codex_home: &Path, + plugin_key: String, + enabled: bool, +) -> std::io::Result<()> { + apply_user_plugin_config_edits( + codex_home, + vec![PluginConfigEdit::SetEnabled { + plugin_key, + enabled, + }], + ) + .await +} + +pub async fn clear_user_plugin(codex_home: &Path, plugin_key: String) -> std::io::Result<()> { + apply_user_plugin_config_edits(codex_home, vec![PluginConfigEdit::Clear { plugin_key }]).await +} + +pub async fn apply_user_plugin_config_edits( + codex_home: &Path, + edits: Vec, +) -> std::io::Result<()> { + let codex_home = codex_home.to_path_buf(); + task::spawn_blocking(move || apply_user_plugin_config_edits_blocking(&codex_home, edits)) + .await + .map_err(|err| std::io::Error::other(format!("config persistence task panicked: {err}")))? +} + +fn apply_user_plugin_config_edits_blocking( + codex_home: &Path, + edits: Vec, +) -> std::io::Result<()> { + if edits.is_empty() { + return Ok(()); + } + + let config_path = codex_home.join(CONFIG_TOML_FILE); + let write_paths = resolve_symlink_write_paths(&config_path)?; + let mut doc = read_or_create_document(write_paths.read_path.as_deref())?; + let mut mutated = false; + for edit in edits { + mutated |= match edit { + PluginConfigEdit::SetEnabled { + plugin_key, + enabled, + } => set_plugin_enabled(&mut doc, &plugin_key, enabled), + PluginConfigEdit::Clear { plugin_key } => clear_plugin(&mut doc, &plugin_key), + }; + } + if !mutated { + return Ok(()); + } + write_atomically(&write_paths.write_path, &doc.to_string()) +} + +fn read_or_create_document(config_path: Option<&Path>) -> std::io::Result { + let Some(config_path) = config_path else { + return Ok(DocumentMut::new()); + }; + match fs::read_to_string(config_path) { + Ok(raw) => raw + .parse::() + .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err)), + Err(err) if err.kind() == ErrorKind::NotFound => Ok(DocumentMut::new()), + Err(err) => Err(err), + } +} + +fn set_plugin_enabled(doc: &mut DocumentMut, plugin_key: &str, enabled: bool) -> bool { + let Some(plugins) = ensure_plugins_table(doc) else { + return false; + }; + let Some(plugin) = ensure_table_for_write(&mut plugins[plugin_key]) else { + return 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_key: &str) -> bool { + let root = doc.as_table_mut(); + let Some(plugins_item) = root.get_mut("plugins") else { + return false; + }; + let Some(plugins) = ensure_table_for_read(plugins_item) else { + return false; + }; + plugins.remove(plugin_key).is_some() +} + +fn ensure_plugins_table(doc: &mut DocumentMut) -> Option<&mut TomlTable> { + let root = doc.as_table_mut(); + if !root.contains_key("plugins") { + root.insert("plugins", TomlItem::Table(new_implicit_table())); + } + ensure_table_for_write(root.get_mut("plugins")?) +} + +fn ensure_table_for_write(item: &mut TomlItem) -> Option<&mut TomlTable> { + match item { + TomlItem::Table(table) => Some(table), + TomlItem::Value(value) => { + let table = value + .as_inline_table() + .map_or_else(new_implicit_table, table_from_inline); + *item = TomlItem::Table(table); + item.as_table_mut() + } + TomlItem::None => { + *item = TomlItem::Table(new_implicit_table()); + item.as_table_mut() + } + _ => None, + } +} + +fn ensure_table_for_read(item: &mut TomlItem) -> Option<&mut TomlTable> { + match item { + TomlItem::Table(_) => {} + TomlItem::Value(value) => { + let inline = value.as_inline_table()?.clone(); + *item = TomlItem::Table(table_from_inline(&inline)); + } + _ => return None, + } + item.as_table_mut() +} + +fn table_from_inline(inline: &toml_edit::InlineTable) -> TomlTable { + let mut table = new_implicit_table(); + for (key, value) in inline.iter() { + let mut value = value.clone(); + value.decor_mut().set_suffix(""); + table.insert(key, TomlItem::Value(value)); + } + table +} + +fn new_implicit_table() -> TomlTable { + let mut table = TomlTable::new(); + table.set_implicit(true); + table +} + +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()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + #[tokio::test] + async fn set_user_plugin_enabled_writes_plugin_entry() { + let codex_home = TempDir::new().unwrap(); + + set_user_plugin_enabled( + codex_home.path(), + "demo@market".to_string(), + /*enabled*/ true, + ) + .await + .unwrap(); + + let config = read_config(codex_home.path()); + let expected: toml::Value = toml::from_str( + r#" +[plugins."demo@market"] +enabled = true + "#, + ) + .unwrap(); + assert_eq!(config, expected); + } + + #[tokio::test] + async fn set_user_plugin_enabled_preserves_existing_plugin_fields() { + let codex_home = TempDir::new().unwrap(); + fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[plugins."demo@market"] +enabled = false +source = "/tmp/plugin" +"#, + ) + .unwrap(); + + set_user_plugin_enabled( + codex_home.path(), + "demo@market".to_string(), + /*enabled*/ true, + ) + .await + .unwrap(); + + let config = read_config(codex_home.path()); + let expected: toml::Value = toml::from_str( + r#" +[plugins."demo@market"] +enabled = true +source = "/tmp/plugin" + "#, + ) + .unwrap(); + assert_eq!(config, expected); + } + + #[tokio::test] + async fn clear_user_plugin_removes_empty_plugins_table() { + let codex_home = TempDir::new().unwrap(); + fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +[plugins."demo@market"] +enabled = true +"#, + ) + .unwrap(); + + clear_user_plugin(codex_home.path(), "demo@market".to_string()) + .await + .unwrap(); + + assert_eq!( + fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).unwrap(), + "" + ); + } + + #[tokio::test] + async fn clear_user_plugin_missing_entry_does_not_create_config() { + let codex_home = TempDir::new().unwrap(); + + clear_user_plugin(codex_home.path(), "demo@market".to_string()) + .await + .unwrap(); + + assert!(!codex_home.path().join(CONFIG_TOML_FILE).exists()); + } + + #[tokio::test] + #[cfg(unix)] + async fn set_user_plugin_enabled_follows_config_symlink() { + use std::os::unix::fs::symlink; + + let codex_home = TempDir::new().unwrap(); + let target_path = codex_home.path().join("target_config.toml"); + symlink(&target_path, codex_home.path().join(CONFIG_TOML_FILE)).unwrap(); + + set_user_plugin_enabled( + codex_home.path(), + "demo@market".to_string(), + /*enabled*/ true, + ) + .await + .unwrap(); + + let config = + toml::from_str::(&fs::read_to_string(target_path).unwrap()).unwrap(); + let expected: toml::Value = toml::from_str( + r#" +[plugins."demo@market"] +enabled = true + "#, + ) + .unwrap(); + assert_eq!(config, expected); + } + + fn read_config(codex_home: &Path) -> toml::Value { + toml::from_str(&fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).unwrap()).unwrap() + } +} diff --git a/codex-rs/core-api/src/lib.rs b/codex-rs/core-api/src/lib.rs index c4eee541c..dca169ed2 100644 --- a/codex-rs/core-api/src/lib.rs +++ b/codex-rs/core-api/src/lib.rs @@ -37,7 +37,6 @@ 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_exec_server::EnvironmentManager; diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index ee477a381..db3059f2b 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -13,6 +13,8 @@ path = "src/lib.rs" workspace = true [dependencies] +anyhow = { workspace = true } +codex-analytics = { workspace = true } codex-app-server-protocol = { workspace = true } codex-config = { workspace = true } codex-core-skills = { workspace = true } @@ -41,7 +43,6 @@ url = { workspace = true } zip = { workspace = true } [dev-dependencies] -anyhow = { workspace = true } libc = { workspace = true } pretty_assertions = { workspace = true } tempfile = { workspace = true } diff --git a/codex-rs/core-plugins/src/lib.rs b/codex-rs/core-plugins/src/lib.rs index 08d004653..9ff6bc24c 100644 --- a/codex-rs/core-plugins/src/lib.rs +++ b/codex-rs/core-plugins/src/lib.rs @@ -1,5 +1,6 @@ pub mod installed_marketplaces; pub mod loader; +mod manager; pub mod manifest; pub mod marketplace; pub mod marketplace_add; @@ -8,8 +9,11 @@ pub mod marketplace_upgrade; pub mod remote; pub mod remote_bundle; pub mod remote_legacy; +pub(crate) mod startup_remote_sync; pub mod startup_sync; pub mod store; +#[cfg(test)] +mod test_support; pub mod toggles; pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated"; @@ -32,3 +36,24 @@ pub const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[ "chrome@openai-bundled", "computer-use@openai-bundled", ]; + +pub type LoadedPlugin = codex_plugin::LoadedPlugin; +pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome; + +pub use manager::ConfiguredMarketplace; +pub use manager::ConfiguredMarketplaceListOutcome; +pub use manager::ConfiguredMarketplacePlugin; +pub use manager::PluginDetail; +pub use manager::PluginDetailsUnavailableReason; +pub use manager::PluginInstallError; +pub use manager::PluginInstallOutcome; +pub use manager::PluginInstallRequest; +pub use manager::PluginReadOutcome; +pub use manager::PluginReadRequest; +pub use manager::PluginRemoteSyncError; +pub use manager::PluginUninstallError; +pub use manager::PluginsConfigInput; +pub use manager::PluginsManager; +pub use manager::RemotePluginSyncResult; +pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeError as PluginMarketplaceUpgradeError; +pub use marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome as PluginMarketplaceUpgradeOutcome; diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core-plugins/src/manager.rs similarity index 90% rename from codex-rs/core/src/plugins/manager.rs rename to codex-rs/core-plugins/src/manager.rs index 595378637..5922b103a 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -1,59 +1,59 @@ use super::PluginLoadOutcome; -use super::startup_sync::start_startup_remote_plugin_sync_once; -use crate::SkillMetadata; -use crate::config::Config; -use crate::config::edit::ConfigEdit; -use crate::config::edit::ConfigEditsBuilder; +use super::startup_remote_sync::start_startup_remote_plugin_sync_once; +use crate::OPENAI_CURATED_MARKETPLACE_NAME; +use crate::installed_marketplaces::installed_marketplace_roots_from_layer_stack; +use crate::loader::configured_curated_plugin_ids_from_codex_home; +use crate::loader::curated_plugin_cache_version; +use crate::loader::installed_plugin_telemetry_metadata; +use crate::loader::load_plugin_apps; +use crate::loader::load_plugin_mcp_servers; +use crate::loader::load_plugin_skills; +use crate::loader::load_plugins_from_layer_stack; +use crate::loader::log_plugin_load_errors; +use crate::loader::materialize_marketplace_plugin_source; +use crate::loader::plugin_telemetry_metadata_from_root; +use crate::loader::refresh_curated_plugin_cache; +use crate::loader::refresh_non_curated_plugin_cache; +use crate::loader::refresh_non_curated_plugin_cache_force_reinstall; +use crate::loader::remote_installed_plugins_to_config; +use crate::manifest::PluginManifestInterface; +use crate::manifest::load_plugin_manifest; +use crate::marketplace::MarketplaceError; +use crate::marketplace::MarketplaceInterface; +use crate::marketplace::MarketplaceListError; +use crate::marketplace::MarketplacePluginAuthPolicy; +use crate::marketplace::MarketplacePluginPolicy; +use crate::marketplace::MarketplacePluginSource; +use crate::marketplace::ResolvedMarketplacePlugin; +use crate::marketplace::find_installable_marketplace_plugin; +use crate::marketplace::find_marketplace_plugin; +use crate::marketplace::list_marketplaces; +use crate::marketplace::load_marketplace; +use crate::marketplace::plugin_interface_with_marketplace_category; +use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeError; +use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome; +use crate::marketplace_upgrade::configured_git_marketplace_names; +use crate::marketplace_upgrade::upgrade_configured_git_marketplaces; +use crate::remote::RemoteInstalledPlugin; +use crate::remote::RemotePluginCatalogError; +use crate::remote::RemotePluginServiceConfig; +use crate::remote_legacy::RemotePluginFetchError; +use crate::remote_legacy::RemotePluginMutationError; +use crate::startup_sync::curated_plugins_repo_path; +use crate::startup_sync::read_curated_plugins_sha; +use crate::startup_sync::sync_openai_plugins_repo; +use crate::store::PluginInstallResult as StorePluginInstallResult; +use crate::store::PluginStore; +use crate::store::PluginStoreError; use codex_analytics::AnalyticsEventsClient; use codex_config::ConfigLayerStack; +use codex_config::PluginConfigEdit; +use codex_config::apply_user_plugin_config_edits; +use codex_config::clear_user_plugin; +use codex_config::set_user_plugin_enabled; use codex_config::types::PluginConfig; use codex_config::version_for_toml; -use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; -use codex_core_plugins::installed_marketplaces::installed_marketplace_roots_from_layer_stack; -use codex_core_plugins::loader::configured_curated_plugin_ids_from_codex_home; -use codex_core_plugins::loader::curated_plugin_cache_version; -use codex_core_plugins::loader::installed_plugin_telemetry_metadata; -use codex_core_plugins::loader::load_plugin_apps; -use codex_core_plugins::loader::load_plugin_mcp_servers; -use codex_core_plugins::loader::load_plugin_skills; -use codex_core_plugins::loader::load_plugins_from_layer_stack; -use codex_core_plugins::loader::log_plugin_load_errors; -use codex_core_plugins::loader::materialize_marketplace_plugin_source; -use codex_core_plugins::loader::plugin_telemetry_metadata_from_root; -use codex_core_plugins::loader::refresh_curated_plugin_cache; -use codex_core_plugins::loader::refresh_non_curated_plugin_cache; -use codex_core_plugins::loader::refresh_non_curated_plugin_cache_force_reinstall; -use codex_core_plugins::loader::remote_installed_plugins_to_config; -use codex_core_plugins::manifest::PluginManifestInterface; -use codex_core_plugins::manifest::load_plugin_manifest; -use codex_core_plugins::marketplace::MarketplaceError; -use codex_core_plugins::marketplace::MarketplaceInterface; -use codex_core_plugins::marketplace::MarketplaceListError; -use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy; -use codex_core_plugins::marketplace::MarketplacePluginPolicy; -use codex_core_plugins::marketplace::MarketplacePluginSource; -use codex_core_plugins::marketplace::ResolvedMarketplacePlugin; -use codex_core_plugins::marketplace::find_installable_marketplace_plugin; -use codex_core_plugins::marketplace::find_marketplace_plugin; -use codex_core_plugins::marketplace::list_marketplaces; -use codex_core_plugins::marketplace::load_marketplace; -use codex_core_plugins::marketplace::plugin_interface_with_marketplace_category; -use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeError; -use codex_core_plugins::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome; -use codex_core_plugins::marketplace_upgrade::configured_git_marketplace_names; -use codex_core_plugins::marketplace_upgrade::upgrade_configured_git_marketplaces; -use codex_core_plugins::remote::RemoteInstalledPlugin; -use codex_core_plugins::remote::RemotePluginCatalogError; -use codex_core_plugins::remote::RemotePluginServiceConfig; -use codex_core_plugins::remote_legacy::RemotePluginFetchError; -use codex_core_plugins::remote_legacy::RemotePluginMutationError; -use codex_core_plugins::startup_sync::curated_plugins_repo_path; -use codex_core_plugins::startup_sync::read_curated_plugins_sha; -use codex_core_plugins::startup_sync::sync_openai_plugins_repo; -use codex_core_plugins::store::PluginInstallResult as StorePluginInstallResult; -use codex_core_plugins::store::PluginStore; -use codex_core_plugins::store::PluginStoreError; -use codex_features::Feature; +use codex_core_skills::SkillMetadata; use codex_login::AuthManager; use codex_login::CodexAuth; use codex_plugin::AppConnectorId; @@ -72,7 +72,6 @@ use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Instant; use tokio::sync::Semaphore; -use toml_edit::value; use tracing::info; use tracing::warn; @@ -80,6 +79,33 @@ 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(Debug, Clone)] +pub struct PluginsConfigInput { + pub config_layer_stack: ConfigLayerStack, + pub plugins_enabled: bool, + pub remote_plugin_enabled: bool, + pub plugin_hooks_enabled: bool, + pub chatgpt_base_url: String, +} + +impl PluginsConfigInput { + pub fn new( + config_layer_stack: ConfigLayerStack, + plugins_enabled: bool, + remote_plugin_enabled: bool, + plugin_hooks_enabled: bool, + chatgpt_base_url: String, + ) -> Self { + Self { + config_layer_stack, + plugins_enabled, + remote_plugin_enabled, + plugin_hooks_enabled, + chatgpt_base_url, + } + } +} + #[derive(Clone, PartialEq, Eq)] struct FeaturedPluginIdsCacheKey { chatgpt_base_url: String, @@ -143,14 +169,14 @@ struct ConfiguredMarketplaceUpgradeState { in_flight: bool, } -fn remote_plugin_service_config(config: &Config) -> RemotePluginServiceConfig { +fn remote_plugin_service_config(config: &PluginsConfigInput) -> RemotePluginServiceConfig { RemotePluginServiceConfig { chatgpt_base_url: config.chatgpt_base_url.clone(), } } fn featured_plugin_ids_cache_key( - config: &Config, + config: &PluginsConfigInput, auth: Option<&CodexAuth>, ) -> FeaturedPluginIdsCacheKey { FeaturedPluginIdsCacheKey { @@ -430,21 +456,21 @@ impl PluginsManager { } } - pub async fn plugins_for_config(&self, config: &Config) -> PluginLoadOutcome { + pub async fn plugins_for_config(&self, config: &PluginsConfigInput) -> PluginLoadOutcome { self.plugins_for_config_with_force_reload(config, /*force_reload*/ false) .await } pub(crate) async fn plugins_for_config_with_force_reload( &self, - config: &Config, + config: &PluginsConfigInput, force_reload: bool, ) -> PluginLoadOutcome { - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return PluginLoadOutcome::default(); } - let plugin_hooks_enabled = config.features.enabled(Feature::PluginHooks); + let plugin_hooks_enabled = config.plugin_hooks_enabled; let config_version = version_for_toml(&config.config_layer_stack.effective_config()); if !force_reload && let Some(outcome) = @@ -495,10 +521,10 @@ impl PluginsManager { pub async fn plugins_for_layer_stack( &self, config_layer_stack: &ConfigLayerStack, - config: &Config, + config: &PluginsConfigInput, plugin_hooks_feature_enabled: bool, ) -> PluginLoadOutcome { - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return PluginLoadOutcome::default(); } load_plugins_from_layer_stack( @@ -515,15 +541,11 @@ impl PluginsManager { pub async fn effective_skill_roots_for_layer_stack( &self, config_layer_stack: &ConfigLayerStack, - config: &Config, + config: &PluginsConfigInput, ) -> Vec { - self.plugins_for_layer_stack( - config_layer_stack, - config, - config.features.enabled(Feature::PluginHooks), - ) - .await - .effective_skill_roots() + self.plugins_for_layer_stack(config_layer_stack, config, config.plugin_hooks_enabled) + .await + .effective_skill_roots() } fn cached_enabled_outcome( @@ -550,8 +572,11 @@ impl PluginsManager { } } - fn remote_installed_plugin_configs(&self, config: &Config) -> HashMap { - if !config.features.enabled(Feature::RemotePlugin) { + fn remote_installed_plugin_configs( + &self, + config: &PluginsConfigInput, + ) -> HashMap { + if !config.remote_plugin_enabled { return HashMap::new(); } @@ -596,7 +621,7 @@ impl PluginsManager { pub fn maybe_start_remote_installed_plugins_cache_refresh( self: &Arc, - config: &Config, + config: &PluginsConfigInput, auth: Option, on_effective_plugins_changed: Option>, ) { @@ -610,7 +635,7 @@ impl PluginsManager { pub fn maybe_start_remote_installed_plugins_cache_refresh_after_mutation( self: &Arc, - config: &Config, + config: &PluginsConfigInput, auth: Option, on_effective_plugins_changed: Option>, ) { @@ -624,14 +649,12 @@ impl PluginsManager { fn maybe_start_remote_installed_plugins_cache_refresh_with_notify( self: &Arc, - config: &Config, + config: &PluginsConfigInput, auth: Option, notify: RemoteInstalledPluginsCacheRefreshNotify, on_effective_plugins_changed: Option>, ) { - if !config.features.enabled(Feature::Plugins) - || !config.features.enabled(Feature::RemotePlugin) - { + if !config.plugins_enabled || !config.remote_plugin_enabled { return; } @@ -647,7 +670,7 @@ impl PluginsManager { pub fn maybe_start_plugin_list_background_tasks_for_config( self: &Arc, - config: &Config, + config: &PluginsConfigInput, auth: Option, roots: &[AbsolutePathBuf], on_effective_plugins_changed: Option>, @@ -710,10 +733,10 @@ impl PluginsManager { pub async fn featured_plugin_ids_for_config( &self, - config: &Config, + config: &PluginsConfigInput, auth: Option<&CodexAuth>, ) -> Result, RemotePluginFetchError> { - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return Ok(Vec::new()); } @@ -721,13 +744,12 @@ impl PluginsManager { if let Some(featured_plugin_ids) = self.cached_featured_plugin_ids(&cache_key) { return Ok(featured_plugin_ids); } - let featured_plugin_ids = - codex_core_plugins::remote_legacy::fetch_remote_featured_plugin_ids( - &remote_plugin_service_config(config), - auth, - self.restriction_product, - ) - .await?; + let featured_plugin_ids = crate::remote_legacy::fetch_remote_featured_plugin_ids( + &remote_plugin_service_config(config), + auth, + self.restriction_product, + ) + .await?; self.write_featured_plugin_ids_cache(cache_key, &featured_plugin_ids); Ok(featured_plugin_ids) } @@ -746,7 +768,7 @@ impl PluginsManager { pub async fn install_plugin_with_remote_sync( &self, - config: &Config, + config: &PluginsConfigInput, auth: Option<&CodexAuth>, request: PluginInstallRequest, ) -> Result { @@ -757,7 +779,7 @@ impl PluginsManager { )?; let plugin_id = resolved.plugin_id.as_key(); // This only forwards the backend mutation before the local install flow. - codex_core_plugins::remote_legacy::enable_remote_plugin( + crate::remote_legacy::enable_remote_plugin( &remote_plugin_service_config(config), auth, &plugin_id, @@ -800,18 +822,13 @@ impl PluginsManager { .await .map_err(PluginInstallError::join)??; - ConfigEditsBuilder::new(&self.codex_home) - .with_edits([ConfigEdit::SetPath { - segments: vec![ - "plugins".to_string(), - result.plugin_id.as_key(), - "enabled".to_string(), - ], - value: value(true), - }]) - .apply() - .await - .map_err(PluginInstallError::from)?; + set_user_plugin_enabled( + &self.codex_home, + result.plugin_id.as_key(), + /*enabled*/ true, + ) + .await + .map_err(anyhow::Error::from)?; let analytics_events_client = match self.analytics_events_client.read() { Ok(client) => client.clone(), @@ -839,7 +856,7 @@ impl PluginsManager { pub async fn uninstall_plugin_with_remote_sync( &self, - config: &Config, + config: &PluginsConfigInput, auth: Option<&CodexAuth>, plugin_id: String, ) -> Result<(), PluginUninstallError> { @@ -848,7 +865,7 @@ impl PluginsManager { let plugin_id = PluginId::parse(&plugin_id)?; let plugin_key = plugin_id.as_key(); // This only forwards the backend mutation before the local uninstall flow. - codex_core_plugins::remote_legacy::uninstall_remote_plugin( + crate::remote_legacy::uninstall_remote_plugin( &remote_plugin_service_config(config), auth, &plugin_key, @@ -870,12 +887,9 @@ impl PluginsManager { .await .map_err(PluginUninstallError::join)??; - ConfigEditsBuilder::new(&self.codex_home) - .with_edits([ConfigEdit::ClearPath { - segments: vec!["plugins".to_string(), plugin_id.as_key()], - }]) - .apply() - .await?; + clear_user_plugin(&self.codex_home, plugin_id.as_key()) + .await + .map_err(anyhow::Error::from)?; let analytics_events_client = match self.analytics_events_client.read() { Ok(client) => client.clone(), @@ -892,7 +906,7 @@ impl PluginsManager { pub async fn sync_plugins_from_remote( &self, - config: &Config, + config: &PluginsConfigInput, auth: Option<&CodexAuth>, additive_only: bool, ) -> Result { @@ -900,12 +914,12 @@ impl PluginsManager { PluginRemoteSyncError::Config(anyhow::anyhow!("remote plugin sync semaphore closed")) })?; - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return Ok(RemotePluginSyncResult::default()); } info!("starting remote plugin sync"); - let remote_plugins = codex_core_plugins::remote_legacy::fetch_remote_plugin_status( + let remote_plugins = crate::remote_legacy::fetch_remote_plugin_status( &remote_plugin_service_config(config), auth, ) @@ -1051,9 +1065,9 @@ impl PluginsManager { if current_enabled != Some(true) { result.enabled_plugin_ids.push(plugin_key.clone()); - config_edits.push(ConfigEdit::SetPath { - segments: vec!["plugins".to_string(), plugin_key, "enabled".to_string()], - value: value(true), + config_edits.push(PluginConfigEdit::SetEnabled { + plugin_key, + enabled: true, }); } } else if !additive_only { @@ -1064,9 +1078,7 @@ impl PluginsManager { result.uninstalled_plugin_ids.push(plugin_key.clone()); } if current_enabled.is_some() { - config_edits.push(ConfigEdit::ClearPath { - segments: vec!["plugins".to_string(), plugin_key], - }); + config_edits.push(PluginConfigEdit::Clear { plugin_key }); } } } @@ -1091,13 +1103,10 @@ impl PluginsManager { let config_result = if config_edits.is_empty() { Ok(()) } else { - ConfigEditsBuilder::new(&self.codex_home) - .with_edits(config_edits) - .apply() - .await + apply_user_plugin_config_edits(&self.codex_home, config_edits).await }; self.clear_cache(); - config_result?; + config_result.map_err(anyhow::Error::from)?; info!( marketplace = %marketplace_name, @@ -1115,10 +1124,10 @@ impl PluginsManager { pub fn list_marketplaces_for_config( &self, - config: &Config, + config: &PluginsConfigInput, additional_roots: &[AbsolutePathBuf], ) -> Result { - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return Ok(ConfiguredMarketplaceListOutcome::default()); } @@ -1175,10 +1184,10 @@ impl PluginsManager { pub async fn read_plugin_for_config( &self, - config: &Config, + config: &PluginsConfigInput, request: &PluginReadRequest, ) -> Result { - if !config.features.enabled(Feature::Plugins) { + if !config.plugins_enabled { return Err(MarketplaceError::PluginsDisabled); } @@ -1216,9 +1225,9 @@ impl PluginsManager { }) } - pub(crate) async fn read_plugin_detail_for_marketplace_plugin( + pub async fn read_plugin_detail_for_marketplace_plugin( &self, - config: &Config, + config: &PluginsConfigInput, marketplace_name: &str, plugin: ConfiguredMarketplacePlugin, ) -> Result { @@ -1332,11 +1341,11 @@ impl PluginsManager { pub fn maybe_start_plugin_startup_tasks_for_config( self: &Arc, - config: &Config, + config: &PluginsConfigInput, auth_manager: Arc, on_effective_plugins_changed: Option>, ) { - if config.features.enabled(Feature::Plugins) { + if config.plugins_enabled { self.start_curated_repo_sync(); let should_spawn_marketplace_auto_upgrade = { let mut state = match self.configured_marketplace_upgrade_state.write() { @@ -1396,7 +1405,7 @@ impl PluginsManager { auth_manager.clone(), ); - if config.features.enabled(Feature::RemotePlugin) { + if config.remote_plugin_enabled { let config = config.clone(); let manager = Arc::clone(self); let auth_manager = auth_manager.clone(); @@ -1430,7 +1439,7 @@ impl PluginsManager { pub fn upgrade_configured_marketplaces_for_config( &self, - config: &Config, + config: &PluginsConfigInput, marketplace_name: Option<&str>, ) -> Result { if let Some(marketplace_name) = marketplace_name @@ -1648,7 +1657,7 @@ impl PluginsManager { } }; - let installed_plugins = codex_core_plugins::remote::fetch_remote_installed_plugins( + let installed_plugins = crate::remote::fetch_remote_installed_plugins( &request.service_config, request.auth.as_ref(), ) @@ -1751,7 +1760,10 @@ impl PluginsManager { } } - fn configured_plugin_states(&self, config: &Config) -> (HashSet, HashSet) { + fn configured_plugin_states( + &self, + config: &PluginsConfigInput, + ) -> (HashSet, HashSet) { let configured_plugins = configured_plugins_from_stack(&config.config_layer_stack); let installed_plugins = configured_plugins .keys() @@ -1771,7 +1783,7 @@ impl PluginsManager { fn marketplace_roots( &self, - config: &Config, + config: &PluginsConfigInput, additional_roots: &[AbsolutePathBuf], ) -> Vec { // Treat the curated catalog as an extra marketplace root so plugin listing can surface it diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs similarity index 98% rename from codex-rs/core/src/plugins/manager_tests.rs rename to codex-rs/core-plugins/src/manager_tests.rs index 73a77e69c..8abff7700 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -1,15 +1,22 @@ use super::*; -use crate::config::CONFIG_TOML_FILE; -use crate::config::ConfigBuilder; -use crate::plugins::LoadedPlugin; -use crate::plugins::PluginLoadOutcome; -use crate::plugins::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION; -use crate::plugins::test_support::TEST_CURATED_PLUGIN_SHA; -use crate::plugins::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha; -use crate::plugins::test_support::write_file; -use crate::plugins::test_support::write_openai_curated_marketplace; +use crate::LoadedPlugin; +use crate::PluginLoadOutcome; +use crate::installed_marketplaces::marketplace_install_root; +use crate::loader::load_plugins_from_layer_stack; +use crate::loader::refresh_non_curated_plugin_cache; +use crate::loader::refresh_non_curated_plugin_cache_force_reinstall; +use crate::marketplace::MarketplacePluginInstallPolicy; +use crate::remote::RemoteInstalledPlugin; +use crate::startup_sync::curated_plugins_repo_path; +use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION; +use crate::test_support::TEST_CURATED_PLUGIN_SHA; +use crate::test_support::load_plugins_config as load_plugins_config_input; +use crate::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha; +use crate::test_support::write_file; +use crate::test_support::write_openai_curated_marketplace; use codex_app_server_protocol::ConfigLayerSource; use codex_config::AppToolApproval; +use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirements; @@ -17,12 +24,6 @@ use codex_config::ConfigRequirementsToml; use codex_config::McpServerConfig; use codex_config::McpServerToolConfig; use codex_config::types::McpServerTransportConfig; -use codex_core_plugins::installed_marketplaces::marketplace_install_root; -use codex_core_plugins::loader::load_plugins_from_layer_stack; -use codex_core_plugins::loader::refresh_non_curated_plugin_cache; -use codex_core_plugins::loader::refresh_non_curated_plugin_cache_force_reinstall; -use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy; -use codex_core_plugins::startup_sync::curated_plugins_repo_path; use codex_login::CodexAuth; use codex_protocol::protocol::Product; use codex_utils_absolute_path::test_support::PathBufExt; @@ -140,13 +141,8 @@ async fn load_plugins_from_config(config_toml: &str, codex_home: &Path) -> Plugi .await } -async fn load_config(codex_home: &Path, cwd: &Path) -> crate::config::Config { - ConfigBuilder::default() - .codex_home(codex_home.to_path_buf()) - .fallback_cwd(Some(cwd.to_path_buf())) - .build() - .await - .expect("config should load") +async fn load_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput { + load_plugins_config_input(codex_home, cwd).await } #[tokio::test] @@ -350,14 +346,12 @@ remote_plugin = true let config = load_config(codex_home.path(), codex_home.path()).await; let manager = PluginsManager::new(codex_home.path().to_path_buf()); - manager.write_remote_installed_plugins_cache(vec![ - codex_core_plugins::remote::RemoteInstalledPlugin { - marketplace_name: "chatgpt-global".to_string(), - id: "plugins~Plugin_linear".to_string(), - name: "linear".to_string(), - enabled: true, - }, - ]); + manager.write_remote_installed_plugins_cache(vec![RemoteInstalledPlugin { + marketplace_name: "chatgpt-global".to_string(), + id: "plugins~Plugin_linear".to_string(), + name: "linear".to_string(), + enabled: true, + }]); let outcome = manager.plugins_for_config(&config).await; assert_eq!( @@ -381,14 +375,12 @@ remote_plugin = true let config = load_config(codex_home.path(), codex_home.path()).await; let manager = PluginsManager::new(codex_home.path().to_path_buf()); - manager.write_remote_installed_plugins_cache(vec![ - codex_core_plugins::remote::RemoteInstalledPlugin { - marketplace_name: "chatgpt-global".to_string(), - id: "plugins~Plugin_linear".to_string(), - name: "linear".to_string(), - enabled: true, - }, - ]); + manager.write_remote_installed_plugins_cache(vec![RemoteInstalledPlugin { + marketplace_name: "chatgpt-global".to_string(), + id: "plugins~Plugin_linear".to_string(), + name: "linear".to_string(), + enabled: true, + }]); let outcome = manager.plugins_for_config(&config).await; assert_eq!(outcome, PluginLoadOutcome::default()); @@ -423,7 +415,7 @@ enabled = false enabled = true "#; let outcome = load_plugins_from_config(config_toml, codex_home.path()).await; - let skill_path = dunce::canonicalize(skill_path) + let skill_path = std::fs::canonicalize(skill_path) .expect("skill path should canonicalize") .abs(); diff --git a/codex-rs/core/src/plugins/startup_sync.rs b/codex-rs/core-plugins/src/startup_remote_sync.rs similarity index 93% rename from codex-rs/core/src/plugins/startup_sync.rs rename to codex-rs/core-plugins/src/startup_remote_sync.rs index 31cf4c75e..90c0e119c 100644 --- a/codex-rs/core/src/plugins/startup_sync.rs +++ b/codex-rs/core-plugins/src/startup_remote_sync.rs @@ -3,9 +3,9 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use crate::config::Config; -use crate::plugins::PluginsManager; -use codex_core_plugins::startup_sync::has_local_curated_plugins_snapshot; +use crate::manager::PluginsConfigInput; +use crate::manager::PluginsManager; +use crate::startup_sync::has_local_curated_plugins_snapshot; use codex_login::AuthManager; use tracing::info; use tracing::warn; @@ -16,7 +16,7 @@ const STARTUP_REMOTE_PLUGIN_SYNC_PREREQUISITE_TIMEOUT: Duration = Duration::from pub(crate) fn start_startup_remote_plugin_sync_once( manager: Arc, codex_home: PathBuf, - config: Config, + config: PluginsConfigInput, auth_manager: Arc, ) { let marker_path = startup_remote_plugin_sync_marker_path(codex_home.as_path()); @@ -96,5 +96,5 @@ async fn write_startup_remote_plugin_sync_marker(codex_home: &Path) -> std::io:: } #[cfg(test)] -#[path = "startup_sync_tests.rs"] +#[path = "startup_remote_sync_tests.rs"] mod tests; diff --git a/codex-rs/core/src/plugins/startup_sync_tests.rs b/codex-rs/core-plugins/src/startup_remote_sync_tests.rs similarity index 84% rename from codex-rs/core/src/plugins/startup_sync_tests.rs rename to codex-rs/core-plugins/src/startup_remote_sync_tests.rs index fb79d65ae..bdbc5e1a7 100644 --- a/codex-rs/core/src/plugins/startup_sync_tests.rs +++ b/codex-rs/core-plugins/src/startup_remote_sync_tests.rs @@ -1,11 +1,12 @@ use super::*; -use crate::config::CONFIG_TOML_FILE; -use crate::plugins::PluginsManager; -use crate::plugins::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION; -use crate::plugins::test_support::write_curated_plugin_sha; -use crate::plugins::test_support::write_file; -use crate::plugins::test_support::write_openai_curated_marketplace; -use codex_core_plugins::startup_sync::curated_plugins_repo_path; +use crate::PluginsManager; +use crate::startup_sync::curated_plugins_repo_path; +use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION; +use crate::test_support::load_plugins_config; +use crate::test_support::write_curated_plugin_sha; +use crate::test_support::write_file; +use crate::test_support::write_openai_curated_marketplace; +use codex_config::CONFIG_TOML_FILE; use codex_login::AuthManager; use codex_login::CodexAuth; use pretty_assertions::assert_eq; @@ -48,7 +49,7 @@ enabled = false .mount(&server) .await; - let mut config = crate::plugins::test_support::load_plugins_config(tmp.path()).await; + let mut config = load_plugins_config(tmp.path(), tmp.path()).await; config.chatgpt_base_url = format!("{}/backend-api/", server.uri()); let manager = Arc::new(PluginsManager::new(tmp.path().to_path_buf())); let auth_manager = diff --git a/codex-rs/core-plugins/src/test_support.rs b/codex-rs/core-plugins/src/test_support.rs new file mode 100644 index 000000000..6be2fbf0d --- /dev/null +++ b/codex-rs/core-plugins/src/test_support.rs @@ -0,0 +1,139 @@ +use std::fs; +use std::path::Path; + +use crate::OPENAI_CURATED_MARKETPLACE_NAME; +use crate::PluginsConfigInput; +use codex_config::CloudRequirementsLoader; +use codex_config::LoaderOverrides; +use codex_config::NoopThreadConfigLoader; +use codex_config::loader::load_config_layers_state; +use codex_exec_server::LOCAL_FS; +use codex_utils_absolute_path::AbsolutePathBuf; +use toml::Value; + +pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; +pub(crate) const TEST_CURATED_PLUGIN_CACHE_VERSION: &str = "01234567"; + +pub(crate) fn write_file(path: &Path, contents: &str) { + fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap(); + fs::write(path, contents).unwrap(); +} + +pub(crate) fn write_curated_plugin(root: &Path, plugin_name: &str) { + let plugin_root = root.join("plugins").join(plugin_name); + write_file( + &plugin_root.join(".codex-plugin/plugin.json"), + &format!( + r#"{{ + "name": "{plugin_name}", + "description": "Plugin that includes skills, MCP servers, and app connectors" +}}"# + ), + ); + write_file( + &plugin_root.join("skills/SKILL.md"), + "---\nname: sample\ndescription: sample\n---\n", + ); + write_file( + &plugin_root.join(".mcp.json"), + r#"{ + "mcpServers": { + "sample-docs": { + "type": "http", + "url": "https://sample.example/mcp" + } + } +}"#, + ); + write_file( + &plugin_root.join(".app.json"), + r#"{ + "apps": { + "calendar": { + "id": "connector_calendar" + } + } +}"#, + ); +} + +pub(crate) fn write_openai_curated_marketplace(root: &Path, plugin_names: &[&str]) { + let plugins = plugin_names + .iter() + .map(|plugin_name| { + format!( + r#"{{ + "name": "{plugin_name}", + "source": {{ + "source": "local", + "path": "./plugins/{plugin_name}" + }} + }}"# + ) + }) + .collect::>() + .join(",\n"); + write_file( + &root.join(".agents/plugins/marketplace.json"), + &format!( + r#"{{ + "name": "{OPENAI_CURATED_MARKETPLACE_NAME}", + "plugins": [ +{plugins} + ] +}}"# + ), + ); + for plugin_name in plugin_names { + write_curated_plugin(root, plugin_name); + } +} + +pub(crate) fn write_curated_plugin_sha(codex_home: &Path) { + write_curated_plugin_sha_with(codex_home, TEST_CURATED_PLUGIN_SHA); +} + +pub(crate) fn write_curated_plugin_sha_with(codex_home: &Path, sha: &str) { + write_file(&codex_home.join(".tmp/plugins.sha"), &format!("{sha}\n")); +} + +pub(crate) async fn load_plugins_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput { + let codex_home = AbsolutePathBuf::try_from(codex_home).expect("codex home should be absolute"); + let cwd = AbsolutePathBuf::try_from(cwd).expect("cwd should be absolute"); + let config_layer_stack = load_config_layers_state( + LOCAL_FS.as_ref(), + codex_home.as_path(), + Some(cwd), + &[], + LoaderOverrides::without_managed_config_for_tests(), + CloudRequirementsLoader::default(), + &NoopThreadConfigLoader, + ) + .await + .expect("config should load"); + let effective_config = config_layer_stack.effective_config(); + PluginsConfigInput::new( + config_layer_stack, + feature_enabled(&effective_config, "plugins", /*default_enabled*/ true), + feature_enabled( + &effective_config, + "remote_plugin", + /*default_enabled*/ false, + ), + feature_enabled( + &effective_config, + "plugin_hooks", + /*default_enabled*/ false, + ), + "https://chatgpt.com/backend-api/".to_string(), + ) +} + +fn feature_enabled(config: &Value, key: &str, default_enabled: bool) -> bool { + config + .get("features") + .and_then(Value::as_table) + .and_then(|features| features.get(key)) + .and_then(Value::as_bool) + .unwrap_or(default_enabled) +} diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index eceaaa920..2550d58f8 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -2,9 +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_core_plugins::PluginsManager; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::Verbosity; use codex_protocol::openai_models::ReasoningEffort; @@ -655,7 +655,8 @@ enabled = false let plugins_manager = Arc::new(PluginsManager::new(home.path().to_path_buf())); let skills_manager = SkillsManager::new(home.path().abs(), /*bundled_skills_enabled*/ true); - let plugin_outcome = plugins_manager.plugins_for_config(&config).await; + let plugins_input = config.plugins_config_input(); + let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); let outcome = skills_manager diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 6c39f1f1e..bf64792d8 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 fba79069d..5199844b7 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -54,6 +54,7 @@ use codex_config::types::TuiKeymap; use codex_config::types::TuiNotificationSettings; use codex_config::types::UriBasedFileOpener; use codex_config::types::WindowsSandboxModeToml; +use codex_core_plugins::PluginsConfigInput; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; use codex_features::AppsMcpPathOverrideConfigToml; @@ -995,11 +996,23 @@ impl Config { } } + /// Build the plugin-manager input from the effective config. + pub fn plugins_config_input(&self) -> PluginsConfigInput { + PluginsConfigInput::new( + self.config_layer_stack.clone(), + self.features.enabled(Feature::Plugins), + self.features.enabled(Feature::RemotePlugin), + self.features.enabled(Feature::PluginHooks), + self.chatgpt_base_url.clone(), + ) + } + 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).await; + let plugins_input = self.plugins_config_input(); + let loaded_plugins = plugins_manager.plugins_for_config(&plugins_input).await; let mut configured_mcp_servers = self.mcp_servers.get().clone(); for plugin in loaded_plugins .plugins() diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index fff7b4c35..b83be7dc8 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -26,13 +26,13 @@ use tracing::warn; use crate::config::Config; use crate::mcp::McpManager; -use crate::plugins::PluginsManager; use crate::plugins::list_tool_suggest_discoverable_plugins; 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; @@ -403,8 +403,9 @@ fn write_cached_accessible_connectors( } async fn tool_suggest_connector_ids(config: &Config) -> HashSet { + let plugins_input = config.plugins_config_input(); let mut connector_ids = PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config(config) + .plugins_for_config(&plugins_input) .await .capability_summaries() .iter() diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 414a587a2..6a61079a3 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -67,7 +67,7 @@ pub use message_history::history_metadata as message_history_metadata; pub use message_history::lookup as lookup_message_history_entry; pub use utils::path_utils; pub mod personality_migration; -pub mod plugins; +pub(crate) mod plugins; #[doc(hidden)] pub(crate) mod prompt_debug; #[doc(hidden)] diff --git a/codex-rs/core/src/mcp.rs b/codex-rs/core/src/mcp.rs index 0d4c26991..60e325c4e 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 d2e990f44..ed8451e35 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -804,7 +804,7 @@ async fn custom_mcp_tool_approval_mode( sess.services .plugins_manager - .plugins_for_config(turn_context.config.as_ref()) + .plugins_for_config(&turn_context.config.plugins_config_input()) .await .plugins() .iter() @@ -1853,7 +1853,7 @@ async fn persist_non_app_mcp_tool_approval( let plugin_config_name = sess .services .plugins_manager - .plugins_for_config(config) + .plugins_for_config(&config.plugins_config_input()) .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 f9244a7fa..6a1412668 100644 --- a/codex-rs/core/src/mcp_tool_call_tests.rs +++ b/codex-rs/core/src/mcp_tool_call_tests.rs @@ -1660,7 +1660,7 @@ enabled = true session .services .plugins_manager - .plugins_for_config(&initial_config) + .plugins_for_config(&initial_config.plugins_config_input()) .await; std::fs::write( codex_home.join(CONFIG_TOML_FILE), diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index ba14f8704..3e328165b 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -3,11 +3,11 @@ use std::collections::HashSet; use tracing::warn; use super::PluginCapabilitySummary; -use super::PluginsManager; use crate::config::Config; use codex_config::types::ToolSuggestDiscoverableType; use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME; use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; +use codex_core_plugins::PluginsManager; use codex_core_plugins::TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST; use codex_features::Feature; use codex_tools::DiscoverablePluginInfo; @@ -25,6 +25,7 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins( } let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf()); + let plugins_input = config.plugins_config_input(); let configured_plugin_ids = config .tool_suggest .discoverables @@ -40,7 +41,7 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins( .map(|disabled_tool| disabled_tool.id.as_str()) .collect::>(); let marketplaces = plugins_manager - .list_marketplaces_for_config(config, &[]) + .list_marketplaces_for_config(&plugins_input, &[]) .context("failed to list plugin marketplaces for tool suggestions")? .marketplaces; let mut discoverable_plugins = Vec::::new(); @@ -62,7 +63,11 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins( let plugin_id = plugin.id.clone(); match plugins_manager - .read_plugin_detail_for_marketplace_plugin(config, &marketplace_name, plugin) + .read_plugin_detail_for_marketplace_plugin( + &plugins_input, + &marketplace_name, + plugin, + ) .await { Ok(plugin) => { diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index f9e385fa8..276e822ae 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -1,11 +1,11 @@ use super::*; -use crate::plugins::PluginInstallRequest; use crate::plugins::test_support::load_plugins_config; use crate::plugins::test_support::write_curated_plugin; use crate::plugins::test_support::write_curated_plugin_sha; use crate::plugins::test_support::write_file; use crate::plugins::test_support::write_openai_curated_marketplace; use crate::plugins::test_support::write_plugins_feature_config; +use codex_core_plugins::PluginInstallRequest; use codex_core_plugins::startup_sync::curated_plugins_repo_path; use codex_tools::DiscoverablePluginInfo; use codex_utils_absolute_path::AbsolutePathBuf; diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index ff3183557..11e2c338b 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -1,43 +1,14 @@ -use codex_config::types::McpServerConfig; - mod discoverable; mod injection; -mod manager; mod mentions; mod render; -mod startup_sync; #[cfg(test)] pub(crate) mod test_support; -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; -pub use codex_plugin::PluginId; -pub use codex_plugin::PluginIdError; -pub use codex_plugin::PluginTelemetryMetadata; -pub use codex_plugin::validate_plugin_segment; - -pub type LoadedPlugin = codex_plugin::LoadedPlugin; -pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome; +pub(crate) use codex_plugin::PluginCapabilitySummary; pub(crate) use discoverable::list_tool_suggest_discoverable_plugins; pub(crate) use injection::build_plugin_injections; -pub use manager::ConfiguredMarketplace; -pub use manager::ConfiguredMarketplaceListOutcome; -pub use manager::ConfiguredMarketplacePlugin; -pub use manager::PluginDetail; -pub use manager::PluginDetailsUnavailableReason; -pub use manager::PluginInstallError; -pub use manager::PluginInstallOutcome; -pub use manager::PluginInstallRequest; -pub use manager::PluginReadOutcome; -pub use manager::PluginReadRequest; -pub use manager::PluginRemoteSyncError; -pub use manager::PluginUninstallError; -pub use manager::PluginsManager; -pub use manager::RemotePluginSyncResult; pub(crate) use render::render_explicit_plugin_instructions; pub(crate) use mentions::build_connector_slug_counts; diff --git a/codex-rs/core/src/plugins/test_support.rs b/codex-rs/core/src/plugins/test_support.rs index 8c1280614..8fbaebb80 100644 --- a/codex-rs/core/src/plugins/test_support.rs +++ b/codex-rs/core/src/plugins/test_support.rs @@ -6,7 +6,6 @@ use std::path::Path; use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; -pub(crate) const TEST_CURATED_PLUGIN_CACHE_VERSION: &str = "01234567"; pub(crate) fn write_file(path: &Path, contents: &str) { fs::create_dir_all(path.parent().expect("file should have a parent")).unwrap(); diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 624331ea6..4baabd56c 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -620,8 +620,9 @@ pub async fn list_skills(sess: &Session, sub_id: String, cwds: Vec, for continue; } }; + let plugins_input = config.plugins_config_input(); let effective_skill_roots = plugins_manager - .effective_skill_roots_for_layer_stack(&config_layer_stack, &config) + .effective_skill_roots_for_layer_stack(&config_layer_stack, &plugins_input) .await; let skills_input = crate::SkillsLoadInput::new( cwd_abs.clone(), diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e45db2084..8333fd2dd 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; @@ -475,7 +475,8 @@ impl Codex { let fs = environment .as_ref() .map(|environment| environment.get_filesystem()); - let plugin_outcome = plugins_manager.plugins_for_config(&config).await; + let plugins_input = config.plugins_config_input(); + let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&config, effective_skill_roots); let loaded_skills = skills_manager.skills_for_config(&skills_input, fs).await; @@ -2666,7 +2667,7 @@ impl Session { let loaded_plugins = self .services .plugins_manager - .plugins_for_config(&turn_context.config) + .plugins_for_config(&turn_context.config.plugins_config_input()) .await; if let Some(plugin_instructions) = AvailablePluginsInstructions::from_plugins(loaded_plugins.capability_summaries()) @@ -3362,7 +3363,8 @@ async fn build_hooks_for_config( let _ = hook_shell_argv.pop(); 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).await; + let plugins_input = config.plugins_config_input(); + let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; ( plugin_outcome.effective_plugin_hook_sources(), plugin_outcome.effective_plugin_hook_warnings(), diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 1afb28f35..068e47bd7 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -3558,7 +3558,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { let plugin_outcome = services .plugins_manager - .plugins_for_config(&per_turn_config) + .plugins_for_config(&per_turn_config.plugins_config_input()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = @@ -4987,7 +4987,7 @@ where let plugin_outcome = services .plugins_manager - .plugins_for_config(&per_turn_config) + .plugins_for_config(&per_turn_config.plugins_config_input()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 07e4e3004..49dc1eb10 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -170,7 +170,7 @@ pub(crate) async fn run_turn( let loaded_plugins = sess .services .plugins_manager - .plugins_for_config(&turn_context.config) + .plugins_for_config(&turn_context.config.plugins_config_input()) .await; // Structured plugin:// mentions are resolved from the current session's // enabled plugins, then converted into turn-scoped guidance below. @@ -1124,7 +1124,7 @@ pub(crate) async fn built_tools( let loaded_plugins = sess .services .plugins_manager - .plugins_for_config(&turn_context.config) + .plugins_for_config(&turn_context.config.plugins_config_input()) .await; let mut effective_explicitly_enabled_connectors = explicitly_enabled_connectors.clone(); diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 09690f93d..410e16703 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -696,7 +696,7 @@ impl Session { let plugin_outcome = self .services .plugins_manager - .plugins_for_config(&per_turn_config) + .plugins_for_config(&per_turn_config.plugins_config_input()) .await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(&per_turn_config, effective_skill_roots); diff --git a/codex-rs/core/src/skills_watcher.rs b/codex-rs/core/src/skills_watcher.rs index 9473282b1..d97b41f1d 100644 --- a/codex-rs/core/src/skills_watcher.rs +++ b/codex-rs/core/src/skills_watcher.rs @@ -16,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); @@ -61,7 +61,8 @@ impl SkillsWatcher { plugins_manager: &PluginsManager, fs: Option>, ) -> WatchRegistration { - let plugin_outcome = plugins_manager.plugins_for_config(config).await; + let plugins_input = config.plugins_config_input(); + let plugin_outcome = plugins_manager.plugins_for_config(&plugins_input).await; let effective_skill_roots = plugin_outcome.effective_skill_roots(); let skills_input = skills_load_input_from_config(config, effective_skill_roots); let roots = skills_manager diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index ddf212217..9cd9e97fb 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 bec12839b..085edfd53 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 e0fa9156f..7f74703ef 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest.rs @@ -314,10 +314,11 @@ 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 { + let plugins_input = config.plugins_config_input(); plugins_manager - .list_marketplaces_for_config(config, &[]) + .list_marketplaces_for_config(&plugins_input, &[]) .ok() .into_iter() .flat_map(|outcome| outcome.marketplaces) 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 2d482874c..65fd2f3a2 100644 --- a/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs +++ b/codex-rs/core/src/tools/handlers/tool_suggest_tests.rs @@ -1,6 +1,4 @@ use super::*; -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; @@ -11,6 +9,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.rs b/codex-rs/tui/src/app.rs index c188ff6b2..c2fb076d3 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -48,7 +48,6 @@ use crate::legacy_core::config::ConfigOverrides; use crate::legacy_core::config::edit::ConfigEdit; use crate::legacy_core::config::edit::ConfigEditsBuilder; use crate::legacy_core::lookup_message_history_entry; -use crate::legacy_core::plugins::PluginsManager; #[cfg(target_os = "windows")] use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::model_catalog::ModelCatalog; @@ -126,6 +125,7 @@ use codex_app_server_protocol::TurnStatus; use codex_config::ConfigLayerStackOrdering; use codex_config::types::ApprovalsReviewer; use codex_config::types::ModelAvailabilityNuxConfig; +use codex_core_plugins::PluginsManager; use codex_exec_server::EnvironmentManager; use codex_features::Feature; use codex_model_provider::create_model_provider; diff --git a/codex-rs/tui/src/app/background_requests.rs b/codex-rs/tui/src/app/background_requests.rs index a8992be71..95bdea006 100644 --- a/codex-rs/tui/src/app/background_requests.rs +++ b/codex-rs/tui/src/app/background_requests.rs @@ -307,8 +307,9 @@ impl App { } tokio::spawn(async move { + let plugins_input = config.plugins_config_input(); let plugins = PluginsManager::new(config.codex_home.to_path_buf()) - .plugins_for_config(&config) + .plugins_for_config(&plugins_input) .await .capability_summaries() .to_vec();