mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## 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
355 lines
9.9 KiB
Rust
355 lines
9.9 KiB
Rust
use codex_protocol::protocol::SkillScope;
|
|
use codex_skills::SkillDependencies;
|
|
use codex_utils_path_uri::PathUri;
|
|
use std::sync::Arc;
|
|
|
|
/// Source authority that owns a skill package and must be used to read it.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub enum SkillSourceKind {
|
|
/// Codex-hosted skills, including bundled, user, repo, plugin-installed,
|
|
/// and downloaded/materialized remote skills.
|
|
Host,
|
|
/// Skills owned by an execution environment.
|
|
Executor,
|
|
/// Skills owned by the orchestrator rather than an execution environment.
|
|
Orchestrator,
|
|
/// Extension-private source kind for future providers that do not fit an
|
|
/// existing transport category.
|
|
Custom(String),
|
|
}
|
|
|
|
impl SkillSourceKind {
|
|
pub fn custom(kind: impl Into<String>) -> Self {
|
|
Self::Custom(kind.into())
|
|
}
|
|
|
|
fn as_str(&self) -> &str {
|
|
match self {
|
|
Self::Host => "host",
|
|
Self::Executor => "executor",
|
|
Self::Orchestrator => "orchestrator",
|
|
Self::Custom(kind) => kind,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SkillSourceKind {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.as_str().fmt(formatter)
|
|
}
|
|
}
|
|
|
|
/// Opaque authority identity for list/read routing.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct SkillAuthority {
|
|
pub kind: SkillSourceKind,
|
|
pub id: String,
|
|
}
|
|
|
|
impl SkillAuthority {
|
|
pub fn new(kind: SkillSourceKind, id: impl Into<String>) -> Self {
|
|
Self {
|
|
kind,
|
|
id: id.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Opaque package id. Callers should not parse local paths out of this value.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct SkillPackageId(pub String);
|
|
|
|
impl SkillPackageId {
|
|
pub(crate) fn relative_resource_path<'a>(&self, resource: &'a str) -> Option<&'a str> {
|
|
let relative = resource
|
|
.strip_prefix(self.0.trim_end_matches('/'))?
|
|
.strip_prefix('/')?;
|
|
(!relative.is_empty()
|
|
&& relative
|
|
.split('/')
|
|
.all(|segment| !matches!(segment, "" | "." | "..")))
|
|
.then_some(relative)
|
|
}
|
|
}
|
|
|
|
/// Opaque resource id inside a skill package, optionally bound to the
|
|
/// environment path that owns its contents.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub struct SkillResourceId {
|
|
id: String,
|
|
environment_path: Option<EnvironmentSkillResource>,
|
|
}
|
|
|
|
impl SkillResourceId {
|
|
pub fn new(id: impl Into<String>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
environment_path: None,
|
|
}
|
|
}
|
|
|
|
pub fn environment(
|
|
id: impl Into<String>,
|
|
environment_id: impl Into<String>,
|
|
path: PathUri,
|
|
) -> Self {
|
|
let package_root = path.parent().unwrap_or_else(|| path.clone());
|
|
Self {
|
|
id: id.into(),
|
|
environment_path: Some(EnvironmentSkillResource {
|
|
environment_id: environment_id.into(),
|
|
package_root,
|
|
path,
|
|
contents: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn environment_with_contents(
|
|
id: impl Into<String>,
|
|
environment_id: impl Into<String>,
|
|
path: PathUri,
|
|
contents: String,
|
|
) -> Self {
|
|
let package_root = path.parent().unwrap_or_else(|| path.clone());
|
|
Self {
|
|
id: id.into(),
|
|
environment_path: Some(EnvironmentSkillResource {
|
|
environment_id: environment_id.into(),
|
|
package_root,
|
|
path,
|
|
contents: Some(contents.into()),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_str(&self) -> &str {
|
|
&self.id
|
|
}
|
|
|
|
pub(crate) fn bind_environment_package_resource(
|
|
&self,
|
|
package: &SkillPackageId,
|
|
resource: impl Into<String>,
|
|
) -> Option<Self> {
|
|
let resource = resource.into();
|
|
let relative = package.relative_resource_path(&resource)?;
|
|
let environment = self.environment_path.as_ref()?;
|
|
let path = environment.package_root.join(relative).ok()?;
|
|
path.starts_with(&environment.package_root).then(|| Self {
|
|
id: resource,
|
|
environment_path: Some(EnvironmentSkillResource {
|
|
environment_id: environment.environment_id.clone(),
|
|
package_root: environment.package_root.clone(),
|
|
path,
|
|
contents: None,
|
|
}),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn environment_path(&self) -> Option<(&str, &PathUri)> {
|
|
self.environment_path
|
|
.as_ref()
|
|
.map(|resource| (resource.environment_id.as_str(), &resource.path))
|
|
}
|
|
|
|
pub(crate) fn environment_contents(&self) -> Option<&str> {
|
|
self.environment_path
|
|
.as_ref()
|
|
.and_then(|resource| resource.contents.as_deref())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
struct EnvironmentSkillResource {
|
|
environment_id: String,
|
|
package_root: PathUri,
|
|
path: PathUri,
|
|
contents: Option<Arc<str>>,
|
|
}
|
|
|
|
/// Metadata shown in the always-visible skills catalog.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SkillCatalogEntry {
|
|
pub id: SkillPackageId,
|
|
pub authority: SkillAuthority,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub short_description: Option<String>,
|
|
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>,
|
|
prompt_scope: Option<SkillScope>,
|
|
pub dependencies: Option<SkillDependencies>,
|
|
pub enabled: bool,
|
|
pub prompt_visible: bool,
|
|
}
|
|
|
|
impl SkillCatalogEntry {
|
|
pub fn new(
|
|
id: SkillPackageId,
|
|
authority: SkillAuthority,
|
|
name: impl Into<String>,
|
|
description: impl Into<String>,
|
|
main_prompt: SkillResourceId,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
authority,
|
|
name: name.into(),
|
|
description: description.into(),
|
|
short_description: None,
|
|
main_prompt,
|
|
display_path: None,
|
|
canonical_skill_id: None,
|
|
plugin_id: None,
|
|
analytics_scope: None,
|
|
alias_root: None,
|
|
alias_root_order: None,
|
|
prompt_scope: None,
|
|
dependencies: None,
|
|
enabled: true,
|
|
prompt_visible: true,
|
|
}
|
|
}
|
|
|
|
pub fn with_short_description(mut self, short_description: Option<String>) -> Self {
|
|
self.short_description = short_description;
|
|
self
|
|
}
|
|
|
|
pub fn with_display_path(mut self, display_path: impl Into<String>) -> Self {
|
|
self.display_path = Some(display_path.into());
|
|
self
|
|
}
|
|
|
|
/// Sets the shared locator prefix that may be compacted in model-visible skill catalogs.
|
|
pub fn with_alias_root(mut self, alias_root: impl Into<String>) -> Self {
|
|
self.alias_root = Some(alias_root.into());
|
|
self
|
|
}
|
|
|
|
pub(crate) fn with_alias_root_order(mut self, alias_root_order: usize) -> Self {
|
|
self.alias_root_order = Some(alias_root_order);
|
|
self
|
|
}
|
|
|
|
pub(crate) fn with_prompt_scope(mut self, prompt_scope: SkillScope) -> Self {
|
|
self.prompt_scope = Some(prompt_scope);
|
|
self
|
|
}
|
|
|
|
pub fn with_dependencies(mut self, dependencies: Option<SkillDependencies>) -> Self {
|
|
self.dependencies = dependencies;
|
|
self
|
|
}
|
|
|
|
pub fn disabled(mut self) -> Self {
|
|
self.enabled = false;
|
|
self
|
|
}
|
|
|
|
pub fn hidden_from_prompt(mut self) -> Self {
|
|
self.prompt_visible = false;
|
|
self
|
|
}
|
|
|
|
pub(crate) fn is_model_visible(&self) -> bool {
|
|
self.enabled && self.prompt_visible
|
|
}
|
|
|
|
pub(crate) fn rendered_path(&self) -> &str {
|
|
self.display_path
|
|
.as_deref()
|
|
.unwrap_or_else(|| self.main_prompt.as_str())
|
|
}
|
|
|
|
pub(crate) fn alias_root(&self) -> Option<&str> {
|
|
self.alias_root.as_deref()
|
|
}
|
|
|
|
pub(crate) fn alias_root_order(&self) -> Option<usize> {
|
|
self.alias_root_order
|
|
}
|
|
|
|
pub(crate) fn prompt_scope(&self) -> Option<SkillScope> {
|
|
self.prompt_scope
|
|
}
|
|
}
|
|
|
|
/// Merged catalog for one turn.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct SkillCatalog {
|
|
pub entries: Vec<SkillCatalogEntry>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
impl SkillCatalog {
|
|
pub fn extend(&mut self, other: SkillCatalog) {
|
|
for entry in other.entries {
|
|
self.push_entry(entry);
|
|
}
|
|
self.warnings.extend(other.warnings);
|
|
}
|
|
|
|
pub fn push_entry(&mut self, entry: SkillCatalogEntry) {
|
|
if self
|
|
.entries
|
|
.iter()
|
|
.any(|existing| existing.authority == entry.authority && existing.id == entry.id)
|
|
{
|
|
return;
|
|
}
|
|
|
|
self.entries.push(entry);
|
|
}
|
|
}
|
|
|
|
/// Contents returned after resolving a skill resource through its owner.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SkillReadResult {
|
|
pub resource: SkillResourceId,
|
|
pub contents: String,
|
|
}
|
|
|
|
/// Search results for a package whose files are not readable through ordinary
|
|
/// executor filesystem access.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct SkillSearchResult {
|
|
pub matches: Vec<SkillSearchMatch>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SkillSearchMatch {
|
|
pub resource: SkillResourceId,
|
|
pub title: String,
|
|
pub snippet: String,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct SkillProviderError {
|
|
pub message: String,
|
|
}
|
|
|
|
impl SkillProviderError {
|
|
pub fn new(message: impl Into<String>) -> Self {
|
|
Self {
|
|
message: message.into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for SkillProviderError {
|
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.message.fmt(formatter)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for SkillProviderError {}
|
|
|
|
pub type SkillProviderResult<T> = Result<T, SkillProviderError>;
|