Expose plugin provenance for listed skills

This commit is contained in:
Noah MacCallum
2026-05-11 16:45:32 -07:00
parent bb6134c028
commit 04ae78c7bf
15 changed files with 554 additions and 11 deletions
@@ -14681,6 +14681,19 @@
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"plugin": {
"anyOf": [
{
"$ref": "#/definitions/v2/SkillPluginMetadata"
},
{
"type": "null"
}
]
},
"provenance": {
"$ref": "#/definitions/v2/SkillProvenance"
},
"scope": {
"$ref": "#/definitions/v2/SkillScope"
},
@@ -14697,10 +14710,77 @@
"enabled",
"name",
"path",
"provenance",
"scope"
],
"type": "object"
},
"SkillPluginMetadata": {
"properties": {
"brandColor": {
"type": [
"string",
"null"
]
},
"composerIcon": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"displayName": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"logo": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"marketplaceName",
"name"
],
"type": "object"
},
"SkillProvenance": {
"enum": [
"personal",
"project",
"system",
"admin",
"openai_marketplace",
"workspace_marketplace",
"custom_marketplace",
"local_plugin",
"debug_plugin",
"ad_hoc_plugin"
],
"type": "string"
},
"SkillScope": {
"enum": [
"user",
@@ -12505,6 +12505,19 @@
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"plugin": {
"anyOf": [
{
"$ref": "#/definitions/SkillPluginMetadata"
},
{
"type": "null"
}
]
},
"provenance": {
"$ref": "#/definitions/SkillProvenance"
},
"scope": {
"$ref": "#/definitions/SkillScope"
},
@@ -12521,10 +12534,77 @@
"enabled",
"name",
"path",
"provenance",
"scope"
],
"type": "object"
},
"SkillPluginMetadata": {
"properties": {
"brandColor": {
"type": [
"string",
"null"
]
},
"composerIcon": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"displayName": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"logo": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"marketplaceName",
"name"
],
"type": "object"
},
"SkillProvenance": {
"enum": [
"personal",
"project",
"system",
"admin",
"openai_marketplace",
"workspace_marketplace",
"custom_marketplace",
"local_plugin",
"debug_plugin",
"ad_hoc_plugin"
],
"type": "string"
},
"SkillScope": {
"enum": [
"user",
@@ -117,6 +117,19 @@
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"plugin": {
"anyOf": [
{
"$ref": "#/definitions/SkillPluginMetadata"
},
{
"type": "null"
}
]
},
"provenance": {
"$ref": "#/definitions/SkillProvenance"
},
"scope": {
"$ref": "#/definitions/SkillScope"
},
@@ -133,10 +146,77 @@
"enabled",
"name",
"path",
"provenance",
"scope"
],
"type": "object"
},
"SkillPluginMetadata": {
"properties": {
"brandColor": {
"type": [
"string",
"null"
]
},
"composerIcon": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"displayName": {
"type": [
"string",
"null"
]
},
"id": {
"type": "string"
},
"logo": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
]
},
"marketplaceName": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"marketplaceName",
"name"
],
"type": "object"
},
"SkillProvenance": {
"enum": [
"personal",
"project",
"system",
"admin",
"openai_marketplace",
"workspace_marketplace",
"custom_marketplace",
"local_plugin",
"debug_plugin",
"ad_hoc_plugin"
],
"type": "string"
},
"SkillScope": {
"enum": [
"user",
@@ -4,10 +4,12 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { SkillDependencies } from "./SkillDependencies";
import type { SkillInterface } from "./SkillInterface";
import type { SkillPluginMetadata } from "./SkillPluginMetadata";
import type { SkillProvenance } from "./SkillProvenance";
import type { SkillScope } from "./SkillScope";
export type SkillMetadata = { name: string, description: string,
/**
* Legacy short_description from SKILL.md. Prefer SKILL.json interface.short_description.
*/
shortDescription?: string, interface?: SkillInterface, dependencies?: SkillDependencies, path: AbsolutePathBuf, scope: SkillScope, enabled: boolean, };
shortDescription?: string, interface?: SkillInterface, dependencies?: SkillDependencies, path: AbsolutePathBuf, scope: SkillScope, provenance: SkillProvenance, plugin: SkillPluginMetadata | null, enabled: boolean, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
export type SkillPluginMetadata = { id: string, name: string, marketplaceName: string, displayName: string | null, brandColor: string | null, composerIcon: AbsolutePathBuf | null, logo: AbsolutePathBuf | null, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillProvenance = "personal" | "project" | "system" | "admin" | "openai_marketplace" | "workspace_marketplace" | "custom_marketplace" | "local_plugin" | "debug_plugin" | "ad_hoc_plugin";
@@ -334,6 +334,8 @@ export type { SkillDependencies } from "./SkillDependencies";
export type { SkillErrorInfo } from "./SkillErrorInfo";
export type { SkillInterface } from "./SkillInterface";
export type { SkillMetadata } from "./SkillMetadata";
export type { SkillPluginMetadata } from "./SkillPluginMetadata";
export type { SkillProvenance } from "./SkillProvenance";
export type { SkillScope } from "./SkillScope";
export type { SkillSummary } from "./SkillSummary";
export type { SkillToolDependency } from "./SkillToolDependency";
@@ -350,6 +350,23 @@ pub enum SkillScope {
Admin,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
#[ts(rename_all = "snake_case")]
#[ts(export_to = "v2/")]
pub enum SkillProvenance {
Personal,
Project,
System,
Admin,
OpenaiMarketplace,
WorkspaceMarketplace,
CustomMarketplace,
LocalPlugin,
DebugPlugin,
AdHocPlugin,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -368,9 +385,24 @@ pub struct SkillMetadata {
pub dependencies: Option<SkillDependencies>,
pub path: AbsolutePathBuf,
pub scope: SkillScope,
pub provenance: SkillProvenance,
pub plugin: Option<SkillPluginMetadata>,
pub enabled: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillPluginMetadata {
pub id: String,
pub name: String,
pub marketplace_name: String,
pub display_name: Option<String>,
pub brand_color: Option<String>,
pub composer_icon: Option<AbsolutePathBuf>,
pub logo: Option<AbsolutePathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -713,11 +745,24 @@ impl From<CoreSkillMetadata> for SkillMetadata {
dependencies: value.dependencies.map(SkillDependencies::from),
path: value.path,
scope: value.scope.into(),
provenance: value.scope.into(),
plugin: None,
enabled: true,
}
}
}
impl From<CoreSkillScope> for SkillProvenance {
fn from(value: CoreSkillScope) -> Self {
match value {
CoreSkillScope::User => Self::Personal,
CoreSkillScope::Repo => Self::Project,
CoreSkillScope::System => Self::System,
CoreSkillScope::Admin => Self::Admin,
}
}
}
impl From<CoreSkillInterface> for SkillInterface {
fn from(value: CoreSkillInterface) -> Self {
Self {
+4
View File
@@ -1470,6 +1470,7 @@ $skill-creator Add a new skill for triaging flaky CI and include step-by-step us
Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, with `forceReload`).
`skills/list` might reuse a cached skills result per `cwd`; setting `forceReload` to `true` refreshes the result from disk.
Each skill includes a user-facing `provenance`; skills loaded from installed/local plugins also include compact `plugin` metadata for display name, marketplace, brand color, and local icon assets.
The server also emits `skills/changed` notifications when watched local skill files change. Treat this as an invalidation signal and re-run `skills/list` with your current params when needed.
```json
@@ -1485,6 +1486,9 @@ The server also emits `skills/changed` notifications when watched local skill fi
"name": "skill-creator",
"description": "Create or update a Codex skill",
"enabled": true,
"scope": "user",
"provenance": "personal",
"plugin": null,
"interface": {
"displayName": "Skill Creator",
"shortDescription": "Create or update a Codex skill",
@@ -12,14 +12,36 @@ pub(crate) struct CatalogRequestProcessor {
const SKILLS_LIST_CWD_CONCURRENCY: usize = 5;
#[derive(Clone)]
struct SkillPluginInfo {
id: String,
name: String,
marketplace_name: String,
display_name: Option<String>,
provenance: codex_app_server_protocol::SkillProvenance,
brand_color: Option<String>,
composer_icon: Option<AbsolutePathBuf>,
logo: Option<AbsolutePathBuf>,
}
fn skills_to_info(
skills: &[codex_core::skills::SkillMetadata],
disabled_paths: &HashSet<AbsolutePathBuf>,
plugins_by_id: &HashMap<String, SkillPluginInfo>,
) -> Vec<codex_app_server_protocol::SkillMetadata> {
skills
.iter()
.map(|skill| {
let enabled = !disabled_paths.contains(&skill.path_to_skills_md);
let plugin = skill
.plugin_id
.as_ref()
.and_then(|plugin_id| plugins_by_id.get(plugin_id))
.cloned();
let provenance = plugin
.as_ref()
.map(|plugin| plugin.provenance)
.unwrap_or_else(|| skill.scope.into());
codex_app_server_protocol::SkillMetadata {
name: skill.name.clone(),
description: skill.description.clone(),
@@ -52,12 +74,73 @@ fn skills_to_info(
}),
path: skill.path_to_skills_md.clone(),
scope: skill.scope.into(),
provenance,
plugin: plugin.map(|plugin| codex_app_server_protocol::SkillPluginMetadata {
id: plugin.id,
name: plugin.name,
marketplace_name: plugin.marketplace_name,
display_name: plugin.display_name,
brand_color: plugin.brand_color,
composer_icon: plugin.composer_icon,
logo: plugin.logo,
}),
enabled,
}
})
.collect()
}
fn plugin_infos_by_id<M>(
plugins: &[codex_plugin::LoadedPlugin<M>],
) -> HashMap<String, SkillPluginInfo> {
plugins
.iter()
.filter(|plugin| plugin.enabled && plugin.error.is_none())
.filter_map(|plugin| {
let plugin_id = codex_plugin::PluginId::parse(&plugin.config_name).ok()?;
let provenance = plugin_provenance(&plugin_id.marketplace_name);
Some((
plugin.config_name.clone(),
SkillPluginInfo {
id: plugin.config_name.clone(),
name: plugin_id.plugin_name,
marketplace_name: plugin_id.marketplace_name,
display_name: plugin.manifest_name.clone(),
provenance,
brand_color: plugin
.manifest_interface
.as_ref()
.and_then(|interface| interface.brand_color.clone()),
composer_icon: plugin
.manifest_interface
.as_ref()
.and_then(|interface| interface.composer_icon.clone()),
logo: plugin
.manifest_interface
.as_ref()
.and_then(|interface| interface.logo.clone()),
},
))
})
.collect()
}
fn plugin_provenance(marketplace_name: &str) -> codex_app_server_protocol::SkillProvenance {
match marketplace_name {
codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME
| codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME => {
codex_app_server_protocol::SkillProvenance::OpenaiMarketplace
}
codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME
| codex_core_plugins::remote::REMOTE_SHARED_WITH_ME_MARKETPLACE_NAME => {
codex_app_server_protocol::SkillProvenance::WorkspaceMarketplace
}
"debug" => codex_app_server_protocol::SkillProvenance::DebugPlugin,
"local" | "personal" => codex_app_server_protocol::SkillProvenance::LocalPlugin,
_ => codex_app_server_protocol::SkillProvenance::CustomMarketplace,
}
}
fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
hooks
.iter()
@@ -425,16 +508,22 @@ impl CatalogRequestProcessor {
);
}
};
let effective_skill_roots = if workspace_codex_plugins_enabled {
let (effective_skill_roots, plugins_by_id) = if workspace_codex_plugins_enabled
{
let plugins_input = config.plugins_config_input();
plugins_manager
.effective_skill_roots_for_layer_stack(
let plugin_outcome = plugins_manager
.plugins_for_layer_stack(
&config_layer_stack,
&plugins_input,
plugins_input.plugin_hooks_enabled,
)
.await
.await;
(
plugin_outcome.effective_plugin_skill_roots(),
plugin_infos_by_id(plugin_outcome.plugins()),
)
} else {
Vec::new()
(Vec::new(), HashMap::new())
};
let skills_input = codex_core::skills::SkillsLoadInput::new(
cwd_abs.clone(),
@@ -446,7 +535,8 @@ impl CatalogRequestProcessor {
.skills_for_cwd(&skills_input, force_reload, fs)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
let skills =
skills_to_info(&outcome.skills, &outcome.disabled_paths, &plugins_by_id);
(
index,
codex_app_server_protocol::SkillsListEntry {
@@ -12,12 +12,14 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::PluginListParams;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SkillProvenance;
use codex_app_server_protocol::SkillsChangedNotification;
use codex_app_server_protocol::SkillsListParams;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::ThreadStartParams;
use codex_config::types::AuthCredentialsStoreMode;
use codex_exec_server::CODEX_EXEC_SERVER_URL_ENV_VAR;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
@@ -40,9 +42,10 @@ fn write_skill(root: &TempDir, name: &str) -> Result<()> {
Ok(())
}
fn write_plugins_enabled_config_with_base_url(
fn write_plugins_enabled_config_with_enabled_plugin(
codex_home: &std::path::Path,
base_url: &str,
plugin_id: &str,
) -> std::io::Result<()> {
std::fs::write(
codex_home.join("config.toml"),
@@ -51,6 +54,9 @@ fn write_plugins_enabled_config_with_base_url(
[features]
plugins = true
[plugins."{plugin_id}"]
enabled = true
"#,
),
)
@@ -100,9 +106,20 @@ fn write_plugin_with_skill(
let plugin_root = repo_root.join(plugin_name);
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::create_dir_all(plugin_root.join("assets"))?;
std::fs::write(plugin_root.join("assets/icon.png"), &[] as &[u8])?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
format!(r#"{{"name":"{plugin_name}"}}"#),
format!(
r##"{{
"name": "{plugin_name}",
"interface": {{
"displayName": "{plugin_name} display",
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png"
}}
}}"##
),
)?;
let skill_dir = plugin_root.join("skills").join(skill_name);
@@ -119,9 +136,17 @@ fn write_cached_remote_plugin_with_skill(
) -> Result<std::path::PathBuf> {
let plugin_root = codex_home.join("plugins/cache/chatgpt-global/linear/local");
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::create_dir_all(plugin_root.join("assets"))?;
std::fs::write(plugin_root.join("assets/linear.png"), &[] as &[u8])?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"linear"}"#,
r##"{
"name": "linear",
"interface": {
"displayName": "Linear",
"composerIcon": "./assets/linear.png"
}
}"##,
)?;
let skill_dir = plugin_root.join("skills/triage-issues");
@@ -134,6 +159,36 @@ fn write_cached_remote_plugin_with_skill(
Ok(skill_path)
}
fn write_cached_local_plugin_with_skill(
codex_home: &std::path::Path,
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
let plugin_root = codex_home.join("plugins/cache/local-marketplace/demo-plugin/local");
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::create_dir_all(plugin_root.join("assets"))?;
let icon_path = plugin_root.join("assets/icon.png");
std::fs::write(&icon_path, &[] as &[u8])?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r##"{
"name": "demo-plugin",
"interface": {
"displayName": "Demo Plugin",
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png"
}
}"##,
)?;
let skill_dir = plugin_root.join("skills/plugin-skill");
std::fs::create_dir_all(&skill_dir)?;
let skill_path = skill_dir.join("SKILL.md");
std::fs::write(
&skill_path,
"---\nname: plugin-skill\ndescription: plugin skill description\n---\n\n# Body\n",
)?;
Ok((skill_path, icon_path))
}
#[tokio::test]
async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -311,6 +366,19 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
std::fs::canonicalize(skill.path.as_path())?,
expected_skill_path
);
assert_eq!(skill.provenance, SkillProvenance::OpenaiMarketplace);
let plugin = skill.plugin.as_ref().expect("expected plugin metadata");
assert_eq!(plugin.id, "linear@chatgpt-global");
assert_eq!(plugin.marketplace_name, "chatgpt-global");
assert_eq!(plugin.display_name.as_deref(), Some("Linear"));
assert_eq!(
plugin.composer_icon,
Some(AbsolutePathBuf::try_from(std::fs::canonicalize(
codex_home
.path()
.join("plugins/cache/chatgpt-global/linear/local/assets/linear.png")
)?)?)
);
assert_eq!(skill.enabled, true);
Ok(())
}
@@ -322,9 +390,10 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
let server = MockServer::start().await;
write_skill(&codex_home, "home-skill")?;
write_plugin_with_skill(repo_root.path(), "demo-plugin", "plugin-skill")?;
write_plugins_enabled_config_with_base_url(
write_plugins_enabled_config_with_enabled_plugin(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
"demo-plugin@local-marketplace",
)?;
write_chatgpt_auth(
codex_home.path(),
@@ -380,6 +449,62 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
Ok(())
}
#[tokio::test]
async fn skills_list_includes_plugin_metadata_for_plugin_skills() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let server = MockServer::start().await;
let (expected_skill_path, expected_icon_path) =
write_cached_local_plugin_with_skill(codex_home.path())?;
write_plugins_enabled_config_with_enabled_plugin(
codex_home.path(),
&format!("{}/backend-api/", server.uri()),
"demo-plugin@local-marketplace",
)?;
let mut mcp = McpProcess::new_without_managed_config(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let SkillsListResponse { data } = to_response(response)?;
assert_eq!(data.len(), 1);
let skill = data[0]
.skills
.iter()
.find(|skill| skill.name == "demo-plugin:plugin-skill")
.expect("expected plugin skill");
assert_eq!(
std::fs::canonicalize(skill.path.as_path())?,
std::fs::canonicalize(expected_skill_path)?
);
assert_eq!(skill.scope, codex_app_server_protocol::SkillScope::User);
assert_eq!(skill.provenance, SkillProvenance::CustomMarketplace);
let plugin = skill.plugin.as_ref().expect("expected plugin metadata");
assert_eq!(plugin.id, "demo-plugin@local-marketplace");
assert_eq!(plugin.name, "demo-plugin");
assert_eq!(plugin.marketplace_name, "local-marketplace");
assert_eq!(plugin.display_name.as_deref(), Some("Demo Plugin"));
assert_eq!(plugin.brand_color.as_deref(), Some("#3B82F6"));
assert_eq!(
plugin.composer_icon,
Some(AbsolutePathBuf::try_from(std::fs::canonicalize(
expected_icon_path
)?)?)
);
Ok(())
}
#[tokio::test]
async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
+11
View File
@@ -26,6 +26,7 @@ use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginHookSource;
use codex_plugin::PluginId;
use codex_plugin::PluginIdError;
use codex_plugin::PluginInterfaceSummary;
use codex_plugin::PluginLoadOutcome;
use codex_plugin::PluginTelemetryMetadata;
use codex_protocol::protocol::Product;
@@ -516,6 +517,7 @@ async fn load_plugin(
config_name,
manifest_name: None,
manifest_description: None,
manifest_interface: None,
root,
enabled: plugin.enabled,
skill_roots: Vec::new(),
@@ -566,6 +568,15 @@ async fn load_plugin(
.map(str::to_string)
.or_else(|| Some(manifest.name.clone()));
loaded_plugin.manifest_description = manifest.description.clone();
loaded_plugin.manifest_interface =
manifest
.interface
.as_ref()
.map(|interface| PluginInterfaceSummary {
brand_color: interface.brand_color.clone(),
composer_icon: interface.composer_icon.clone(),
logo: interface.logo.clone(),
});
loaded_plugin.skill_roots = plugin_skill_roots(&plugin_root, manifest_paths);
let resolved_skills = load_plugin_skills(
&plugin_root,
@@ -205,6 +205,7 @@ async fn load_plugins_loads_default_skills_and_mcp_servers() {
manifest_description: Some(
"Plugin that includes the sample MCP server and Skills".to_string(),
),
manifest_interface: None,
root: AbsolutePathBuf::try_from(plugin_root.clone()).unwrap(),
enabled: true,
skill_roots: vec![plugin_root.join("skills").abs()],
@@ -855,6 +856,7 @@ async fn load_plugins_preserves_disabled_plugins_without_effective_contributions
config_name: "sample@test".to_string(),
manifest_name: None,
manifest_description: None,
manifest_interface: None,
root: AbsolutePathBuf::try_from(plugin_root).unwrap(),
enabled: false,
skill_roots: Vec::new(),
@@ -974,6 +976,7 @@ fn capability_index_filters_inactive_and_zero_capability_plugins() {
config_name: config_name.to_string(),
manifest_name: Some(manifest_name.to_string()),
manifest_description: None,
manifest_interface: None,
root: AbsolutePathBuf::try_from(codex_home.path().join(dir_name)).unwrap(),
enabled: true,
skill_roots: Vec::new(),
+1
View File
@@ -10,6 +10,7 @@ use codex_config::HookEventsToml;
use codex_utils_absolute_path::AbsolutePathBuf;
pub use load_outcome::EffectiveSkillRoots;
pub use load_outcome::LoadedPlugin;
pub use load_outcome::PluginInterfaceSummary;
pub use load_outcome::PluginLoadOutcome;
pub use load_outcome::prompt_safe_plugin_description;
pub use plugin_id::PluginId;
+9
View File
@@ -16,6 +16,7 @@ pub struct LoadedPlugin<M> {
pub config_name: String,
pub manifest_name: Option<String>,
pub manifest_description: Option<String>,
pub manifest_interface: Option<PluginInterfaceSummary>,
pub root: AbsolutePathBuf,
pub enabled: bool,
pub skill_roots: Vec<AbsolutePathBuf>,
@@ -28,6 +29,13 @@ pub struct LoadedPlugin<M> {
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginInterfaceSummary {
pub brand_color: Option<String>,
pub composer_icon: Option<AbsolutePathBuf>,
pub logo: Option<AbsolutePathBuf>,
}
impl<M> LoadedPlugin<M> {
pub fn is_active(&self) -> bool {
self.enabled && self.error.is_none()
@@ -219,6 +227,7 @@ mod tests {
config_name: config_name.to_string(),
manifest_name: None,
manifest_description: None,
manifest_interface: None,
root: test_path(config_name),
enabled: true,
skill_roots,