From ad74543a6f6e5590ae8839142259df571405da28 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 25 Mar 2026 11:05:35 -0700 Subject: [PATCH] Extract codex-utils-plugins crate (#15746) ## Summary - extract shared plugin path and manifest helpers into codex-utils-plugins - update codex-core to consume the utility crate ## Testing - CI --------- Co-authored-by: Codex --- codex-rs/Cargo.lock | 10 +++ codex-rs/Cargo.toml | 2 + codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/mention_syntax.rs | 6 +- codex-rs/core/src/plugins/manifest.rs | 3 +- codex-rs/utils/plugins/BUILD.bazel | 6 ++ codex-rs/utils/plugins/Cargo.toml | 21 ++++++ codex-rs/utils/plugins/src/lib.rs | 7 ++ codex-rs/utils/plugins/src/mention_syntax.rs | 7 ++ .../utils/plugins/src/plugin_namespace.rs | 70 +++++++++++++++++++ 10 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 codex-rs/utils/plugins/BUILD.bazel create mode 100644 codex-rs/utils/plugins/Cargo.toml create mode 100644 codex-rs/utils/plugins/src/lib.rs create mode 100644 codex-rs/utils/plugins/src/mention_syntax.rs create mode 100644 codex-rs/utils/plugins/src/plugin_namespace.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b57b6164b..d9f7512fc 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1905,6 +1905,7 @@ dependencies = [ "codex-utils-image", "codex-utils-output-truncation", "codex-utils-path", + "codex-utils-plugins", "codex-utils-pty", "codex-utils-readiness", "codex-utils-stream-parser", @@ -2947,6 +2948,15 @@ dependencies = [ "tempfile", ] +[[package]] +name = "codex-utils-plugins" +version = "0.0.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "codex-utils-pty" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 126351131..571e03790 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -69,6 +69,7 @@ members = [ "utils/oss", "utils/output-truncation", "utils/path-utils", + "utils/plugins", "utils/fuzzy-match", "utils/stream-parser", "utils/template", @@ -163,6 +164,7 @@ codex-utils-json-to-toml = { path = "utils/json-to-toml" } codex-utils-oss = { path = "utils/oss" } codex-utils-output-truncation = { path = "utils/output-truncation" } codex-utils-path = { path = "utils/path-utils" } +codex-utils-plugins = { path = "utils/plugins" } codex-utils-pty = { path = "utils/pty" } codex-utils-readiness = { path = "utils/readiness" } codex-utils-rustls-provider = { path = "utils/rustls-provider" } diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index d335dc6cc..ebff813ab 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -58,6 +58,7 @@ codex-utils-image = { workspace = true } codex-utils-home-dir = { workspace = true } codex-utils-output-truncation = { workspace = true } codex-utils-path = { workspace = true } +codex-utils-plugins = { workspace = true } codex-utils-pty = { workspace = true } codex-utils-readiness = { workspace = true } codex-secrets = { workspace = true } diff --git a/codex-rs/core/src/mention_syntax.rs b/codex-rs/core/src/mention_syntax.rs index e58f419b5..690cd1a2f 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_utils_plugins::mention_syntax::PLUGIN_TEXT_MENTION_SIGIL; +pub use codex_utils_plugins::mention_syntax::TOOL_MENTION_SIGIL; diff --git a/codex-rs/core/src/plugins/manifest.rs b/codex-rs/core/src/plugins/manifest.rs index 91c7cbbb3..d1b2c648e 100644 --- a/codex-rs/core/src/plugins/manifest.rs +++ b/codex-rs/core/src/plugins/manifest.rs @@ -1,11 +1,10 @@ use codex_utils_absolute_path::AbsolutePathBuf; +pub(crate) use codex_utils_plugins::PLUGIN_MANIFEST_PATH; 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/utils/plugins/BUILD.bazel b/codex-rs/utils/plugins/BUILD.bazel new file mode 100644 index 000000000..273e98fc4 --- /dev/null +++ b/codex-rs/utils/plugins/BUILD.bazel @@ -0,0 +1,6 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "plugins", + crate_name = "codex_utils_plugins", +) diff --git a/codex-rs/utils/plugins/Cargo.toml b/codex-rs/utils/plugins/Cargo.toml new file mode 100644 index 000000000..4e1826c25 --- /dev/null +++ b/codex-rs/utils/plugins/Cargo.toml @@ -0,0 +1,21 @@ + +[package] +edition.workspace = true +license.workspace = true +name = "codex-utils-plugins" +version.workspace = true + +[lib] +doctest = false +name = "codex_utils_plugins" +path = "src/lib.rs" + +[lints] +workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/codex-rs/utils/plugins/src/lib.rs b/codex-rs/utils/plugins/src/lib.rs new file mode 100644 index 000000000..5e990eb3c --- /dev/null +++ b/codex-rs/utils/plugins/src/lib.rs @@ -0,0 +1,7 @@ +//! Plugin path resolution and plaintext mention sigils shared across Codex crates. + +pub mod mention_syntax; +pub mod plugin_namespace; + +pub use plugin_namespace::PLUGIN_MANIFEST_PATH; +pub use plugin_namespace::plugin_namespace_for_skill_path; diff --git a/codex-rs/utils/plugins/src/mention_syntax.rs b/codex-rs/utils/plugins/src/mention_syntax.rs new file mode 100644 index 000000000..de5d1432b --- /dev/null +++ b/codex-rs/utils/plugins/src/mention_syntax.rs @@ -0,0 +1,7 @@ +//! Sigils for tool/plugin mentions in plaintext (shared across Codex crates). + +/// 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 = '@'; diff --git a/codex-rs/utils/plugins/src/plugin_namespace.rs b/codex-rs/utils/plugins/src/plugin_namespace.rs new file mode 100644 index 000000000..6688ae046 --- /dev/null +++ b/codex-rs/utils/plugins/src/plugin_namespace.rs @@ -0,0 +1,70 @@ +//! Resolve plugin namespace from skill file paths by walking ancestors for `plugin.json`. + +use std::fs; +use std::path::Path; + +/// Relative path from a plugin root to its manifest file. +pub const PLUGIN_MANIFEST_PATH: &str = ".codex-plugin/plugin.json"; + +#[derive(serde::Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawPluginManifestName { + #[serde(default)] + name: String, +} + +fn plugin_manifest_name(plugin_root: &Path) -> Option { + let manifest_path = plugin_root.join(PLUGIN_MANIFEST_PATH); + if !manifest_path.is_file() { + return None; + } + let contents = fs::read_to_string(&manifest_path).ok()?; + let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?; + Some( + plugin_root + .file_name() + .and_then(|entry| entry.to_str()) + .filter(|_| raw_name.trim().is_empty()) + .unwrap_or(raw_name.as_str()) + .to_string(), + ) +} + +/// Returns the plugin manifest `name` for the nearest ancestor of `path` that contains a valid +/// plugin manifest (same `name` rules as full manifest loading in codex-core). +pub fn plugin_namespace_for_skill_path(path: &Path) -> Option { + for ancestor in path.ancestors() { + if let Some(name) = plugin_manifest_name(ancestor) { + return Some(name); + } + } + None +} + +#[cfg(test)] +mod tests { + use super::plugin_namespace_for_skill_path; + use std::fs; + use tempfile::tempdir; + + #[test] + fn uses_manifest_name() { + let tmp = tempdir().expect("tempdir"); + let plugin_root = tmp.path().join("plugins/sample"); + let skill_path = plugin_root.join("skills/search/SKILL.md"); + + fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir"); + fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("mkdir manifest"); + fs::write( + plugin_root.join(".codex-plugin/plugin.json"), + r#"{"name":"sample"}"#, + ) + .expect("write manifest"); + fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill"); + + assert_eq!( + plugin_namespace_for_skill_path(&skill_path), + Some("sample".to_string()) + ); + } +}