From 7cc6d8ad196b043125e166dcc5ebc2af7a41877a Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 25 Mar 2026 00:05:36 -0700 Subject: [PATCH] fix --- codex-rs/Cargo.lock | 34 + codex-rs/Cargo.toml | 4 + codex-rs/app-server/src/message_processor.rs | 7 +- codex-rs/config/src/lib.rs | 3 + codex-rs/core-skills/Cargo.toml | 3 + codex-rs/core-skills/src/skills/injection.rs | 14 +- .../src/skills/invocation_utils.rs | 6 +- codex-rs/core-skills/src/skills/loader.rs | 8 +- codex-rs/core-skills/src/skills/mod.rs | 2 + codex-rs/core/Cargo.toml | 2 + codex-rs/core/src/analytics_client.rs | 777 ------------------ codex-rs/core/src/analytics_client_tests.rs | 289 ------- codex-rs/core/src/codex.rs | 11 +- codex-rs/core/src/codex_tests.rs | 10 +- codex-rs/core/src/config_loader/mod.rs | 53 +- codex-rs/core/src/contextual_user_message.rs | 60 +- codex-rs/core/src/instructions/mod.rs | 8 +- .../src/instructions/user_instructions.rs | 57 -- .../instructions/user_instructions_tests.rs | 68 -- codex-rs/core/src/lib.rs | 12 +- codex-rs/core/src/mcp_tool_call.rs | 6 +- codex-rs/core/src/mention_syntax.rs | 6 +- codex-rs/core/src/mentions.rs | 136 --- codex-rs/core/src/mentions_tests.rs | 155 ---- codex-rs/core/src/plugins/manager.rs | 12 +- codex-rs/core/src/plugins/manager_tests.rs | 18 - codex-rs/core/src/plugins/manifest.rs | 3 +- codex-rs/core/src/plugins/mod.rs | 9 +- codex-rs/core/src/plugins/store.rs | 2 +- codex-rs/core/src/state/service.rs | 2 +- codex-rs/plugin/Cargo.toml | 5 + codex-rs/plugin/src/lib.rs | 5 + 32 files changed, 121 insertions(+), 1666 deletions(-) delete mode 100644 codex-rs/core/src/analytics_client.rs delete mode 100644 codex-rs/core/src/analytics_client_tests.rs delete mode 100644 codex-rs/core/src/instructions/user_instructions.rs delete mode 100644 codex-rs/core/src/instructions/user_instructions_tests.rs delete mode 100644 codex-rs/core/src/mentions.rs delete mode 100644 codex-rs/core/src/mentions_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index eef8be97a3..a0d713e0b9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1384,6 +1384,23 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21" +[[package]] +name = "codex-analytics" +version = "0.0.0" +dependencies = [ + "codex-git-utils", + "codex-login", + "codex-plugin", + "codex-protocol", + "pretty_assertions", + "reqwest", + "serde", + "serde_json", + "sha1", + "tokio", + "tracing", +] + [[package]] name = "codex-ansi-escape" version = "0.0.0" @@ -1870,6 +1887,7 @@ dependencies = [ "chardetng", "chrono", "clap", + "codex-analytics", "codex-api", "codex-app-server-protocol", "codex-apply-patch", @@ -1885,6 +1903,7 @@ dependencies = [ "codex-features", "codex-git-utils", "codex-hooks", + "codex-instructions", "codex-login", "codex-network-proxy", "codex-otel", @@ -1979,10 +1998,13 @@ name = "codex-core-skills" version = "0.0.0" dependencies = [ "anyhow", + "codex-analytics", "codex-app-server-protocol", "codex-config", + "codex-instructions", "codex-login", "codex-otel", + "codex-plugin", "codex-protocol", "codex-skills", "codex-utils-absolute-path", @@ -2201,6 +2223,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "codex-instructions" +version = "0.0.0" +dependencies = [ + "codex-protocol", + "pretty_assertions", + "serde", +] + [[package]] name = "codex-keyring-store" version = "0.0.0" @@ -2414,6 +2445,9 @@ dependencies = [ name = "codex-plugin" version = "0.0.0" dependencies = [ + "serde", + "serde_json", + "tempfile", "thiserror 2.0.18", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 14d48cc210..97e520e320 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "analytics", "backend-client", "ansi-escape", "async-utils", @@ -26,6 +27,7 @@ members = [ "core", "core-skills", "hooks", + "instructions", "secrets", "exec", "exec-server", @@ -96,6 +98,7 @@ license = "Apache-2.0" # Internal app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } +codex-analytics = { path = "analytics" } codex-api = { path = "codex-api" } codex-artifacts = { path = "artifacts" } codex-code-mode = { path = "code-mode" } @@ -125,6 +128,7 @@ codex-features = { path = "features" } codex-file-search = { path = "file-search" } codex-git-utils = { path = "git-utils" } codex-hooks = { path = "hooks" } +codex-instructions = { path = "instructions" } codex-keyring-store = { path = "keyring-store" } codex-linux-sandbox = { path = "linux-sandbox" } codex-lmstudio = { path = "lmstudio" } diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index a67c810bd0..7f7123895c 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -220,8 +220,11 @@ impl MessageProcessor { auth_manager.set_external_auth_refresher(Arc::new(ExternalAuthRefreshBridge { outgoing: outgoing.clone(), })); - let analytics_events_client = - AnalyticsEventsClient::new(Arc::clone(&config), Arc::clone(&auth_manager)); + let analytics_events_client = AnalyticsEventsClient::new( + Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, + ); thread_manager .plugins_manager() .set_analytics_events_client(analytics_events_client.clone()); diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 3c0f66beb6..7bd572fea3 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -5,6 +5,7 @@ mod diagnostics; mod fingerprint; mod merge; mod overrides; +mod project_root_markers; mod requirements_exec_policy; mod skills_config; mod state; @@ -47,6 +48,8 @@ pub use diagnostics::io_error_from_config_error; pub use fingerprint::version_for_toml; pub use merge::merge_toml_values; pub use overrides::build_cli_overrides_layer; +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; pub use requirements_exec_policy::RequirementsExecPolicyDecisionToml; pub use requirements_exec_policy::RequirementsExecPolicyParseError; diff --git a/codex-rs/core-skills/Cargo.toml b/codex-rs/core-skills/Cargo.toml index 484ed7d2b5..4f4d23cc19 100644 --- a/codex-rs/core-skills/Cargo.toml +++ b/codex-rs/core-skills/Cargo.toml @@ -14,10 +14,13 @@ workspace = true [dependencies] anyhow = { workspace = true } +codex-analytics = { workspace = true } codex-app-server-protocol = { workspace = true } codex-config = { workspace = true } +codex-instructions = { workspace = true } codex-login = { workspace = true } codex-otel = { workspace = true } +codex-plugin = { workspace = true } codex-protocol = { workspace = true } codex-skills = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/core-skills/src/skills/injection.rs b/codex-rs/core-skills/src/skills/injection.rs index b83be2322c..657be4016f 100644 --- a/codex-rs/core-skills/src/skills/injection.rs +++ b/codex-rs/core-skills/src/skills/injection.rs @@ -2,15 +2,15 @@ use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; -use crate::analytics_client::AnalyticsEventsClient; -use crate::analytics_client::InvocationType; -use crate::analytics_client::SkillInvocation; -use crate::analytics_client::TrackEventsContext; -use crate::instructions::SkillInstructions; -use crate::mention_syntax::TOOL_MENTION_SIGIL; -use crate::mentions::build_skill_name_counts; use crate::skills::SkillMetadata; +use crate::skills::build_skill_name_counts; +use codex_analytics::AnalyticsEventsClient; +use codex_analytics::InvocationType; +use codex_analytics::SkillInvocation; +use codex_analytics::TrackEventsContext; +use codex_instructions::SkillInstructions; use codex_otel::SessionTelemetry; +use codex_plugin::mention_syntax::TOOL_MENTION_SIGIL; use codex_protocol::models::ResponseItem; use codex_protocol::user_input::UserInput; use tokio::fs; diff --git a/codex-rs/core-skills/src/skills/invocation_utils.rs b/codex-rs/core-skills/src/skills/invocation_utils.rs index 122158bda7..084670fa86 100644 --- a/codex-rs/core-skills/src/skills/invocation_utils.rs +++ b/codex-rs/core-skills/src/skills/invocation_utils.rs @@ -2,13 +2,13 @@ use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; -use crate::analytics_client::InvocationType; -use crate::analytics_client::SkillInvocation; -use crate::analytics_client::build_track_events_context; use crate::codex::Session; use crate::codex::TurnContext; use crate::skills::SkillLoadOutcome; use crate::skills::SkillMetadata; +use codex_analytics::InvocationType; +use codex_analytics::SkillInvocation; +use codex_analytics::build_track_events_context; pub(crate) fn build_implicit_skill_path_indexes( skills: Vec, diff --git a/codex-rs/core-skills/src/skills/loader.rs b/codex-rs/core-skills/src/skills/loader.rs index 726efb0db4..f2e650594f 100644 --- a/codex-rs/core-skills/src/skills/loader.rs +++ b/codex-rs/core-skills/src/skills/loader.rs @@ -1,7 +1,3 @@ -use crate::config_loader::default_project_root_markers; -use crate::config_loader::merge_toml_values; -use crate::config_loader::project_root_markers_from_config; -use crate::plugins::plugin_namespace_for_skill_path; use crate::skills::model::SkillDependencies; use crate::skills::model::SkillError; use crate::skills::model::SkillInterface; @@ -14,6 +10,10 @@ use crate::skills::system::system_cache_root_dir; use codex_app_server_protocol::ConfigLayerSource; use codex_config::ConfigLayerStack; use codex_config::ConfigLayerStackOrdering; +use codex_config::default_project_root_markers; +use codex_config::merge_toml_values; +use codex_config::project_root_markers_from_config; +use codex_plugin::plugin_namespace_for_skill_path; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::MacOsSeatbeltProfileExtensions; use codex_protocol::models::NetworkPermissions; diff --git a/codex-rs/core-skills/src/skills/mod.rs b/codex-rs/core-skills/src/skills/mod.rs index 870b398cae..264770a9f7 100644 --- a/codex-rs/core-skills/src/skills/mod.rs +++ b/codex-rs/core-skills/src/skills/mod.rs @@ -4,6 +4,7 @@ pub mod injection; pub(crate) mod invocation_utils; pub mod loader; pub mod manager; +mod mention_counts; pub mod model; pub mod remote; pub mod render; @@ -11,6 +12,7 @@ pub mod system; pub(crate) use invocation_utils::build_implicit_skill_path_indexes; pub use manager::SkillsManager; +pub use mention_counts::build_skill_name_counts; pub use model::SkillError; pub use model::SkillLoadOutcome; pub use model::SkillMetadata; diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 1ff094b9e2..6727f985d7 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -27,6 +27,7 @@ bm25 = { workspace = true } chardetng = { workspace = true } chrono = { workspace = true, features = ["serde"] } clap = { workspace = true, features = ["derive"] } +codex-analytics = { workspace = true } codex-api = { workspace = true } codex-app-server-protocol = { workspace = true } codex-apply-patch = { workspace = true } @@ -43,6 +44,7 @@ codex-skills = { workspace = true } codex-execpolicy = { workspace = true } codex-git-utils = { workspace = true } codex-hooks = { workspace = true } +codex-instructions = { workspace = true } codex-network-proxy = { workspace = true } codex-otel = { workspace = true } codex-artifacts = { workspace = true } diff --git a/codex-rs/core/src/analytics_client.rs b/codex-rs/core/src/analytics_client.rs deleted file mode 100644 index 2506080878..0000000000 --- a/codex-rs/core/src/analytics_client.rs +++ /dev/null @@ -1,777 +0,0 @@ -use crate::AuthManager; -use crate::default_client::create_client; -use crate::plugins::PluginTelemetryMetadata; -use codex_git_utils::collect_git_info; -use codex_git_utils::get_git_repo_root; -use codex_protocol::protocol::SkillScope; -use serde::Serialize; -use sha1::Digest; -use sha1::Sha1; -use std::collections::HashSet; -use std::path::Path; -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::Mutex; -use std::time::Duration; -use tokio::sync::mpsc; - -#[derive(Clone)] -pub(crate) struct TrackEventsContext { - pub(crate) model_slug: String, - pub(crate) thread_id: String, - pub(crate) turn_id: String, -} - -pub(crate) fn build_track_events_context( - model_slug: String, - thread_id: String, - turn_id: String, -) -> TrackEventsContext { - TrackEventsContext { - model_slug, - thread_id, - turn_id, - } -} - -#[derive(Clone, Debug)] -pub(crate) struct SkillInvocation { - pub(crate) skill_name: String, - pub(crate) skill_scope: SkillScope, - pub(crate) skill_path: PathBuf, - pub(crate) invocation_type: InvocationType, -} - -#[derive(Clone, Copy, Debug, Serialize)] -#[serde(rename_all = "lowercase")] -pub(crate) enum InvocationType { - Explicit, - Implicit, -} - -pub(crate) struct AppInvocation { - pub(crate) connector_id: Option, - pub(crate) app_name: Option, - pub(crate) invocation_type: Option, -} - -#[derive(Clone)] -pub(crate) struct AnalyticsEventsQueue { - sender: mpsc::Sender, - app_used_emitted_keys: Arc>>, - plugin_used_emitted_keys: Arc>>, -} - -#[derive(Clone)] -pub struct AnalyticsEventsClient { - queue: AnalyticsEventsQueue, - analytics_enabled: Option, - base_url: String, -} - -impl AnalyticsEventsQueue { - pub(crate) fn new(auth_manager: Arc) -> Self { - let (sender, mut receiver) = mpsc::channel(ANALYTICS_EVENTS_QUEUE_SIZE); - tokio::spawn(async move { - while let Some(job) = receiver.recv().await { - match job { - TrackEventsJob::SkillInvocations(job) => { - send_track_skill_invocations(&auth_manager, job).await; - } - TrackEventsJob::AppMentioned(job) => { - send_track_app_mentioned(&auth_manager, job).await; - } - TrackEventsJob::AppUsed(job) => { - send_track_app_used(&auth_manager, job).await; - } - TrackEventsJob::PluginUsed(job) => { - send_track_plugin_used(&auth_manager, job).await; - } - TrackEventsJob::PluginInstalled(job) => { - send_track_plugin_installed(&auth_manager, job).await; - } - TrackEventsJob::PluginUninstalled(job) => { - send_track_plugin_uninstalled(&auth_manager, job).await; - } - TrackEventsJob::PluginEnabled(job) => { - send_track_plugin_enabled(&auth_manager, job).await; - } - TrackEventsJob::PluginDisabled(job) => { - send_track_plugin_disabled(&auth_manager, job).await; - } - } - } - }); - Self { - sender, - app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - } - } - - fn try_send(&self, job: TrackEventsJob) { - if self.sender.try_send(job).is_err() { - //TODO: add a metric for this - tracing::warn!("dropping analytics events: queue is full"); - } - } - - fn should_enqueue_app_used(&self, tracking: &TrackEventsContext, app: &AppInvocation) -> bool { - let Some(connector_id) = app.connector_id.as_ref() else { - return true; - }; - let mut emitted = self - .app_used_emitted_keys - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS { - emitted.clear(); - } - emitted.insert((tracking.turn_id.clone(), connector_id.clone())) - } - - fn should_enqueue_plugin_used( - &self, - tracking: &TrackEventsContext, - plugin: &PluginTelemetryMetadata, - ) -> bool { - let mut emitted = self - .plugin_used_emitted_keys - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - if emitted.len() >= ANALYTICS_EVENT_DEDUPE_MAX_KEYS { - emitted.clear(); - } - emitted.insert((tracking.turn_id.clone(), plugin.plugin_id.as_key())) - } -} - -impl AnalyticsEventsClient { - pub fn new( - auth_manager: Arc, - base_url: String, - analytics_enabled: Option, - ) -> Self { - Self { - queue: AnalyticsEventsQueue::new(Arc::clone(&auth_manager)), - analytics_enabled, - base_url, - } - } - - pub(crate) fn track_skill_invocations( - &self, - tracking: TrackEventsContext, - invocations: Vec, - ) { - track_skill_invocations( - &self.queue, - self.analytics_enabled, - Some(tracking), - invocations, - ); - } - - pub(crate) fn track_app_mentioned( - &self, - tracking: TrackEventsContext, - mentions: Vec, - ) { - track_app_mentioned( - &self.queue, - self.analytics_enabled, - Some(tracking), - mentions, - ); - } - - pub(crate) fn track_app_used(&self, tracking: TrackEventsContext, app: AppInvocation) { - track_app_used(&self.queue, self.analytics_enabled, Some(tracking), app); - } - - pub(crate) fn track_plugin_used( - &self, - tracking: TrackEventsContext, - plugin: PluginTelemetryMetadata, - ) { - track_plugin_used( - &self.queue, - self.analytics_enabled, - Some(tracking), - plugin, - ); - } - - pub fn track_plugin_installed(&self, plugin: PluginTelemetryMetadata) { - track_plugin_management( - &self.queue, - self.analytics_enabled, - PluginManagementEventType::Installed, - plugin, - ); - } - - pub fn track_plugin_uninstalled(&self, plugin: PluginTelemetryMetadata) { - track_plugin_management( - &self.queue, - self.analytics_enabled, - PluginManagementEventType::Uninstalled, - plugin, - ); - } - - pub fn track_plugin_enabled(&self, plugin: PluginTelemetryMetadata) { - track_plugin_management( - &self.queue, - self.analytics_enabled, - PluginManagementEventType::Enabled, - plugin, - ); - } - - pub fn track_plugin_disabled(&self, plugin: PluginTelemetryMetadata) { - track_plugin_management( - &self.queue, - self.analytics_enabled, - PluginManagementEventType::Disabled, - plugin, - ); - } -} - -enum TrackEventsJob { - SkillInvocations(TrackSkillInvocationsJob), - AppMentioned(TrackAppMentionedJob), - AppUsed(TrackAppUsedJob), - PluginUsed(TrackPluginUsedJob), - PluginInstalled(TrackPluginManagementJob), - PluginUninstalled(TrackPluginManagementJob), - PluginEnabled(TrackPluginManagementJob), - PluginDisabled(TrackPluginManagementJob), -} - -struct TrackSkillInvocationsJob { - analytics_enabled: Option, - tracking: TrackEventsContext, - invocations: Vec, -} - -struct TrackAppMentionedJob { - analytics_enabled: Option, - tracking: TrackEventsContext, - mentions: Vec, -} - -struct TrackAppUsedJob { - analytics_enabled: Option, - tracking: TrackEventsContext, - app: AppInvocation, -} - -struct TrackPluginUsedJob { - analytics_enabled: Option, - tracking: TrackEventsContext, - plugin: PluginTelemetryMetadata, -} - -struct TrackPluginManagementJob { - analytics_enabled: Option, - plugin: PluginTelemetryMetadata, -} - -#[derive(Clone, Copy)] -enum PluginManagementEventType { - Installed, - Uninstalled, - Enabled, - Disabled, -} - -const ANALYTICS_EVENTS_QUEUE_SIZE: usize = 256; -const ANALYTICS_EVENTS_TIMEOUT: Duration = Duration::from_secs(10); -const ANALYTICS_EVENT_DEDUPE_MAX_KEYS: usize = 4096; - -#[derive(Serialize)] -struct TrackEventsRequest { - events: Vec, -} - -#[derive(Serialize)] -#[serde(untagged)] -enum TrackEventRequest { - SkillInvocation(SkillInvocationEventRequest), - AppMentioned(CodexAppMentionedEventRequest), - AppUsed(CodexAppUsedEventRequest), - PluginUsed(CodexPluginUsedEventRequest), - PluginInstalled(CodexPluginEventRequest), - PluginUninstalled(CodexPluginEventRequest), - PluginEnabled(CodexPluginEventRequest), - PluginDisabled(CodexPluginEventRequest), -} - -#[derive(Serialize)] -struct SkillInvocationEventRequest { - event_type: &'static str, - skill_id: String, - skill_name: String, - event_params: SkillInvocationEventParams, -} - -#[derive(Serialize)] -struct SkillInvocationEventParams { - product_client_id: Option, - skill_scope: Option, - repo_url: Option, - thread_id: Option, - invoke_type: Option, - model_slug: Option, -} - -#[derive(Serialize)] -struct CodexAppMetadata { - connector_id: Option, - thread_id: Option, - turn_id: Option, - app_name: Option, - product_client_id: Option, - invoke_type: Option, - model_slug: Option, -} - -#[derive(Serialize)] -struct CodexAppMentionedEventRequest { - event_type: &'static str, - event_params: CodexAppMetadata, -} - -#[derive(Serialize)] -struct CodexAppUsedEventRequest { - event_type: &'static str, - event_params: CodexAppMetadata, -} - -#[derive(Serialize)] -struct CodexPluginMetadata { - plugin_id: Option, - plugin_name: Option, - marketplace_name: Option, - has_skills: Option, - mcp_server_count: Option, - connector_ids: Option>, - product_client_id: Option, -} - -#[derive(Serialize)] -struct CodexPluginUsedMetadata { - #[serde(flatten)] - plugin: CodexPluginMetadata, - thread_id: Option, - turn_id: Option, - model_slug: Option, -} - -#[derive(Serialize)] -struct CodexPluginEventRequest { - event_type: &'static str, - event_params: CodexPluginMetadata, -} - -#[derive(Serialize)] -struct CodexPluginUsedEventRequest { - event_type: &'static str, - event_params: CodexPluginUsedMetadata, -} - -pub(crate) fn track_skill_invocations( - queue: &AnalyticsEventsQueue, - analytics_enabled: Option, - tracking: Option, - invocations: Vec, -) { - if analytics_enabled == Some(false) { - return; - } - let Some(tracking) = tracking else { - return; - }; - if invocations.is_empty() { - return; - } - let job = TrackEventsJob::SkillInvocations(TrackSkillInvocationsJob { - analytics_enabled, - tracking, - invocations, - }); - queue.try_send(job); -} - -pub(crate) fn track_app_mentioned( - queue: &AnalyticsEventsQueue, - analytics_enabled: Option, - tracking: Option, - mentions: Vec, -) { - if analytics_enabled == Some(false) { - return; - } - let Some(tracking) = tracking else { - return; - }; - if mentions.is_empty() { - return; - } - let job = TrackEventsJob::AppMentioned(TrackAppMentionedJob { - analytics_enabled, - tracking, - mentions, - }); - queue.try_send(job); -} - -pub(crate) fn track_app_used( - queue: &AnalyticsEventsQueue, - analytics_enabled: Option, - tracking: Option, - app: AppInvocation, -) { - if analytics_enabled == Some(false) { - return; - } - let Some(tracking) = tracking else { - return; - }; - if !queue.should_enqueue_app_used(&tracking, &app) { - return; - } - let job = TrackEventsJob::AppUsed(TrackAppUsedJob { - analytics_enabled, - tracking, - app, - }); - queue.try_send(job); -} - -pub(crate) fn track_plugin_used( - queue: &AnalyticsEventsQueue, - analytics_enabled: Option, - tracking: Option, - plugin: PluginTelemetryMetadata, -) { - if analytics_enabled == Some(false) { - return; - } - let Some(tracking) = tracking else { - return; - }; - if !queue.should_enqueue_plugin_used(&tracking, &plugin) { - return; - } - let job = TrackEventsJob::PluginUsed(TrackPluginUsedJob { - analytics_enabled, - tracking, - plugin, - }); - queue.try_send(job); -} - -fn track_plugin_management( - queue: &AnalyticsEventsQueue, - analytics_enabled: Option, - event_type: PluginManagementEventType, - plugin: PluginTelemetryMetadata, -) { - if config.analytics_enabled == Some(false) { - return; - } - let job = TrackPluginManagementJob { - analytics_enabled, - plugin, - }; - let job = match event_type { - PluginManagementEventType::Installed => TrackEventsJob::PluginInstalled(job), - PluginManagementEventType::Uninstalled => TrackEventsJob::PluginUninstalled(job), - PluginManagementEventType::Enabled => TrackEventsJob::PluginEnabled(job), - PluginManagementEventType::Disabled => TrackEventsJob::PluginDisabled(job), - }; - queue.try_send(job); -} - -async fn send_track_skill_invocations(auth_manager: &AuthManager, job: TrackSkillInvocationsJob) { - let TrackSkillInvocationsJob { - analytics_enabled, - tracking, - invocations, - } = job; - let mut events = Vec::with_capacity(invocations.len()); - for invocation in invocations { - let skill_scope = match invocation.skill_scope { - SkillScope::User => "user", - SkillScope::Repo => "repo", - SkillScope::System => "system", - SkillScope::Admin => "admin", - }; - let repo_root = get_git_repo_root(invocation.skill_path.as_path()); - let repo_url = if let Some(root) = repo_root.as_ref() { - collect_git_info(root) - .await - .and_then(|info| info.repository_url) - } else { - None - }; - let skill_id = skill_id_for_local_skill( - repo_url.as_deref(), - repo_root.as_deref(), - invocation.skill_path.as_path(), - invocation.skill_name.as_str(), - ); - events.push(TrackEventRequest::SkillInvocation( - SkillInvocationEventRequest { - event_type: "skill_invocation", - skill_id, - skill_name: invocation.skill_name.clone(), - event_params: SkillInvocationEventParams { - thread_id: Some(tracking.thread_id.clone()), - invoke_type: Some(invocation.invocation_type), - model_slug: Some(tracking.model_slug.clone()), - product_client_id: Some(crate::default_client::originator().value), - repo_url, - skill_scope: Some(skill_scope.to_string()), - }, - }, - )); - } - - send_track_events(auth_manager, config, events).await; -} - -async fn send_track_app_mentioned(auth_manager: &AuthManager, job: TrackAppMentionedJob) { - let TrackAppMentionedJob { - analytics_enabled, - tracking, - mentions, - } = job; - let events = mentions - .into_iter() - .map(|mention| { - let event_params = codex_app_metadata(&tracking, mention); - TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest { - event_type: "codex_app_mentioned", - event_params, - }) - }) - .collect::>(); - - send_track_events(auth_manager, config, events).await; -} - -async fn send_track_app_used(auth_manager: &AuthManager, job: TrackAppUsedJob) { - let TrackAppUsedJob { - analytics_enabled, - tracking, - app, - } = job; - let event_params = codex_app_metadata(&tracking, app); - let events = vec![TrackEventRequest::AppUsed(CodexAppUsedEventRequest { - event_type: "codex_app_used", - event_params, - })]; - - send_track_events(auth_manager, config, events).await; -} - -async fn send_track_plugin_used(auth_manager: &AuthManager, job: TrackPluginUsedJob) { - let TrackPluginUsedJob { - analytics_enabled, - tracking, - plugin, - } = job; - let events = vec![TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest { - event_type: "codex_plugin_used", - event_params: codex_plugin_used_metadata(&tracking, plugin), - })]; - - send_track_events(auth_manager, config, events).await; -} - -async fn send_track_plugin_installed(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_installed").await; -} - -async fn send_track_plugin_uninstalled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_uninstalled").await; -} - -async fn send_track_plugin_enabled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_enabled").await; -} - -async fn send_track_plugin_disabled(auth_manager: &AuthManager, job: TrackPluginManagementJob) { - send_track_plugin_management_event(auth_manager, job, "codex_plugin_disabled").await; -} - -async fn send_track_plugin_management_event( - auth_manager: &AuthManager, - job: TrackPluginManagementJob, - event_type: &'static str, -) { - let TrackPluginManagementJob { - analytics_enabled, - plugin, - } = job; - let event_params = codex_plugin_metadata(plugin); - let event = CodexPluginEventRequest { - event_type, - event_params, - }; - let events = vec![match event_type { - "codex_plugin_installed" => TrackEventRequest::PluginInstalled(event), - "codex_plugin_uninstalled" => TrackEventRequest::PluginUninstalled(event), - "codex_plugin_enabled" => TrackEventRequest::PluginEnabled(event), - "codex_plugin_disabled" => TrackEventRequest::PluginDisabled(event), - _ => unreachable!("unknown plugin management event type"), - }]; - - send_track_events(auth_manager, config, events).await; -} - -fn codex_app_metadata(tracking: &TrackEventsContext, app: AppInvocation) -> CodexAppMetadata { - CodexAppMetadata { - connector_id: app.connector_id, - thread_id: Some(tracking.thread_id.clone()), - turn_id: Some(tracking.turn_id.clone()), - app_name: app.app_name, - product_client_id: Some(crate::default_client::originator().value), - invoke_type: app.invocation_type, - model_slug: Some(tracking.model_slug.clone()), - } -} - -fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata { - let capability_summary = plugin.capability_summary; - CodexPluginMetadata { - plugin_id: Some(plugin.plugin_id.as_key()), - plugin_name: Some(plugin.plugin_id.plugin_name), - marketplace_name: Some(plugin.plugin_id.marketplace_name), - has_skills: capability_summary - .as_ref() - .map(|summary| summary.has_skills), - mcp_server_count: capability_summary - .as_ref() - .map(|summary| summary.mcp_server_names.len()), - connector_ids: capability_summary.map(|summary| { - summary - .app_connector_ids - .into_iter() - .map(|connector_id| connector_id.0) - .collect() - }), - product_client_id: Some(crate::default_client::originator().value), - } -} - -fn codex_plugin_used_metadata( - tracking: &TrackEventsContext, - plugin: PluginTelemetryMetadata, -) -> CodexPluginUsedMetadata { - CodexPluginUsedMetadata { - plugin: codex_plugin_metadata(plugin), - thread_id: Some(tracking.thread_id.clone()), - turn_id: Some(tracking.turn_id.clone()), - model_slug: Some(tracking.model_slug.clone()), - } -} - -async fn send_track_events( - auth_manager: &AuthManager, - analytics_enabled: Option, - events: Vec, -) { - if events.is_empty() { - return; - } - let Some(auth) = auth_manager.auth().await else { - return; - }; - if !auth.is_chatgpt_auth() { - return; - } - let access_token = match auth.get_token() { - Ok(token) => token, - Err(_) => return, - }; - let Some(account_id) = auth.get_account_id() else { - return; - }; - - let base_url = config.chatgpt_base_url.trim_end_matches('/'); - let url = format!("{base_url}/codex/analytics-events/events"); - let payload = TrackEventsRequest { events }; - - let response = create_client() - .post(&url) - .timeout(ANALYTICS_EVENTS_TIMEOUT) - .bearer_auth(&access_token) - .header("chatgpt-account-id", &account_id) - .header("Content-Type", "application/json") - .json(&payload) - .send() - .await; - - match response { - Ok(response) if response.status().is_success() => {} - Ok(response) => { - let status = response.status(); - let body = response.text().await.unwrap_or_default(); - tracing::warn!("events failed with status {status}: {body}"); - } - Err(err) => { - tracing::warn!("failed to send events request: {err}"); - } - } -} - -pub(crate) fn skill_id_for_local_skill( - repo_url: Option<&str>, - repo_root: Option<&Path>, - skill_path: &Path, - skill_name: &str, -) -> String { - let path = normalize_path_for_skill_id(repo_url, repo_root, skill_path); - let prefix = if let Some(url) = repo_url { - format!("repo_{url}") - } else { - "personal".to_string() - }; - let raw_id = format!("{prefix}_{path}_{skill_name}"); - let mut hasher = Sha1::new(); - hasher.update(raw_id.as_bytes()); - format!("{:x}", hasher.finalize()) -} - -/// Returns a normalized path for skill ID construction. -/// -/// - Repo-scoped skills use a path relative to the repo root. -/// - User/admin/system skills use an absolute path. -fn normalize_path_for_skill_id( - repo_url: Option<&str>, - repo_root: Option<&Path>, - skill_path: &Path, -) -> String { - let resolved_path = - std::fs::canonicalize(skill_path).unwrap_or_else(|_| skill_path.to_path_buf()); - match (repo_url, repo_root) { - (Some(_), Some(root)) => { - let resolved_root = std::fs::canonicalize(root).unwrap_or_else(|_| root.to_path_buf()); - resolved_path - .strip_prefix(&resolved_root) - .unwrap_or(resolved_path.as_path()) - .to_string_lossy() - .replace('\\', "/") - } - _ => resolved_path.to_string_lossy().replace('\\', "/"), - } -} - -#[cfg(test)] -#[path = "analytics_client_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/analytics_client_tests.rs b/codex-rs/core/src/analytics_client_tests.rs deleted file mode 100644 index a038aae047..0000000000 --- a/codex-rs/core/src/analytics_client_tests.rs +++ /dev/null @@ -1,289 +0,0 @@ -use super::AnalyticsEventsQueue; -use super::AppInvocation; -use super::CodexAppMentionedEventRequest; -use super::CodexAppUsedEventRequest; -use super::CodexPluginEventRequest; -use super::CodexPluginUsedEventRequest; -use super::InvocationType; -use super::TrackEventRequest; -use super::TrackEventsContext; -use super::codex_app_metadata; -use super::codex_plugin_metadata; -use super::codex_plugin_used_metadata; -use super::normalize_path_for_skill_id; -use crate::plugins::AppConnectorId; -use crate::plugins::PluginCapabilitySummary; -use crate::plugins::PluginId; -use crate::plugins::PluginTelemetryMetadata; -use pretty_assertions::assert_eq; -use serde_json::json; -use std::collections::HashSet; -use std::path::PathBuf; -use std::sync::Arc; -use std::sync::Mutex; -use tokio::sync::mpsc; - -fn expected_absolute_path(path: &PathBuf) -> String { - std::fs::canonicalize(path) - .unwrap_or_else(|_| path.to_path_buf()) - .to_string_lossy() - .replace('\\', "/") -} - -#[test] -fn normalize_path_for_skill_id_repo_scoped_uses_relative_path() { - let repo_root = PathBuf::from("/repo/root"); - let skill_path = PathBuf::from("/repo/root/.codex/skills/doc/SKILL.md"); - - let path = normalize_path_for_skill_id( - Some("https://example.com/repo.git"), - Some(repo_root.as_path()), - skill_path.as_path(), - ); - - assert_eq!(path, ".codex/skills/doc/SKILL.md"); -} - -#[test] -fn normalize_path_for_skill_id_user_scoped_uses_absolute_path() { - let skill_path = PathBuf::from("/Users/abc/.codex/skills/doc/SKILL.md"); - - let path = normalize_path_for_skill_id(None, None, skill_path.as_path()); - let expected = expected_absolute_path(&skill_path); - - assert_eq!(path, expected); -} - -#[test] -fn normalize_path_for_skill_id_admin_scoped_uses_absolute_path() { - let skill_path = PathBuf::from("/etc/codex/skills/doc/SKILL.md"); - - let path = normalize_path_for_skill_id(None, None, skill_path.as_path()); - let expected = expected_absolute_path(&skill_path); - - assert_eq!(path, expected); -} - -#[test] -fn normalize_path_for_skill_id_repo_root_not_in_skill_path_uses_absolute_path() { - let repo_root = PathBuf::from("/repo/root"); - let skill_path = PathBuf::from("/other/path/.codex/skills/doc/SKILL.md"); - - let path = normalize_path_for_skill_id( - Some("https://example.com/repo.git"), - Some(repo_root.as_path()), - skill_path.as_path(), - ); - let expected = expected_absolute_path(&skill_path); - - assert_eq!(path, expected); -} - -#[test] -fn app_mentioned_event_serializes_expected_shape() { - let tracking = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - }; - let event = TrackEventRequest::AppMentioned(CodexAppMentionedEventRequest { - event_type: "codex_app_mentioned", - event_params: codex_app_metadata( - &tracking, - AppInvocation { - connector_id: Some("calendar".to_string()), - app_name: Some("Calendar".to_string()), - invocation_type: Some(InvocationType::Explicit), - }, - ), - }); - - let payload = serde_json::to_value(&event).expect("serialize app mentioned event"); - - assert_eq!( - payload, - json!({ - "event_type": "codex_app_mentioned", - "event_params": { - "connector_id": "calendar", - "thread_id": "thread-1", - "turn_id": "turn-1", - "app_name": "Calendar", - "product_client_id": crate::default_client::originator().value, - "invoke_type": "explicit", - "model_slug": "gpt-5" - } - }) - ); -} - -#[test] -fn app_used_event_serializes_expected_shape() { - let tracking = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-2".to_string(), - turn_id: "turn-2".to_string(), - }; - let event = TrackEventRequest::AppUsed(CodexAppUsedEventRequest { - event_type: "codex_app_used", - event_params: codex_app_metadata( - &tracking, - AppInvocation { - connector_id: Some("drive".to_string()), - app_name: Some("Google Drive".to_string()), - invocation_type: Some(InvocationType::Implicit), - }, - ), - }); - - let payload = serde_json::to_value(&event).expect("serialize app used event"); - - assert_eq!( - payload, - json!({ - "event_type": "codex_app_used", - "event_params": { - "connector_id": "drive", - "thread_id": "thread-2", - "turn_id": "turn-2", - "app_name": "Google Drive", - "product_client_id": crate::default_client::originator().value, - "invoke_type": "implicit", - "model_slug": "gpt-5" - } - }) - ); -} - -#[test] -fn app_used_dedupe_is_keyed_by_turn_and_connector() { - let (sender, _receiver) = mpsc::channel(1); - let queue = AnalyticsEventsQueue { - sender, - app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - }; - let app = AppInvocation { - connector_id: Some("calendar".to_string()), - app_name: Some("Calendar".to_string()), - invocation_type: Some(InvocationType::Implicit), - }; - - let turn_1 = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - }; - let turn_2 = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-1".to_string(), - turn_id: "turn-2".to_string(), - }; - - assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), true); - assert_eq!(queue.should_enqueue_app_used(&turn_1, &app), false); - assert_eq!(queue.should_enqueue_app_used(&turn_2, &app), true); -} - -#[test] -fn plugin_used_event_serializes_expected_shape() { - let tracking = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-3".to_string(), - turn_id: "turn-3".to_string(), - }; - let event = TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest { - event_type: "codex_plugin_used", - event_params: codex_plugin_used_metadata(&tracking, sample_plugin_metadata()), - }); - - let payload = serde_json::to_value(&event).expect("serialize plugin used event"); - - assert_eq!( - payload, - json!({ - "event_type": "codex_plugin_used", - "event_params": { - "plugin_id": "sample@test", - "plugin_name": "sample", - "marketplace_name": "test", - "has_skills": true, - "mcp_server_count": 2, - "connector_ids": ["calendar", "drive"], - "product_client_id": crate::default_client::originator().value, - "thread_id": "thread-3", - "turn_id": "turn-3", - "model_slug": "gpt-5" - } - }) - ); -} - -#[test] -fn plugin_management_event_serializes_expected_shape() { - let event = TrackEventRequest::PluginInstalled(CodexPluginEventRequest { - event_type: "codex_plugin_installed", - event_params: codex_plugin_metadata(sample_plugin_metadata()), - }); - - let payload = serde_json::to_value(&event).expect("serialize plugin installed event"); - - assert_eq!( - payload, - json!({ - "event_type": "codex_plugin_installed", - "event_params": { - "plugin_id": "sample@test", - "plugin_name": "sample", - "marketplace_name": "test", - "has_skills": true, - "mcp_server_count": 2, - "connector_ids": ["calendar", "drive"], - "product_client_id": crate::default_client::originator().value - } - }) - ); -} - -#[test] -fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() { - let (sender, _receiver) = mpsc::channel(1); - let queue = AnalyticsEventsQueue { - sender, - app_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - plugin_used_emitted_keys: Arc::new(Mutex::new(HashSet::new())), - }; - let plugin = sample_plugin_metadata(); - - let turn_1 = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-1".to_string(), - turn_id: "turn-1".to_string(), - }; - let turn_2 = TrackEventsContext { - model_slug: "gpt-5".to_string(), - thread_id: "thread-1".to_string(), - turn_id: "turn-2".to_string(), - }; - - assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), true); - assert_eq!(queue.should_enqueue_plugin_used(&turn_1, &plugin), false); - assert_eq!(queue.should_enqueue_plugin_used(&turn_2, &plugin), true); -} - -fn sample_plugin_metadata() -> PluginTelemetryMetadata { - PluginTelemetryMetadata { - plugin_id: PluginId::parse("sample@test").expect("valid plugin id"), - capability_summary: Some(PluginCapabilitySummary { - config_name: "sample@test".to_string(), - display_name: "sample".to_string(), - description: None, - has_skills: true, - mcp_server_names: vec!["mcp-1".to_string(), "mcp-2".to_string()], - app_connector_ids: vec![ - AppConnectorId("calendar".to_string()), - AppConnectorId("drive".to_string()), - ], - }), - } -} diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index d699ec5a87..d84d5a49b0 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -12,10 +12,6 @@ use crate::SandboxState; use crate::agent::AgentControl; use crate::agent::AgentStatus; use crate::agent::agent_status_from_event; -use crate::analytics_client::AnalyticsEventsClient; -use crate::analytics_client::AppInvocation; -use crate::analytics_client::InvocationType; -use crate::analytics_client::build_track_events_context; use crate::apps::render_apps_section; use crate::auth_env_telemetry::collect_auth_env_telemetry; use crate::commit_attribution::commit_message_trailer_instruction; @@ -52,6 +48,10 @@ use async_channel::Receiver; use async_channel::Sender; use chrono::Local; use chrono::Utc; +use codex_analytics::AnalyticsEventsClient; +use codex_analytics::AppInvocation; +use codex_analytics::InvocationType; +use codex_analytics::build_track_events_context; use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use codex_exec_server::Environment; @@ -1841,8 +1841,9 @@ impl Session { shell_zsh_path: config.zsh_path.clone(), main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( - Arc::clone(&config), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks, rollout: Mutex::new(rollout_recorder), diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index c4c3d50ac9..df3338b197 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -2637,10 +2637,9 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { shell_zsh_path: None, main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( - Arc::new( - crate::analytics_client::AnalyticsEventsSettings::from_config(config.as_ref()), - ), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks: Hooks::new(HooksConfig { legacy_notify_argv: config.notify.clone(), @@ -3474,10 +3473,9 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx( shell_zsh_path: None, main_execve_wrapper_exe: config.main_execve_wrapper_exe.clone(), analytics_events_client: AnalyticsEventsClient::new( - Arc::new( - crate::analytics_client::AnalyticsEventsSettings::from_config(config.as_ref()), - ), Arc::clone(&auth_manager), + config.chatgpt_base_url.trim_end_matches('/').to_string(), + config.analytics_enabled, ), hooks: Hooks::new(HooksConfig { legacy_notify_argv: config.notify.clone(), diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 767ffe635d..8b4dd2ce88 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -52,10 +52,12 @@ pub use codex_config::TextRange; pub use codex_config::WebSearchModeRequirement; pub(crate) use codex_config::build_cli_overrides_layer; pub(crate) use codex_config::config_error_from_toml; +pub use codex_config::default_project_root_markers; pub use codex_config::format_config_error; pub use codex_config::format_config_error_with_source; pub(crate) use codex_config::io_error_from_config_error; pub use codex_config::merge_toml_values; +pub use codex_config::project_root_markers_from_config; #[cfg(test)] pub(crate) use codex_config::version_for_toml; @@ -67,8 +69,6 @@ pub const SYSTEM_CONFIG_TOML_FILE_UNIX: &str = "/etc/codex/config.toml"; #[cfg(windows)] const DEFAULT_PROGRAM_DATA_DIR_WINDOWS: &str = r"C:\ProgramData"; -const DEFAULT_PROJECT_ROOT_MARKERS: &[&str] = &[".git"]; - pub(crate) async fn first_layer_config_error(layers: &ConfigLayerStack) -> Option { codex_config::first_layer_config_error::(layers, CONFIG_TOML_FILE).await } @@ -524,55 +524,6 @@ async fn load_requirements_from_legacy_scheme( Ok(()) } -/// Reads `project_root_markers` from the [toml::Value] produced by merging -/// `config.toml` from the config layers in the stack preceding -/// [ConfigLayerSource::Project]. -/// -/// Invariants: -/// - If `project_root_markers` is not specified, returns `Ok(None)`. -/// - If `project_root_markers` is specified, returns `Ok(Some(markers))` where -/// `markers` is a `Vec` (including `Ok(Some(Vec::new()))` for an -/// empty array, which indicates that root detection should be disabled). -/// - Returns an error if `project_root_markers` is specified but is not an -/// array of strings. -pub(crate) fn project_root_markers_from_config( - config: &TomlValue, -) -> io::Result>> { - let Some(table) = config.as_table() else { - return Ok(None); - }; - let Some(markers_value) = table.get("project_root_markers") else { - return Ok(None); - }; - let TomlValue::Array(entries) = markers_value else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "project_root_markers must be an array of strings", - )); - }; - if entries.is_empty() { - return Ok(Some(Vec::new())); - } - let mut markers = Vec::new(); - for entry in entries { - let Some(marker) = entry.as_str() else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "project_root_markers must be an array of strings", - )); - }; - markers.push(marker.to_string()); - } - Ok(Some(markers)) -} - -pub(crate) fn default_project_root_markers() -> Vec { - DEFAULT_PROJECT_ROOT_MARKERS - .iter() - .map(ToString::to_string) - .collect() -} - struct ProjectTrustContext { project_root: AbsolutePathBuf, project_root_key: String, diff --git a/codex-rs/core/src/contextual_user_message.rs b/codex-rs/core/src/contextual_user_message.rs index 4df05f0da1..01863498e9 100644 --- a/codex-rs/core/src/contextual_user_message.rs +++ b/codex-rs/core/src/contextual_user_message.rs @@ -1,3 +1,6 @@ +use codex_instructions::AGENTS_MD_FRAGMENT; +use codex_instructions::ContextualUserFragmentDefinition; +use codex_instructions::SKILL_FRAGMENT; use codex_protocol::items::HookPromptItem; use codex_protocol::items::parse_hook_prompt_fragment; use codex_protocol::models::ContentItem; @@ -5,10 +8,6 @@ use codex_protocol::models::ResponseItem; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_CLOSE_TAG; use codex_protocol::protocol::ENVIRONMENT_CONTEXT_OPEN_TAG; -pub(crate) const AGENTS_MD_START_MARKER: &str = "# AGENTS.md instructions for "; -pub(crate) const AGENTS_MD_END_MARKER: &str = ""; -pub(crate) const SKILL_OPEN_TAG: &str = ""; -pub(crate) const SKILL_CLOSE_TAG: &str = ""; pub(crate) const USER_SHELL_COMMAND_OPEN_TAG: &str = ""; pub(crate) const USER_SHELL_COMMAND_CLOSE_TAG: &str = ""; pub(crate) const TURN_ABORTED_OPEN_TAG: &str = ""; @@ -16,64 +15,11 @@ pub(crate) const TURN_ABORTED_CLOSE_TAG: &str = ""; pub(crate) const SUBAGENT_NOTIFICATION_OPEN_TAG: &str = ""; pub(crate) const SUBAGENT_NOTIFICATION_CLOSE_TAG: &str = ""; -#[derive(Clone, Copy)] -pub(crate) struct ContextualUserFragmentDefinition { - start_marker: &'static str, - end_marker: &'static str, -} - -impl ContextualUserFragmentDefinition { - pub(crate) const fn new(start_marker: &'static str, end_marker: &'static str) -> Self { - Self { - start_marker, - end_marker, - } - } - - pub(crate) fn matches_text(&self, text: &str) -> bool { - let trimmed = text.trim_start(); - let starts_with_marker = trimmed - .get(..self.start_marker.len()) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.start_marker)); - let trimmed = trimmed.trim_end(); - let ends_with_marker = trimmed - .get(trimmed.len().saturating_sub(self.end_marker.len())..) - .is_some_and(|candidate| candidate.eq_ignore_ascii_case(self.end_marker)); - starts_with_marker && ends_with_marker - } - - pub(crate) const fn start_marker(&self) -> &'static str { - self.start_marker - } - - pub(crate) const fn end_marker(&self) -> &'static str { - self.end_marker - } - - pub(crate) fn wrap(&self, body: String) -> String { - format!("{}\n{}\n{}", self.start_marker, body, self.end_marker) - } - - pub(crate) fn into_message(self, text: String) -> ResponseItem { - ResponseItem::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { text }], - end_turn: None, - phase: None, - } - } -} - -pub(crate) const AGENTS_MD_FRAGMENT: ContextualUserFragmentDefinition = - ContextualUserFragmentDefinition::new(AGENTS_MD_START_MARKER, AGENTS_MD_END_MARKER); pub(crate) const ENVIRONMENT_CONTEXT_FRAGMENT: ContextualUserFragmentDefinition = ContextualUserFragmentDefinition::new( ENVIRONMENT_CONTEXT_OPEN_TAG, ENVIRONMENT_CONTEXT_CLOSE_TAG, ); -pub(crate) const SKILL_FRAGMENT: ContextualUserFragmentDefinition = - ContextualUserFragmentDefinition::new(SKILL_OPEN_TAG, SKILL_CLOSE_TAG); pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ContextualUserFragmentDefinition = ContextualUserFragmentDefinition::new( USER_SHELL_COMMAND_OPEN_TAG, diff --git a/codex-rs/core/src/instructions/mod.rs b/codex-rs/core/src/instructions/mod.rs index 9f1d95d2f6..0ef48ff620 100644 --- a/codex-rs/core/src/instructions/mod.rs +++ b/codex-rs/core/src/instructions/mod.rs @@ -1,5 +1,3 @@ -mod user_instructions; - -pub(crate) use user_instructions::SkillInstructions; -pub use user_instructions::USER_INSTRUCTIONS_PREFIX; -pub(crate) use user_instructions::UserInstructions; +pub(crate) use codex_instructions::SkillInstructions; +pub use codex_instructions::USER_INSTRUCTIONS_PREFIX; +pub(crate) use codex_instructions::UserInstructions; diff --git a/codex-rs/core/src/instructions/user_instructions.rs b/codex-rs/core/src/instructions/user_instructions.rs deleted file mode 100644 index a0389c9ff8..0000000000 --- a/codex-rs/core/src/instructions/user_instructions.rs +++ /dev/null @@ -1,57 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use codex_protocol::models::ResponseItem; - -use crate::contextual_user_message::AGENTS_MD_FRAGMENT; -use crate::contextual_user_message::SKILL_FRAGMENT; - -pub const USER_INSTRUCTIONS_PREFIX: &str = "# AGENTS.md instructions for "; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename = "user_instructions", rename_all = "snake_case")] -pub(crate) struct UserInstructions { - pub directory: String, - pub text: String, -} - -impl UserInstructions { - pub(crate) fn serialize_to_text(&self) -> String { - format!( - "{prefix}{directory}\n\n\n{contents}\n{suffix}", - prefix = AGENTS_MD_FRAGMENT.start_marker(), - directory = self.directory, - contents = self.text, - suffix = AGENTS_MD_FRAGMENT.end_marker(), - ) - } -} - -impl From for ResponseItem { - fn from(ui: UserInstructions) -> Self { - AGENTS_MD_FRAGMENT.into_message(ui.serialize_to_text()) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename = "skill_instructions", rename_all = "snake_case")] -pub(crate) struct SkillInstructions { - pub name: String, - pub path: String, - pub contents: String, -} - -impl SkillInstructions {} - -impl From for ResponseItem { - fn from(si: SkillInstructions) -> Self { - SKILL_FRAGMENT.into_message(SKILL_FRAGMENT.wrap(format!( - "{}\n{}\n{}", - si.name, si.path, si.contents - ))) - } -} - -#[cfg(test)] -#[path = "user_instructions_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/instructions/user_instructions_tests.rs b/codex-rs/core/src/instructions/user_instructions_tests.rs deleted file mode 100644 index 58442600a8..0000000000 --- a/codex-rs/core/src/instructions/user_instructions_tests.rs +++ /dev/null @@ -1,68 +0,0 @@ -use super::*; -use codex_protocol::models::ContentItem; -use pretty_assertions::assert_eq; - -#[test] -fn test_user_instructions() { - let user_instructions = UserInstructions { - directory: "test_directory".to_string(), - text: "test_text".to_string(), - }; - let response_item: ResponseItem = user_instructions.into(); - - let ResponseItem::Message { role, content, .. } = response_item else { - panic!("expected ResponseItem::Message"); - }; - - assert_eq!(role, "user"); - - let [ContentItem::InputText { text }] = content.as_slice() else { - panic!("expected one InputText content item"); - }; - - assert_eq!( - text, - "# AGENTS.md instructions for test_directory\n\n\ntest_text\n", - ); -} - -#[test] -fn test_is_user_instructions() { - assert!(AGENTS_MD_FRAGMENT.matches_text( - "# AGENTS.md instructions for test_directory\n\n\ntest_text\n" - )); - assert!(!AGENTS_MD_FRAGMENT.matches_text("test_text")); -} - -#[test] -fn test_skill_instructions() { - let skill_instructions = SkillInstructions { - name: "demo-skill".to_string(), - path: "skills/demo/SKILL.md".to_string(), - contents: "body".to_string(), - }; - let response_item: ResponseItem = skill_instructions.into(); - - let ResponseItem::Message { role, content, .. } = response_item else { - panic!("expected ResponseItem::Message"); - }; - - assert_eq!(role, "user"); - - let [ContentItem::InputText { text }] = content.as_slice() else { - panic!("expected one InputText content item"); - }; - - assert_eq!( - text, - "\ndemo-skill\nskills/demo/SKILL.md\nbody\n", - ); -} - -#[test] -fn test_is_skill_instructions() { - assert!(SKILL_FRAGMENT.matches_text( - "\ndemo-skill\nskills/demo/SKILL.md\nbody\n" - )); - assert!(!SKILL_FRAGMENT.matches_text("regular text")); -} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 036e9cfb55..df7c7e3467 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -5,7 +5,6 @@ // the TUI or the tracing stack). #![deny(clippy::print_stdout, clippy::print_stderr)] -mod analytics_client; pub mod api_bridge; mod apply_patch; mod apps; @@ -62,13 +61,20 @@ pub use text_encoding::bytes_to_string_smart; mod mcp_tool_call; mod memories; pub mod mention_syntax; -mod mentions; pub mod message_history; mod model_provider_info; pub mod utils; pub use utils::path_utils; pub mod personality_migration; pub mod plugins; +pub(crate) mod mentions { + pub use crate::plugins::CollectedToolMentions; + pub use crate::plugins::build_connector_slug_counts; + pub use crate::plugins::build_skill_name_counts; + pub use crate::plugins::collect_explicit_app_ids; + pub use crate::plugins::collect_explicit_plugin_mentions; + pub use crate::plugins::collect_tool_mentions_from_messages; +} mod sandbox_tags; pub mod sandboxing; mod session_prefix; @@ -108,9 +114,9 @@ pub type NewConversation = NewThread; #[deprecated(note = "use CodexThread")] pub type CodexConversation = CodexThread; // Re-export common auth types for workspace consumers -pub use analytics_client::AnalyticsEventsClient; pub use auth::AuthManager; pub use auth::CodexAuth; +pub use codex_analytics::AnalyticsEventsClient; mod default_client_forwarding; /// Default Codex HTTP client headers and reqwest construction. diff --git a/codex-rs/core/src/mcp_tool_call.rs b/codex-rs/core/src/mcp_tool_call.rs index f82cc73bb5..39bdbea65d 100644 --- a/codex-rs/core/src/mcp_tool_call.rs +++ b/codex-rs/core/src/mcp_tool_call.rs @@ -8,9 +8,6 @@ use codex_app_server_protocol::McpServerElicitationRequest; use codex_app_server_protocol::McpServerElicitationRequestParams; use tracing::error; -use crate::analytics_client::AppInvocation; -use crate::analytics_client::InvocationType; -use crate::analytics_client::build_track_events_context; use crate::arc_monitor::ArcMonitorOutcome; use crate::arc_monitor::monitor_action; use crate::codex::Session; @@ -32,6 +29,9 @@ use crate::protocol::McpInvocation; use crate::protocol::McpToolCallBeginEvent; use crate::protocol::McpToolCallEndEvent; use crate::state_db; +use codex_analytics::AppInvocation; +use codex_analytics::InvocationType; +use codex_analytics::build_track_events_context; use codex_features::Feature; use codex_protocol::mcp::CallToolResult; use codex_protocol::openai_models::InputModality; diff --git a/codex-rs/core/src/mention_syntax.rs b/codex-rs/core/src/mention_syntax.rs index e58f419b54..48bfb99eca 100644 --- a/codex-rs/core/src/mention_syntax.rs +++ b/codex-rs/core/src/mention_syntax.rs @@ -1,4 +1,2 @@ -// Default plaintext sigil for tools. -pub const TOOL_MENTION_SIGIL: char = '$'; -// Plugins use `@` in linked plaintext outside TUI. -pub const PLUGIN_TEXT_MENTION_SIGIL: char = '@'; +pub use codex_plugin::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; +pub use codex_plugin::mention_syntax::TOOL_MENTION_SIGIL; diff --git a/codex-rs/core/src/mentions.rs b/codex-rs/core/src/mentions.rs deleted file mode 100644 index fec7c40e4b..0000000000 --- a/codex-rs/core/src/mentions.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::collections::HashMap; -use std::collections::HashSet; -use std::path::PathBuf; - -use codex_protocol::user_input::UserInput; - -use crate::connectors; -use crate::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; -use crate::mention_syntax::TOOL_MENTION_SIGIL; -use crate::plugins::PluginCapabilitySummary; -use crate::skills::SkillMetadata; -use crate::skills::injection::ToolMentionKind; -use crate::skills::injection::app_id_from_path; -use crate::skills::injection::extract_tool_mentions_with_sigil; -use crate::skills::injection::plugin_config_name_from_path; -use crate::skills::injection::tool_kind_for_path; - -pub(crate) struct CollectedToolMentions { - pub(crate) plain_names: HashSet, - pub(crate) paths: HashSet, -} - -pub(crate) fn collect_tool_mentions_from_messages(messages: &[String]) -> CollectedToolMentions { - collect_tool_mentions_from_messages_with_sigil(messages, TOOL_MENTION_SIGIL) -} - -fn collect_tool_mentions_from_messages_with_sigil( - messages: &[String], - sigil: char, -) -> CollectedToolMentions { - let mut plain_names = HashSet::new(); - let mut paths = HashSet::new(); - for message in messages { - let mentions = extract_tool_mentions_with_sigil(message, sigil); - plain_names.extend(mentions.plain_names().map(str::to_string)); - paths.extend(mentions.paths().map(str::to_string)); - } - CollectedToolMentions { plain_names, paths } -} - -pub(crate) fn collect_explicit_app_ids(input: &[UserInput]) -> HashSet { - let messages = input - .iter() - .filter_map(|item| match item { - UserInput::Text { text, .. } => Some(text.clone()), - _ => None, - }) - .collect::>(); - - input - .iter() - .filter_map(|item| match item { - UserInput::Mention { path, .. } => Some(path.clone()), - _ => None, - }) - .chain(collect_tool_mentions_from_messages(&messages).paths) - .filter(|path| tool_kind_for_path(path.as_str()) == ToolMentionKind::App) - .filter_map(|path| app_id_from_path(path.as_str()).map(str::to_string)) - .collect() -} - -/// Collect explicit structured or linked `plugin://...` mentions. -pub(crate) fn collect_explicit_plugin_mentions( - input: &[UserInput], - plugins: &[PluginCapabilitySummary], -) -> Vec { - if plugins.is_empty() { - return Vec::new(); - } - - let messages = input - .iter() - .filter_map(|item| match item { - UserInput::Text { text, .. } => Some(text.clone()), - _ => None, - }) - .collect::>(); - - let mentioned_config_names: HashSet = input - .iter() - .filter_map(|item| match item { - UserInput::Mention { path, .. } => Some(path.clone()), - _ => None, - }) - .chain( - // Plugin plaintext links use `@`, not the default `$` tool sigil. - collect_tool_mentions_from_messages_with_sigil(&messages, PLUGIN_TEXT_MENTION_SIGIL) - .paths, - ) - .filter(|path| tool_kind_for_path(path.as_str()) == ToolMentionKind::Plugin) - .filter_map(|path| plugin_config_name_from_path(path.as_str()).map(str::to_string)) - .collect(); - - if mentioned_config_names.is_empty() { - return Vec::new(); - } - - plugins - .iter() - .filter(|plugin| mentioned_config_names.contains(plugin.config_name.as_str())) - .cloned() - .collect() -} - -pub(crate) fn build_skill_name_counts( - skills: &[SkillMetadata], - disabled_paths: &HashSet, -) -> (HashMap, HashMap) { - let mut exact_counts: HashMap = HashMap::new(); - let mut lower_counts: HashMap = HashMap::new(); - for skill in skills { - if disabled_paths.contains(&skill.path_to_skills_md) { - continue; - } - *exact_counts.entry(skill.name.clone()).or_insert(0) += 1; - *lower_counts - .entry(skill.name.to_ascii_lowercase()) - .or_insert(0) += 1; - } - (exact_counts, lower_counts) -} - -pub(crate) fn build_connector_slug_counts( - connectors: &[connectors::AppInfo], -) -> HashMap { - let mut counts: HashMap = HashMap::new(); - for connector in connectors { - let slug = connectors::connector_mention_slug(connector); - *counts.entry(slug).or_insert(0) += 1; - } - counts -} - -#[cfg(test)] -#[path = "mentions_tests.rs"] -mod tests; diff --git a/codex-rs/core/src/mentions_tests.rs b/codex-rs/core/src/mentions_tests.rs deleted file mode 100644 index 37c9adb886..0000000000 --- a/codex-rs/core/src/mentions_tests.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::collections::HashSet; - -use codex_protocol::user_input::UserInput; -use pretty_assertions::assert_eq; - -use super::collect_explicit_app_ids; -use super::collect_explicit_plugin_mentions; -use crate::plugins::PluginCapabilitySummary; - -fn text_input(text: &str) -> UserInput { - UserInput::Text { - text: text.to_string(), - text_elements: Vec::new(), - } -} - -fn plugin(config_name: &str, display_name: &str) -> PluginCapabilitySummary { - PluginCapabilitySummary { - config_name: config_name.to_string(), - display_name: display_name.to_string(), - description: None, - has_skills: true, - mcp_server_names: Vec::new(), - app_connector_ids: Vec::new(), - } -} - -#[test] -fn collect_explicit_app_ids_from_linked_text_mentions() { - let input = vec![text_input("use [$calendar](app://calendar)")]; - - let app_ids = collect_explicit_app_ids(&input); - - assert_eq!(app_ids, HashSet::from(["calendar".to_string()])); -} - -#[test] -fn collect_explicit_app_ids_dedupes_structured_and_linked_mentions() { - let input = vec![ - text_input("use [$calendar](app://calendar)"), - UserInput::Mention { - name: "calendar".to_string(), - path: "app://calendar".to_string(), - }, - ]; - - let app_ids = collect_explicit_app_ids(&input); - - assert_eq!(app_ids, HashSet::from(["calendar".to_string()])); -} - -#[test] -fn collect_explicit_app_ids_ignores_non_app_paths() { - let input = vec![ - text_input( - "use [$docs](mcp://docs) and [$skill](skill://team/skill) and [$file](/tmp/file.txt)", - ), - UserInput::Mention { - name: "docs".to_string(), - path: "mcp://docs".to_string(), - }, - UserInput::Mention { - name: "skill".to_string(), - path: "skill://team/skill".to_string(), - }, - UserInput::Mention { - name: "file".to_string(), - path: "/tmp/file.txt".to_string(), - }, - ]; - - let app_ids = collect_explicit_app_ids(&input); - - assert_eq!(app_ids, HashSet::::new()); -} - -#[test] -fn collect_explicit_plugin_mentions_from_structured_paths() { - let plugins = vec![ - plugin("sample@test", "sample"), - plugin("other@test", "other"), - ]; - - let mentioned = collect_explicit_plugin_mentions( - &[UserInput::Mention { - name: "sample".to_string(), - path: "plugin://sample@test".to_string(), - }], - &plugins, - ); - - assert_eq!(mentioned, vec![plugin("sample@test", "sample")]); -} - -#[test] -fn collect_explicit_plugin_mentions_from_linked_text_mentions() { - let plugins = vec![ - plugin("sample@test", "sample"), - plugin("other@test", "other"), - ]; - - let mentioned = collect_explicit_plugin_mentions( - &[text_input("use [@sample](plugin://sample@test)")], - &plugins, - ); - - assert_eq!(mentioned, vec![plugin("sample@test", "sample")]); -} - -#[test] -fn collect_explicit_plugin_mentions_dedupes_structured_and_linked_mentions() { - let plugins = vec![ - plugin("sample@test", "sample"), - plugin("other@test", "other"), - ]; - - let mentioned = collect_explicit_plugin_mentions( - &[ - text_input("use [@sample](plugin://sample@test)"), - UserInput::Mention { - name: "sample".to_string(), - path: "plugin://sample@test".to_string(), - }, - ], - &plugins, - ); - - assert_eq!(mentioned, vec![plugin("sample@test", "sample")]); -} - -#[test] -fn collect_explicit_plugin_mentions_ignores_non_plugin_paths() { - let plugins = vec![plugin("sample@test", "sample")]; - - let mentioned = collect_explicit_plugin_mentions( - &[text_input( - "use [$app](app://calendar) and [$skill](skill://team/skill) and [$file](/tmp/file.txt)", - )], - &plugins, - ); - - assert_eq!(mentioned, Vec::::new()); -} - -#[test] -fn collect_explicit_plugin_mentions_ignores_dollar_linked_plugin_mentions() { - let plugins = vec![plugin("sample@test", "sample")]; - - let mentioned = collect_explicit_plugin_mentions( - &[text_input("use [$sample](plugin://sample@test)")], - &plugins, - ); - - assert_eq!(mentioned, Vec::::new()); -} diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index 009760fe86..15ca0b573c 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -25,7 +25,6 @@ use super::store::PluginStore; use super::store::PluginStoreError; use super::sync_openai_plugins_repo; use crate::AuthManager; -use crate::analytics_client::AnalyticsEventsClient; use crate::auth::CodexAuth; use crate::config::Config; use crate::config::ConfigService; @@ -41,6 +40,7 @@ use crate::skills::config_rules::resolve_disabled_skill_paths; use crate::skills::config_rules::skill_config_rules_from_stack; use crate::skills::loader::SkillRoot; use crate::skills::loader::load_skills_from_roots; +use codex_analytics::AnalyticsEventsClient; use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::MergeStrategy; use codex_features::Feature; @@ -1377,16 +1377,6 @@ pub(crate) fn load_plugins_from_layer_stack( PluginLoadOutcome::from_plugins(plugins) } -pub(crate) fn plugin_namespace_for_skill_path(path: &Path) -> Option { - for ancestor in path.ancestors() { - if let Some(manifest) = load_plugin_manifest(ancestor) { - return Some(manifest.name); - } - } - - None -} - fn refresh_curated_plugin_cache( codex_home: &Path, plugin_version: &str, diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index 3d61c39375..cf16e5b5fb 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -805,24 +805,6 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() { ); } -#[test] -fn plugin_namespace_for_skill_path_uses_manifest_name() { - let codex_home = TempDir::new().unwrap(); - let plugin_root = codex_home.path().join("plugins/sample"); - let skill_path = plugin_root.join("skills/search/SKILL.md"); - - write_file( - &plugin_root.join(".codex-plugin/plugin.json"), - r#"{"name":"sample"}"#, - ); - write_file(&skill_path, "---\ndescription: search\n---\n"); - - assert_eq!( - plugin_namespace_for_skill_path(&skill_path), - Some("sample".to_string()) - ); -} - #[test] fn load_plugins_returns_empty_when_feature_disabled() { let codex_home = TempDir::new().unwrap(); diff --git a/codex-rs/core/src/plugins/manifest.rs b/codex-rs/core/src/plugins/manifest.rs index 91c7cbbb30..0df4613719 100644 --- a/codex-rs/core/src/plugins/manifest.rs +++ b/codex-rs/core/src/plugins/manifest.rs @@ -1,11 +1,10 @@ +use codex_plugin::PLUGIN_MANIFEST_PATH; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde_json::Value as JsonValue; use std::fs; use std::path::Component; use std::path::Path; - -pub(crate) const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json"; const MAX_DEFAULT_PROMPT_COUNT: usize = 3; const MAX_DEFAULT_PROMPT_LEN: usize = 128; diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index cc3e1626b6..e3acc7dde7 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -3,6 +3,7 @@ mod injection; mod manager; mod manifest; mod marketplace; +mod mentions; mod remote; mod render; mod startup_sync; @@ -37,7 +38,6 @@ pub use manager::RemotePluginSyncResult; pub use manager::installed_plugin_telemetry_metadata; pub use manager::load_plugin_apps; pub use manager::load_plugin_mcp_servers; -pub(crate) use manager::plugin_namespace_for_skill_path; pub use manager::plugin_telemetry_metadata_from_root; pub use manifest::PluginManifestInterface; pub(crate) use manifest::PluginManifestPaths; @@ -56,3 +56,10 @@ pub(crate) use startup_sync::curated_plugins_repo_path; pub(crate) use startup_sync::read_curated_plugins_sha; pub(crate) use startup_sync::sync_openai_plugins_repo; pub use toggles::collect_plugin_enabled_candidates; + +pub(crate) use mentions::CollectedToolMentions; +pub(crate) use mentions::build_connector_slug_counts; +pub(crate) use mentions::build_skill_name_counts; +pub(crate) use mentions::collect_explicit_app_ids; +pub(crate) use mentions::collect_explicit_plugin_mentions; +pub(crate) use mentions::collect_tool_mentions_from_messages; diff --git a/codex-rs/core/src/plugins/store.rs b/codex-rs/core/src/plugins/store.rs index a222fb7f70..e1a060c420 100644 --- a/codex-rs/core/src/plugins/store.rs +++ b/codex-rs/core/src/plugins/store.rs @@ -1,5 +1,5 @@ use super::load_plugin_manifest; -use super::manifest::PLUGIN_MANIFEST_PATH; +use codex_plugin::PLUGIN_MANIFEST_PATH; use codex_plugin::PluginId; use codex_plugin::PluginIdError; use codex_plugin::validate_plugin_segment; diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index f12f7ef971..ad980db1a7 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use crate::AuthManager; use crate::RolloutRecorder; use crate::agent::AgentControl; -use crate::analytics_client::AnalyticsEventsClient; use crate::client::ModelClient; use crate::config::StartedNetworkProxy; use crate::exec_policy::ExecPolicyManager; @@ -20,6 +19,7 @@ use crate::tools::network_approval::NetworkApprovalService; use crate::tools::runtimes::ExecveSessionApproval; use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; +use codex_analytics::AnalyticsEventsClient; use codex_exec_server::Environment; use codex_hooks::Hooks; use codex_otel::SessionTelemetry; diff --git a/codex-rs/plugin/Cargo.toml b/codex-rs/plugin/Cargo.toml index 45c9a37d3f..7caccc496d 100644 --- a/codex-rs/plugin/Cargo.toml +++ b/codex-rs/plugin/Cargo.toml @@ -13,4 +13,9 @@ path = "src/lib.rs" workspace = true [dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } thiserror = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/codex-rs/plugin/src/lib.rs b/codex-rs/plugin/src/lib.rs index 51e98ceba4..d789f64d04 100644 --- a/codex-rs/plugin/src/lib.rs +++ b/codex-rs/plugin/src/lib.rs @@ -1,10 +1,15 @@ //! Shared plugin identifiers and telemetry-facing summaries. +pub mod mention_syntax; + mod plugin_id; +mod plugin_namespace; pub use plugin_id::PluginId; pub use plugin_id::PluginIdError; pub use plugin_id::validate_plugin_segment; +pub use plugin_namespace::PLUGIN_MANIFEST_PATH; +pub use plugin_namespace::plugin_namespace_for_skill_path; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct AppConnectorId(pub String);