Track resource-backed skill invocations (#38066)

## What changed

- Emit skill invocation analytics when a resource-backed skill is explicitly selected or its main resource is successfully read from the first page.
- Preserve provider-supplied skill IDs and user scope when available, and derive a stable ID from the main resource otherwise.
- Deduplicate repeated implicit reads of the same main resource within a turn.

## Testing

- Extend the orchestrator skill integration test to cover explicit and implicit invocation events, stable fallback IDs, failed reads, and repeated-read deduplication.

GitOrigin-RevId: 7d8453b31b5a2205847e65cda2c46183d6c97b45
This commit is contained in:
rphilizaire-openai authored and copyberry committed 2026-08-11 19:52:54 +00:00
1 parent 104e25ac5a
commit c8f673fddc
14 files changed
+311 -71

No files matched your search

@@ -4,6 +4,7 @@ use std::time::Duration;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_protocol::mcp::Resource;
use codex_protocol::mcp::ResourceContent;
use codex_protocol::protocol::SkillScope;
use url::Url;
use crate::catalog::SkillAuthority;
@@ -253,7 +254,8 @@ fn catalog_entry_from_resource(resource: &Resource) -> Option<SkillCatalogEntry>
.and_then(serde_json::Value::as_bool)
.unwrap_or(true);
let skill_name = normalized_label(meta.get("skill_name")?.as_str()?, MAX_SKILL_NAME_CHARS)?;
let name = if meta.get("source").and_then(|value| value.as_str()) == Some("user") {
let user_owned = meta.get("source").and_then(|value| value.as_str()) == Some("user");
let name = if user_owned {
skill_name
} else {
let plugin_name =
@@ -265,7 +267,7 @@ fn catalog_entry_from_resource(resource: &Resource) -> Option<SkillCatalogEntry>
let description = normalized_description(resource.description.as_deref().unwrap_or_default())?;
let main_prompt = main_prompt_uri(uri);
let entry = SkillCatalogEntry::new(
let mut entry = SkillCatalogEntry::new(
SkillPackageId(uri.to_string()),
SkillAuthority::new(SkillSourceKind::Orchestrator, CODEX_APPS_MCP_SERVER_NAME),
name,
@@ -274,6 +276,11 @@ fn catalog_entry_from_resource(resource: &Resource) -> Option<SkillCatalogEntry>
)
.with_display_path(uri)
.with_alias_root(format!("skill://{namespace}"));
entry.canonical_skill_id = meta
.get("skill_id")
.and_then(serde_json::Value::as_str)
.map(str::to_owned);
entry.analytics_scope = user_owned.then_some(SkillScope::User);
Some(if allow_implicit_invocation {
entry