mirror of
https://github.com/openai/codex.git
synced 2026-09-04 15:08:45 +00:00
core-plugins: decouple manifest resource resolution
This commit is contained in:
@@ -4,6 +4,7 @@ pub mod installed_marketplaces;
|
||||
pub mod loader;
|
||||
mod manager;
|
||||
pub mod manifest;
|
||||
mod manifest_document;
|
||||
pub mod marketplace;
|
||||
pub mod marketplace_add;
|
||||
pub mod marketplace_remove;
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
use codex_config::HooksFile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::find_plugin_manifest_path;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::fs;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::manifest_document::RawPluginManifest;
|
||||
use crate::manifest_document::RawPluginManifestDefaultPrompt;
|
||||
use crate::manifest_document::RawPluginManifestDefaultPromptEntry;
|
||||
use crate::manifest_document::RawPluginManifestHooks;
|
||||
use crate::manifest_document::RawPluginManifestInterface;
|
||||
use crate::manifest_document::RawPluginManifestMcpServers;
|
||||
use crate::manifest_document::RawPluginManifestPaths;
|
||||
|
||||
const MAX_DEFAULT_PROMPT_COUNT: usize = 3;
|
||||
const MAX_DEFAULT_PROMPT_LEN: usize = 128;
|
||||
|
||||
@@ -16,108 +23,6 @@ pub type PluginManifestMcpServers =
|
||||
codex_plugin::manifest::PluginManifestMcpServers<AbsolutePathBuf>;
|
||||
pub type PluginManifestPaths = codex_plugin::manifest::PluginManifestPaths<AbsolutePathBuf>;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawPluginManifest {
|
||||
#[serde(default)]
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
version: Option<String>,
|
||||
#[serde(default)]
|
||||
description: Option<String>,
|
||||
#[serde(default)]
|
||||
keywords: Vec<String>,
|
||||
// Keep manifest paths as raw strings so we can validate the required `./...` syntax before
|
||||
// resolving them under the plugin root.
|
||||
#[serde(default)]
|
||||
skills: Option<RawPluginManifestPaths>,
|
||||
#[serde(default)]
|
||||
mcp_servers: Option<RawPluginManifestMcpServers>,
|
||||
#[serde(default)]
|
||||
apps: Option<String>,
|
||||
#[serde(default)]
|
||||
hooks: Option<RawPluginManifestHooks>,
|
||||
#[serde(default)]
|
||||
interface: Option<RawPluginManifestInterface>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawPluginManifestInterface {
|
||||
#[serde(default)]
|
||||
display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
short_description: Option<String>,
|
||||
#[serde(default)]
|
||||
long_description: Option<String>,
|
||||
#[serde(default)]
|
||||
developer_name: Option<String>,
|
||||
#[serde(default)]
|
||||
category: Option<String>,
|
||||
#[serde(default)]
|
||||
capabilities: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "websiteURL")]
|
||||
website_url: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "privacyPolicyURL")]
|
||||
privacy_policy_url: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "termsOfServiceURL")]
|
||||
terms_of_service_url: Option<String>,
|
||||
#[serde(default)]
|
||||
default_prompt: Option<RawPluginManifestDefaultPrompt>,
|
||||
#[serde(default)]
|
||||
brand_color: Option<String>,
|
||||
#[serde(default)]
|
||||
composer_icon: Option<String>,
|
||||
#[serde(default)]
|
||||
logo: Option<String>,
|
||||
#[serde(default)]
|
||||
screenshots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawPluginManifestDefaultPrompt {
|
||||
String(String),
|
||||
List(Vec<RawPluginManifestDefaultPromptEntry>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawPluginManifestDefaultPromptEntry {
|
||||
String(String),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawPluginManifestPaths {
|
||||
Path(String),
|
||||
Paths(Vec<String>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawPluginManifestMcpServers {
|
||||
Path(String),
|
||||
Object(std::collections::BTreeMap<String, JsonValue>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawPluginManifestHooks {
|
||||
Path(String),
|
||||
Paths(Vec<String>),
|
||||
Inline(HooksFile),
|
||||
InlineList(Vec<HooksFile>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
/// Loads a plugin manifest from the local host filesystem.
|
||||
pub fn load_plugin_manifest(plugin_root: &Path) -> Option<PluginManifest> {
|
||||
let manifest_path = find_plugin_manifest_path(plugin_root)?;
|
||||
@@ -139,6 +44,19 @@ pub(crate) fn parse_plugin_manifest(
|
||||
manifest_path: &Path,
|
||||
contents: &str,
|
||||
) -> Result<PluginManifest, serde_json::Error> {
|
||||
let fallback_name = plugin_root.file_name().and_then(|entry| entry.to_str());
|
||||
let manifest_display = manifest_path.display().to_string();
|
||||
parse_plugin_manifest_with(fallback_name, &manifest_display, contents, |field, path| {
|
||||
resolve_manifest_path(plugin_root, field, Some(path))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_plugin_manifest_with<Resource>(
|
||||
fallback_name: Option<&str>,
|
||||
manifest_display: &str,
|
||||
contents: &str,
|
||||
mut resolve_resource: impl FnMut(&'static str, &str) -> Option<Resource>,
|
||||
) -> Result<codex_plugin::manifest::PluginManifest<Resource>, serde_json::Error> {
|
||||
let RawPluginManifest {
|
||||
name: raw_name,
|
||||
version,
|
||||
@@ -149,10 +67,8 @@ pub(crate) fn parse_plugin_manifest(
|
||||
apps,
|
||||
hooks,
|
||||
interface,
|
||||
} = serde_json::from_str::<RawPluginManifest>(contents)?;
|
||||
let name = plugin_root
|
||||
.file_name()
|
||||
.and_then(|entry| entry.to_str())
|
||||
} = crate::manifest_document::parse(contents)?;
|
||||
let name = fallback_name
|
||||
.filter(|_| raw_name.trim().is_empty())
|
||||
.unwrap_or(&raw_name)
|
||||
.to_string();
|
||||
@@ -178,7 +94,7 @@ pub(crate) fn parse_plugin_manifest(
|
||||
screenshots,
|
||||
} = interface;
|
||||
|
||||
let interface = PluginManifestInterface {
|
||||
let interface = codex_plugin::manifest::PluginManifestInterface {
|
||||
display_name,
|
||||
short_description,
|
||||
long_description,
|
||||
@@ -188,23 +104,17 @@ pub(crate) fn parse_plugin_manifest(
|
||||
website_url,
|
||||
privacy_policy_url,
|
||||
terms_of_service_url,
|
||||
default_prompt: resolve_default_prompts(manifest_path, default_prompt.as_ref()),
|
||||
default_prompt: resolve_default_prompts(manifest_display, default_prompt.as_ref()),
|
||||
brand_color,
|
||||
composer_icon: resolve_interface_asset_path(
|
||||
plugin_root,
|
||||
"interface.composerIcon",
|
||||
composer_icon.as_deref(),
|
||||
),
|
||||
logo: resolve_interface_asset_path(plugin_root, "interface.logo", logo.as_deref()),
|
||||
composer_icon: composer_icon
|
||||
.as_deref()
|
||||
.and_then(|path| resolve_resource("interface.composerIcon", path)),
|
||||
logo: logo
|
||||
.as_deref()
|
||||
.and_then(|path| resolve_resource("interface.logo", path)),
|
||||
screenshots: screenshots
|
||||
.iter()
|
||||
.filter_map(|screenshot| {
|
||||
resolve_interface_asset_path(
|
||||
plugin_root,
|
||||
"interface.screenshots",
|
||||
Some(screenshot),
|
||||
)
|
||||
})
|
||||
.filter_map(|screenshot| resolve_resource("interface.screenshots", screenshot))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
@@ -225,38 +135,41 @@ pub(crate) fn parse_plugin_manifest(
|
||||
|
||||
has_fields.then_some(interface)
|
||||
});
|
||||
Ok(PluginManifest {
|
||||
Ok(codex_plugin::manifest::PluginManifest {
|
||||
name,
|
||||
version,
|
||||
description,
|
||||
keywords,
|
||||
paths: PluginManifestPaths {
|
||||
skills: resolve_manifest_paths(plugin_root, "skills", skills.as_ref()),
|
||||
mcp_servers: resolve_manifest_mcp_servers(plugin_root, mcp_servers),
|
||||
apps: resolve_manifest_path(plugin_root, "apps", apps.as_deref()),
|
||||
hooks: resolve_manifest_hooks(plugin_root, hooks),
|
||||
paths: codex_plugin::manifest::PluginManifestPaths {
|
||||
skills: resolve_manifest_paths("skills", skills.as_ref(), &mut resolve_resource),
|
||||
mcp_servers: resolve_manifest_mcp_servers(mcp_servers, &mut resolve_resource),
|
||||
apps: apps
|
||||
.as_deref()
|
||||
.and_then(|path| resolve_resource("apps", path)),
|
||||
hooks: resolve_manifest_hooks(hooks, &mut resolve_resource),
|
||||
},
|
||||
interface,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_manifest_hooks(
|
||||
plugin_root: &Path,
|
||||
fn resolve_manifest_hooks<Resource>(
|
||||
hooks: Option<RawPluginManifestHooks>,
|
||||
) -> Option<PluginManifestHooks> {
|
||||
resolve_resource: &mut impl FnMut(&'static str, &str) -> Option<Resource>,
|
||||
) -> Option<codex_plugin::manifest::PluginManifestHooks<Resource>> {
|
||||
use codex_plugin::manifest::PluginManifestHooks;
|
||||
|
||||
match hooks? {
|
||||
RawPluginManifestHooks::Path(path) => {
|
||||
resolve_manifest_path(plugin_root, "hooks", Some(&path))
|
||||
.map(|path| PluginManifestHooks::Paths(vec![path]))
|
||||
resolve_resource("hooks", &path).map(|path| PluginManifestHooks::Paths(vec![path]))
|
||||
}
|
||||
RawPluginManifestHooks::Paths(paths) => {
|
||||
let hooks = paths
|
||||
.iter()
|
||||
.filter_map(|path| resolve_manifest_path(plugin_root, "hooks", Some(path)))
|
||||
.filter_map(|path| resolve_resource("hooks", path))
|
||||
.collect::<Vec<_>>();
|
||||
(!hooks.is_empty()).then_some(PluginManifestHooks::Paths(hooks))
|
||||
}
|
||||
RawPluginManifestHooks::Inline(hooks) => Some(PluginManifestHooks::Inline(vec![hooks])),
|
||||
RawPluginManifestHooks::Inline(hooks) => Some(PluginManifestHooks::Inline(vec![*hooks])),
|
||||
RawPluginManifestHooks::InlineList(hooks) => {
|
||||
(!hooks.is_empty()).then_some(PluginManifestHooks::Inline(hooks))
|
||||
}
|
||||
@@ -270,14 +183,15 @@ fn resolve_manifest_hooks(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_manifest_mcp_servers(
|
||||
plugin_root: &Path,
|
||||
fn resolve_manifest_mcp_servers<Resource>(
|
||||
mcp_servers: Option<RawPluginManifestMcpServers>,
|
||||
) -> Option<PluginManifestMcpServers> {
|
||||
resolve_resource: &mut impl FnMut(&'static str, &str) -> Option<Resource>,
|
||||
) -> Option<codex_plugin::manifest::PluginManifestMcpServers<Resource>> {
|
||||
use codex_plugin::manifest::PluginManifestMcpServers;
|
||||
|
||||
match mcp_servers? {
|
||||
RawPluginManifestMcpServers::Path(path) => {
|
||||
resolve_manifest_path(plugin_root, "mcpServers", Some(&path))
|
||||
.map(PluginManifestMcpServers::Path)
|
||||
resolve_resource("mcpServers", &path).map(PluginManifestMcpServers::Path)
|
||||
}
|
||||
RawPluginManifestMcpServers::Object(servers) => match serde_json::to_string(&servers) {
|
||||
Ok(servers) => Some(PluginManifestMcpServers::Object(servers)),
|
||||
@@ -296,21 +210,13 @@ fn resolve_manifest_mcp_servers(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_interface_asset_path(
|
||||
plugin_root: &Path,
|
||||
field: &'static str,
|
||||
path: Option<&str>,
|
||||
) -> Option<AbsolutePathBuf> {
|
||||
resolve_manifest_path(plugin_root, field, path)
|
||||
}
|
||||
|
||||
fn resolve_default_prompts(
|
||||
manifest_path: &Path,
|
||||
manifest_display: &str,
|
||||
value: Option<&RawPluginManifestDefaultPrompt>,
|
||||
) -> Option<Vec<String>> {
|
||||
match value? {
|
||||
RawPluginManifestDefaultPrompt::String(prompt) => {
|
||||
resolve_default_prompt_str(manifest_path, "interface.defaultPrompt", prompt)
|
||||
resolve_default_prompt_str(manifest_display, "interface.defaultPrompt", prompt)
|
||||
.map(|prompt| vec![prompt])
|
||||
}
|
||||
RawPluginManifestDefaultPrompt::List(values) => {
|
||||
@@ -318,7 +224,7 @@ fn resolve_default_prompts(
|
||||
for (index, item) in values.iter().enumerate() {
|
||||
if prompts.len() >= MAX_DEFAULT_PROMPT_COUNT {
|
||||
warn_invalid_default_prompt(
|
||||
manifest_path,
|
||||
manifest_display,
|
||||
"interface.defaultPrompt",
|
||||
&format!("maximum of {MAX_DEFAULT_PROMPT_COUNT} prompts is supported"),
|
||||
);
|
||||
@@ -329,7 +235,7 @@ fn resolve_default_prompts(
|
||||
RawPluginManifestDefaultPromptEntry::String(prompt) => {
|
||||
let field = format!("interface.defaultPrompt[{index}]");
|
||||
if let Some(prompt) =
|
||||
resolve_default_prompt_str(manifest_path, &field, prompt)
|
||||
resolve_default_prompt_str(manifest_display, &field, prompt)
|
||||
{
|
||||
prompts.push(prompt);
|
||||
}
|
||||
@@ -337,7 +243,7 @@ fn resolve_default_prompts(
|
||||
RawPluginManifestDefaultPromptEntry::Invalid(value) => {
|
||||
let field = format!("interface.defaultPrompt[{index}]");
|
||||
warn_invalid_default_prompt(
|
||||
manifest_path,
|
||||
manifest_display,
|
||||
&field,
|
||||
&format!("expected a string, found {}", json_value_type(value)),
|
||||
);
|
||||
@@ -349,7 +255,7 @@ fn resolve_default_prompts(
|
||||
}
|
||||
RawPluginManifestDefaultPrompt::Invalid(value) => {
|
||||
warn_invalid_default_prompt(
|
||||
manifest_path,
|
||||
manifest_display,
|
||||
"interface.defaultPrompt",
|
||||
&format!(
|
||||
"expected a string or array of strings, found {}",
|
||||
@@ -361,15 +267,15 @@ fn resolve_default_prompts(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_default_prompt_str(manifest_path: &Path, field: &str, prompt: &str) -> Option<String> {
|
||||
fn resolve_default_prompt_str(manifest_display: &str, field: &str, prompt: &str) -> Option<String> {
|
||||
let prompt = prompt.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
if prompt.is_empty() {
|
||||
warn_invalid_default_prompt(manifest_path, field, "prompt must not be empty");
|
||||
warn_invalid_default_prompt(manifest_display, field, "prompt must not be empty");
|
||||
return None;
|
||||
}
|
||||
if prompt.chars().count() > MAX_DEFAULT_PROMPT_LEN {
|
||||
warn_invalid_default_prompt(
|
||||
manifest_path,
|
||||
manifest_display,
|
||||
field,
|
||||
&format!("prompt must be at most {MAX_DEFAULT_PROMPT_LEN} characters"),
|
||||
);
|
||||
@@ -378,11 +284,8 @@ fn resolve_default_prompt_str(manifest_path: &Path, field: &str, prompt: &str) -
|
||||
Some(prompt)
|
||||
}
|
||||
|
||||
fn warn_invalid_default_prompt(manifest_path: &Path, field: &str, message: &str) {
|
||||
tracing::warn!(
|
||||
path = %manifest_path.display(),
|
||||
"ignoring {field}: {message}"
|
||||
);
|
||||
fn warn_invalid_default_prompt(manifest_display: &str, field: &str, message: &str) {
|
||||
tracing::warn!(path = manifest_display, "ignoring {field}: {message}");
|
||||
}
|
||||
|
||||
fn json_value_type(value: &JsonValue) -> &'static str {
|
||||
@@ -396,20 +299,18 @@ fn json_value_type(value: &JsonValue) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_manifest_paths(
|
||||
plugin_root: &Path,
|
||||
fn resolve_manifest_paths<Resource>(
|
||||
field: &'static str,
|
||||
paths: Option<&RawPluginManifestPaths>,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
resolve_resource: &mut impl FnMut(&'static str, &str) -> Option<Resource>,
|
||||
) -> Vec<Resource> {
|
||||
match paths {
|
||||
Some(RawPluginManifestPaths::Path(path)) => {
|
||||
resolve_manifest_path(plugin_root, field, Some(path))
|
||||
.map(|path| vec![path])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
Some(RawPluginManifestPaths::Path(path)) => resolve_resource(field, path)
|
||||
.map(|path| vec![path])
|
||||
.unwrap_or_default(),
|
||||
Some(RawPluginManifestPaths::Paths(paths)) => paths
|
||||
.iter()
|
||||
.filter_map(|path| resolve_manifest_path(plugin_root, field, Some(path)))
|
||||
.filter_map(|path| resolve_resource(field, path))
|
||||
.collect(),
|
||||
Some(RawPluginManifestPaths::Invalid(value)) => {
|
||||
tracing::warn!(
|
||||
@@ -470,6 +371,7 @@ mod tests {
|
||||
use super::MAX_DEFAULT_PROMPT_LEN;
|
||||
use super::PluginManifest;
|
||||
use super::load_plugin_manifest;
|
||||
use super::parse_plugin_manifest_with;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
|
||||
use codex_plugin::PluginProvider;
|
||||
@@ -516,6 +418,95 @@ mod tests {
|
||||
load_plugin_manifest(plugin_root).expect("load plugin manifest")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_parser_passes_raw_resource_strings_to_resolver() {
|
||||
let mut observed = Vec::new();
|
||||
let manifest = parse_plugin_manifest_with(
|
||||
Some("fallback-plugin"),
|
||||
"manifest.json",
|
||||
r#"{
|
||||
"name": "",
|
||||
"skills": ["./skills/search", "../shared-skill"],
|
||||
"mcpServers": "./.mcp.json",
|
||||
"apps": "./apps/connectors.json",
|
||||
"hooks": ["./hooks/one.json", "../hooks/two.json"],
|
||||
"interface": {
|
||||
"displayName": "Fallback Plugin",
|
||||
"composerIcon": "./assets/icon.svg",
|
||||
"logo": "../assets/logo.svg",
|
||||
"screenshots": ["./assets/one.png", "../assets/two.png"]
|
||||
}
|
||||
}"#,
|
||||
|field, path| {
|
||||
observed.push((field.to_string(), path.to_string()));
|
||||
Some(format!("{field}:{path}"))
|
||||
},
|
||||
)
|
||||
.expect("parse plugin manifest");
|
||||
|
||||
assert_eq!(
|
||||
manifest,
|
||||
codex_plugin::manifest::PluginManifest {
|
||||
name: "fallback-plugin".to_string(),
|
||||
version: None,
|
||||
description: None,
|
||||
keywords: Vec::new(),
|
||||
paths: codex_plugin::manifest::PluginManifestPaths {
|
||||
skills: vec![
|
||||
"skills:./skills/search".to_string(),
|
||||
"skills:../shared-skill".to_string(),
|
||||
],
|
||||
mcp_servers: Some(codex_plugin::manifest::PluginManifestMcpServers::Path(
|
||||
"mcpServers:./.mcp.json".to_string(),
|
||||
),),
|
||||
apps: Some("apps:./apps/connectors.json".to_string()),
|
||||
hooks: Some(codex_plugin::manifest::PluginManifestHooks::Paths(vec![
|
||||
"hooks:./hooks/one.json".to_string(),
|
||||
"hooks:../hooks/two.json".to_string(),
|
||||
])),
|
||||
},
|
||||
interface: Some(codex_plugin::manifest::PluginManifestInterface {
|
||||
display_name: Some("Fallback Plugin".to_string()),
|
||||
composer_icon: Some("interface.composerIcon:./assets/icon.svg".to_string(),),
|
||||
logo: Some("interface.logo:../assets/logo.svg".to_string()),
|
||||
screenshots: vec![
|
||||
"interface.screenshots:./assets/one.png".to_string(),
|
||||
"interface.screenshots:../assets/two.png".to_string(),
|
||||
],
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
observed.sort();
|
||||
let mut expected_observed = vec![
|
||||
("apps".to_string(), "./apps/connectors.json".to_string()),
|
||||
("hooks".to_string(), "../hooks/two.json".to_string()),
|
||||
("hooks".to_string(), "./hooks/one.json".to_string()),
|
||||
(
|
||||
"interface.composerIcon".to_string(),
|
||||
"./assets/icon.svg".to_string(),
|
||||
),
|
||||
(
|
||||
"interface.logo".to_string(),
|
||||
"../assets/logo.svg".to_string(),
|
||||
),
|
||||
(
|
||||
"interface.screenshots".to_string(),
|
||||
"../assets/two.png".to_string(),
|
||||
),
|
||||
(
|
||||
"interface.screenshots".to_string(),
|
||||
"./assets/one.png".to_string(),
|
||||
),
|
||||
("mcpServers".to_string(), "./.mcp.json".to_string()),
|
||||
("skills".to_string(), "../shared-skill".to_string()),
|
||||
("skills".to_string(), "./skills/search".to_string()),
|
||||
];
|
||||
expected_observed.sort();
|
||||
assert_eq!(observed, expected_observed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_interface_accepts_legacy_default_prompt_string() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
111
codex-rs/core-plugins/src/manifest_document.rs
Normal file
111
codex-rs/core-plugins/src/manifest_document.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! Raw plugin manifest document types.
|
||||
//!
|
||||
//! Resource references stay as strings here so each caller can resolve them for its file system.
|
||||
|
||||
use codex_config::HooksFile;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RawPluginManifest {
|
||||
#[serde(default)]
|
||||
pub(super) name: String,
|
||||
#[serde(default)]
|
||||
pub(super) version: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) keywords: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(super) skills: Option<RawPluginManifestPaths>,
|
||||
#[serde(default)]
|
||||
pub(super) mcp_servers: Option<RawPluginManifestMcpServers>,
|
||||
#[serde(default)]
|
||||
pub(super) apps: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) hooks: Option<RawPluginManifestHooks>,
|
||||
#[serde(default)]
|
||||
pub(super) interface: Option<RawPluginManifestInterface>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RawPluginManifestInterface {
|
||||
#[serde(default)]
|
||||
pub(super) display_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) short_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) long_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) developer_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) category: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) capabilities: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "websiteURL")]
|
||||
pub(super) website_url: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "privacyPolicyURL")]
|
||||
pub(super) privacy_policy_url: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "termsOfServiceURL")]
|
||||
pub(super) terms_of_service_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) default_prompt: Option<RawPluginManifestDefaultPrompt>,
|
||||
#[serde(default)]
|
||||
pub(super) brand_color: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) composer_icon: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) logo: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(super) screenshots: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(super) enum RawPluginManifestDefaultPrompt {
|
||||
String(String),
|
||||
List(Vec<RawPluginManifestDefaultPromptEntry>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(super) enum RawPluginManifestDefaultPromptEntry {
|
||||
String(String),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(super) enum RawPluginManifestPaths {
|
||||
Path(String),
|
||||
Paths(Vec<String>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(super) enum RawPluginManifestMcpServers {
|
||||
Path(String),
|
||||
Object(std::collections::BTreeMap<String, JsonValue>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub(super) enum RawPluginManifestHooks {
|
||||
Path(String),
|
||||
Paths(Vec<String>),
|
||||
Inline(Box<HooksFile>),
|
||||
InlineList(Vec<HooksFile>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
|
||||
pub(super) fn parse(contents: &str) -> Result<RawPluginManifest, serde_json::Error> {
|
||||
serde_json::from_str(contents)
|
||||
}
|
||||
Reference in New Issue
Block a user