Attribute executor skill invocations to plugins (#39309)

## What changed

- Carry selected plugin identities from MCP discovery into per-turn extension data, limited to capability roots that are ready for the turn.
- Annotate executor skill catalog entries with the matching plugin ID and `user` scope.
- Include that attribution in `skill_invocation` analytics for both explicit and implicit executor skill invocations.

## Testing

- Extend the executor skills app-server test to verify plugin ID, skill scope, and invocation type for explicit and implicit analytics events.

GitOrigin-RevId: 9345600ae29bcb8eb894fffd2d6f7b05f22ebf74
This commit is contained in:
rphilizaire-openai authored and copyberry committed 2026-08-18 22:53:31 +00:00
1 parent c97bd2dcb5
commit 280d56b1d8
17 files changed
+129 -18

No files matched your search

+2
View File
@@ -179,6 +179,7 @@ pub struct SkillCatalogEntry {
pub main_prompt: SkillResourceId,
pub display_path: Option<String>,
pub(crate) canonical_skill_id: Option<String>,
pub(crate) plugin_id: Option<String>,
pub(crate) analytics_scope: Option<SkillScope>,
alias_root: Option<String>,
alias_root_order: Option<usize>,
@@ -205,6 +206,7 @@ impl SkillCatalogEntry {
main_prompt,
display_path: None,
canonical_skill_id: None,
plugin_id: None,
analytics_scope: None,
alias_root: None,
alias_root_order: None,
+5 -10
View File
@@ -18,6 +18,7 @@ use codex_extension_api::ExtensionMetrics;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ExtensionWarning;
use codex_extension_api::PromptFragment;
use codex_extension_api::SelectedPluginSnapshot;
use codex_extension_api::SkillInvocationContributor;
use codex_extension_api::SkillInvocationInput;
use codex_extension_api::SkillInvocationKind;
@@ -276,6 +277,7 @@ where
session_store,
thread_store,
/*executor_query*/ None,
/*selected_plugins*/ None,
/*sandbox_contexts*/ None,
)
}
@@ -310,6 +312,7 @@ where
session_store,
thread_store,
executor_query,
step_store.get::<SelectedPluginSnapshot>(),
step_store.get::<HashMap<String, FileSystemSandboxContext>>(),
)
}
@@ -550,23 +553,15 @@ impl<C> SkillsExtension<C> {
session_store: &ExtensionData,
thread_store: &ExtensionData,
executor_query: Option<SkillListQuery>,
selected_plugins: Option<Arc<SelectedPluginSnapshot>>,
sandbox_contexts: Option<Arc<HashMap<String, FileSystemSandboxContext>>>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
let Some(thread_state) = thread_store.get::<SkillsThreadState>() else {
return Vec::new();
};
let orchestrator_available = self.providers.has_orchestrator_provider()
&& thread_state.orchestrator_skills_enabled();
if !orchestrator_available && executor_query.is_none() {
return Vec::new();
}
skill_tools(
self.providers.clone(),
session_store,
thread_store,
orchestrator_available,
executor_query,
selected_plugins,
sandbox_contexts,
Arc::clone(&self.shadow_selection),
)
+1 -1
View File
@@ -71,7 +71,7 @@ pub fn detect_implicit_skill_invocation(
skill_id: entry.canonical_skill_id.clone(),
scope: entry.analytics_scope,
},
plugin_id: None,
plugin_id: entry.plugin_id.clone(),
remote_plugin_id: None,
invocation_type: InvocationType::Implicit,
});
+1
View File
@@ -22,6 +22,7 @@ use crate::catalog::SkillResourceId;
use crate::catalog::SkillSearchResult;
pub use executor::ExecutorSkillProvider;
pub(crate) use executor::attribute_executor_plugins;
pub use host::HostSkillProvider;
pub use orchestrator::OrchestratorSkillProvider;
@@ -2,8 +2,10 @@ use std::sync::Arc;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::FileSystemSandboxContext;
use codex_extension_api::SelectedPluginSnapshot;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_skills::EnvironmentSkillMetadata;
use codex_utils_path_uri::PathConvention;
use codex_utils_path_uri::PathUri;
@@ -46,6 +48,22 @@ impl ExecutorSkillProvider {
}
}
pub(crate) fn attribute_executor_plugins(
catalog: &mut SkillCatalog,
snapshot: &SelectedPluginSnapshot,
) {
for skill in &mut catalog.entries {
if let Some(plugin) = snapshot
.plugins
.iter()
.find(|plugin| plugin.selected_root_id == skill.authority.id)
{
skill.plugin_id = Some(plugin.plugin_id.clone());
skill.analytics_scope = Some(SkillScope::User);
}
}
}
impl SkillProvider for ExecutorSkillProvider {
fn list(&self, query: SkillListQuery) -> SkillProviderFuture<'_, SkillCatalog> {
Box::pin(async move {
+18 -5
View File
@@ -14,6 +14,7 @@ use codex_extension_api::ExtensionData;
use codex_extension_api::FunctionCallError;
use codex_extension_api::JsonToolOutput;
use codex_extension_api::ResponsesApiTool;
use codex_extension_api::SelectedPluginSnapshot;
use codex_extension_api::ThreadOriginator;
use codex_extension_api::ToolCall;
use codex_extension_api::ToolExecutor;
@@ -37,6 +38,7 @@ use crate::catalog::SkillCatalog;
use crate::catalog::SkillCatalogEntry;
use crate::catalog::SkillSourceKind;
use crate::provider::SkillListQuery;
use crate::provider::attribute_executor_plugins;
use crate::shadow_selection_experiment::ShadowSelectionExperiment;
use crate::sources::SkillProviders;
use crate::state::SkillsSessionState;
@@ -53,14 +55,19 @@ pub(crate) fn skill_tools(
providers: SkillProviders,
session_store: &ExtensionData,
thread_store: &ExtensionData,
orchestrator_available: bool,
executor_query: Option<SkillListQuery>,
selected_plugins: Option<Arc<SelectedPluginSnapshot>>,
sandbox_contexts: Option<Arc<HashMap<String, FileSystemSandboxContext>>>,
shadow_selection: Arc<ShadowSelectionExperiment>,
) -> Vec<Arc<dyn ToolExecutor<ToolCall>>> {
let Some(thread_state) = thread_store.get::<SkillsThreadState>() else {
return Vec::new();
};
let orchestrator_available =
providers.has_orchestrator_provider() && thread_state.orchestrator_skills_enabled();
if !orchestrator_available && executor_query.is_none() {
return Vec::new();
}
let mcp_resources = session_store
.get::<SkillsSessionState>()
.and_then(|state| state.mcp_resources.clone());
@@ -72,6 +79,7 @@ pub(crate) fn skill_tools(
analytics,
orchestrator_available,
executor_query,
selected_plugins,
sandbox_contexts,
executor_catalog: Arc::new(OnceCell::new()),
shadow_selection,
@@ -127,8 +135,7 @@ impl SkillAnalytics {
skill_id: skill.canonical_skill_id.clone(),
scope: skill.analytics_scope,
},
// TODO: Include plugin identifiers once skills can be attributed to their plugin.
plugin_id: None,
plugin_id: skill.plugin_id.clone(),
remote_plugin_id: None,
invocation_type,
}],
@@ -144,6 +151,7 @@ struct SkillToolContext {
analytics: Option<SkillAnalytics>,
orchestrator_available: bool,
executor_query: Option<SkillListQuery>,
selected_plugins: Option<Arc<SelectedPluginSnapshot>>,
sandbox_contexts: Option<Arc<HashMap<String, FileSystemSandboxContext>>>,
executor_catalog: Arc<OnceCell<SkillCatalog>>,
shadow_selection: Arc<ShadowSelectionExperiment>,
@@ -178,10 +186,15 @@ impl SkillToolContext {
return SkillCatalog::default();
};
query.turn_id = turn_id.to_string();
self.executor_catalog
let mut catalog = self
.executor_catalog
.get_or_init(|| self.providers.list_executor_for_turn(query))
.await
.clone()
.clone();
if let Some(selected_plugins) = &self.selected_plugins {
attribute_executor_plugins(&mut catalog, selected_plugins);
}
catalog
}
}
}
@@ -3,6 +3,7 @@ use std::sync::Arc;
use codex_extension_api::ContextualUserFragment;
use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionWarning;
use codex_extension_api::SelectedPluginSnapshot;
use codex_extension_api::WorldStateContributionInput;
use codex_extension_api::WorldStateSectionContribution;
use codex_protocol::openai_models::ModelInfo;
@@ -11,6 +12,7 @@ use crate::HostSkillsSnapshot;
use crate::SkillsExtensionConfig;
use crate::catalog::SkillCatalog;
use crate::provider::SkillListQuery;
use crate::provider::attribute_executor_plugins;
use crate::render::AvailableSkillsRender;
use crate::render::RenderedSkillCatalogs;
use crate::render::SkillMetadataBudget;
@@ -167,10 +169,13 @@ impl<'a> CatalogContext<'a> {
}
async fn discover_executor_catalog(&self, query: SkillListQuery) -> CatalogContribution {
let catalog = self
let mut catalog = self
.thread_state
.executor_catalog_snapshot(self.providers, query)
.await;
if let Some(selected_plugins) = self.input.turn_store.get::<SelectedPluginSnapshot>() {
attribute_executor_plugins(&mut catalog, &selected_plugins);
}
self.input
.turn_store
.insert(ExecutorSkillsStepState(catalog.clone()));