From 56c11cf6586c0579e4e3eca14eefb0916b14c78c Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Mon, 20 Jul 2026 23:08:22 +0000 Subject: [PATCH] Move shared skill models into `codex-skills` (#34429) ## What changed - Define and export skill metadata, policy, dependency, interface, and configuration rule types from `codex-skills`. - Update core, plugin, and extension consumers to use the shared models while retaining compatibility re-exports from `codex-core-skills`. - Share product-restriction handling between host and environment skill metadata. ## Testing - Add unit coverage for default and explicit implicit-invocation and product policies. GitOrigin-RevId: 9093469f8fff8e2d902dc9781235f64709827a87 --- codex-rs/Cargo.lock | 5 + codex-rs/core-plugins/Cargo.toml | 1 + codex-rs/core-plugins/src/loader.rs | 4 +- codex-rs/core-plugins/src/manager.rs | 4 +- codex-rs/core-plugins/src/manager_tests.rs | 2 +- .../core-plugins/src/tool_suggest_metadata.rs | 2 +- codex-rs/core-skills/src/config_rules.rs | 20 +-- .../core-skills/src/loader/environment.rs | 33 +---- codex-rs/core-skills/src/model.rs | 77 +---------- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/skills.rs | 4 +- codex-rs/ext/skills/Cargo.toml | 1 + codex-rs/ext/skills/src/catalog.rs | 2 +- codex-rs/ext/skills/src/provider/executor.rs | 2 +- codex-rs/ext/skills/src/provider/host.rs | 2 +- codex-rs/ext/skills/tests/skills_extension.rs | 2 +- codex-rs/skills/Cargo.toml | 2 + codex-rs/skills/src/lib.rs | 12 ++ codex-rs/skills/src/model.rs | 127 ++++++++++++++++++ codex-rs/skills/src/model_tests.rs | 36 +++++ 20 files changed, 206 insertions(+), 133 deletions(-) create mode 100644 codex-rs/skills/src/model.rs create mode 100644 codex-rs/skills/src/model_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 02b2a9aed9..3db2c3d36c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2665,6 +2665,7 @@ dependencies = [ "codex-sandboxing", "codex-shell-command", "codex-shell-escalation", + "codex-skills", "codex-state", "codex-terminal-detection", "codex-test-binary-support", @@ -2777,6 +2778,7 @@ dependencies = [ "codex-otel", "codex-plugin", "codex-protocol", + "codex-skills", "codex-tools", "codex-utils-absolute-path", "codex-utils-path", @@ -3950,7 +3952,9 @@ dependencies = [ name = "codex-skills" version = "0.0.0" dependencies = [ + "codex-protocol", "codex-utils-absolute-path", + "codex-utils-path-uri", "include_dir", "thiserror 2.0.18", ] @@ -3965,6 +3969,7 @@ dependencies = [ "codex-mcp", "codex-otel", "codex-protocol", + "codex-skills", "codex-tools", "codex-utils-absolute-path", "codex-utils-path-uri", diff --git a/codex-rs/core-plugins/Cargo.toml b/codex-rs/core-plugins/Cargo.toml index 0c98eb2b3e..ebcd1d4832 100644 --- a/codex-rs/core-plugins/Cargo.toml +++ b/codex-rs/core-plugins/Cargo.toml @@ -28,6 +28,7 @@ codex-model-provider = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } codex-protocol = { workspace = true } +codex-skills = { workspace = true } codex-tools = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-path = { workspace = true } diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index b3fd3c9759..e01cd04957 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -26,8 +26,6 @@ use codex_config::types::PluginMcpServerConfig; use codex_connectors::parse_plugin_app_config; use codex_connectors::parse_plugin_app_config_value; use codex_core_skills::PluginSkillSnapshots; -use codex_core_skills::SkillMetadata; -use codex_core_skills::config_rules::SkillConfigRules; use codex_core_skills::config_rules::resolve_disabled_skill_paths; use codex_core_skills::config_rules::skill_config_rules_from_stack; use codex_core_skills::loader::SkillRoot; @@ -44,6 +42,8 @@ use codex_plugin::app_connector_ids_from_declarations; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; +use codex_skills::SkillConfigRules; +use codex_skills::SkillMetadata; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_plugins::find_plugin_manifest_path; use serde_json::Value as JsonValue; diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 1cea26990a..d64d8fabdf 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -67,8 +67,6 @@ use codex_config::types::PluginConfig; use codex_config::types::ToolSuggestDisabledTool; use codex_config::types::ToolSuggestDiscoverableType; use codex_core_skills::PluginSkillSnapshots; -use codex_core_skills::SkillMetadata; -use codex_core_skills::config_rules::SkillConfigRules; use codex_core_skills::config_rules::skill_config_rules_from_stack; use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS; use codex_hooks::plugin_hook_declarations; @@ -84,6 +82,8 @@ use codex_plugin::prompt_safe_plugin_description; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::Product; +use codex_skills::SkillConfigRules; +use codex_skills::SkillMetadata; use codex_tools::DiscoverablePluginInfo; use codex_tools::DiscoverableTool; use codex_tools::filter_request_plugin_install_discoverable_tools_for_client; diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index a975623620..c6fdc26db8 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -42,13 +42,13 @@ use codex_config::types::McpServerTransportConfig; use codex_core_skills::PluginSkillSnapshots; use codex_core_skills::SkillsLoadInput; use codex_core_skills::SkillsService; -use codex_core_skills::config_rules::SkillConfigRules; use codex_login::CodexAuth; use codex_plugin::AppDeclaration; use codex_plugin::PluginId; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::HookEventName; use codex_protocol::protocol::Product; +use codex_skills::SkillConfigRules; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::test_support::PathBufExt; use pretty_assertions::assert_eq; diff --git a/codex-rs/core-plugins/src/tool_suggest_metadata.rs b/codex-rs/core-plugins/src/tool_suggest_metadata.rs index d535f18a2f..2f9102a3da 100644 --- a/codex-rs/core-plugins/src/tool_suggest_metadata.rs +++ b/codex-rs/core-plugins/src/tool_suggest_metadata.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; use std::sync::Arc; use std::sync::RwLock; -use codex_core_skills::config_rules::SkillConfigRules; use codex_plugin::AppDeclaration; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; @@ -11,6 +10,7 @@ use codex_plugin::app_connector_ids_from_declarations; use codex_plugin::prompt_safe_plugin_description; use codex_protocol::auth::AuthMode; use codex_protocol::protocol::Product; +use codex_skills::SkillConfigRules; use tokio::sync::Semaphore; use crate::app_mcp_routing::apply_app_mcp_routing_policy; diff --git a/codex-rs/core-skills/src/config_rules.rs b/codex-rs/core-skills/src/config_rules.rs index 05f3e48c14..8a64adfa69 100644 --- a/codex-rs/core-skills/src/config_rules.rs +++ b/codex-rs/core-skills/src/config_rules.rs @@ -5,28 +5,14 @@ use codex_config::ConfigLayerStack; use codex_config::ConfigLayerStackOrdering; use codex_config::SkillConfig; use codex_config::SkillsConfig; +pub use codex_skills::SkillConfigRule; +pub use codex_skills::SkillConfigRuleSelector; +pub use codex_skills::SkillConfigRules; use codex_utils_absolute_path::AbsolutePathBuf; use tracing::warn; use crate::SkillMetadata; -#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub enum SkillConfigRuleSelector { - Name(String), - Path(AbsolutePathBuf), -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct SkillConfigRule { - pub selector: SkillConfigRuleSelector, - pub enabled: bool, -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] -pub struct SkillConfigRules { - pub entries: Vec, -} - pub fn skill_config_rules_from_stack(config_layer_stack: &ConfigLayerStack) -> SkillConfigRules { let mut entries = Vec::new(); for layer in config_layer_stack.get_layers( diff --git a/codex-rs/core-skills/src/loader/environment.rs b/codex-rs/core-skills/src/loader/environment.rs index 5bef5481f7..15e2227b03 100644 --- a/codex-rs/core-skills/src/loader/environment.rs +++ b/codex-rs/core-skills/src/loader/environment.rs @@ -4,6 +4,7 @@ use std::io; use codex_exec_server::CapabilityRootDiscovery; use codex_exec_server::ExecutorFileSystem; use codex_protocol::protocol::Product; +pub use codex_skills::EnvironmentSkillMetadata; use codex_utils_path_uri::PathUri; use futures::StreamExt; @@ -36,17 +37,6 @@ struct ParsedEnvironmentSkill { policy: Option, } -/// URI-native metadata for one skill owned by an execution environment. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct EnvironmentSkillMetadata { - pub path_to_skills_md: PathUri, - pub name: String, - pub description: String, - pub short_description: Option, - pub dependencies: Option, - pub policy: Option, -} - /// Parsed executor skill plus the instructions already materialized by capability discovery. #[derive(Clone, Debug, PartialEq, Eq)] pub struct EnvironmentSkillSnapshot { @@ -60,27 +50,6 @@ pub struct EnvironmentSkillSnapshotOutcome { pub warnings: Vec, } -impl EnvironmentSkillMetadata { - pub fn allows_implicit_invocation(&self) -> bool { - self.policy - .as_ref() - .and_then(|policy| policy.allow_implicit_invocation) - .unwrap_or(true) - } - - fn matches_product_restriction(&self, restriction_product: Option) -> bool { - match &self.policy { - Some(policy) => { - policy.products.is_empty() - || restriction_product.is_some_and(|product| { - product.matches_product_restriction(&policy.products) - }) - } - None => true, - } - } -} - impl ParsedEnvironmentSkill { async fn load( file_system: &dyn ExecutorFileSystem, diff --git a/codex-rs/core-skills/src/model.rs b/codex-rs/core-skills/src/model.rs index 6afbde6cc9..fd6d9a3b36 100644 --- a/codex-rs/core-skills/src/model.rs +++ b/codex-rs/core-skills/src/model.rs @@ -7,81 +7,14 @@ use std::sync::Arc; use codex_exec_server::ExecutorFileSystem; use codex_exec_server::LOCAL_FS; use codex_protocol::protocol::Product; -use codex_protocol::protocol::SkillScope; +pub use codex_skills::SkillDependencies; +pub use codex_skills::SkillInterface; +pub use codex_skills::SkillMetadata; +pub use codex_skills::SkillPolicy; +pub use codex_skills::SkillToolDependency; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; -#[derive(Debug, Clone, PartialEq)] -pub struct SkillMetadata { - pub name: String, - pub description: String, - pub short_description: Option, - pub interface: Option, - pub dependencies: Option, - pub policy: Option, - /// Path to the SKILLS.md file that declares this skill. - pub path_to_skills_md: AbsolutePathBuf, - pub scope: SkillScope, - pub plugin_id: Option, -} - -impl SkillMetadata { - pub fn allows_implicit_invocation(&self) -> bool { - self.policy - .as_ref() - .and_then(|policy| policy.allow_implicit_invocation) - .unwrap_or(true) - } - - pub fn matches_product_restriction_for_product( - &self, - restriction_product: Option, - ) -> bool { - match &self.policy { - Some(policy) => { - policy.products.is_empty() - || restriction_product.is_some_and(|product| { - product.matches_product_restriction(&policy.products) - }) - } - None => true, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct SkillPolicy { - pub allow_implicit_invocation: Option, - // TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and - // storing this metadata. - pub products: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillInterface { - pub display_name: Option, - pub short_description: Option, - pub icon_small: Option, - pub icon_large: Option, - pub brand_color: Option, - pub default_prompt: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillDependencies { - pub tools: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SkillToolDependency { - pub r#type: String, - pub value: String, - pub description: Option, - pub transport: Option, - pub command: Option, - pub url: Option, -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct SkillError { pub path: AbsolutePathBuf, diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index f3767b57c2..4ca914efdf 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -63,6 +63,7 @@ codex-rollout = { workspace = true } codex-rollout-trace = { workspace = true } codex-rmcp-client = { workspace = true } codex-sandboxing = { workspace = true } +codex-skills = { workspace = true } codex-state = { workspace = true } codex-terminal-detection = { workspace = true } codex-thread-store = { workspace = true } diff --git a/codex-rs/core/src/skills.rs b/codex-rs/core/src/skills.rs index c5182a529c..640e1825f6 100644 --- a/codex-rs/core/src/skills.rs +++ b/codex-rs/core/src/skills.rs @@ -12,8 +12,6 @@ use codex_utils_plugins::PluginSkillRoot; pub use codex_core_skills::SkillError; pub use codex_core_skills::SkillLoadOutcome; -pub use codex_core_skills::SkillMetadata; -pub use codex_core_skills::SkillPolicy; pub use codex_core_skills::SkillRenderReport; pub use codex_core_skills::SkillsLoadInput; pub use codex_core_skills::SkillsService; @@ -34,6 +32,8 @@ pub use codex_core_skills::render; pub use codex_core_skills::render::SkillRenderSideEffects; pub use codex_core_skills::service; pub use codex_core_skills::system; +pub use codex_skills::SkillMetadata; +pub use codex_skills::SkillPolicy; pub(crate) fn skills_load_input_from_config( config: &Config, diff --git a/codex-rs/ext/skills/Cargo.toml b/codex-rs/ext/skills/Cargo.toml index c12bb8e1a3..2bf3a9e671 100644 --- a/codex-rs/ext/skills/Cargo.toml +++ b/codex-rs/ext/skills/Cargo.toml @@ -19,6 +19,7 @@ codex-extension-api = { workspace = true } codex-mcp = { workspace = true } codex-otel = { workspace = true } codex-protocol = { workspace = true } +codex-skills = { workspace = true } codex-tools = { workspace = true } codex-utils-path-uri = { workspace = true } codex-utils-string = { workspace = true } diff --git a/codex-rs/ext/skills/src/catalog.rs b/codex-rs/ext/skills/src/catalog.rs index d6e30ed0e6..e9a217bfbf 100644 --- a/codex-rs/ext/skills/src/catalog.rs +++ b/codex-rs/ext/skills/src/catalog.rs @@ -1,4 +1,4 @@ -use codex_core_skills::model::SkillDependencies; +use codex_skills::SkillDependencies; use codex_utils_path_uri::PathUri; use std::sync::Arc; diff --git a/codex-rs/ext/skills/src/provider/executor.rs b/codex-rs/ext/skills/src/provider/executor.rs index 97ee7e9a4e..8e330d9639 100644 --- a/codex-rs/ext/skills/src/provider/executor.rs +++ b/codex-rs/ext/skills/src/provider/executor.rs @@ -1,11 +1,11 @@ use std::sync::Arc; -use codex_core_skills::loader::EnvironmentSkillMetadata; use codex_core_skills::loader::load_environment_skills_from_discovery; use codex_core_skills::loader::load_environment_skills_from_root; use codex_exec_server::EnvironmentManager; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::protocol::Product; +use codex_skills::EnvironmentSkillMetadata; use codex_utils_path_uri::PathConvention; use crate::catalog::SkillAuthority; diff --git a/codex-rs/ext/skills/src/provider/host.rs b/codex-rs/ext/skills/src/provider/host.rs index 2cee694216..493ce51eb0 100644 --- a/codex-rs/ext/skills/src/provider/host.rs +++ b/codex-rs/ext/skills/src/provider/host.rs @@ -1,5 +1,5 @@ use codex_core_skills::SkillLoadOutcome; -use codex_core_skills::SkillMetadata; +use codex_skills::SkillMetadata; use crate::catalog::SkillAuthority; use crate::catalog::SkillCatalog; diff --git a/codex-rs/ext/skills/tests/skills_extension.rs b/codex-rs/ext/skills/tests/skills_extension.rs index 4e139f5959..c6bc1e5e4f 100644 --- a/codex-rs/ext/skills/tests/skills_extension.rs +++ b/codex-rs/ext/skills/tests/skills_extension.rs @@ -7,7 +7,6 @@ use std::sync::atomic::Ordering; use codex_core_skills::HostSkillsSnapshot; use codex_core_skills::SKILLS_INTRO_WITH_ABSOLUTE_PATHS; use codex_core_skills::SkillLoadOutcome; -use codex_core_skills::SkillMetadata; use codex_core_skills::injection::InjectedHostSkillPrompts; use codex_extension_api::ConversationHistory; use codex_extension_api::ExtensionData; @@ -31,6 +30,7 @@ use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::TruncationPolicy; use codex_protocol::protocol::TurnEnvironmentSelection; use codex_protocol::user_input::UserInput; +use codex_skills::SkillMetadata; use codex_skills_extension::SkillProviders; use codex_skills_extension::SkillsExtensionConfig; use codex_skills_extension::catalog::SkillAuthority; diff --git a/codex-rs/skills/Cargo.toml b/codex-rs/skills/Cargo.toml index babb5a177a..010e675186 100644 --- a/codex-rs/skills/Cargo.toml +++ b/codex-rs/skills/Cargo.toml @@ -14,6 +14,8 @@ path = "src/lib.rs" workspace = true [dependencies] +codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-path-uri = { workspace = true } include_dir = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/skills/src/lib.rs b/codex-rs/skills/src/lib.rs index 971e3f34c0..76ce83b6c7 100644 --- a/codex-rs/skills/src/lib.rs +++ b/codex-rs/skills/src/lib.rs @@ -1,3 +1,15 @@ +mod model; + +pub use model::EnvironmentSkillMetadata; +pub use model::SkillConfigRule; +pub use model::SkillConfigRuleSelector; +pub use model::SkillConfigRules; +pub use model::SkillDependencies; +pub use model::SkillInterface; +pub use model::SkillMetadata; +pub use model::SkillPolicy; +pub use model::SkillToolDependency; + use codex_utils_absolute_path::AbsolutePathBuf; use include_dir::Dir; use std::collections::hash_map::DefaultHasher; diff --git a/codex-rs/skills/src/model.rs b/codex-rs/skills/src/model.rs new file mode 100644 index 0000000000..c388718ad6 --- /dev/null +++ b/codex-rs/skills/src/model.rs @@ -0,0 +1,127 @@ +use codex_protocol::protocol::Product; +use codex_protocol::protocol::SkillScope; +use codex_utils_absolute_path::AbsolutePathBuf; +use codex_utils_path_uri::PathUri; + +/// Metadata for one skill materialized on the host filesystem. +#[derive(Debug, Clone, PartialEq)] +pub struct SkillMetadata { + pub name: String, + pub description: String, + pub short_description: Option, + pub interface: Option, + pub dependencies: Option, + pub policy: Option, + /// Path to the SKILL.md file that declares this skill. + pub path_to_skills_md: AbsolutePathBuf, + pub scope: SkillScope, + pub plugin_id: Option, +} + +impl SkillMetadata { + pub fn allows_implicit_invocation(&self) -> bool { + self.policy + .as_ref() + .and_then(|policy| policy.allow_implicit_invocation) + .unwrap_or(true) + } + + pub fn matches_product_restriction_for_product( + &self, + restriction_product: Option, + ) -> bool { + matches_product_restriction(self.policy.as_ref(), restriction_product) + } +} + +/// URI-native metadata for one skill owned by an execution environment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EnvironmentSkillMetadata { + pub path_to_skills_md: PathUri, + pub name: String, + pub description: String, + pub short_description: Option, + pub dependencies: Option, + pub policy: Option, +} + +impl EnvironmentSkillMetadata { + pub fn allows_implicit_invocation(&self) -> bool { + self.policy + .as_ref() + .and_then(|policy| policy.allow_implicit_invocation) + .unwrap_or(true) + } + + pub fn matches_product_restriction(&self, restriction_product: Option) -> bool { + matches_product_restriction(self.policy.as_ref(), restriction_product) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct SkillPolicy { + pub allow_implicit_invocation: Option, + // TODO: Enforce product gating in Codex skill selection/injection instead of only parsing and + // storing this metadata. + pub products: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillInterface { + pub display_name: Option, + pub short_description: Option, + pub icon_small: Option, + pub icon_large: Option, + pub brand_color: Option, + pub default_prompt: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillDependencies { + pub tools: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillToolDependency { + pub r#type: String, + pub value: String, + pub description: Option, + pub transport: Option, + pub command: Option, + pub url: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum SkillConfigRuleSelector { + Name(String), + Path(AbsolutePathBuf), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct SkillConfigRule { + pub selector: SkillConfigRuleSelector, + pub enabled: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct SkillConfigRules { + pub entries: Vec, +} + +fn matches_product_restriction( + policy: Option<&SkillPolicy>, + restriction_product: Option, +) -> bool { + match policy { + Some(policy) => { + policy.products.is_empty() + || restriction_product + .is_some_and(|product| product.matches_product_restriction(&policy.products)) + } + None => true, + } +} + +#[cfg(test)] +#[path = "model_tests.rs"] +mod tests; diff --git a/codex-rs/skills/src/model_tests.rs b/codex-rs/skills/src/model_tests.rs new file mode 100644 index 0000000000..0ec524ac7b --- /dev/null +++ b/codex-rs/skills/src/model_tests.rs @@ -0,0 +1,36 @@ +use super::*; + +#[test] +fn missing_policy_allows_implicit_invocation_and_all_products() { + let skill = EnvironmentSkillMetadata { + path_to_skills_md: PathUri::parse("file:///skills/demo/SKILL.md").expect("valid skill URI"), + name: "demo".to_string(), + description: "Demo skill".to_string(), + short_description: None, + dependencies: None, + policy: None, + }; + + assert!(skill.allows_implicit_invocation()); + assert!(skill.matches_product_restriction(/*restriction_product*/ None)); +} + +#[test] +fn policy_restricts_implicit_invocation_and_products() { + let policy = SkillPolicy { + allow_implicit_invocation: Some(false), + products: vec![Product::Codex], + }; + let skill = EnvironmentSkillMetadata { + path_to_skills_md: PathUri::parse("file:///skills/demo/SKILL.md").expect("valid skill URI"), + name: "demo".to_string(), + description: "Demo skill".to_string(), + short_description: None, + dependencies: None, + policy: Some(policy), + }; + + assert!(!skill.allows_implicit_invocation()); + assert!(skill.matches_product_restriction(Some(Product::Codex))); + assert!(!skill.matches_product_restriction(/*restriction_product*/ None)); +}