mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Expose plugin provenance for listed skills
This commit is contained in:
+80
@@ -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",
|
||||
|
||||
+80
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user