use codex_protocol_test_support::ContentItemTestExt; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::sync::Mutex; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use assert_matches::assert_matches; use codex_attachment_store::InlineAttachmentStore; use codex_config::ConfigLayerEntry; use codex_config::ConfigLayerSource; use codex_config::ConfigLayerStack; use codex_config::ConfigRequirementsToml; use codex_exec_server::CapabilityRootDiscovery; use codex_exec_server::DiscoverV2CapabilitiesResponse; use codex_exec_server::EnvironmentManager; use codex_exec_server::ExecutorCapabilityDiscoverySnapshot; use codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot; use codex_exec_server::ExecutorSkill; use codex_exec_server::FileSystemEnvironmentAccessor; use codex_exec_server::LOCAL_FS; use codex_exec_server::SkillDiscoveryRoot; use codex_extension_api::ConversationHistory; use codex_extension_api::ExtensionData; use codex_extension_api::ExtensionEventSink; use codex_extension_api::ExtensionMetrics; use codex_extension_api::ExtensionRegistryBuilder; use codex_extension_api::ExtensionWarning; use codex_extension_api::FunctionCallError; use codex_extension_api::NoopTurnItemEmitter; use codex_extension_api::PreviousWorldStateSection; use codex_extension_api::RenderedWorldStateFragment; use codex_extension_api::SelectedPluginSnapshot; use codex_extension_api::SkillInvocationInput; use codex_extension_api::SkillInvocationKind; use codex_extension_api::ThreadStartInput; use codex_extension_api::ToolCall; use codex_extension_api::ToolCallSource; use codex_extension_api::ToolPayload; use codex_extension_api::TurnInputContext; use codex_extension_api::TurnStartInput; use codex_extension_api::WorldStateContributionInput; use codex_extension_api::WorldStateSectionContribution; use codex_models_manager::model_info::model_info_from_slug; use codex_otel::MetricsClient; use codex_otel::MetricsConfig; use codex_otel::THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC; use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC; use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; use codex_protocol::capabilities::CapabilityRootLocation; use codex_protocol::capabilities::SelectedCapabilityRoot; use codex_protocol::openai_models::ModelInfo; use codex_protocol::protocol::EnvironmentConfigState; use codex_protocol::protocol::Event; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; use codex_protocol::protocol::SessionSource; 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::ExecutorSkillProvider; use codex_skills_extension::HostSkillProvider; use codex_skills_extension::HostSkillsLoadInput; use codex_skills_extension::HostSkillsService; use codex_skills_extension::HostSkillsSnapshot; use codex_skills_extension::InjectedHostSkillPrompts; use codex_skills_extension::SkillLoadOutcome; use codex_skills_extension::SkillProviders; use codex_skills_extension::SkillsExtensionConfig; use codex_skills_extension::catalog::SkillAuthority; use codex_skills_extension::catalog::SkillCatalog; use codex_skills_extension::catalog::SkillCatalogEntry; use codex_skills_extension::catalog::SkillPackageId; use codex_skills_extension::catalog::SkillProviderError; use codex_skills_extension::catalog::SkillReadResult; use codex_skills_extension::catalog::SkillResourceId; use codex_skills_extension::catalog::SkillSearchResult; use codex_skills_extension::catalog::SkillSourceKind; use codex_skills_extension::install; use codex_skills_extension::install_with_providers; use codex_skills_extension::install_with_providers_and_metrics; use codex_skills_extension::provider::SkillListQuery; use codex_skills_extension::provider::SkillProvider; use codex_skills_extension::provider::SkillProviderFuture; use codex_skills_extension::provider::SkillReadRequest; use codex_skills_extension::provider::SkillSearchRequest; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; use opentelemetry_sdk::metrics::InMemoryMetricExporter; use opentelemetry_sdk::metrics::data::AggregatedMetrics; use opentelemetry_sdk::metrics::data::MetricData; use pretty_assertions::assert_eq; type TestResult = Result<(), Box>; #[path = "skills_extension/shadow_task_context_tests.rs"] mod shadow_task_context_tests; #[path = "skills_extension/presentation_dedup_tests.rs"] mod presentation_dedup_tests; static NEXT_CODEX_HOME_ID: AtomicUsize = AtomicUsize::new(0); const SKILLS_INTRO_WITH_ABSOLUTE_PATHS: &str = "A skill is a set of instructions provided through a `SKILL.md` source. Below is the list of skills that can be used. Each entry includes a name, description, and source locator. `file` locators are on the host filesystem, `executor package` locators are owned by their execution environment, `cloud package` locators are opaque package identifiers, and `custom resource` locators use their provider's access mechanism."; const DEMO_SKILL_CONTENTS: &str = "---\nname: demo\ndescription: Demo skill.\n---\n# Demo\n\nUse the demo skill.\n"; fn catalog_model_info() -> ModelInfo { ModelInfo { context_window: None, max_context_window: None, include_skills_usage_instructions: false, ..model_info_from_slug("test-model") } } fn world_state_section<'a>( sections: &'a [WorldStateSectionContribution], id: &str, ) -> &'a WorldStateSectionContribution { sections .iter() .find(|section| section.id() == id) .unwrap_or_else(|| panic!("world-state section {id} should exist")) } async fn prepare_step( registry: &codex_extension_api::ExtensionRegistry, input: codex_extension_api::StepPreparationInput<'_>, ) { for contributor in registry.tool_contributors() { contributor.prepare_for_step(input).await; } } async fn prepared_world_state( registry: &codex_extension_api::ExtensionRegistry, ready_selected_capability_roots: &[SelectedCapabilityRoot], input: WorldStateContributionInput<'_>, ) -> Vec { let discovery = input .step_store .get::(); let snapshots = input .step_store .get::>(); let selected_plugins = input .step_store .get::() .unwrap_or_default(); prepare_step( registry, codex_extension_api::StepPreparationInput { turn_id: input.turn_id, ready_selected_capability_roots, executor_capability_discovery: discovery.as_deref(), executor_capability_discovery_v2: snapshots.as_deref().map(Vec::as_slice), selected_plugins: &selected_plugins, session_store: input.session_store, thread_store: input.thread_store, turn_store: input.turn_store, step_store: input.step_store, }, ) .await; registry.context_contributors()[0] .contribute_world_state(input) .await } async fn prepared_turn_input( registry: &codex_extension_api::ExtensionRegistry, input: TurnInputContext<'_>, extension_metrics: Option>, session_store: &ExtensionData, thread_store: &ExtensionData, turn_store: &ExtensionData, ) -> Vec> { let roots = input .step_store .get::>() .map(|roots| { roots .iter() .map(|root| root.selected_root().clone()) .collect::>() }) .unwrap_or_default(); let discovery = input .step_store .get::(); let snapshots = input .step_store .get::>(); let selected_plugins = input .step_store .get::() .unwrap_or_default(); prepare_step( registry, codex_extension_api::StepPreparationInput { selected_plugins: &selected_plugins, turn_id: &input.turn_id, ready_selected_capability_roots: &roots, executor_capability_discovery: discovery.as_deref(), executor_capability_discovery_v2: snapshots.as_deref().map(Vec::as_slice), session_store, thread_store, turn_store, step_store: input.step_store, }, ) .await; registry.turn_input_contributors()[0] .contribute( input, extension_metrics, session_store, thread_store, turn_store, ) .await } async fn skill_world_state_fragments( registry: &codex_extension_api::ExtensionRegistry, session_store: &ExtensionData, thread_store: &ExtensionData, turn_id: &str, ) -> Result<(RenderedWorldStateFragment, RenderedWorldStateFragment), Box> { let turn_store = ExtensionData::new(turn_id); let selected_roots = [SelectedCapabilityRoot { id: "skills".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills")?, }, }]; let sections = prepared_world_state( registry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id, environments: &[], extension_metrics: None, session_store, thread_store, turn_store: &turn_store, }, ) .await; let executor = world_state_section(§ions, "skills") .render_diff(PreviousWorldStateSection::Absent) .1 .ok_or("executor skills should render through world state")?; let cloud = world_state_section(§ions, "cloud_skills") .render_diff(PreviousWorldStateSection::Absent) .1 .ok_or("cloud skills should render through world state")?; Ok((executor, cloud)) } async fn start_registered_turn( registry: &codex_extension_api::ExtensionRegistry, session_store: &ExtensionData, thread_store: &ExtensionData, turn_id: &str, ) { let turn_store = ExtensionData::new(turn_id); let mode = codex_protocol::config_types::CollaborationMode { mode: codex_protocol::config_types::ModeKind::Default, settings: codex_protocol::config_types::Settings { model: "test".to_string(), reasoning_effort: None, developer_instructions: None, }, }; for contributor in registry.turn_lifecycle_contributors() { contributor .on_turn_start(TurnStartInput { turn_id, collaboration_mode: &mode, token_usage_at_turn_start: None, session_store, thread_store, turn_store: &turn_store, }) .await; } } #[tokio::test] async fn installed_extension_uses_host_service_snapshot() -> TestResult { let codex_home = test_codex_home(); let skill_path = codex_home.join("skills").join("demo").join("SKILL.md"); std::fs::create_dir_all( skill_path .parent() .ok_or("skill path should have a parent")?, )?; std::fs::write(&skill_path, DEMO_SKILL_CONTENTS)?; let mut config = default_config(); config.shadow_selection_enabled = true; let mut builder = ExtensionRegistryBuilder::new(); install(&mut builder, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let skill_path = AbsolutePathBuf::try_from(skill_path)?; let skill_path_string = skill_path.to_string_lossy().into_owned(); let mut outcome = SkillLoadOutcome::default(); outcome.skills.push(SkillMetadata { name: "demo".to_string(), description: "Demo skill.".to_string(), short_description: None, interface: None, dependencies: None, policy: None, path_to_skills_md: PathUri::from_abs_path(&skill_path), scope: SkillScope::User, plugin_id: None, remote_plugin_id: None, }); let loaded_skills = Arc::new(outcome); let skill_prompt_path = skill_path_string.replace('\\', "/"); let turn_store = ExtensionData::new("turn-1"); turn_store.insert(HostSkillsSnapshot::new(Arc::clone(&loaded_skills))); let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &ExtensionData::new("step"), turn_id: "turn-1".to_string(), user_input: vec![UserInput::Text { text: "$demo".to_string(), text_elements: Vec::new(), }], environments: Vec::new(), }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; let expected_catalog = format!( "{SKILLS_INSTRUCTIONS_OPEN_TAG}\n## Skills\n{SKILLS_INTRO_WITH_ABSOLUTE_PATHS}\n### Available skills\n- demo: Demo skill. (file: {skill_prompt_path})\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}" ); let expected_skill = format!( "\ndemo\n{skill_prompt_path}\n{DEMO_SKILL_CONTENTS}\n" ); assert_eq!( vec![("developer", expected_catalog), ("user", expected_skill),], fragments .iter() .map(|fragment| (fragment.role(), fragment.render().expect_text())) .collect::>() ); let injected_host_skill_prompts = turn_store .get::() .ok_or("host skill prompt marker should be set")?; assert!(injected_host_skill_prompts.contains_path(&skill_path_string)); std::fs::remove_dir_all(codex_home)?; Ok(()) } #[tokio::test] async fn host_world_state_records_catalog_metrics_on_publish_and_change() -> TestResult { let mut builder = ExtensionRegistryBuilder::new(); install(&mut builder, skills_extension_config); let registry = builder.build(); let startup_metrics = Arc::new(RecordingMetrics::default()); let turn_metrics = Arc::new(RecordingMetrics::default()); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: Some(startup_metrics.clone()), session_store: &session_store, thread_store: &thread_store, }) .await; let skill_path = AbsolutePathBuf::try_from(test_codex_home().join("skills/demo/SKILL.md"))?; let mut outcome = SkillLoadOutcome::default(); outcome.skills.push(SkillMetadata { name: "demo".to_string(), description: "Demo skill.".to_string(), short_description: None, interface: None, dependencies: None, policy: None, path_to_skills_md: PathUri::from_abs_path(&skill_path), scope: SkillScope::User, plugin_id: None, remote_plugin_id: None, }); let turn_store = ExtensionData::new("turn-1"); turn_store.insert(HostSkillsSnapshot::new(Arc::new(outcome.clone()))); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: Some(turn_metrics.clone()), session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert_eq!(sections.len(), 2); let host_section = world_state_section(§ions, "host_skills"); let (published_snapshot, fragment) = host_section.render_diff(PreviousWorldStateSection::Absent); let published_snapshot = published_snapshot.unwrap(); assert!(fragment.is_some()); let mut expected = expected_catalog_metric_samples("host_world_state", /*count*/ 1); assert!(startup_metrics.samples().is_empty()); assert_eq!(turn_metrics.samples(), expected); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: Some(turn_metrics.clone()), session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert!( world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Known(&published_snapshot)) .1 .is_none() ); assert!(startup_metrics.samples().is_empty()); assert_eq!(turn_metrics.samples(), expected); let second_skill_path = AbsolutePathBuf::try_from(test_codex_home().join("skills/other/SKILL.md"))?; outcome.skills.push(SkillMetadata { name: "other".to_string(), description: "Other skill.".to_string(), short_description: None, interface: None, dependencies: None, policy: None, path_to_skills_md: PathUri::from_abs_path(&second_skill_path), scope: SkillScope::User, plugin_id: None, remote_plugin_id: None, }); turn_store.insert(HostSkillsSnapshot::new(Arc::new(outcome))); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: Some(turn_metrics.clone()), session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert!( world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Known(&published_snapshot)) .1 .is_some() ); expected.extend(expected_catalog_metric_samples( "host_world_state", /*count*/ 2, )); assert!(startup_metrics.samples().is_empty()); assert_eq!(turn_metrics.samples(), expected); Ok(()) } #[tokio::test] async fn persisted_host_snapshot_deduplicates_warning_after_reinitialization() -> TestResult { let (event_tx, event_rx) = std::sync::mpsc::channel(); let mut builder = ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); install(&mut builder, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let config = default_config(); let skill_path = AbsolutePathBuf::try_from(test_codex_home().join("skills/demo/SKILL.md"))?; let mut outcome = SkillLoadOutcome::default(); outcome.skills.push(SkillMetadata { name: "demo".to_string(), description: "Demo skill.".to_string(), short_description: None, interface: None, dependencies: None, policy: None, path_to_skills_md: PathUri::from_abs_path(&skill_path), scope: SkillScope::User, plugin_id: None, remote_plugin_id: None, }); let host_snapshot = HostSkillsSnapshot::new(Arc::new(outcome)); let thread_store = ExtensionData::new("thread"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let mut model_info = model_info_from_slug("test-model"); model_info.context_window = Some(50); let turn_store = ExtensionData::new("turn-1"); turn_store.insert(host_snapshot.clone()); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &model_info, thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; let host_section = world_state_section(§ions, "host_skills"); let (published_snapshot, fragment) = host_section.render_diff(PreviousWorldStateSection::Absent); let published_snapshot = published_snapshot.unwrap(); assert!(fragment.is_some()); event_rx.try_recv()?.into_warning(); assert!(event_rx.try_recv().is_err()); let resumed_thread_store = ExtensionData::new("thread"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &resumed_thread_store, }) .await; let mut model_info = model_info_from_slug("test-model"); model_info.context_window = Some(50); let resumed_turn_store = ExtensionData::new("turn-2"); resumed_turn_store.insert(host_snapshot); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &model_info, thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-2", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &resumed_thread_store, turn_store: &resumed_turn_store, }, ) .await; assert!( world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Known(&published_snapshot)) .1 .is_none() ); assert!(event_rx.try_recv().is_err()); Ok(()) } #[tokio::test] async fn executor_cloud_and_host_share_catalog_world_state_flow() -> TestResult { let provider = |mut entry: SkillCatalogEntry| { if matches!( entry.authority.kind, SkillSourceKind::Cloud | SkillSourceKind::Host ) { entry.name = "shared-skill".to_string(); } Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![entry], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, }) }; let providers = SkillProviders::new() .with_executor_provider(provider(test_entry( SkillSourceKind::Executor, "env-1", "executor/executor-skill", "skill://executor/executor-skill/SKILL.md", ))) .with_cloud_provider(provider( test_entry( SkillSourceKind::Cloud, "codex_apps", "cloud/cloud-skill", "skill://cloud/cloud-skill/SKILL.md", ) .with_remote_plugin_id(Some("shared-plugin".to_string())), )) .with_host_provider(provider( test_entry( SkillSourceKind::Host, "host", "host/host-skill", "/skills/host-skill/SKILL.md", ) .with_display_path("/skills/host-skill/SKILL.md") .with_remote_plugin_id(Some("shared-plugin".to_string())), )); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &default_config(), session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; // Host rendering stays separate even when its identity matches a Cloud skill. start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; let selected_roots = [SelectedCapabilityRoot { id: "skills".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills")?, }, }]; let turn_store = ExtensionData::new("turn-1"); turn_store.insert(HostSkillsSnapshot::new(Arc::new( SkillLoadOutcome::default(), ))); let metrics = Arc::new(RecordingMetrics::default()); let sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: Some(metrics.clone()), session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert_eq!( sections .iter() .map(WorldStateSectionContribution::id) .collect::>(), vec!["skills", "cloud_skills", "host_skills"] ); assert!(metrics.samples().is_empty()); for (section_id, expected_line) in [ ( "skills", "- executor-skill: Fix lint errors. (executor package: executor/executor-skill)", ), ( "cloud_skills", "- shared-skill: Fix lint errors. (cloud package: cloud/cloud-skill)", ), ( "host_skills", "- shared-skill: Fix lint errors. (file: /skills/host-skill/SKILL.md)", ), ] { let fragment = world_state_section(§ions, section_id) .render_diff(PreviousWorldStateSection::Absent) .1 .ok_or("skill catalog should render through world state")?; let body = fragment.body().clone().expect_text(); assert!(body.contains(expected_line)); } let expected_metrics = [ "executor_world_state", "cloud_world_state", "host_world_state", ] .into_iter() .flat_map(|surface| expected_catalog_metric_samples(surface, /*count*/ 1)) .collect::>(); assert_eq!(metrics.samples(), expected_metrics); Ok(()) } #[tokio::test] async fn nonempty_executor_empty_host_records_catalog_metrics() -> TestResult { let executor_provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![test_entry( SkillSourceKind::Executor, "env-1", "executor/lint-fix", "lint-fix/SKILL.md", )], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, }); let providers = SkillProviders::new() .with_host_provider(Arc::new(HostSkillProvider::new())) .with_executor_provider(executor_provider); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let metrics = Arc::new(RecordingMetrics::default()); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let selected_roots = vec![SelectedCapabilityRoot { id: "lint-fix".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills/lint-fix").expect("skill root URI"), }, }]; let turn_store = ExtensionData::new("turn-1"); turn_store.insert(HostSkillsSnapshot::new(Arc::new( SkillLoadOutcome::default(), ))); let sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: Some(metrics.clone()), session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert_eq!(sections.len(), 2); assert!( sections[0] .render_diff(PreviousWorldStateSection::Absent) .1 .is_some() ); assert!( world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Absent) .1 .is_none() ); let expected = expected_catalog_metric_samples("executor_world_state", /*count*/ 1); assert_eq!(metrics.samples(), expected); Ok(()) } #[tokio::test] async fn host_world_state_uses_provider_catalog_with_core_compatible_rendering() -> TestResult { let list_calls = Arc::new(AtomicUsize::new(0)); let provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![ test_entry( SkillSourceKind::Host, "host", "host/provider-skill", "provider-skill/SKILL.md", ) .with_short_description(Some("Short description.".to_string())), ], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: Some(Arc::clone(&list_calls)), fail_first_list: false, }); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers( &mut builder, SkillProviders::new().with_host_provider(provider), skills_extension_config, ); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let turn_store = ExtensionData::new("turn-1"); turn_store.insert(HostSkillsSnapshot::new(Arc::new( SkillLoadOutcome::default(), ))); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; let host_fragment = world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Absent) .1 .ok_or("host provider catalog should render")?; let body = host_fragment.body().clone().expect_text(); assert!(body.contains("Fix lint errors.")); assert!(!body.contains("Short description.")); assert_eq!(1, list_calls.load(Ordering::Relaxed)); Ok(()) } #[tokio::test] async fn shadow_selection_uses_host_catalog_when_instructions_are_disabled() -> TestResult { let list_calls = Arc::new(AtomicUsize::new(0)); let provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![test_entry( SkillSourceKind::Host, "host", "host/lint-fix", "lint-fix/SKILL.md", )], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: Some(Arc::clone(&list_calls)), fail_first_list: false, }); let metrics = MetricsClient::new( MetricsConfig::in_memory( "test", "codex-skills-extension", env!("CARGO_PKG_VERSION"), InMemoryMetricExporter::default(), ) .with_runtime_reader(), )?; let mut builder = ExtensionRegistryBuilder::new(); install_with_providers_and_metrics( &mut builder, SkillProviders::new().with_host_provider(provider), Some(metrics.clone()), skills_extension_config, ); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let mut config = default_config(); config.include_instructions = false; config.shadow_selection_enabled = true; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let turn_store = ExtensionData::new("turn-1"); let step_store = ExtensionData::new("step"); turn_store.insert(HostSkillsSnapshot::new(Arc::new( SkillLoadOutcome::default(), ))); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &step_store, turn_id: "turn-1".to_string(), user_input: vec![UserInput::Text { text: "Fix lint errors.".to_string(), text_elements: Vec::new(), }], environments: Vec::new(), }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; assert!( world_state_section(§ions, "host_skills") .render_diff(PreviousWorldStateSection::Absent) .1 .is_none() ); assert!(fragments.is_empty()); let snapshots = shadow_task_context_tests::collect_shadow_observations( &metrics, "codex.skills.shadow_selection.catalog_entries", /*expected*/ 12, ) .await?; let catalog_entry_counts = snapshots .iter() .flat_map(opentelemetry_sdk::metrics::data::ResourceMetrics::scope_metrics) .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) .filter(|metric| metric.name() == "codex.skills.shadow_selection.catalog_entries") .flat_map(|metric| match metric.data() { AggregatedMetrics::F64(MetricData::Histogram(histogram)) => histogram .data_points() .map(opentelemetry_sdk::metrics::data::HistogramDataPoint::sum), data => panic!("unexpected shadow catalog metric data: {data:?}"), }) .collect::>(); assert!( catalog_entry_counts.iter().all(|count| *count == 1.0), "every shadow selector should see the cached host skill: {catalog_entry_counts:?}" ); assert_eq!(1, list_calls.load(Ordering::Relaxed)); Ok(()) } #[tokio::test] async fn shadow_lru_selector_recovers_a_skill_invoked_on_an_earlier_turn() -> TestResult { let provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![test_entry( SkillSourceKind::Host, "host", "host/lint-fix", "lint-fix/SKILL.md", )], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, }); let metrics = MetricsClient::new( MetricsConfig::in_memory( "test", "codex-skills-extension", env!("CARGO_PKG_VERSION"), InMemoryMetricExporter::default(), ) .with_runtime_reader(), )?; let mut builder = ExtensionRegistryBuilder::new(); install_with_providers_and_metrics( &mut builder, SkillProviders::new().with_host_provider(provider), Some(metrics.clone()), skills_extension_config, ); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let mut config = default_config(); config.include_instructions = false; config.shadow_selection_enabled = true; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; for (turn_id, text) in [("turn-1", "Fix lint errors."), ("turn-2", "continue")] { let turn_store = ExtensionData::new(turn_id); let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &ExtensionData::new("step"), turn_id: turn_id.to_string(), user_input: vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), }], environments: vec![codex_extension_api::TurnInputEnvironment { environment_id: "test".to_string(), cwd: PathUri::from_host_native_path(std::env::current_dir().expect("test cwd")) .expect("absolute cwd"), is_primary: true, fs: &FileSystemEnvironmentAccessor::unrestricted(&LOCAL_FS), }], }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; assert!(fragments.is_empty()); registry.skill_invocation_contributors()[0] .on_skill_invocation(SkillInvocationInput { session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, turn_id, skill_resource: "lint-fix/SKILL.md", kind: SkillInvocationKind::Implicit, }) .await; } let snapshots = shadow_task_context_tests::collect_shadow_observations( &metrics, "codex.skills.shadow_selection.invocation", /*expected*/ 24, ) .await?; let selector_hits = snapshots .iter() .flat_map(opentelemetry_sdk::metrics::data::ResourceMetrics::scope_metrics) .flat_map(opentelemetry_sdk::metrics::data::ScopeMetrics::metrics) .filter(|metric| metric.name() == "codex.skills.shadow_selection.invocation") .flat_map(|metric| match metric.data() { AggregatedMetrics::U64(MetricData::Sum(sum)) => sum.data_points(), data => panic!("unexpected shadow invocation metric data: {data:?}"), }) .filter_map(|point| { let method = point .attributes() .find(|attribute| attribute.key.as_str() == "method")? .value .as_str(); if !matches!( method.as_ref(), "lru_v1" | "lru_plus_lexical_v1" | "lru_plus_character_routing_v1" | "lru_plus_lexical_character_routing_v1" ) { return None; } let hit = point .attributes() .find(|attribute| attribute.key.as_str() == "hit")? .value .as_str() .to_string(); Some((method.to_string(), hit, point.value())) }) .fold( std::collections::BTreeMap::new(), |mut totals, (method, hit, count)| { *totals.entry((method, hit)).or_insert(0) += count; totals }, ) .into_iter() .map(|((method, hit), count)| (method, hit, count)) .collect::>(); assert_eq!( vec![ ( "lru_plus_character_routing_v1".to_string(), "true".to_string(), 2, ), ( "lru_plus_lexical_character_routing_v1".to_string(), "true".to_string(), 2, ), ("lru_plus_lexical_v1".to_string(), "true".to_string(), 2), ("lru_v1".to_string(), "false".to_string(), 1), ("lru_v1".to_string(), "true".to_string(), 1), ], selector_hits ); Ok(()) } #[tokio::test] async fn selected_executor_catalog_caches_root_selections_per_turn_and_reuses_discovery_snapshots() -> TestResult { let step_store = ExtensionData::new("step-1"); let read_requests = Arc::new(Mutex::new(Vec::new())); let list_calls = Arc::new(AtomicUsize::new(0)); let resource = SkillResourceId::environment( "lint-fix/SKILL.md", "env-1", PathUri::parse("file:///skills/lint-fix/SKILL.md")?, ); let mut entry = test_entry( SkillSourceKind::Executor, "env-1", "executor/lint-fix", "lint-fix/SKILL.md", ); entry.main_prompt = resource.clone(); let executor_provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![entry], warnings: Vec::new(), }, read_requests: Arc::clone(&read_requests), list_calls: Some(Arc::clone(&list_calls)), fail_first_list: false, }); let providers = SkillProviders::new().with_executor_provider(executor_provider); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let selected_roots = vec![SelectedCapabilityRoot { id: "lint-fix".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills/lint-fix").expect("skill root URI"), }, }]; let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let prompt_fragments = registry.context_contributors()[0] .contribute_thread_context(&session_store, &thread_store) .await; assert!(prompt_fragments.is_empty()); let turn_store = ExtensionData::new("turn-1"); let turn_environment = TurnEnvironmentSelection { selected_capability_roots: Default::default(), environment_id: "env-1".to_string(), cwd: PathUri::parse("file:///workspace").expect("cwd URI"), workspace_roots: Vec::new(), config: EnvironmentConfigState::FromThread, }; let available_sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: std::slice::from_ref(&turn_environment), extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert_eq!(1, available_sections.len()); let (available_snapshot, available_fragment) = available_sections[0].render_diff(PreviousWorldStateSection::Absent); let available_snapshot = available_snapshot.unwrap(); let available_fragment = available_fragment.ok_or("available skills should render")?; let available_body = available_fragment.body().clone().expect_text(); assert!(available_body.contains("lint-fix")); assert!(available_body.contains("(executor package: executor/lint-fix)")); let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &step_store, turn_id: "turn-1".to_string(), user_input: vec![UserInput::Text { text: "$lint-fix please".to_string(), text_elements: Vec::new(), }], environments: vec![codex_extension_api::TurnInputEnvironment { environment_id: turn_environment.environment_id.clone(), cwd: turn_environment.cwd.clone(), is_primary: true, fs: &FileSystemEnvironmentAccessor::unrestricted(&LOCAL_FS), }], }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; assert_eq!(1, fragments.len()); assert_eq!("user", fragments[0].role()); let rendered = fragments[0].render().expect_text(); assert!(rendered.contains("lint-fix")); assert!(rendered.contains("# Lint Fix")); assert_eq!( vec![( SkillAuthority::new(SkillSourceKind::Executor, "env-1"), SkillPackageId("executor/lint-fix".to_string()), resource, )], read_request_keys(&read_requests) ); let available_world_state = serde_json::Map::from_iter([("skills".to_string(), available_snapshot.clone())]); let unavailable_turn_store = ExtensionData::new("turn-2"); let unavailable_sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: Some(&available_world_state), step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-2", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &unavailable_turn_store, }, ) .await; let (unavailable_snapshot, unavailable_fragment) = unavailable_sections[0].render_diff(PreviousWorldStateSection::Known(&available_snapshot)); let unavailable_snapshot = unavailable_snapshot.unwrap(); assert_eq!( unavailable_snapshot, serde_json::json!({ "body": null, "includeInstructions": true, "lastAvailableFingerprint": blake3::hash( available_snapshot["body"].as_str().ok_or("available catalog")?.as_bytes() ).to_hex().to_string(), }), ); let unavailable_fragment = unavailable_fragment.ok_or("removed skills should render")?; let unavailable_body = unavailable_fragment.body().clone().expect_text(); assert!(unavailable_body.contains("No selected-environment skills")); let unavailable_world_state = serde_json::Map::from_iter([("skills".to_string(), unavailable_snapshot.clone())]); let restored_turn_store = ExtensionData::new("turn-3"); let restored_sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: Some(&unavailable_world_state), step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-3", environments: &[turn_environment], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &restored_turn_store, }, ) .await; let (restored_snapshot, restored_fragment) = restored_sections[0].render_diff(PreviousWorldStateSection::Known(&unavailable_snapshot)); let restored_snapshot = restored_snapshot.unwrap(); let restored_fragment = restored_fragment.ok_or("restored skills should render")?; assert_eq!( restored_fragment.body().clone().expect_text(), "\n## Skills update\nThe previously listed selected-environment skills are available again.\n", ); assert!(restored_sections[0].matches_retained_fragment("developer", &available_body)); assert!(!restored_sections[0].matches_retained_fragment("developer", &unavailable_body)); assert_eq!( restored_sections[0] .render_diff(PreviousWorldStateSection::Absent) .1, Some(available_fragment), ); assert_eq!(2, list_calls.load(Ordering::Relaxed)); // Retained steps keep their catalog even after the selected roots change. let retained_sections = registry.context_contributors()[0] .contribute_world_state(WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }) .await; assert_eq!( retained_sections[0] .render_diff(PreviousWorldStateSection::Absent) .0 .as_ref(), Some(&available_snapshot) ); assert_eq!(2, list_calls.load(Ordering::Relaxed)); let failed_discovery = ExecutorCapabilityDiscoverySnapshot::new( &selected_roots, vec![Err("exec-server transport disconnected".to_string())], Default::default(), ); let recovered_discovery = ExecutorCapabilityDiscoverySnapshot::new( &selected_roots, vec![Ok(Arc::new(CapabilityRootDiscovery { id: "lint-fix".to_string(), path: PathUri::parse("file:///skills/lint-fix")?, plugin: None, skills: Vec::new(), namespace_manifests: Vec::new(), warnings: Vec::new(), error: None, }))], Default::default(), ); for (turn_id, discovery, expected_list_calls) in [ ("failed-discovery", &failed_discovery, 3), ("recovered-discovery", &recovered_discovery, 4), ("cached-discovery", &recovered_discovery, 4), ] { let step_store = ExtensionData::new("step"); step_store.insert(discovery.clone()); prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id, environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &ExtensionData::new(turn_id), }, ) .await; assert_eq!(expected_list_calls, list_calls.load(Ordering::Relaxed)); } let mut listing_disabled_config = config.clone(); listing_disabled_config.include_instructions = false; registry.config_contributors()[0].on_config_changed( &session_store, &thread_store, &config, &listing_disabled_config, ); let listing_disabled_turn_store = ExtensionData::new("turn-4"); let listing_disabled_sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-4", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &listing_disabled_turn_store, }, ) .await; let (listing_disabled_snapshot, listing_disabled_fragment) = listing_disabled_sections[0] .render_diff(PreviousWorldStateSection::Known(&restored_snapshot)); let listing_disabled_fragment = listing_disabled_fragment.ok_or("disabled skill listing should render")?; assert_eq!( "\n## Skills update\nSelected-environment skills are not listed automatically. Explicit skill mentions can still be resolved when available.\n", listing_disabled_fragment.body().clone().expect_text() ); let mut normalized_listing_disabled_snapshot = listing_disabled_snapshot.unwrap(); normalized_listing_disabled_snapshot .as_object_mut() .ok_or("skills snapshot should be an object")? .remove("body"); assert!( listing_disabled_sections[0] .render_diff(PreviousWorldStateSection::Known( &normalized_listing_disabled_snapshot )) .1 .is_none() ); let first_root = selected_roots[0].clone(); let second_root = SelectedCapabilityRoot { id: "second-root".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills/second-root")?, }, }; let cache_environment = Arc::new(codex_exec_server::Environment::create_for_tests( /*exec_server_url*/ None, )?); let cache_manager = EnvironmentManager::default_for_tests(); let previous_list_calls = list_calls.load(Ordering::Relaxed); let mut current_config = listing_disabled_config; for (turn_id, roots, include_bundled_skills, expected_new_calls) in [ ("cache-turn-1", vec![first_root.clone()], true, 1), ("cache-turn-1", vec![first_root.clone()], true, 1), ( "cache-turn-1", vec![first_root.clone(), second_root.clone()], true, 2, ), ( "cache-turn-1", vec![second_root.clone(), first_root.clone()], true, 3, ), ("cache-turn-1", vec![first_root.clone()], true, 4), ("cache-turn-2", vec![first_root.clone()], true, 5), ("cache-turn-2", vec![first_root.clone()], false, 6), ("cache-turn-2", vec![first_root], false, 6), ] { let mut next_config = current_config.clone(); next_config.bundled_skills_enabled = include_bundled_skills; registry.config_contributors()[0].on_config_changed( &session_store, &thread_store, ¤t_config, &next_config, ); current_config = next_config; let cache_step = ExtensionData::new("cache-step"); let captured = HashMap::from([("env-1".to_string(), Some(Arc::clone(&cache_environment)))]); cache_step.insert( cache_manager .resolve_selected_capability_roots(&roots, &captured) .await, ); prepared_world_state( ®istry, &roots, WorldStateContributionInput { previous_world_state: None, step_store: &cache_step, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id, environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &ExtensionData::new(turn_id), }, ) .await; assert_eq!( previous_list_calls + expected_new_calls, list_calls.load(Ordering::Relaxed) ); } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn overlapping_executor_catalogs_render_their_own_roots() -> TestResult { use tracing::Subscriber; use tracing::instrument::WithSubscriber; use tracing_subscriber::Layer; use tracing_subscriber::layer::Context; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::LookupSpan; // Pause after A's refresh returns, before World State can read the catalog. Pausing the // provider would be too early: A would still publish its result after B finishes. struct PauseAfterRefresh { entered: Mutex>>, resume: Mutex>, } impl LookupSpan<'a>> Layer for PauseAfterRefresh { fn on_close(&self, id: tracing::span::Id, context: Context<'_, S>) { if context .span(&id) .is_some_and(|span| span.name() == "skills.executor.refresh_executor_catalog") && let Some(entered) = self.entered.lock().unwrap().take() { entered.send(()).unwrap(); self.resume .lock() .unwrap() .recv_timeout(std::time::Duration::from_secs(/*secs*/ 10)) .expect("B should finish before A reads its catalog"); } } } let temp = tempfile::tempdir()?; let root = |name: &str| -> Result> { let path = temp.path().join(name); std::fs::create_dir_all(path.join(name))?; std::fs::write( path.join(name).join("SKILL.md"), DEMO_SKILL_CONTENTS.replace("demo", name), )?; Ok(SelectedCapabilityRoot { id: name.to_string(), location: CapabilityRootLocation::Environment { environment_id: codex_exec_server::LOCAL_ENVIRONMENT_ID.to_string(), path: PathUri::from_host_native_path(path)?, }, }) }; let root_a = root("skill-a")?; let root_b = root("skill-b")?; let mut builder = ExtensionRegistryBuilder::new(); let provider = codex_skills_extension::ExecutorSkillProvider::new_with_restriction_product( Arc::new(codex_exec_server::EnvironmentManager::default_for_tests()), /*restriction_product*/ None, ); install_with_providers( &mut builder, SkillProviders::new().with_executor_provider(Arc::new(provider)), skills_extension_config, ); let registry = Arc::new(builder.build()); let session_store = Arc::new(ExtensionData::new("session")); let thread_store = Arc::new(ExtensionData::new("thread")); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &default_config(), session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let thread_id = codex_protocol::ThreadId::new(); let render = |root: SelectedCapabilityRoot| { let registry = Arc::clone(®istry); let session_store = Arc::clone(&session_store); let thread_store = Arc::clone(&thread_store); async move { let step_store = ExtensionData::new(&root.id); let sections = prepared_world_state( ®istry, &[root], WorldStateContributionInput { model_info: &catalog_model_info(), thread_id, turn_id: "turn", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &step_store, step_store: &step_store, previous_world_state: None, }, ) .await; world_state_section(§ions, "skills") .render_diff(PreviousWorldStateSection::Absent) .1 .expect("executor skills should render") .body() .clone() .expect_text() } }; let (entered, paused) = tokio::sync::oneshot::channel(); let (release, resume) = std::sync::mpsc::channel(); // Keep tracing aware of uninstrumented threads so a parallel test cannot register // the refresh callsite as globally disabled before this subscriber reaches it. let _interest_cache_guard = tracing::Dispatch::new(tracing::subscriber::NoSubscriber::default()); let subscriber = tracing_subscriber::registry().with( PauseAfterRefresh { entered: Mutex::new(Some(entered)), resume: Mutex::new(resume), } .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { metadata.name() == "skills.executor.refresh_executor_catalog" })), ); let a = tokio::spawn(render(root_a).with_subscriber(subscriber)); tokio::time::timeout(std::time::Duration::from_secs(/*secs*/ 10), paused).await??; let b = render(root_b).await; release.send(())?; let a = a.await?; assert!(a.contains("skill-a") && !a.contains("skill-b"), "{a}"); assert!(b.contains("skill-b") && !b.contains("skill-a"), "{b}"); Ok(()) } #[tokio::test] async fn default_context_truncates_catalog_descriptions() -> TestResult { let description = "x".repeat(1_025); // A small cloud catalog leaves its unused allowance to filesystem descriptions. let executor_entries = (0..5) .map(|index| { let package_id = format!("executor/executor-long-description-{index}"); let mut entry = test_entry( SkillSourceKind::Executor, "env-1", &package_id, &format!("skill://{package_id}/SKILL.md"), ); entry.description = description.clone(); entry }) .collect(); let mut cloud_entry = test_entry( SkillSourceKind::Cloud, "codex_apps", "cloud/cloud-long-description", "skill://cloud/cloud-long-description/SKILL.md", ); cloud_entry.description = description.clone(); let providers = SkillProviders::new() .with_executor_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: executor_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })) .with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![cloud_entry], warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; let (executor, cloud) = skill_world_state_fragments(®istry, &session_store, &thread_store, "turn-1").await?; let executor = executor.body().clone().expect_text(); let cloud = cloud.body().clone().expect_text(); let expected_description = "x".repeat(1_021) + "..."; for index in 0..5 { assert!(executor.contains(&format!( "- executor-long-description-{index}: {expected_description} (executor package:" ))); } assert!(cloud.contains("- cloud-long-description:")); for rendered in [executor, cloud] { assert!(rendered.contains(&expected_description)); assert!(!rendered.contains(&"x".repeat(1_024))); assert!(!rendered.contains(&description)); } Ok(()) } #[tokio::test] async fn catalog_rebalances_only_to_avoid_omissions_and_retains_the_allocation() -> TestResult { enum Phase { Initial, FirstReady, Disconnected, Reconnected, Resumed, CloudCatalogGrew, BudgetChanged, } // Description-only pressure, filesystem omissions that can be rescued, and // unavoidable omissions. Check both token and fallback character accounting. for (executor_count, cloud_count, rebalance, all_fit) in [ (17, 82, false, true), (40, 5, true, true), (40, 160, false, false), ] { for context_window in [None, Some(100_000)] { let description = "x".repeat(1_025); let executor_entries: Vec<_> = (0..executor_count) .map(|index| { let package_id = format!("executor/executor-skill-{index:02}"); let mut entry = test_entry( SkillSourceKind::Executor, "env-1", &package_id, &format!("skill://{package_id}/SKILL.md"), ); entry.description = description.clone(); entry }) .collect(); let registry_for_cloud_count = |cloud_count| { let cloud_entries = (0..cloud_count) .map(|index| { let package_id = format!("cloud/cloud-skill-{index:02}"); let mut entry = test_entry( SkillSourceKind::Cloud, "codex_apps", &package_id, &format!("skill://{package_id}/SKILL.md"), ); entry.description = description.clone(); entry }) .collect(); let providers = SkillProviders::new() .with_executor_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: executor_entries.clone(), warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })) .with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: cloud_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); builder.build() }; let mut registry = registry_for_cloud_count(cloud_count); let selected_roots = [SelectedCapabilityRoot { id: "skills".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills")?, }, }]; let mut model = ModelInfo { context_window, ..catalog_model_info() }; let session_store = ExtensionData::new("session"); let mut thread_store = ExtensionData::new("thread"); let mut previous = serde_json::Map::new(); let mut initial_cloud = None; let mut accepted_cloud = None; let mut phases = vec![ Phase::Initial, Phase::FirstReady, Phase::Disconnected, Phase::Reconnected, Phase::Disconnected, Phase::Reconnected, Phase::Resumed, ]; if rebalance { phases.extend([ Phase::CloudCatalogGrew, Phase::Disconnected, Phase::Reconnected, Phase::Resumed, ]); } phases.push(Phase::BudgetChanged); for (step, phase) in phases.into_iter().enumerate() { if matches!(phase, Phase::CloudCatalogGrew) { // Refresh the cloud inventory on resume while retaining the saved allocation. registry = registry_for_cloud_count(/*cloud_count*/ 15); } if matches!( phase, Phase::Initial | Phase::Resumed | Phase::CloudCatalogGrew ) { thread_store = ExtensionData::new("thread"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &default_config(), session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; } if matches!(phase, Phase::BudgetChanged) { model.context_window = Some(200_000); } let ready = matches!( phase, Phase::FirstReady | Phase::Reconnected | Phase::Resumed | Phase::CloudCatalogGrew ); let turn_id = format!("turn-{step}"); let turn_store = ExtensionData::new(&turn_id); start_registered_turn(®istry, &session_store, &thread_store, &turn_id).await; let sections = prepared_world_state( ®istry, if ready { &selected_roots } else { &[] }, WorldStateContributionInput { previous_world_state: Some(&previous), model_info: &model, thread_id: codex_protocol::ThreadId::new(), turn_id: &turn_id, environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, step_store: &turn_store, }, ) .await; let rendered = sections .iter() .map(|section| { let prior = previous.get(section.id()).map_or( PreviousWorldStateSection::Absent, PreviousWorldStateSection::Known, ); (section.id().to_string(), section.render_diff(prior)) }) .collect::>(); let cloud = &rendered["cloud_skills"]; let cloud_snapshot = cloud.0.as_ref().expect("cloud snapshot"); let cloud_body = cloud_snapshot["body"].as_str().ok_or("cloud catalog")?; match phase { Phase::Initial => initial_cloud = Some(cloud_body.to_string()), Phase::FirstReady => { assert_eq!(initial_cloud.as_deref() != Some(cloud_body), rebalance); accepted_cloud = Some(cloud_snapshot.clone()); let executor = rendered["skills"].0.as_ref().expect("executor snapshot"); let executor_body = executor["body"].as_str().ok_or("executor catalog")?; assert_eq!( executor_body .lines() .filter(|line| line.starts_with("- executor-skill-")) .count() == executor_count, all_fit ); if all_fit { assert_eq!( cloud_body .lines() .filter(|line| line.starts_with("- cloud-skill-")) .count(), cloud_count ); } } Phase::CloudCatalogGrew => { let allocation = &cloud_snapshot["allocation"]; let previous_limit = previous["cloud_skills"]["allocation"]["cloudLimit"] .as_u64() .ok_or("previous cloud limit")?; assert!( allocation["cloudLimit"].as_u64().ok_or("cloud limit")? > previous_limit ); assert_ne!( allocation["cloudCatalogFingerprint"], previous["cloud_skills"]["allocation"]["cloudCatalogFingerprint"] ); assert_eq!( cloud_body .lines() .filter(|line| line.starts_with("- cloud-skill-")) .count(), 15 ); let executor = rendered["skills"].0.as_ref().expect("executor snapshot"); assert_eq!( executor["body"] .as_str() .ok_or("executor catalog")? .lines() .filter(|line| line.starts_with("- executor-skill-")) .count(), executor_count ); accepted_cloud = Some(cloud_snapshot.clone()); } Phase::Disconnected | Phase::Reconnected | Phase::Resumed => { assert_eq!(Some(cloud_snapshot), accepted_cloud.as_ref()); assert!(cloud.1.is_none()); } Phase::BudgetChanged => { // A genuinely different total budget starts a new allocation. assert_eq!( cloud_snapshot["allocation"], serde_json::json!({ "totalBudget": {"tokens": 4_000}, "cloudLimit": 3_000, "cloudCatalogFingerprint": previous["cloud_skills"]["allocation"]["cloudCatalogFingerprint"], }) ); } } previous = rendered .into_iter() .map(|(id, (snapshot, _))| (id, snapshot.expect("skills snapshot"))) .collect(); } } } Ok(()) } #[tokio::test] async fn extreme_budget_pressure_removes_descriptions_before_omitting_entries() -> TestResult { let executor_entries = (0..40) .map(|index| { let package_id = format!("executor/executor-skill-{index:03}"); let mut entry = test_entry( SkillSourceKind::Executor, "env-1", &package_id, &format!("skill://{package_id}/SKILL.md"), ); entry.description = format!("executor-description-{index:03}"); entry }) .collect(); let cloud_entries = (0..160) .map(|index| { let package_id = format!("cloud/cloud-skill-{index:03}"); let mut entry = test_entry( SkillSourceKind::Cloud, "codex_apps", &package_id, &format!("skill://{package_id}/SKILL.md"), ); entry.description = format!("cloud-description-{index:03}"); entry }) .collect(); let providers = SkillProviders::new() .with_executor_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: executor_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })) .with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: cloud_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })); let (event_tx, event_rx) = std::sync::mpsc::channel(); let mut builder = ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; let (executor, cloud) = skill_world_state_fragments(®istry, &session_store, &thread_store, "turn-1").await?; let executor = executor.body().clone().expect_text(); let cloud = cloud.body().clone().expect_text(); let included_executor_count = executor .lines() .filter(|line| line.starts_with("- executor-skill-")) .count(); let included_cloud_count = cloud .lines() .filter(|line| line.starts_with("- cloud-skill-")) .count(); assert!(included_executor_count > 0); assert!(included_executor_count < 40); assert!(included_cloud_count > 0); assert!(included_cloud_count < 160); assert!(executor.contains("- executor-skill-000: (executor package:")); assert!(cloud.contains("- cloud-skill-000: (cloud package:")); assert!(!cloud.contains("- cloud-skill-159:")); for (total, included, rendered) in [ (40, included_executor_count, executor), (160, included_cloud_count, cloud), ] { assert!(!rendered.contains("description-")); assert!(rendered.contains("additional skills omitted from this bounded skills list")); let omitted_count = total - included; let warning = event_rx.try_recv()?.into_warning(); assert_eq!(warning.thread_id, "thread"); assert_eq!(warning.turn_id.as_deref(), Some("turn-1")); assert_eq!( warning.message, format!( "Exceeded skills context budget. All skill descriptions were removed and {omitted_count} additional skills were not included in the model-visible skills list." ) ); } assert!(event_rx.try_recv().is_err()); Ok(()) } #[tokio::test] async fn skills_list_only_returns_model_visible_bounded_metadata() -> TestResult { const OVERSIZED_WARNING: &str = "Some skills were omitted because their metadata is too large."; const PROVIDER_WARNING: &str = "Some cloud skills could not be discovered because the provider disconnected before returning the complete catalog."; let supplementary_warning = "w".repeat(256); let opaque_suffix = "\\".repeat(1_500); let mut entry = test_entry( SkillSourceKind::Cloud, "codex_apps", &format!("cloud/{opaque_suffix}"), &format!("skill://cloud/{opaque_suffix}/SKILL.md"), ); entry.description = "x".repeat(1_025); let [first_entry, mut middle_entry, mut final_entry] = ["p0", "p1", "p2"].map(|name| { test_entry( SkillSourceKind::Cloud, "codex_apps", name, &format!("skill://{name}/SKILL.md"), ) }); middle_entry.description = "d".repeat(110); final_entry.description = "d".repeat(50); let providers = SkillProviders::new().with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![ entry, first_entry, middle_entry, final_entry, test_entry( SkillSourceKind::Cloud, "codex_apps", "cloud/hidden", "skill://cloud/hidden/SKILL.md", ) .hidden_from_prompt(), ], warnings: vec![ PROVIDER_WARNING.to_string(), supplementary_warning.clone(), supplementary_warning.clone(), supplementary_warning.clone(), ], }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; let step_store = ExtensionData::new("step"); prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &ExtensionData::new("turn-1"), }, ) .await; let tools = registry.tool_contributors()[0].tools_for_step(&session_store, &thread_store, &step_store); let list_tool = tools .iter() .find(|tool| tool.tool_name().name == "list") .ok_or("skills.list tool should be registered")?; let payload = ToolPayload::Function { arguments: serde_json::json!({"authority": {"kind": "cloud"}}).to_string(), }; let call = ToolCall { turn_id: "turn-1".to_string(), call_id: "call-1".to_string(), tool_name: list_tool.tool_name(), model: "gpt-test".to_string(), codex_turn_metadata: None, truncation_policy: TruncationPolicy::Bytes(10_000), source: ToolCallSource::Direct, conversation_history: ConversationHistory::default(), turn_item_emitter: Arc::new(NoopTurnItemEmitter), attachment_store: Arc::new(InlineAttachmentStore), ephemeral: false, environments: Vec::new(), payload: payload.clone(), }; let output = list_tool.handle(call.clone()).await?; let complete_response = output .post_tool_use_response("call-1", &payload) .ok_or("skills.list should expose structured output")?; let complete_skills = complete_response["skills"] .as_array() .ok_or("skills.list should return skills")?; assert_eq!(complete_skills.len(), 4); assert_eq!(complete_skills[0]["description"], "x".repeat(1_021) + "..."); assert_eq!( complete_response["warnings"].as_array().map(Vec::len), Some(4) ); assert_eq!(complete_response["next_cursor"], serde_json::Value::Null); let mut cursor = None; for ( index, (truncation_limit, byte_budget, expected_indices, expected_warnings, cursor_offset), ) in [ // The escaped entry fits alone, but not alongside even one provider warning. (6_458, 7_750, vec![], vec![PROVIDER_WARNING], Some("0")), (6_458, 7_750, vec![0], vec![], Some("1")), (266, 320, vec![1], vec![], Some("2")), // A smaller budget omits the middle entry and must report the new omission. (183, 220, vec![], vec![OVERSIZED_WARNING], Some("2")), (183, 220, vec![3], vec![], None), // Neither report may hide the other when they need separate pages. (183, 220, vec![], vec![PROVIDER_WARNING], Some("0")), (183, 220, vec![], vec![OVERSIZED_WARNING], Some("0")), // The last retained entry fits without a cursor, despite an oversized suffix. (151, 182, vec![], vec![OVERSIZED_WARNING], Some("0")), (151, 182, vec![1], vec![], None), // An omission notice must not displace an affordable provider warning. ( 2_000, 2_400, vec![1, 2, 3], vec![ PROVIDER_WARNING, &supplementary_warning, &supplementary_warning, &supplementary_warning, OVERSIZED_WARNING, ], None, ), ] .into_iter() .enumerate() { let payload = ToolPayload::Function { arguments: serde_json::json!({ "authority": {"kind": "cloud"}, "cursor": cursor, }) .to_string(), }; let call_id = format!("bounded-page-{index}"); let output = list_tool .handle(ToolCall { call_id: call_id.clone(), truncation_policy: TruncationPolicy::Bytes(truncation_limit), payload: payload.clone(), ..call.clone() }) .await?; assert!(output.contains_external_context()); let response = output .post_tool_use_response(&call_id, &payload) .ok_or("skills.list should expose structured output")?; assert!(serde_json::to_vec(&response)?.len() <= byte_budget); let next_cursor = response["next_cursor"].as_str().map(str::to_owned); assert_eq!( next_cursor .as_deref() .and_then(|value| value.split(':').nth(1)), cursor_offset ); let expected_skills = expected_indices .into_iter() .map(|index| &complete_skills[index]) .collect::>(); assert_eq!( response, serde_json::json!({ "skills": expected_skills, "warnings": expected_warnings, "next_cursor": next_cursor, }) ); if index == 2 { let (legacy_cursor, _) = next_cursor .as_deref() .and_then(|cursor| cursor.rsplit_once(':')) .ok_or("cursor should include its budget")?; let legacy_payload = ToolPayload::Function { arguments: serde_json::json!({ "authority": {"kind": "cloud"}, "cursor": legacy_cursor, }) .to_string(), }; let output = list_tool .handle(ToolCall { payload: legacy_payload.clone(), ..call.clone() }) .await?; assert_eq!( output.post_tool_use_response(&call.call_id, &legacy_payload), Some(serde_json::json!({ "skills": &complete_skills[2..], "warnings": [], "next_cursor": null, })) ); } cursor = next_cursor; } assert_eq!( list_tool .handle(ToolCall { call_id: "omitted-call".to_string(), truncation_policy: TruncationPolicy::Bytes(64), ..call }) .await .err(), Some(FunctionCallError::RespondToModel( "skills.list response budget leaves no room for discovery warnings".to_string() )) ); let read_tool = tools .iter() .find(|tool| tool.tool_name().name == "read") .ok_or("skills.read tool should be registered")?; // The resource fits the 2400-byte budget before escaping, but not after. let insufficient_budget_payload = ToolPayload::Function { arguments: serde_json::json!({ "package": format!("cloud/{opaque_suffix}"), }) .to_string(), }; let error = read_tool .handle(ToolCall { turn_id: "turn-1".to_string(), call_id: "insufficient-read-budget".to_string(), tool_name: read_tool.tool_name(), model: "gpt-test".to_string(), codex_turn_metadata: None, truncation_policy: TruncationPolicy::Bytes(2_000), source: ToolCallSource::Direct, conversation_history: ConversationHistory::default(), turn_item_emitter: Arc::new(NoopTurnItemEmitter), attachment_store: Arc::new(InlineAttachmentStore), ephemeral: false, environments: Vec::new(), payload: insufficient_budget_payload, }) .await .err() .expect("skills.read should reject a response that cannot fit any contents"); let error = assert_matches!(error, FunctionCallError::RespondToModel(error) => error); assert_eq!( error, "skills.read response budget leaves no room for contents" ); Ok(()) } #[tokio::test] async fn cloud_discovery_is_gated_and_reports_failures() -> TestResult { for (cloud_skill_enabled, has_cloud_provider) in [(false, true), (true, true), (true, false)] { let list_calls = Arc::new(AtomicUsize::new(0)); let mut providers = SkillProviders::new(); if has_cloud_provider { providers = providers.with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog::default(), read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: Some(Arc::clone(&list_calls)), fail_first_list: true, })); } let cloud_available = cloud_skill_enabled && has_cloud_provider; let (event_tx, event_rx) = std::sync::mpsc::channel(); let mut builder = ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; let config = TestConfig { cloud_skill_enabled, ..default_config() }; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; assert_eq!( registry .turn_lifecycle_contributors() .iter() .map(|contributor| contributor.turn_start_phase(&thread_store)) .collect::>(), vec![ codex_extension_api::TurnStartPhase::BeforeTaskRegistration, if cloud_available { codex_extension_api::TurnStartPhase::RegularTaskStart } else { codex_extension_api::TurnStartPhase::BeforeTaskRegistration }, ] ); assert_eq!( registry .turn_lifecycle_contributors() .iter() .map(|contributor| contributor.requires_mcp_runtime(&thread_store)) .collect::>(), vec![false, cloud_available] ); start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; assert_eq!( list_calls.load(Ordering::Relaxed), usize::from(cloud_available) ); for discovery_v2 in [None, Some([].as_slice())] { let step_store = ExtensionData::new("step"); if let Some(snapshots) = discovery_v2 { step_store.insert(Vec::< codex_exec_server::ExecutorCapabilityDiscoveryV2Snapshot, >::from(snapshots)); } let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &ExtensionData::new("turn-1"), }, ) .await; if has_cloud_provider { // Resume may retain a catalog even when the new Cloud discovery fails. let previous = serde_json::json!({ "body": "Previously advertised Cloud skills", "includeInstructions": true, "enabled": true, }); let update = world_state_section(§ions, "cloud_skills") .render_diff(PreviousWorldStateSection::Known(&previous)) .1 .ok_or("unavailable Cloud skills should withdraw the previous catalog")?; assert_eq!( update.body().clone().expect_text(), "\n## Cloud skills update\nNo cloud skills are currently available.\n" ); } else { assert!( sections .iter() .all(|section| section.id() != "cloud_skills") ); } } if !cloud_available { assert!(event_rx.try_recv().is_err()); let tools = registry.tool_contributors()[0].tools_for_step( &session_store, &thread_store, &ExtensionData::new("step"), ); assert!(tools.iter().all(|tool| tool.tool_name().name != "list")); continue; } let warning = event_rx.try_recv()?.into_warning(); assert_eq!(warning.thread_id, thread_store.level_id()); assert_eq!(warning.turn_id.as_deref(), Some("turn-1")); assert!(warning.message.contains("temporary cloud failure")); } Ok(()) } #[tokio::test] async fn v2_root_aliases_and_read_cache_use_the_captured_environment() -> TestResult { let directory = tempfile::tempdir()?; let root = directory.path().join("skills"); let path = root.join("nested/demo/SKILL.md"); std::fs::create_dir_all(path.parent().ok_or("skill directory")?)?; std::fs::write(&path, "original\n".repeat(2_000))?; let manager = Arc::new(EnvironmentManager::default_for_tests()); let environment = manager .get_environment("local") .ok_or("local environment")?; let replacement = EnvironmentManager::default_for_tests() .get_environment("local") .ok_or("replacement environment")?; let providers = SkillProviders::new().with_executor_provider(Arc::new( ExecutorSkillProvider::new_with_restriction_product( manager, /*restriction_product*/ None, ), )); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &default_config(), session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let snapshots = vec![ExecutorCapabilityDiscoveryV2Snapshot { environment_id: "captured".to_string(), result: Ok(Arc::new(DiscoverV2CapabilitiesResponse { skills: ["demo", "build", "check", "deploy", "review", "test"] .into_iter() .map(|name| { Ok(ExecutorSkill { name: name.to_string(), description: "Demo".to_string(), short_description: None, path: PathUri::from_host_native_path( root.join(format!("nested/{name}/SKILL.md")), )?, discovery_root: Some(SkillDiscoveryRoot { id: "codex-home-skills".to_string(), path: PathUri::from_host_native_path(&root)?, }), dependencies: None, policy: None, }) }) .collect::, Box>>()?, ..Default::default() })), }]; let package = format!( "skill://codex-home-skills/{}", PathUri::from_host_native_path(path.parent().ok_or("skill directory")?)? .inferred_native_path_string() .replace('\\', "/") .trim_start_matches('/') ); let turn_store = ExtensionData::new("turn-1"); let mut cursor = None; for (handle, expected_error) in [ (Some(Arc::clone(&environment)), None), (Some(environment), None), ( Some(replacement), Some("skills.read cursor is stale; restart from the first page"), ), ( None, Some("skill environment is not available for this callback"), ), ] { let file_system = handle.as_ref().map(|handle| handle.get_filesystem()); let access = file_system .as_ref() .map(FileSystemEnvironmentAccessor::unrestricted); let step_store = ExtensionData::new("turn-1"); step_store.insert(snapshots.clone()); step_store.insert( handle .map(|handle| ("captured".to_string(), handle)) .into_iter() .collect::>(), ); let sections = prepared_world_state( ®istry, &[], WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; let rendered = sections .iter() .filter_map(|section| section.render_diff(PreviousWorldStateSection::Absent).1) .map(|fragment| fragment.body().clone().expect_text()) .collect::>() .join("\n"); assert!(rendered.contains("e0/nested/demo"), "{rendered}"); let tools = registry.tool_contributors()[0].tools_for_step( &session_store, &thread_store, &step_store, ); let read = tools .iter() .find(|tool| tool.tool_name().name == "read") .ok_or("skills.read")?; let payload = ToolPayload::Function { arguments: serde_json::json!({"package": "e0/nested/demo", "cursor": cursor}) .to_string(), }; let result = read .handle(ToolCall { turn_id: "turn-1".to_string(), call_id: "read".to_string(), tool_name: read.tool_name(), model: "gpt-test".to_string(), codex_turn_metadata: None, truncation_policy: TruncationPolicy::Bytes(2_000), source: ToolCallSource::Direct, conversation_history: ConversationHistory::default(), turn_item_emitter: Arc::new(NoopTurnItemEmitter), attachment_store: Arc::new(InlineAttachmentStore), ephemeral: false, environments: access .iter() .map(|access| { codex_extension_api::ToolEnvironment::new( "captured".to_string(), PathUri::from_host_native_path(directory.path()).expect("fixture cwd"), access, ) }) .collect(), payload: payload.clone(), }) .await; if let Some(expected) = expected_error { assert!( matches!(result, Err(FunctionCallError::RespondToModel(message)) if message == expected) ); } else { let response = result? .post_tool_use_response("read", &payload) .ok_or("read output")?; assert_eq!(response["resource"], format!("{package}/SKILL.md")); assert_eq!( response["installations"], serde_json::json!([{ "environment_id": "captured", "skill_root": PathUri::from_host_native_path(path.parent().ok_or("skill directory")?)? .inferred_native_path_string(), }]) ); assert!( response["contents"] .as_str() .ok_or("contents")? .contains("original") ); cursor = Some( response["next_cursor"] .as_str() .ok_or("next cursor")? .to_string(), ); // The next page keeps the cached contents only while the captured handle is unchanged. std::fs::write(&path, "updated\n".repeat(2_000))?; } } Ok(()) } #[tokio::test] async fn root_qualified_locator_selects_only_the_matching_executor_skill() -> TestResult { let step_store = ExtensionData::new("step-1"); let read_requests = Arc::new(Mutex::new(Vec::new())); let root_a_locator = "skill://root-a/shared/lint-fix/SKILL.md"; let root_b_locator = "skill://root-b/shared/lint-fix/SKILL.md"; let skill_path = PathUri::parse("file:///shared/lint-fix/SKILL.md")?; let executor_provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: [("root-a", root_a_locator), ("root-b", root_b_locator)] .into_iter() .map(|(root_id, locator)| { SkillCatalogEntry::new( SkillPackageId(locator.to_string()), SkillAuthority::new(SkillSourceKind::Executor, root_id), "lint-fix", "Fix lint errors.", SkillResourceId::environment(locator, "env-1", skill_path.clone()), ) .with_display_path(locator) }) .collect(), warnings: Vec::new(), }, read_requests: Arc::clone(&read_requests), list_calls: None, fail_first_list: false, }); let providers = SkillProviders::new().with_executor_provider(executor_provider); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let selected_roots = [("root-a", "/skills/root-a"), ("root-b", "/skills/root-b")] .into_iter() .map(|(id, path)| SelectedCapabilityRoot { id: id.to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse(&format!("file://{path}")).expect("skill root URI"), }, }) .collect::>(); let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let turn_store = ExtensionData::new("turn-1"); prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[TurnEnvironmentSelection { selected_capability_roots: Default::default(), environment_id: "env-1".to_string(), cwd: PathUri::parse("file:///workspace").expect("cwd URI"), workspace_roots: Vec::new(), config: EnvironmentConfigState::FromThread, }], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &step_store, turn_id: "turn-1".to_string(), user_input: vec![UserInput::Mention { name: "lint-fix".to_string(), path: root_b_locator.to_string(), }], environments: vec![codex_extension_api::TurnInputEnvironment { environment_id: "env-1".to_string(), cwd: PathUri::parse("file:///workspace")?, is_primary: true, fs: &FileSystemEnvironmentAccessor::unrestricted(&LOCAL_FS), }], }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; assert_eq!(1, fragments.len()); assert!(fragments[0].render().expect_text().contains(root_b_locator)); assert_eq!( vec![( SkillAuthority::new(SkillSourceKind::Executor, "root-b"), SkillPackageId(root_b_locator.to_string()), SkillResourceId::environment(root_b_locator, "env-1", skill_path), )], read_request_keys(&read_requests) ); Ok(()) } #[tokio::test] async fn model_context_window_scales_executor_and_cloud_catalogs() -> TestResult { let cloud_entries = (0..40) .map(|index| { test_entry( SkillSourceKind::Cloud, "cloud", &format!("cloud/skill-{index:02}"), &format!("skill-{index:02}/SKILL.md"), ) }) .collect(); let executor_entries = (0..200) .map(|index| { test_entry( SkillSourceKind::Executor, "env-1", &format!("executor/skill-{index:02}"), &format!("skill-{index:02}/SKILL.md"), ) }) .collect(); let providers = SkillProviders::new() .with_cloud_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: cloud_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })) .with_executor_provider(Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: executor_entries, warnings: Vec::new(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, })); let (event_tx, event_rx) = std::sync::mpsc::channel(); let mut builder = ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let mut config = default_config(); config.bundled_skills_enabled = false; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; start_registered_turn(®istry, &session_store, &thread_store, "turn-1").await; let mut model_info = model_info_from_slug("test-model"); model_info.context_window = Some(10_000); let thread_fragments = registry.context_contributors()[0] .contribute_thread_context(&session_store, &thread_store) .await; assert!(thread_fragments.is_empty()); let selected_roots = vec![SelectedCapabilityRoot { id: "skills".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills").expect("skill root URI"), }, }]; let turn_store = ExtensionData::new("turn-1"); let sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &model_info, thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; // Core rebuilds world state before each sampling step. let _repeated_sections = prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &ExtensionData::new("step"), model_info: &model_info, thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; assert!(event_rx.try_recv().is_err()); let executor_section = world_state_section(§ions, "skills"); let (snapshot, executor_fragment) = executor_section.render_diff(PreviousWorldStateSection::Absent); let snapshot = snapshot.unwrap(); let executor_fragment = executor_fragment.ok_or("bounded executor catalog should render")?; assert!( !executor_fragment .body() .clone() .expect_text() .contains("skill-39") ); let cloud_fragment = world_state_section(§ions, "cloud_skills") .render_diff(PreviousWorldStateSection::Absent) .1 .ok_or("bounded cloud catalog should render")?; assert!( cloud_fragment .body() .clone() .expect_text() .contains("additional skills omitted") ); let warnings = event_rx .try_iter() .map(CapturedExtensionEvent::into_warning) .collect::>(); assert_eq!(warnings.len(), 2); for warning in warnings { assert_eq!(warning.thread_id, thread_store.level_id()); assert_eq!(warning.turn_id.as_deref(), Some("turn-1")); assert!( warning .message .starts_with("Exceeded skills context budget.") ); assert!( warning .message .ends_with("additional skills were not included in the model-visible skills list.") ); } assert!( executor_section .render_diff(PreviousWorldStateSection::Known(&snapshot)) .1 .is_none() ); assert!(event_rx.try_recv().is_err()); Ok(()) } #[tokio::test] async fn executor_catalog_emits_at_most_four_warnings() -> TestResult { let step_store = ExtensionData::new("step-1"); let executor_provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: Vec::new(), warnings: (0..6).map(|index| format!("warning-{index}")).collect(), }, read_requests: Arc::new(Mutex::new(Vec::new())), list_calls: None, fail_first_list: false, }); let providers = SkillProviders::new().with_executor_provider(executor_provider); let (event_tx, event_rx) = std::sync::mpsc::channel(); let mut builder = ExtensionRegistryBuilder::with_event_sink(Arc::new(ChannelEventSink(event_tx))); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let mut config = default_config(); config.bundled_skills_enabled = false; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let selected_roots = vec![SelectedCapabilityRoot { id: "skills".to_string(), location: CapabilityRootLocation::Environment { environment_id: "env-1".to_string(), path: PathUri::parse("file:///skills").expect("skill root URI"), }, }]; let turn_store = ExtensionData::new("turn-1"); prepared_world_state( ®istry, &selected_roots, WorldStateContributionInput { previous_world_state: None, step_store: &step_store, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "turn-1", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, }, ) .await; prepared_turn_input( ®istry, TurnInputContext { step_store: &step_store, turn_id: "turn-1".to_string(), user_input: Vec::new(), environments: Vec::new(), }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; let messages = event_rx .try_iter() .map(|event| event.into_warning().message) .collect::>(); assert_eq!( messages, vec!["warning-0", "warning-1", "warning-2", "warning-3"] ); Ok(()) } #[tokio::test] async fn host_catalog_compacts_shared_paths_under_budget_pressure() -> TestResult { let test_root = test_codex_home(); let root = test_root.join( "plugins/cache/openai-curated/example/hash1234567890/skills-with-a-very-long-shared-prefix", ); for index in 0..12 { let skill_dir = root.join(format!("skill-{index:02}")); std::fs::create_dir_all(&skill_dir)?; std::fs::write( skill_dir.join("SKILL.md"), format!( "---\nname: skill-{index:02}\ndescription: Fix lint errors.\n---\n# Skill {index:02}\n" ), )?; } #[cfg(unix)] { let source_skill_dir = test_root.join("shared-skills/linked-skill"); std::fs::create_dir_all(&source_skill_dir)?; std::fs::write( source_skill_dir.join("SKILL.md"), "---\nname: linked-skill\ndescription: Linked skill.\n---\n# Linked skill\n", )?; std::os::unix::fs::symlink(&source_skill_dir, root.join("linked-skill"))?; } let root = AbsolutePathBuf::try_from(std::fs::canonicalize(root)?)?; let rendered_root = root.to_string_lossy().replace('\\', "/"); let config_layer_stack = ConfigLayerStack::new( vec![ConfigLayerEntry::new( ConfigLayerSource::SessionFlags, toml::from_str("[skills.bundled]\nenabled = false\n")?, )], Default::default(), ConfigRequirementsToml::default(), )?; let codex_home = AbsolutePathBuf::try_from(test_root.clone())?; let service = HostSkillsService::new_with_restriction_product( codex_home.clone(), /*bundled_skills_enabled*/ false, /*restriction_product*/ None, ); service.set_extra_roots(vec![root]); let snapshot = service .snapshot_for_config( &HostSkillsLoadInput::new(codex_home, Vec::new(), config_layer_stack), Some(Arc::clone(&LOCAL_FS)), ) .await; assert_eq!(snapshot.outcome().errors, Vec::new()); assert_eq!( snapshot.outcome().skills.len(), 12 + usize::from(cfg!(unix)) ); let mut builder = ExtensionRegistryBuilder::new(); install(&mut builder, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let mut config = default_config(); config.bundled_skills_enabled = false; registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let mut model_info = model_info_from_slug("test-model"); model_info.context_window = Some(10_000); thread_store.insert(model_info); let turn_store = ExtensionData::new("turn-1"); turn_store.insert(snapshot); let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &ExtensionData::new("step"), turn_id: "turn-1".to_string(), user_input: vec![UserInput::Text { text: "hello".to_string(), text_elements: Vec::new(), }], environments: Vec::new(), }, /*extension_metrics*/ None, &session_store, &thread_store, &turn_store, ) .await; let catalog = fragments .iter() .find(|fragment| fragment.role() == "developer") .ok_or("host catalog should render")? .render() .expect_text(); assert!( catalog.contains(&format!("- `r0` = `{rendered_root}`")), "{catalog}" ); assert!(catalog.contains("(file: r0/skill-00/SKILL.md)")); assert!(catalog.contains("(file: r0/skill-11/SKILL.md)")); #[cfg(unix)] assert!(catalog.contains("(file: r0/linked-skill/SKILL.md)")); assert!(!catalog.contains("additional skills omitted")); std::fs::remove_dir_all(test_root)?; Ok(()) } #[tokio::test] async fn prompt_hidden_skill_can_still_be_invoked() -> TestResult { let read_requests = Arc::new(Mutex::new(Vec::new())); let provider = Arc::new(StaticSkillProvider { catalog: SkillCatalog { is_partial: false, entries: vec![ test_entry( SkillSourceKind::Host, "host", "host/visible-skill", "visible-skill/SKILL.md", ), test_entry( SkillSourceKind::Host, "host", "host/hidden-skill", "hidden-skill/SKILL.md", ) .hidden_from_prompt(), ], warnings: Vec::new(), }, read_requests: Arc::clone(&read_requests), list_calls: None, fail_first_list: false, }); let providers = SkillProviders::new().with_host_provider(provider); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers(&mut builder, providers, skills_extension_config); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let session_source = SessionSource::Cli; let config = default_config(); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &config, session_source: &session_source, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; let fragments = prepared_turn_input( ®istry, TurnInputContext { step_store: &ExtensionData::new("step"), turn_id: "turn-1".to_string(), user_input: vec![UserInput::Text { text: "$hidden-skill".to_string(), text_elements: Vec::new(), }], environments: Vec::new(), }, /*extension_metrics*/ None, &session_store, &thread_store, &ExtensionData::new("turn-1"), ) .await; assert_eq!(2, fragments.len()); let visible = fragments[0].render().expect_text(); let hidden = fragments[1].render().expect_text(); assert!(visible.contains("visible-skill")); assert!(!visible.contains("hidden-skill")); assert!(hidden.contains("hidden-skill")); assert_eq!( vec![( SkillAuthority::new(SkillSourceKind::Host, "host"), SkillPackageId("host/hidden-skill".to_string()), SkillResourceId::new("hidden-skill/SKILL.md"), )], read_request_keys(&read_requests) ); Ok(()) } #[derive(Clone)] struct StaticSkillProvider { catalog: SkillCatalog, read_requests: Arc>>, list_calls: Option>, fail_first_list: bool, } #[derive(Debug)] enum CapturedExtensionEvent { Event(Box), Warning(ExtensionWarning), } impl CapturedExtensionEvent { fn into_warning(self) -> ExtensionWarning { match self { Self::Warning(warning) => warning, Self::Event(event) => panic!("expected extension warning, got {event:?}"), } } } struct ChannelEventSink(std::sync::mpsc::Sender); impl ExtensionEventSink for ChannelEventSink { fn emit(&self, event: Event) { let _ = self.0.send(CapturedExtensionEvent::Event(Box::new(event))); } fn emit_warning(&self, warning: ExtensionWarning) { let _ = self.0.send(CapturedExtensionEvent::Warning(warning)); } } #[derive(Clone, Debug, Eq, PartialEq)] struct RecordedHistogram { name: String, value: i64, tags: Vec<(String, String)>, } #[derive(Default)] struct RecordingMetrics { samples: Mutex>, } impl RecordingMetrics { fn samples(&self) -> Vec { self.samples .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } } impl ExtensionMetrics for RecordingMetrics { fn histogram_with_boundaries( &self, name: &str, value: i64, _boundaries: &[f64], tags: &[(&str, &str)], ) { self.histogram(name, value, tags); } fn counter(&self, name: &str, _inc: i64, _tags: &[(&str, &str)]) { panic!("unexpected counter: {name}"); } fn histogram(&self, name: &str, value: i64, tags: &[(&str, &str)]) { self.samples .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(RecordedHistogram { name: name.to_string(), value, tags: tags .iter() .map(|(key, value)| ((*key).to_string(), (*value).to_string())) .collect(), }); } } fn expected_catalog_metric_samples(catalog_surface: &str, count: i64) -> Vec { let tags = vec![("catalog_surface".to_string(), catalog_surface.to_string())]; vec![ RecordedHistogram { name: THREAD_SKILLS_ENABLED_TOTAL_METRIC.to_string(), value: count, tags: tags.clone(), }, RecordedHistogram { name: THREAD_SKILLS_KEPT_TOTAL_METRIC.to_string(), value: count, tags: tags.clone(), }, RecordedHistogram { name: THREAD_SKILLS_TRUNCATED_METRIC.to_string(), value: 0, tags: tags.clone(), }, RecordedHistogram { name: THREAD_SKILLS_DESCRIPTION_TRUNCATED_CHARS_METRIC.to_string(), value: 0, tags, }, ] } impl SkillProvider for StaticSkillProvider { fn list(&self, _query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> { let list_call = self .list_calls .as_ref() .map(|list_calls| list_calls.fetch_add(1, Ordering::Relaxed)); let fail = self.fail_first_list && list_call == Some(0); let catalog = self.catalog.clone(); Box::pin(async move { if fail { Err(SkillProviderError::new("temporary cloud failure")) } else { Ok(catalog) } }) } fn read<'a>( &'a self, request: SkillReadRequest<'a>, ) -> SkillProviderFuture<'a, SkillReadResult> { let read_requests = Arc::clone(&self.read_requests); Box::pin(async move { read_requests .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(( request.authority.clone(), request.package.clone(), request.resource.clone(), )); Ok(SkillReadResult { resource: request.resource, contents: "# Lint Fix\n\nRun the formatter.".to_string(), }) }) } fn search(&self, _request: SkillSearchRequest) -> SkillProviderFuture<'_, SkillSearchResult> { Box::pin(async { Ok(SkillSearchResult::default()) }) } } fn test_entry( kind: SkillSourceKind, authority_id: &str, package_id: &str, main_prompt: &str, ) -> SkillCatalogEntry { let name = package_id.rsplit('/').next().unwrap_or(package_id); SkillCatalogEntry::new( SkillPackageId(package_id.to_string()), SkillAuthority::new(kind, authority_id), name, "Fix lint errors.", SkillResourceId::new(main_prompt), ) .with_display_path(format!("skill://{package_id}/SKILL.md")) } #[derive(Clone, Debug, Eq, PartialEq)] struct TestConfig { include_instructions: bool, bundled_skills_enabled: bool, cloud_skill_enabled: bool, shadow_selection_enabled: bool, } fn default_config() -> TestConfig { TestConfig { include_instructions: true, bundled_skills_enabled: true, cloud_skill_enabled: true, shadow_selection_enabled: false, } } fn skills_extension_config(config: &TestConfig) -> SkillsExtensionConfig { SkillsExtensionConfig { include_instructions: config.include_instructions, max_context_tokens: None, bundled_skills_enabled: config.bundled_skills_enabled, cloud_skill_enabled: config.cloud_skill_enabled, shadow_selection_enabled: config.shadow_selection_enabled, } } fn test_codex_home() -> PathBuf { let id = NEXT_CODEX_HOME_ID.fetch_add(1, Ordering::Relaxed); std::env::temp_dir().join(format!( "codex-skills-extension-test-{}-{id}", std::process::id(), )) } fn read_request_keys( requests: &Mutex>, ) -> Vec<(SkillAuthority, SkillPackageId, SkillResourceId)> { requests .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } #[path = "skills_extension/merge_catalog_tests.rs"] mod merge_catalog_tests; #[path = "skills_extension/discovery_v2_tests.rs"] mod discovery_v2_tests; #[tokio::test] async fn shared_catalog_keeps_captured_executor_and_separates_bindings_in_one_turn() -> TestResult { let root_dir = tempfile::tempdir()?; let skill_dir = root_dir.path().join("demo"); std::fs::create_dir_all(&skill_dir)?; std::fs::write(skill_dir.join("SKILL.md"), DEMO_SKILL_CONTENTS)?; let manager = Arc::new(EnvironmentManager::default_for_tests()); let original = manager.get_environment("local").ok_or("local executor")?; let replacement = EnvironmentManager::default_for_tests() .get_environment("local") .ok_or("replacement")?; let roots = vec![SelectedCapabilityRoot { id: "skills".into(), location: CapabilityRootLocation::Environment { environment_id: "captured".into(), path: PathUri::from_host_native_path(root_dir.path())?, }, }]; let captured = manager .resolve_selected_capability_roots( &roots, &HashMap::from([("captured".into(), Some(original))]), ) .await; let newer = manager .resolve_selected_capability_roots( &roots, &HashMap::from([("captured".into(), Some(replacement))]), ) .await; let listener = std::net::TcpListener::bind("127.0.0.1:0")?; let address = listener.local_addr()?; drop(listener); manager.upsert_environment( "captured".into(), format!("ws://{address}"), Some(std::time::Duration::from_millis(100)), )?; let provider = Arc::new(ExecutorSkillProvider::new_with_restriction_product( manager, /*restriction_product*/ None, )); let mut builder = ExtensionRegistryBuilder::new(); install_with_providers( &mut builder, SkillProviders::new().with_executor_provider(provider), skills_extension_config, ); let registry = builder.build(); let session_store = ExtensionData::new("session"); let thread_store = ExtensionData::new("thread"); let turn_store = ExtensionData::new("same-turn"); registry.thread_lifecycle_contributors()[0] .on_thread_start(ThreadStartInput { config: &default_config(), session_source: &SessionSource::Cli, persistent_thread_state_available: true, environments: &[], mcp_resource_client: None, extension_metrics: None, session_store: &session_store, thread_store: &thread_store, }) .await; for (index, (resolved, expected)) in [ (captured.clone(), Some("Demo skill.")), (captured, Some("Demo skill.")), (newer, Some("New executor inventory.")), (Vec::new(), None), ] .into_iter() .enumerate() { if index == 1 { std::fs::write( skill_dir.join("SKILL.md"), "---\nname: demo\ndescription: New executor inventory.\n---\nNew body\n", )?; } let step_store = ExtensionData::new(format!("step-{index}")); step_store.insert(resolved); let sections = prepared_world_state( ®istry, &roots, WorldStateContributionInput { previous_world_state: None, model_info: &catalog_model_info(), thread_id: codex_protocol::ThreadId::new(), turn_id: "same-turn", environments: &[], extension_metrics: None, session_store: &session_store, thread_store: &thread_store, turn_store: &turn_store, step_store: &step_store, }, ) .await; let rendered = sections .iter() .filter_map(|section| section.render_diff(PreviousWorldStateSection::Absent).1) .map(|fragment| fragment.body().clone().expect_text()) .collect::>() .join("\n"); if let Some(expected) = expected { assert!(rendered.contains(expected), "step {index}: {rendered}"); } else { assert!(!rendered.contains("- demo:"), "{rendered}"); } } Ok(()) } #[path = "skills_extension/preparation_tests.rs"] mod preparation_tests;